# 实现方案 — 2026-04-16-20-24-11 ## 根因分析 当前问题由两个核心缺陷共同导致: ### 1. `saveDraftToStorage` 的闭包陷阱 + DOM 引用失效 此前为修复 `stateRef` 不同步的问题,将 `saveDraftToStorage` 重构为直接从 React state 读取: ```tsx const saveDraftToStorage = React.useCallback(() => { storage.set(key, { content: editorRef.current?.innerHTML || '', reportData, videos, capturedFrames, ... }); }, [reportData, videos, capturedFrames, ...]); ``` 这引入了新的问题: - **闭包陷阱**:用户操作后常见写法是 `setCapturedFrames(nextFrames); saveDraftToStorage();`。由于 `setState` 异步,`saveDraftToStorage` 闭包中读取到的 `capturedFrames` 仍是旧值(空数组),导致旧值覆盖 localStorage。 - **卸载时 DOM 失效**:组件卸载时 React 开始销毁 DOM,`editorRef.current` 可能已经变为 `null` 或其 `innerHTML` 已为空,导致 `content: editorRef.current?.innerHTML || ''` 保存了空字符串,覆盖了已有的报告内容。 ### 2. `contentRef` 更新遗漏 代码中部分修改编辑器 DOM 的路径没有同步更新 `contentRef.current`。例如 `handleEditorClick` 中通过 `document.execCommand('delete')` 删除 placeholder 后,直接调用了 `saveDraftToStorage()`,但没有先更新 `contentRef`。 ## 修改文件清单 | 文件 | 修改类型 | 说明 | |------|---------|------| | `src/pages/ReportEditor.tsx` | 修改 | 重构 `saveDraftToStorage` + 补齐 `contentRef` 遗漏点 | ## 具体代码变更 ### 变更 1:重构 `saveDraftToStorage` 从 Ref 读取 **修改为:** ```tsx const saveDraftToStorage = React.useCallback(() => { const user = storage.get('currentUser', null); const key = user ? `reportEditorDraft_${user.username}` : ''; if (key) { const currentContent = contentRef.current || editorRef.current?.innerHTML || ''; storage.set(key, { content: currentContent, draftReportId: reportId || null, reportData: stateRef.current.reportData, videos: stateRef.current.videos, capturedFrames: stateRef.current.capturedFrames, activeTab: stateRef.current.activeTab, loadedTemplateId: stateRef.current.loadedTemplateId }); } }, [reportId]); ``` **效果**: - 数据源回归 `stateRef` 和 `contentRef`,彻底摆脱 React state 的闭包陷阱。 - `content` 优先读取 `contentRef.current`(在内存中稳定存在),回退到 `editorRef.current?.innerHTML`(兼容某些未更新 contentRef 的旧路径),兜底为空字符串。即使组件卸载时 DOM 已被销毁,`contentRef.current` 仍保存着最新的编辑器 HTML。 ### 变更 2:补齐 `contentRef` 更新遗漏 在 `handleEditorClick` 的 `document.execCommand('delete')` 分支中,增加 `contentRef.current` 的同步: **当前代码(约第 296-304 行):** ```tsx } else { const range = document.createRange(); range.selectNode(placeholder); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('delete'); saveDraftToStorage(); } ``` **修改为:** ```tsx } else { const range = document.createRange(); range.selectNode(placeholder); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('delete'); if (editorRef.current) contentRef.current = editorRef.current.innerHTML; saveDraftToStorage(); } ``` ### 变更 3:确保自动保存 effect 绑定最新 `saveDraftToStorage` 当前自动保存 effect 已经绑定 `[saveDraftToStorage]`,由于 `saveDraftToStorage` 的 dependency 现在只有 `[reportId]`,effect 不会因为 state 变化而频繁重新注册,但 cleanup 中仍然指向最新的保存函数。 ### 变更 4:初始化恢复时 `contentRef` 同步(已有,确认无误) 在 `useEffect` 和 `useLayoutEffect` 的各恢复分支中,设置 `editorRef.current.innerHTML = draft.content`(或 `found.content`)时,代码已经同步设置了 `contentRef.current = ...`,这部分无需修改。 ## 风险点 | 风险 | 级别 | 应对措施 | |------|------|---------| | 仍有未被发现的 `contentRef` 更新遗漏点 | 低 | 已全面搜索 `innerHTML` 修改点和 `saveDraftToStorage` 调用点,仅发现 1 处遗漏 | | `stateRef` 在某些新功能中再次不同步 | 低 | `saveDraftToStorage` 从 `stateRef` 读取,后续新增功能只要保持「setState 后立即同步 stateRef」的习惯即可 | ## 回滚策略 本次修改仅调整 `saveDraftToStorage` 的数据源和补齐一处 `contentRef` 更新,不改变数据结构和接口。如出现异常,可直接 `git revert` 回滚。 --- **⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。**