diff --git a/package-lock.json b/package-lock.json
index 1e29445..d4e90c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "program1",
"version": "0.0.0",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
@@ -307,6 +309,45 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
diff --git a/package.json b/package.json
index 62d6425..ac9da1b 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx
index 28f3ab0..433ffee 100644
--- a/src/components/editor/Editor.tsx
+++ b/src/components/editor/Editor.tsx
@@ -22,7 +22,7 @@ import {
Bold, Italic, Underline as UnderlineIcon, Strikethrough, Code,
Highlighter, List, ListOrdered, Quote,
Heading1, Heading2, Heading3, Minus, CheckSquare, Table as TableIcon,
- Type, Star, ImageIcon, Maximize2, Minimize2,
+ Type, Star, ImageIcon, Maximize2, Minimize2, Hash, X,
} from 'lucide-react'
import { useAppStore } from '../../stores/appStore'
import { countWords } from '../../lib/utils'
@@ -82,6 +82,7 @@ export function Editor() {
const [title, setTitle] = useState(activeNote?.title ?? '')
const [wordCount, setWordCount] = useState(0)
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')
+ const [tagInput, setTagInput] = useState('')
const [slashOpen, setSlashOpen] = useState(false)
const [slashQuery, setSlashQuery] = useState('')
@@ -265,6 +266,7 @@ export function Editor() {
setWordCount(activeNote.wordCount)
setSaveStatus('saved')
setSlashOpen(false)
+ setTagInput('')
setTimeout(() => { isLoadingRef.current = false }, 100)
}, [activeNoteId, editor])
@@ -318,6 +320,47 @@ export function Editor() {
+ {/* Tag row */}
+
+
+ {activeNote.tags.map(tag => (
+
+
+ {tag}
+
+
+ ))}
+ setTagInput(e.target.value)}
+ onKeyDown={e => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ const val = tagInput.trim()
+ if (val && !activeNote.tags.includes(val)) {
+ updateNote(activeNoteId!, { tags: [...activeNote.tags, val] }, { silent: true })
+ }
+ setTagInput('')
+ } else if (e.key === 'Backspace' && tagInput === '' && activeNote.tags.length > 0) {
+ updateNote(activeNoteId!, { tags: activeNote.tags.slice(0, -1) }, { silent: true })
+ }
+ }}
+ placeholder={activeNote.tags.length === 0 ? '添加标签…' : '+'}
+ className="bg-transparent outline-none text-xs"
+ style={{ color: 'var(--text-muted)', minWidth: 60, width: tagInput.length * 8 + 60 }}
+ />
+
+
+
{/* Floating toolbar */}
{editor && (
diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx
index 24e0690..e5708af 100644
--- a/src/components/sidebar/Sidebar.tsx
+++ b/src/components/sidebar/Sidebar.tsx
@@ -3,7 +3,14 @@ import {
Search, Plus, Star, FileText, Folder, FolderOpen,
ChevronRight, ChevronDown,
Trash2, Edit2, Moon, Sun, FolderPlus, Hash, BookOpen,
+ ArrowUpDown, Check, X,
} from 'lucide-react'
+import {
+ DndContext, DragOverlay, useDraggable, useDroppable,
+ useSensor, useSensors, MouseSensor, TouchSensor,
+ type DragEndEvent, type DragStartEvent,
+} from '@dnd-kit/core'
+import { CSS } from '@dnd-kit/utilities'
import { useAppStore } from '../../stores/appStore'
import { formatDate } from '../../lib/utils'
import type { Folder as FolderType, Note } from '../../db'
@@ -12,8 +19,9 @@ export function Sidebar() {
const {
notes, folders, activeNoteId, activeFolderId, searchQuery,
theme, createNote, createFolder, deleteNote, deleteFolder,
- updateFolder, setActiveNote, setActiveFolder, setSearch,
+ updateNote, updateFolder, setActiveNote, setActiveFolder, setSearch,
toggleTheme, toggleStar, filteredNotes,
+ activeTag, setActiveTag, sortBy, sortOrder, setSortBy, setSortOrder,
} = useAppStore()
const [expandedFolders, setExpandedFolders] = useState
>(new Set())
@@ -21,27 +29,41 @@ export function Sidebar() {
const [newFolderName, setNewFolderName] = useState('')
const [creatingFolder, setCreatingFolder] = useState(false)
const [newFolderInput, setNewFolderInput] = useState('')
+ const [editingNoteId, setEditingNoteId] = useState(null)
+ const [noteEditValue, setNoteEditValue] = useState('')
+ const [sortMenuOpen, setSortMenuOpen] = useState(false)
const [contextMenu, setContextMenu] = useState<{
type: 'note' | 'folder'
id: string
x: number
y: number
} | null>(null)
+ const [draggingNoteId, setDraggingNoteId] = useState(null)
- const editInputRef = useRef(null)
- const newFolderRef = useRef(null)
+ const sensors = useSensors(
+ useSensor(MouseSensor, { activationConstraint: { distance: 6 } }),
+ useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 5 } }),
+ )
+
+ const editInputRef = useRef(null)
+ const noteEditRef = useRef(null)
+ const newFolderRef = useRef(null)
const displayed = filteredNotes()
useEffect(() => {
if (editingFolderId && editInputRef.current) editInputRef.current.focus()
}, [editingFolderId])
+ useEffect(() => {
+ if (editingNoteId && noteEditRef.current) noteEditRef.current.focus()
+ }, [editingNoteId])
+
useEffect(() => {
if (creatingFolder && newFolderRef.current) newFolderRef.current.focus()
}, [creatingFolder])
useEffect(() => {
- const close = () => setContextMenu(null)
+ const close = () => { setContextMenu(null); setSortMenuOpen(false) }
window.addEventListener('click', close)
return () => window.removeEventListener('click', close)
}, [])
@@ -60,6 +82,11 @@ export function Sidebar() {
setEditingFolderId(null)
}
+ const handleRenameNote = async (id: string) => {
+ await updateNote(id, { title: noteEditValue.trim() || '无标题笔记' })
+ setEditingNoteId(null)
+ }
+
const handleCreateFolder = async () => {
if (!newFolderInput.trim()) { setCreatingFolder(false); return }
await createFolder(newFolderInput.trim())
@@ -74,68 +101,27 @@ export function Sidebar() {
await createNote(folderId)
}
- const rootFolders = folders.filter(f => f.parentId === null)
+ const handleDragStart = (event: DragStartEvent) => {
+ setDraggingNoteId(event.active.id as string)
+ }
- return (
-