Files
Mdeical_Sur_Report/过往经验/实现方案-2026-04-16-20-24-11.md

4.8 KiB
Raw Blame History

实现方案 — 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 开始销毁 DOMeditorRef.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]);

效果

  • 数据源回归 stateRefcontentRef,彻底摆脱 React state 的闭包陷阱。
  • content 优先读取 contentRef.current(在内存中稳定存在),回退到 editorRef.current?.innerHTML(兼容某些未更新 contentRef 的旧路径),兜底为空字符串。即使组件卸载时 DOM 已被销毁,contentRef.current 仍保存着最新的编辑器 HTML。

变更 2补齐 contentRef 更新遗漏

handleEditorClickdocument.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 同步(已有,确认无误)

useEffectuseLayoutEffect 的各恢复分支中,设置 editorRef.current.innerHTML = draft.content(或 found.content)时,代码已经同步设置了 contentRef.current = ...,这部分无需修改。

风险点

风险 级别 应对措施
仍有未被发现的 contentRef 更新遗漏点 已全面搜索 innerHTML 修改点和 saveDraftToStorage 调用点,仅发现 1 处遗漏
stateRef 在某些新功能中再次不同步 saveDraftToStoragestateRef 读取后续新增功能只要保持「setState 后立即同步 stateRef」的习惯即可

回滚策略

本次修改仅调整 saveDraftToStorage 的数据源和补齐一处 contentRef 更新,不改变数据结构和接口。如出现异常,可直接 git revert 回滚。


⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。