diff --git a/src/components/note/NoteEditor.tsx b/src/components/note/NoteEditor.tsx index 0c87ed0..7f01c5c 100644 --- a/src/components/note/NoteEditor.tsx +++ b/src/components/note/NoteEditor.tsx @@ -1,25 +1,85 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { Paper, - Textarea, Group, Text, Stack, - Button, Box, } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import rehypeSanitize from 'rehype-sanitize'; -import { IconDeviceFloppy } from '@tabler/icons-react'; import { useAppStore } from '../../stores'; interface NoteEditorProps { onSave?: () => void; } -type ViewMode = 'edit' | 'preview'; +// Simple Markdown to HTML converter +function parseMarkdown(text: string): string { + if (!text) return ''; + + let html = text; + + // Escape HTML first (but preserve our line breaks) + html = html.replace(/&/g, '&').replace(//g, '>'); + + // Code blocks (must be before inline code) + html = html.replace(/```([\s\S]*?)```/g, '
$1');
+
+ // Inline code
+ html = html.replace(/`([^`]+)`/g, '$1');
+
+ // Headers
+ html = html.replace(/^######\s+(.*)$/gm, '$1'); + // Wrap consecutive blockquotes + html = html.replace(/(
.*<\/blockquote>\n?)+/g, (match) => match); + + // Horizontal rule + html = html.replace(/^---$/gm, '
'); + + // Line breaks (convert \n to
) + html = html.replace(/\n/g, '
'); + + return html; +} export function NoteEditor({ onSave }: NoteEditorProps) { const notes = useAppStore((state) => state.notes); @@ -28,255 +88,141 @@ export function NoteEditor({ onSave }: NoteEditorProps) { const [content, setContent] = useState(''); const [saving, setSaving] = useState(false); - const [lastSaved, setLastSaved] = useState(null); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [viewMode] = useState ('edit'); + const [lastSaved, setLastSaved] = useState (null); + const [isEditing, setIsEditing] = useState(false); + const [now, setNow] = useState(Date.now()); + const textareaRef = useRef (null); // Initialize content from notes useEffect(() => { - if (notes) { + if (notes?.content) { setContent(notes.content); - setHasUnsavedChanges(false); } }, [notes]); - // Track unsaved changes + // Timer to update time display every second useEffect(() => { - if (notes && content !== notes.content) { - setHasUnsavedChanges(true); - } - }, [content, notes]); + const interval = setInterval(() => { + setNow(Date.now()); + }, 1000); + return () => clearInterval(interval); + }, []); // Fetch notes on mount useEffect(() => { fetchNotes(); }, [fetchNotes]); - // Auto-save with 3 second debounce - const [debouncedContent] = useDebouncedValue(content, 3000); + // Debounce content for auto-save + const [debouncedContent] = useDebouncedValue(content, 2000); - // 滚动条样式 - 仅在悬停时显示 - const scrollbarStyle = ` - .note-scroll::-webkit-scrollbar { - width: 4px; - height: 4px; - } - .note-scroll::-webkit-scrollbar-track { - background: transparent; - } - .note-scroll::-webkit-scrollbar-thumb { - background: transparent; - border-radius: 2px; - } - .note-scroll:hover::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.15); - } - `; + // Save function + const handleSave = useCallback(async (value: string) => { + if (!value) return; + if (saving) return; - const handleSave = useCallback( - async (value: string) => { - // 即使 notes 为 null,saveNotes 也会自动创建便签 - if (!value) return; - // 防止重复保存 - if (saving) return; + setSaving(true); + try { + await saveNotes(value); + setLastSaved(Date.now()); + onSave?.(); + } catch (error) { + console.error('Failed to save note:', error); + } finally { + setSaving(false); + } + }, [saving, saveNotes, onSave]); - setSaving(true); - try { - // 注意:不要在这里调用 updateNotesContent - // 因为 saveNotes 会在成功后更新 store 中的 notes - // 如果这里先更新,会触发 notes useEffect,导致 content 被重置 - await saveNotes(value); - setLastSaved(new Date()); - setHasUnsavedChanges(false); - onSave?.(); - } catch (error) { - console.error('Failed to save note:', error); - } finally { - setSaving(false); - } - }, - [saving, saveNotes, onSave] - ); - - // Auto-save with debounce + // Auto-save when debounced content changes useEffect(() => { - // 即使 notes 为 null 也可以自动保存(会创建新便签) - if (debouncedContent !== undefined && debouncedContent !== content) { + if (debouncedContent && debouncedContent !== notes?.content) { handleSave(debouncedContent); } - }, [debouncedContent, content, handleSave]); + }, [debouncedContent, handleSave, notes?.content]); + // Format saved time const formatLastSaved = () => { - if (saving) return '保存中...'; - if (hasUnsavedChanges && !lastSaved) return '未保存'; + if (saving) return '保存中'; if (!lastSaved) return ''; - const now = new Date(); - const diff = now.getTime() - lastSaved.getTime(); - if (diff < 1000) return '已保存'; - if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`; - return lastSaved.toLocaleTimeString('zh-CN'); + const diff = now - lastSaved; + if (diff < 60000) return `${Math.floor(diff / 1000)}秒前`; + return new Date(lastSaved).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); }; - const handleManualSave = () => { - if (content) { + // Enter edit mode + const handleClick = () => { + setIsEditing(true); + setTimeout(() => textareaRef.current?.focus(), 50); + }; + + // Exit edit mode and save + const handleBlur = () => { + setIsEditing(false); + // Blur 后立即保存 + if (content !== notes?.content) { handleSave(content); } }; - const scrollContainerRef = useRef (null); - - // 处理滚轮事件,实现列表独立滚动 - const handleWheel = (e: React.WheelEvent) => { - const container = scrollContainerRef.current; - if (!container) return; - - const { scrollTop, scrollHeight, clientHeight } = container; - // 使用 1px 缓冲避免浮点数精度问题 - const isAtTop = scrollTop <= 0; - const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; - - // 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡 - if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) { - e.stopPropagation(); - } - // 如果在滚动范围内,允许事件继续传递以实现正常滚动 - }; - return ( - - - + + {/* Header */} - - {/* Editor/Preview Area */} + {/* Content Area */}- -- 便签 - -- } - onClick={handleManualSave} - loading={saving} - style={{ - borderColor: '#ccc', - color: '#1a1a1a', - borderRadius: 2, - }} - > - 保存 - - +- {formatLastSaved()} - -便签 +{formatLastSaved()} - {viewMode === 'edit' && ( -