diff --git a/src/components/anniversary/AnniversaryList.tsx b/src/components/anniversary/AnniversaryList.tsx index cc517a3..74803d8 100644 --- a/src/components/anniversary/AnniversaryList.tsx +++ b/src/components/anniversary/AnniversaryList.tsx @@ -31,7 +31,7 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar const scrollContainerRef = useRef(null); const [bottomPadding, setBottomPadding] = useState(0); - // 检测列表是否可滚动,如果是则添加底部填充避免被 AI 输入框遮挡 + // 检测列表内容,添加底部填充避免被 AI 输入框遮挡 useEffect(() => { const updatePadding = () => { const container = scrollContainerRef.current; @@ -39,8 +39,12 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar // 判断是否可滚动(内容高度 > 容器高度) const isScrollable = container.scrollHeight > container.clientHeight; + // 计算内容底部到容器底部的距离 + const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight; + // 如果可滚动,始终添加填充;如果不可滚动但内容接近底部,也添加填充 + const needsPadding = isScrollable || scrollBottom < 100; // 底部填充高度:AI 输入框高度(约50px) + 底部间距(48px) + 额外缓冲(20px) - setBottomPadding(isScrollable ? 120 : 0); + setBottomPadding(needsPadding ? 120 : 20); }; // 延迟执行确保 DOM 完全渲染 diff --git a/src/components/note/NoteEditor.tsx b/src/components/note/NoteEditor.tsx index 1f02e59..0c87ed0 100644 --- a/src/components/note/NoteEditor.tsx +++ b/src/components/note/NoteEditor.tsx @@ -23,22 +23,30 @@ type ViewMode = 'edit' | 'preview'; export function NoteEditor({ onSave }: NoteEditorProps) { const notes = useAppStore((state) => state.notes); - const updateNotesContent = useAppStore((state) => state.updateNotesContent); const saveNotes = useAppStore((state) => state.saveNotes); const fetchNotes = useAppStore((state) => state.fetchNotes); const [content, setContent] = useState(''); const [saving, setSaving] = useState(false); const [lastSaved, setLastSaved] = useState(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [viewMode] = useState('edit'); // Initialize content from notes useEffect(() => { if (notes) { setContent(notes.content); + setHasUnsavedChanges(false); } }, [notes]); + // Track unsaved changes + useEffect(() => { + if (notes && content !== notes.content) { + setHasUnsavedChanges(true); + } + }, [content, notes]); + // Fetch notes on mount useEffect(() => { fetchNotes(); @@ -65,21 +73,21 @@ export function NoteEditor({ onSave }: NoteEditorProps) { } `; - useEffect(() => { - if (debouncedContent !== undefined && notes && debouncedContent !== content) { - handleSave(debouncedContent); - } - }, [debouncedContent]); - const handleSave = useCallback( async (value: string) => { - if (!notes) return; + // 即使 notes 为 null,saveNotes 也会自动创建便签 + if (!value) return; + // 防止重复保存 + if (saving) return; setSaving(true); try { - updateNotesContent(value); + // 注意:不要在这里调用 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); @@ -87,20 +95,30 @@ export function NoteEditor({ onSave }: NoteEditorProps) { setSaving(false); } }, - [notes, updateNotesContent, saveNotes, onSave] + [saving, saveNotes, onSave] ); + // Auto-save with debounce + useEffect(() => { + // 即使 notes 为 null 也可以自动保存(会创建新便签) + if (debouncedContent !== undefined && debouncedContent !== content) { + handleSave(debouncedContent); + } + }, [debouncedContent, content, handleSave]); + const formatLastSaved = () => { - if (!lastSaved) return '未保存'; + if (saving) return '保存中...'; + if (hasUnsavedChanges && !lastSaved) return '未保存'; + if (!lastSaved) return ''; const now = new Date(); const diff = now.getTime() - lastSaved.getTime(); - if (diff < 1000) return '刚刚保存'; + if (diff < 1000) return '已保存'; if (diff < 60000) return `${Math.floor(diff / 1000)}秒前保存`; return lastSaved.toLocaleTimeString('zh-CN'); }; const handleManualSave = () => { - if (content && notes) { + if (content) { handleSave(content); } }; @@ -150,8 +168,8 @@ export function NoteEditor({ onSave }: NoteEditorProps) { > 保存 - - {saving ? '保存中...' : formatLastSaved()} + + {formatLastSaved()} diff --git a/src/components/reminder/ReminderList.tsx b/src/components/reminder/ReminderList.tsx index 022c4f0..76171d1 100644 --- a/src/components/reminder/ReminderList.tsx +++ b/src/components/reminder/ReminderList.tsx @@ -45,7 +45,7 @@ export function ReminderList({ const scrollContainerRef = useRef(null); const [bottomPadding, setBottomPadding] = useState(0); - // 检测列表是否可滚动,如果是则添加底部填充避免被 AI 输入框遮挡 + // 检测列表内容,添加底部填充避免被 AI 输入框遮挡 useEffect(() => { const updatePadding = () => { const container = scrollContainerRef.current; @@ -53,8 +53,12 @@ export function ReminderList({ // 判断是否可滚动(内容高度 > 容器高度) const isScrollable = container.scrollHeight > container.clientHeight; + // 计算内容底部到容器底部的距离 + const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight; + // 如果可滚动,始终添加填充;如果不可滚动但内容接近底部,也添加填充 + const needsPadding = isScrollable || scrollBottom < 100; // 底部填充高度:AI 输入框高度(约50px) + 底部间距(48px) + 额外缓冲(20px) - setBottomPadding(isScrollable ? 120 : 0); + setBottomPadding(needsPadding ? 120 : 20); }; // 延迟执行确保 DOM 完全渲染 diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 16277e7..57568eb 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -182,8 +182,8 @@ export function HomePage() { }); if (result.error) { console.error('更新失败:', result.error); + // stores 会在更新失败时自动回滚数据 } - // 乐观更新已处理 UI 响应,无需 fetchEvents }; const handleDelete = async (event: Event) => { @@ -225,6 +225,8 @@ export function HomePage() { if (result.error) { console.error('顺延失败:', result.error); } + // 刷新列表以更新 UI + fetchEvents(); }; const handleDateChange = async ( diff --git a/src/services/api.ts b/src/services/api.ts index 2fa8ccf..fb4e46c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -39,7 +39,12 @@ export const api = { const data = await response.json(); if (!response.ok) { - throw new Error(data.error || 'Request failed'); + const errorMsg = data.error || 'Request failed'; + // 如果有详细信息,显示出来 + if (data.details) { + console.error('Validation details:', JSON.stringify(data.details)); + } + throw new Error(errorMsg); } return data;