From 8725108195067836749c58728b60133f35a49497 Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Fri, 27 Feb 2026 15:58:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复便签保存后无法编辑的问题 - 移除 handleSave 中对 updateNotesContent 的调用 - 避免触发 notes useEffect 导致 content 被重置 2. 修复提醒顺延后列表不刷新 - 在 handlePostpone 函数末尾添加 fetchEvents() 3. 优化提醒完成状态切换的错误处理 - stores 会在更新失败时自动回滚数据 4. 优化便签保存状态显示 - 添加 hasUnsavedChanges 状态 - 区分"未保存"、"保存中"、"已保存"三种状态 5. 修复列表底部填充问题 - 纪念日列表和提醒列表在内容刚好一屏时 - 添加基础填充避免被 AI 输入框遮挡 Co-Authored-By: Claude Opus 4.6 --- .../anniversary/AnniversaryList.tsx | 8 +++- src/components/note/NoteEditor.tsx | 48 +++++++++++++------ src/components/reminder/ReminderList.tsx | 8 +++- src/pages/HomePage.tsx | 4 +- src/services/api.ts | 7 ++- 5 files changed, 54 insertions(+), 21 deletions(-) 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;