Compare commits

..

No commits in common. "005a608369f74383821396cc9ef6a53b7ecbc965" and "27ebe43e540dc1e0d271e4774d014a7d09e96932" have entirely different histories.

4 changed files with 9 additions and 153 deletions

View File

@ -15,7 +15,6 @@ import {
} from '@dnd-kit/core' } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppStore } from '../../stores/appStore' import { useAppStore } from '../../stores/appStore'
import { TrashView } from './TrashView'
import { formatDate } from '../../lib/utils' import { formatDate } from '../../lib/utils'
import type { Folder as FolderType, Note } from '../../db' import type { Folder as FolderType, Note } from '../../db'
@ -26,7 +25,6 @@ export function Sidebar() {
updateNote, updateFolder, setActiveNote, setActiveFolder, setSearch, updateNote, updateFolder, setActiveNote, setActiveFolder, setSearch,
toggleTheme, toggleStar, filteredNotes, toggleTheme, toggleStar, filteredNotes,
activeTag, setActiveTag, sortBy, sortOrder, setSortBy, setSortOrder, activeTag, setActiveTag, sortBy, sortOrder, setSortBy, setSortOrder,
trashNotes,
} = useAppStore() } = useAppStore()
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set()) const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
@ -231,13 +229,6 @@ export function Sidebar() {
<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) }} />
<NavItem
icon={<Trash2 size={14} />}
label="回收站"
count={trashNotes().length || undefined}
active={activeFolderId === 'trash'}
onClick={() => { setActiveFolder('trash'); if (activeNoteId === '__welcome__') setActiveNote(null) }}
/>
</nav> </nav>
{/* Folders section */} {/* Folders section */}
@ -273,10 +264,6 @@ export function Sidebar() {
{/* Note list */} {/* Note list */}
<div className="flex-1 overflow-y-auto mt-2" style={{ borderTop: '1px solid var(--border)' }}> <div className="flex-1 overflow-y-auto mt-2" style={{ borderTop: '1px solid var(--border)' }}>
{activeFolderId === 'trash' ? (
<TrashView />
) : (
<>
<div className="px-3 py-2 flex items-center justify-between sticky top-0 z-10" style={{ background: 'var(--bg-subtle)' }}> <div className="px-3 py-2 flex items-center justify-between sticky top-0 z-10" style={{ background: 'var(--bg-subtle)' }}>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}> <span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>
@ -356,9 +343,9 @@ export function Sidebar() {
/> />
))} ))}
</div> </div>
</>
)}
</div> </div>
{/* Context menu */}
{contextMenu && ( <div {contextMenu && ( <div
className="fixed z-50 rounded-lg py-1 shadow-xl" className="fixed z-50 rounded-lg py-1 shadow-xl"
style={{ top: contextMenu.y, left: contextMenu.x, background: 'var(--bg)', border: '1px solid var(--border)', minWidth: 160 }} style={{ top: contextMenu.y, left: contextMenu.x, background: 'var(--bg)', border: '1px solid var(--border)', minWidth: 160 }}
@ -449,8 +436,8 @@ export function Sidebar() {
style={{ background: 'var(--bg)', border: '1px solid var(--border)', width: 320 }} style={{ background: 'var(--bg)', border: '1px solid var(--border)', width: 320 }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<p className="text-sm font-semibold" style={{ color: 'var(--text)' }}></p> <p className="text-sm font-semibold" style={{ color: 'var(--text)' }}></p>
<p className="text-xs" style={{ color: 'var(--muted)' }}> 30 </p> <p className="text-xs" style={{ color: 'var(--muted)' }}></p>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button <button
onClick={() => setDeleteConfirm(null)} onClick={() => setDeleteConfirm(null)}
@ -463,7 +450,7 @@ export function Sidebar() {
onClick={async () => { await deleteNote(deleteConfirm); setDeleteConfirm(null) }} onClick={async () => { await deleteNote(deleteConfirm); setDeleteConfirm(null) }}
className="px-3 py-1.5 rounded-lg text-sm font-medium" className="px-3 py-1.5 rounded-lg text-sm font-medium"
style={{ background: '#ef4444', color: '#fff' }} style={{ background: '#ef4444', color: '#fff' }}
></button> ></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,75 +0,0 @@
import { Trash2, RotateCcw } from 'lucide-react'
import { useAppStore } from '../../stores/appStore'
function daysLeft(deletedAt: number): number {
const elapsed = Date.now() - deletedAt
return Math.max(0, 30 - Math.floor(elapsed / (24 * 60 * 60 * 1000)))
}
export function TrashView() {
const { trashNotes, restoreNote, emptyTrash } = useAppStore()
const notes = trashNotes()
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-3 py-2 shrink-0"
style={{ borderBottom: '1px solid var(--border)' }}>
<span className="text-xs" style={{ color: 'var(--text-faint)' }}>
{notes.length > 0 ? `${notes.length} 条笔记` : '回收站为空'}
</span>
{notes.length > 0 && (
<button
onClick={() => {
if (confirm('确认清空回收站?此操作无法撤销。')) emptyTrash()
}}
className="text-xs px-2 py-1 rounded"
style={{ color: '#ef4444' }}
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(239,68,68,0.1)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
</button>
)}
</div>
<div className="flex-1 overflow-y-auto py-1">
{notes.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-2 px-4"
style={{ color: 'var(--text-faint)' }}>
<Trash2 size={28} strokeWidth={1.5} />
<p className="text-xs text-center"></p>
</div>
) : (
notes.map(note => (
<div
key={note.id}
className="flex items-center gap-2 px-3 py-2 mx-1 rounded-lg group"
style={{ minHeight: 44 }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-muted)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<div className="flex-1 min-w-0">
<p className="text-sm truncate" style={{ color: 'var(--text)' }}>
{note.title || '无标题'}
</p>
<p className="text-xs" style={{ color: 'var(--text-faint)' }}>
{daysLeft(note.deletedAt!)}
</p>
</div>
<button
onClick={() => restoreNote(note.id)}
className="shrink-0 p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity"
title="恢复笔记"
style={{ color: 'var(--accent)' }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-subtle)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<RotateCcw size={13} />
</button>
</div>
))
)}
</div>
</div>
)
}

View File

@ -10,7 +10,6 @@ export interface Note {
createdAt: number createdAt: number
updatedAt: number updatedAt: number
wordCount: number wordCount: number
deletedAt: number | null
} }
export interface Folder { export interface Folder {
@ -39,17 +38,6 @@ class NotesDB extends Dexie {
folders: 'id, parentId, order', folders: 'id, parentId, order',
tags: 'id, name', tags: 'id, name',
}) })
this.version(2).stores({
notes: 'id, folderId, starred, updatedAt, createdAt, deletedAt, *tags',
folders: 'id, parentId, order',
tags: 'id, name',
}).upgrade(tx => {
return tx.table('notes').toCollection().modify(note => {
if (note.deletedAt === undefined) {
note.deletedAt = null
}
})
})
} }
} }
@ -98,7 +86,6 @@ async function _doSeed() {
createdAt: now - 86400000, createdAt: now - 86400000,
updatedAt: now - 3600000, updatedAt: now - 3600000,
wordCount: 30, wordCount: 30,
deletedAt: null,
}, },
{ {
id: crypto.randomUUID(), id: crypto.randomUUID(),
@ -117,7 +104,6 @@ async function _doSeed() {
createdAt: now - 172800000, createdAt: now - 172800000,
updatedAt: now - 172800000, updatedAt: now - 172800000,
wordCount: 45, wordCount: 45,
deletedAt: null,
}, },
]) ])
} }

View File

@ -6,7 +6,7 @@ interface AppState {
notes: Note[] notes: Note[]
folders: Folder[] folders: Folder[]
activeNoteId: string | null activeNoteId: string | null
activeFolderId: string | null | 'all' | 'starred' | 'trash' activeFolderId: string | null | 'all' | 'starred'
searchQuery: string searchQuery: string
theme: 'light' | 'dark' theme: 'light' | 'dark'
focusMode: boolean focusMode: boolean
@ -39,9 +39,6 @@ interface AppState {
setSortOrder: (order: 'asc' | 'desc') => void setSortOrder: (order: 'asc' | 'desc') => void
filteredNotes: () => Note[] filteredNotes: () => Note[]
restoreNote: (id: string) => Promise<void>
emptyTrash: () => Promise<void>
trashNotes: () => Note[]
} }
export const useAppStore = create<AppState>((set, get) => ({ export const useAppStore = create<AppState>((set, get) => ({
@ -60,16 +57,10 @@ export const useAppStore = create<AppState>((set, get) => ({
_filteredCache: null, _filteredCache: null,
loadAll: async () => { loadAll: async () => {
let [notes, folders] = await Promise.all([ const [notes, folders] = await Promise.all([
db.notes.orderBy('updatedAt').reverse().toArray(), db.notes.orderBy('updatedAt').reverse().toArray(),
db.folders.orderBy('order').toArray(), db.folders.orderBy('order').toArray(),
]) ])
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000
const expired = notes.filter(n => n.deletedAt !== null && Date.now() - n.deletedAt > THIRTY_DAYS)
if (expired.length > 0) {
await db.notes.bulkDelete(expired.map(n => n.id))
notes = notes.filter(n => !expired.some(e => e.id === n.id))
}
set({ notes, folders, _notesVersion: get()._notesVersion + 1, _filteredCache: null }) set({ notes, folders, _notesVersion: get()._notesVersion + 1, _filteredCache: null })
// Keep welcome screen as default; only auto-select if already on a real note // Keep welcome screen as default; only auto-select if already on a real note
const cur = get().activeNoteId const cur = get().activeNoteId
@ -91,7 +82,6 @@ export const useAppStore = create<AppState>((set, get) => ({
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
wordCount: 0, wordCount: 0,
deletedAt: null,
} }
await db.notes.add(note) await db.notes.add(note)
set(s => ({ notes: [note, ...s.notes], activeNoteId: id, _notesVersion: s._notesVersion + 1, _filteredCache: null })) set(s => ({ notes: [note, ...s.notes], activeNoteId: id, _notesVersion: s._notesVersion + 1, _filteredCache: null }))
@ -113,36 +103,14 @@ export const useAppStore = create<AppState>((set, get) => ({
}, },
deleteNote: async (id) => { deleteNote: async (id) => {
const deletedAt = Date.now() await db.notes.delete(id)
await db.notes.update(id, { deletedAt })
set(s => { set(s => {
const notes = s.notes.map(n => n.id === id ? { ...n, deletedAt } : n) const notes = s.notes.filter(n => n.id !== id)
const activeNoteId = s.activeNoteId === id ? '__welcome__' : s.activeNoteId const activeNoteId = s.activeNoteId === id ? '__welcome__' : s.activeNoteId
return { notes, activeNoteId, _notesVersion: s._notesVersion + 1, _filteredCache: null } return { notes, activeNoteId, _notesVersion: s._notesVersion + 1, _filteredCache: null }
}) })
}, },
restoreNote: async (id) => {
await db.notes.update(id, { deletedAt: null })
set(s => ({
notes: s.notes.map(n => n.id === id ? { ...n, deletedAt: null } : n),
_notesVersion: s._notesVersion + 1,
_filteredCache: null,
}))
},
emptyTrash: async () => {
const trashed = await db.notes.where('deletedAt').above(0).toArray()
const ids = trashed.map(n => n.id)
await db.notes.bulkDelete(ids)
set(s => ({
notes: s.notes.filter(n => n.deletedAt === null),
activeNoteId: ids.includes(s.activeNoteId ?? '') ? '__welcome__' : s.activeNoteId,
_notesVersion: s._notesVersion + 1,
_filteredCache: null,
}))
},
toggleStar: async (id) => { toggleStar: async (id) => {
const note = get().notes.find(n => n.id === id) const note = get().notes.find(n => n.id === id)
if (!note) return if (!note) return
@ -194,10 +162,7 @@ export const useAppStore = create<AppState>((set, get) => ({
const cacheKey = `${_notesVersion}|${activeFolderId}|${searchQuery}|${activeTag}|${sortBy}|${sortOrder}` const cacheKey = `${_notesVersion}|${activeFolderId}|${searchQuery}|${activeTag}|${sortBy}|${sortOrder}`
if (_filteredCache?.key === cacheKey) return _filteredCache.result if (_filteredCache?.key === cacheKey) return _filteredCache.result
if (activeFolderId === 'trash') return []
let result = notes let result = notes
result = result.filter(n => n.deletedAt === null)
if (activeFolderId === 'starred') { if (activeFolderId === 'starred') {
result = result.filter(n => n.starred) result = result.filter(n => n.starred)
@ -231,11 +196,4 @@ export const useAppStore = create<AppState>((set, get) => ({
set({ _filteredCache: { key: cacheKey, result } }) set({ _filteredCache: { key: cacheKey, result } })
return result return result
}, },
trashNotes: () => {
const { notes } = get()
return notes
.filter(n => n.deletedAt !== null)
.sort((a, b) => (b.deletedAt ?? 0) - (a.deletedAt ?? 0))
},
})) }))