117 lines
4.8 KiB
Markdown
117 lines
4.8 KiB
Markdown
# 实现方案 — 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<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 行):**
|
||
```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` 回滚。
|
||
|
||
---
|
||
|
||
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。**
|