Compare commits
No commits in common. "005a608369f74383821396cc9ef6a53b7ecbc965" and "27ebe43e540dc1e0d271e4774d014a7d09e96932" have entirely different histories.
005a608369
...
27ebe43e54
@ -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>
|
||||||
|
|||||||
@ -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
|
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,
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user