修复笔记侧边栏多个 bug
- 移除管理员面板入口(UserMenu 中的按钮和相关 import) - 修复重复文件夹问题:seed 判断同时检查 notes 和 folders 数量 - 文件夹新增 hover 删除按钮,附带确认弹窗 - 文件夹区域添加 overflow-y-auto 和 maxHeight 限制,防止过多文件夹遮挡笔记列表 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
689c8b2d86
commit
b02f23729b
@ -76,6 +76,7 @@ export function UserMenu() {
|
|||||||
<span>输入邀请码开启云存储</span>
|
<span>输入邀请码开启云存储</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showProfile && <ProfileModal onClose={() => setShowProfile(false)} />}
|
{showProfile && <ProfileModal onClose={() => setShowProfile(false)} />}
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export function Sidebar() {
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [draggingNoteId, setDraggingNoteId] = useState<string | null>(null)
|
const [draggingNoteId, setDraggingNoteId] = useState<string | null>(null)
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
|
||||||
|
const [deleteFolderConfirm, setDeleteFolderConfirm] = useState<string | null>(null)
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(MouseSensor, { activationConstraint: { distance: 6 } }),
|
useSensor(MouseSensor, { activationConstraint: { distance: 6 } }),
|
||||||
@ -178,6 +179,7 @@ export function Sidebar() {
|
|||||||
if (e.key === 'Enter') handleRenameFolder(folder.id)
|
if (e.key === 'Enter') handleRenameFolder(folder.id)
|
||||||
if (e.key === 'Escape') setEditingFolderId(null)
|
if (e.key === 'Escape') setEditingFolderId(null)
|
||||||
}}
|
}}
|
||||||
|
onDelete={() => setDeleteFolderConfirm(folder.id)}
|
||||||
/>
|
/>
|
||||||
{expandedFolders.has(folder.id) && renderFolder(folder.id, depth + 1)}
|
{expandedFolders.has(folder.id) && renderFolder(folder.id, depth + 1)}
|
||||||
</div>
|
</div>
|
||||||
@ -211,7 +213,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2 shrink-0">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded-lg px-3 py-1.5"
|
className="flex items-center gap-2 rounded-lg px-3 py-1.5"
|
||||||
style={{ background: 'var(--bg-muted)', border: '1px solid var(--border)' }}
|
style={{ background: 'var(--bg-muted)', border: '1px solid var(--border)' }}
|
||||||
@ -228,7 +230,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav shortcuts */}
|
{/* Nav shortcuts */}
|
||||||
<nav className="px-2 space-y-0.5">
|
<nav className="px-2 space-y-0.5 shrink-0">
|
||||||
<NavItem icon={<BookOpen size={14} />} label="使用指南" active={activeNoteId === '__welcome__'} onClick={() => setActiveNote('__welcome__')} />
|
<NavItem icon={<BookOpen size={14} />} label="使用指南" active={activeNoteId === '__welcome__'} onClick={() => setActiveNote('__welcome__')} />
|
||||||
<NavItem icon={<FileText size={14} />} label="所有笔记" count={notes.length} active={activeFolderId === 'all' && activeNoteId !== '__welcome__'} onClick={() => { setActiveFolder('all'); if (activeNoteId === '__welcome__') setActiveNote(null) }} />
|
<NavItem icon={<FileText size={14} />} label="所有笔记" count={notes.length} active={activeFolderId === 'all' && activeNoteId !== '__welcome__'} onClick={() => { setActiveFolder('all'); if (activeNoteId === '__welcome__') setActiveNote(null) }} />
|
||||||
<NavItem icon={<Star size={14} />} label="收藏" count={notes.filter(n => n.starred).length} active={activeFolderId === 'starred'} onClick={() => { setActiveFolder('starred'); if (activeNoteId === '__welcome__') setActiveNote(null) }} />
|
<NavItem icon={<Star size={14} />} label="收藏" count={notes.filter(n => n.starred).length} active={activeFolderId === 'starred'} onClick={() => { setActiveFolder('starred'); if (activeNoteId === '__welcome__') setActiveNote(null) }} />
|
||||||
@ -242,14 +244,14 @@ export function Sidebar() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Folders section */}
|
{/* Folders section */}
|
||||||
<div className="px-3 pt-3 pb-1 flex items-center justify-between">
|
<div className="px-3 pt-3 pb-1 flex items-center justify-between shrink-0">
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>文件夹</span>
|
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>文件夹</span>
|
||||||
<button onClick={() => setCreatingFolder(true)} className="toolbar-btn" title="新建文件夹">
|
<button onClick={() => setCreatingFolder(true)} className="toolbar-btn" title="新建文件夹">
|
||||||
<FolderPlus size={13} />
|
<FolderPlus size={13} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-2 space-y-0.5">
|
<div className="px-2 space-y-0.5 overflow-y-auto" style={{ maxHeight: '30%' }}>
|
||||||
{renderFolder(null, 0)}
|
{renderFolder(null, 0)}
|
||||||
|
|
||||||
{creatingFolder && (
|
{creatingFolder && (
|
||||||
@ -472,6 +474,36 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{deleteFolderConfirm && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
style={{ background: 'rgba(0,0,0,0.4)' }}
|
||||||
|
onClick={() => setDeleteFolderConfirm(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-2xl shadow-2xl p-5 flex flex-col gap-4"
|
||||||
|
style={{ background: 'var(--bg)', border: '1px solid var(--border)', width: 320 }}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-semibold" style={{ color: 'var(--text)' }}>删除文件夹?</p>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-faint)' }}>文件夹内的笔记将移至「所有笔记」,文件夹本身将被永久删除。</p>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteFolderConfirm(null)}
|
||||||
|
className="px-3 py-1.5 rounded-lg text-sm"
|
||||||
|
style={{ color: 'var(--text-muted)' }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-muted)')}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||||
|
>取消</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => { await deleteFolder(deleteFolderConfirm); setDeleteFolderConfirm(null) }}
|
||||||
|
className="px-3 py-1.5 rounded-lg text-sm font-medium"
|
||||||
|
style={{ background: '#ef4444', color: '#fff' }}
|
||||||
|
>永久删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DndContext>
|
</DndContext>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -489,13 +521,15 @@ function NavItem({ icon, label, count, active, onClick }: { icon: React.ReactNod
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DroppableFolderItem({ folder, depth, expanded, active, editing, editValue, editRef, onToggle, onClick, onContextMenu, onEditChange, onEditBlur, onEditKeyDown }: {
|
function DroppableFolderItem({ folder, depth, expanded, active, editing, editValue, editRef, onToggle, onClick, onContextMenu, onEditChange, onEditBlur, onEditKeyDown, onDelete }: {
|
||||||
folder: FolderType; depth: number; expanded: boolean; active: boolean; editing: boolean
|
folder: FolderType; depth: number; expanded: boolean; active: boolean; editing: boolean
|
||||||
editValue: string; editRef: React.RefObject<HTMLInputElement | null>
|
editValue: string; editRef: React.RefObject<HTMLInputElement | null>
|
||||||
onToggle: () => void; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void
|
onToggle: () => void; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void
|
||||||
onEditChange: (v: string) => void; onEditBlur: () => void; onEditKeyDown: (e: React.KeyboardEvent) => void
|
onEditChange: (v: string) => void; onEditBlur: () => void; onEditKeyDown: (e: React.KeyboardEvent) => void
|
||||||
|
onDelete: () => void
|
||||||
}) {
|
}) {
|
||||||
const { isOver, setNodeRef } = useDroppable({ id: folder.id })
|
const { isOver, setNodeRef } = useDroppable({ id: folder.id })
|
||||||
|
const [hovered, setHovered] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -503,6 +537,7 @@ function DroppableFolderItem({ folder, depth, expanded, active, editing, editVal
|
|||||||
className="flex items-center gap-1.5 py-1.5 rounded-lg cursor-pointer text-sm"
|
className="flex items-center gap-1.5 py-1.5 rounded-lg cursor-pointer text-sm"
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: 8 + depth * 16,
|
paddingLeft: 8 + depth * 16,
|
||||||
|
paddingRight: 8,
|
||||||
background: isOver ? 'var(--accent-subtle)' : active ? 'var(--accent-subtle)' : 'transparent',
|
background: isOver ? 'var(--accent-subtle)' : active ? 'var(--accent-subtle)' : 'transparent',
|
||||||
color: active || isOver ? 'var(--accent)' : 'var(--text-muted)',
|
color: active || isOver ? 'var(--accent)' : 'var(--text-muted)',
|
||||||
outline: isOver ? '2px solid var(--accent)' : 'none',
|
outline: isOver ? '2px solid var(--accent)' : 'none',
|
||||||
@ -510,6 +545,8 @@ function DroppableFolderItem({ folder, depth, expanded, active, editing, editVal
|
|||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<button onClick={e => { e.stopPropagation(); onToggle() }} className="toolbar-btn" style={{ width: 16, height: 16, flexShrink: 0 }}>
|
<button onClick={e => { e.stopPropagation(); onToggle() }} className="toolbar-btn" style={{ width: 16, height: 16, flexShrink: 0 }}>
|
||||||
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||||
@ -523,6 +560,18 @@ function DroppableFolderItem({ folder, depth, expanded, active, editing, editVal
|
|||||||
) : (
|
) : (
|
||||||
<span className="flex-1 truncate" style={{ fontWeight: active ? 500 : 400 }}>{folder.name}</span>
|
<span className="flex-1 truncate" style={{ fontWeight: active ? 500 : 400 }}>{folder.name}</span>
|
||||||
)}
|
)}
|
||||||
|
{hovered && !editing && (
|
||||||
|
<button
|
||||||
|
className="toolbar-btn"
|
||||||
|
style={{ width: 20, height: 20, color: '#ef4444', opacity: 0.8, flexShrink: 0 }}
|
||||||
|
title="删除文件夹"
|
||||||
|
onClick={e => { e.stopPropagation(); onDelete() }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.opacity = '1')}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.opacity = '0.8')}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,8 +64,12 @@ export function seedIfEmpty(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function _doSeed() {
|
async function _doSeed() {
|
||||||
const count = await db.notes.count()
|
// 用文件夹数量做判断,防止 notes 为空但文件夹已存在时重复创建
|
||||||
if (count > 0) return
|
const [noteCount, folderCount] = await Promise.all([
|
||||||
|
db.notes.count(),
|
||||||
|
db.folders.count(),
|
||||||
|
])
|
||||||
|
if (noteCount > 0 || folderCount > 0) return
|
||||||
|
|
||||||
const folderId1 = crypto.randomUUID()
|
const folderId1 = crypto.randomUUID()
|
||||||
const folderId2 = crypto.randomUUID()
|
const folderId2 = crypto.randomUUID()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user