Compare commits
No commits in common. "005a608369f74383821396cc9ef6a53b7ecbc965" and "27ebe43e540dc1e0d271e4774d014a7d09e96932" have entirely different histories.
005a608369
...
27ebe43e54
@ -15,7 +15,6 @@ import {
|
||||
} from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppStore } from '../../stores/appStore'
|
||||
import { TrashView } from './TrashView'
|
||||
import { formatDate } from '../../lib/utils'
|
||||
import type { Folder as FolderType, Note } from '../../db'
|
||||
|
||||
@ -26,7 +25,6 @@ export function Sidebar() {
|
||||
updateNote, updateFolder, setActiveNote, setActiveFolder, setSearch,
|
||||
toggleTheme, toggleStar, filteredNotes,
|
||||
activeTag, setActiveTag, sortBy, sortOrder, setSortBy, setSortOrder,
|
||||
trashNotes,
|
||||
} = useAppStore()
|
||||
|
||||
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={<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={<Trash2 size={14} />}
|
||||
label="回收站"
|
||||
count={trashNotes().length || undefined}
|
||||
active={activeFolderId === 'trash'}
|
||||
onClick={() => { setActiveFolder('trash'); if (activeNoteId === '__welcome__') setActiveNote(null) }}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{/* Folders section */}
|
||||
@ -273,10 +264,6 @@ export function Sidebar() {
|
||||
|
||||
{/* Note list */}
|
||||
<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="flex items-center gap-1.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>
|
||||
@ -356,9 +343,9 @@ export function Sidebar() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context menu */}
|
||||
{contextMenu && ( <div
|
||||
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 }}
|
||||
@ -449,8 +436,8 @@ export function Sidebar() {
|
||||
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(--muted)' }}>笔记将在 30 天后永久删除,期间可随时恢复。</p>
|
||||
<p className="text-sm font-semibold" style={{ color: 'var(--text)' }}>确认删除笔记?</p>
|
||||
<p className="text-xs" style={{ color: 'var(--muted)' }}>此操作无法撤销。</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(null)}
|
||||
@ -463,7 +450,7 @@ export function Sidebar() {
|
||||
onClick={async () => { await deleteNote(deleteConfirm); setDeleteConfirm(null) }}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium"
|
||||
style={{ background: '#ef4444', color: '#fff' }}
|
||||
>移入回收站</button>
|
||||
>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -10,7 +10,6 @@ export interface Note {
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
wordCount: number
|
||||
deletedAt: number | null
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@ -39,17 +38,6 @@ class NotesDB extends Dexie {
|
||||
folders: 'id, parentId, order',
|
||||
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,
|
||||
updatedAt: now - 3600000,
|
||||
wordCount: 30,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
@ -117,7 +104,6 @@ async function _doSeed() {
|
||||
createdAt: now - 172800000,
|
||||
updatedAt: now - 172800000,
|
||||
wordCount: 45,
|
||||
deletedAt: null,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ interface AppState {
|
||||
notes: Note[]
|
||||
folders: Folder[]
|
||||
activeNoteId: string | null
|
||||
activeFolderId: string | null | 'all' | 'starred' | 'trash'
|
||||
activeFolderId: string | null | 'all' | 'starred'
|
||||
searchQuery: string
|
||||
theme: 'light' | 'dark'
|
||||
focusMode: boolean
|
||||
@ -39,9 +39,6 @@ interface AppState {
|
||||
setSortOrder: (order: 'asc' | 'desc') => void
|
||||
|
||||
filteredNotes: () => Note[]
|
||||
restoreNote: (id: string) => Promise<void>
|
||||
emptyTrash: () => Promise<void>
|
||||
trashNotes: () => Note[]
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
@ -60,16 +57,10 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
_filteredCache: null,
|
||||
|
||||
loadAll: async () => {
|
||||
let [notes, folders] = await Promise.all([
|
||||
const [notes, folders] = await Promise.all([
|
||||
db.notes.orderBy('updatedAt').reverse().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 })
|
||||
// Keep welcome screen as default; only auto-select if already on a real note
|
||||
const cur = get().activeNoteId
|
||||
@ -91,7 +82,6 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
wordCount: 0,
|
||||
deletedAt: null,
|
||||
}
|
||||
await db.notes.add(note)
|
||||
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) => {
|
||||
const deletedAt = Date.now()
|
||||
await db.notes.update(id, { deletedAt })
|
||||
await db.notes.delete(id)
|
||||
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
|
||||
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) => {
|
||||
const note = get().notes.find(n => n.id === id)
|
||||
if (!note) return
|
||||
@ -194,10 +162,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
const cacheKey = `${_notesVersion}|${activeFolderId}|${searchQuery}|${activeTag}|${sortBy}|${sortOrder}`
|
||||
if (_filteredCache?.key === cacheKey) return _filteredCache.result
|
||||
|
||||
if (activeFolderId === 'trash') return []
|
||||
|
||||
let result = notes
|
||||
result = result.filter(n => n.deletedAt === null)
|
||||
|
||||
if (activeFolderId === 'starred') {
|
||||
result = result.filter(n => n.starred)
|
||||
@ -231,11 +196,4 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
set({ _filteredCache: { key: cacheKey, result } })
|
||||
return result
|
||||
},
|
||||
|
||||
trashNotes: () => {
|
||||
const { notes } = get()
|
||||
return notes
|
||||
.filter(n => n.deletedAt !== null)
|
||||
.sort((a, b) => (b.deletedAt ?? 0) - (a.deletedAt ?? 0))
|
||||
},
|
||||
}))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user