修复笔记侧边栏多个 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:
MikiVL 2026-07-26 09:02:27 +08:00
parent 689c8b2d86
commit b02f23729b
3 changed files with 61 additions and 7 deletions

View File

@ -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)} />}

View File

@ -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>
) )
} }

View File

@ -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()