4.8 KiB
实现方案 — 2026-04-16-20-24-11
根因分析
当前问题由两个核心缺陷共同导致:
1. saveDraftToStorage 的闭包陷阱 + DOM 引用失效
此前为修复 stateRef 不同步的问题,将 saveDraftToStorage 重构为直接从 React state 读取:
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 读取
修改为:
const saveDraftToStorage = React.useCallback(() => {
const user = storage.get<User | null>('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 行):
} else {
const range = document.createRange();
range.selectNode(placeholder);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('delete');
saveDraftToStorage();
}
修改为:
} 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 回滚。
⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。