Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-16-19-28-04.md

142 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现方案 — 2026-04-16-19-28-04
## 根因分析
当前 `ReportEditor.tsx` 的自动保存机制过度依赖两个 `useRef``stateRef``contentRef`)作为"数据快照"
1. **用户操作时**:各事件处理器先更新 React state再手动同步 `stateRef.current`,然后调用 `saveDraftToStorage()` 写入 localStorage。
2. **组件卸载时**`useEffect` 的 cleanup 调用 `save()`,同样读取 `stateRef.current``contentRef.current` 来保存 draft。
这个机制存在致命缺陷:
- **React 18 `StrictMode`** 在开发/预览环境下会"挂载 → 立即卸载 → 重新挂载"。在首次模拟卸载时,`stateRef.current` 还是组件创建时的初始空值(`videos: []``capturedFrames: []``reportData: 默认值`。cleanup 中的 `save()` 会用这个空值 **覆盖** localStorage 里已经存在的正确 draft。
- 即使不在 StrictMode 下,只要任何恢复路径或用户操作路径遗漏了 `stateRef.current` 的同步,卸载保存时就会丢失数据。
此前两次修复只把 `stateRef.current` 同步移到了更多恢复分支中,但**没有从根本上消除对 `stateRef` 的依赖**,因此问题依旧。
## 修改文件清单
| 文件 | 修改类型 | 说明 |
|------|---------|------|
| `src/pages/ReportEditor.tsx` | 修改 | 重构 `saveDraftToStorage` + 自动保存 effect + `useLayoutEffect` 依赖 |
## 具体代码变更
### 变更 1重构 `saveDraftToStorage`
**当前实现(依赖 ref**
```tsx
const saveDraftToStorage = React.useCallback(() => {
const user = storage.get<User | null>('currentUser', null);
const key = user ? `reportEditorDraft_${user.username}` : '';
if (key) {
storage.set(key, {
content: contentRef.current,
draftReportId: reportId || null,
...stateRef.current
});
}
}, [reportId]);
```
**修改为(直接从最新 state 和 DOM 读取):**
```tsx
const saveDraftToStorage = React.useCallback(() => {
const user = storage.get<User | null>('currentUser', null);
const key = user ? `reportEditorDraft_${user.username}` : '';
if (key) {
storage.set(key, {
content: editorRef.current?.innerHTML || '',
draftReportId: reportId || null,
reportData,
videos,
capturedFrames,
activeTab,
loadedTemplateId
});
}
}, [reportData, videos, capturedFrames, activeTab, loadedTemplateId, reportId]);
```
**效果**
- `saveDraftToStorage` 的闭包永远绑定当前渲染周期的最新 state不再需要 `stateRef` 作为中转。
- 编辑器内容直接从 `editorRef.current?.innerHTML` 读取,不再依赖可能滞后的 `contentRef`
- 用户操作中所有调用 `saveDraftToStorage()` 的地方(表单 onChange、上传视频、截图等会自动生效。
### 变更 2重构自动保存 effect
**当前实现:**
```tsx
useEffect(() => {
const save = () => { ... };
window.addEventListener('beforeunload', save);
document.addEventListener('visibilitychange', save);
return () => {
window.removeEventListener('beforeunload', save);
document.removeEventListener('visibilitychange', save);
save();
};
}, [reportId]);
```
**修改为:**
```tsx
useEffect(() => {
const handleBeforeUnload = () => saveDraftToStorage();
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') saveDraftToStorage();
};
window.addEventListener('beforeunload', handleBeforeUnload);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('visibilitychange', handleVisibilityChange);
saveDraftToStorage();
};
}, [saveDraftToStorage]);
```
**效果**
- 由于 `saveDraftToStorage` 的 dependency 包含了所有关键 state每次 state 变化后 effect 都会重新注册,但最重要的是 **cleanup 中调用的 `saveDraftToStorage` 永远指向最新的闭包**,不会因为 ref 滞后而用空值覆盖 draft。
### 变更 3给 `useLayoutEffect` 安全网添加 `[]` 依赖
**当前:**
```tsx
React.useLayoutEffect(() => {
if (contentLoadedRef.current || !editorRef.current) return;
// ... 恢复逻辑 ...
});
```
**修改为:**
```tsx
React.useLayoutEffect(() => {
if (contentLoadedRef.current || !editorRef.current) return;
// ... 恢复逻辑 ...
}, []);
```
**效果**
- 避免在每次渲染后重复执行安全网,防止潜在的意外覆盖或性能损耗。
- 保留其原有功能:仅在组件挂载时,如果 `useEffect` 初始化因 ref 未 ready 未能恢复内容,则作为兜底恢复编辑器 DOM。
### 变更 4可选但建议移除对 `contentRef` 的强依赖
`contentRef` 在旧代码中用于 draft 保存。修改 `saveDraftToStorage` 后直接读取 `editorRef.current?.innerHTML``contentRef` 仍可保留供其他旧代码使用,不影响功能。
## 风险点
| 风险 | 级别 | 应对措施 |
|------|------|---------|
| `saveDraftToStorage` dependency 数组较长,可能导致 effect 频繁重新注册 | 低 | 重新注册事件监听器的开销极小,远小于 localStorage 写入本身 |
| `editorRef.current?.innerHTML` 在卸载时读取可能拿到不完整 DOM | 极低 | `editorRef` 指向的 DOM 在 cleanup 执行时尚未被 React 移除,内容完整 |
| `useLayoutEffect` 添加 `[]` 后闭包值陈旧 | 低 | `useLayoutEffect` 内部仅读取 `reportId`(来自 URL在生命周期内不变和 localStorage不受影响 |
## 回滚策略
本次修改仅重构保存函数的实现方式,不改变数据结构或存储 key。如出现异常可直接 `git revert` 回滚。
---
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。**