133 lines
5.5 KiB
Markdown
133 lines
5.5 KiB
Markdown
# 实现方案 — 2026-04-16-19-06-18
|
||
|
||
## 根因分析
|
||
|
||
### 问题 1:路由切换后所有内容丢失
|
||
`ReportEditor.tsx` 的初始化 `useEffect` 在从 draft 恢复数据时,将 `stateRef.current` 的同步代码放在了 `if (editorRef.current && draft.content.trim().length > 0)` 条件块的**内部**。
|
||
|
||
这导致当以下任一情况发生时,`stateRef.current` 不会被同步:
|
||
- `editorRef.current` 在 useEffect 执行时尚未挂载(组件首次渲染时常见);
|
||
- `draft.content` 为空字符串或仅包含空白(新建报告时常见)。
|
||
|
||
当 `stateRef.current` 保持为初始值(空的 `reportData`、空的 `videos`、空的 `capturedFrames`)时,用户离开页面触发的 `saveDraftToStorage()` 会**用这些空值覆盖 localStorage 中的 draft**。再次返回 `/report-editor` 时,系统读取了这个被清空的 draft,导致所有内容全部丢失。
|
||
|
||
### 问题 2:自动帧插入 UI 批量刷新
|
||
`autoCaptureFrames` 是一个 `async` 函数,内部通过 `for` 循环逐帧处理。循环中每次摘取到新帧后都会调用 `setCapturedFrames(accumulatedFrames)`,但由于 React 18 的**自动批处理**机制,在异步函数中连续调用的状态更新会被合并,DOM 重渲染被推迟到整个循环结束后才执行一次。因此用户看不到逐帧实时摘取的过程,只有等全部帧处理完后,关键帧列表和 placeholder 中的图片才会一次性批量出现。
|
||
|
||
## 修改文件清单
|
||
|
||
| 文件 | 修改类型 | 说明 |
|
||
|------|---------|------|
|
||
| `src/pages/ReportEditor.tsx` | 修改 | 移动 stateRef 同步位置 + 引入 `flushSync` |
|
||
|
||
## 具体代码变更
|
||
|
||
### 变更 1:useEffect — draft 恢复已有报告(约第 123 行区域)
|
||
|
||
**当前代码(有问题的结构):**
|
||
```tsx
|
||
if (editorRef.current && typeof draft.content === 'string' && draft.content.trim().length > 0) {
|
||
editorRef.current.innerHTML = draft.content;
|
||
// ...
|
||
stateRef.current = { ...stateRef.current, reportData: draft.reportData, ... };
|
||
}
|
||
```
|
||
|
||
**修改为:**
|
||
将 `stateRef.current` 的赋值提取到 `if (editorRef.current && ...)` 的**外部**,紧接在 `setCapturedFrames` 之后:
|
||
```tsx
|
||
if (draft.activeTab) setActiveTab(draft.activeTab);
|
||
stateRef.current = {
|
||
...stateRef.current,
|
||
reportData: draft.reportData,
|
||
videos: draft.videos,
|
||
capturedFrames: draft.capturedFrames,
|
||
loadedTemplateId: draft.loadedTemplateId || ''
|
||
};
|
||
if (editorRef.current && typeof draft.content === 'string' && draft.content.trim().length > 0) {
|
||
editorRef.current.innerHTML = draft.content;
|
||
contentRef.current = draft.content;
|
||
contentLoadedRef.current = true;
|
||
setLoadedTemplateId(draft.loadedTemplateId || '');
|
||
setTimeout(() => updatePageHeight(), 0);
|
||
}
|
||
```
|
||
|
||
### 变更 2:useEffect — found 恢复已有报告(约第 141 行区域)
|
||
|
||
**修改为:**
|
||
将 `stateRef.current` 的赋值提取到 `if (editorRef.current)` 的**外部**,紧接在 `setReportData(found)` 之后:
|
||
```tsx
|
||
setReportData(found);
|
||
stateRef.current = {
|
||
...stateRef.current,
|
||
reportData: found,
|
||
videos: found.videos || [],
|
||
capturedFrames: found.capturedFrames || []
|
||
};
|
||
if (editorRef.current) {
|
||
// ... 恢复 editor content ...
|
||
}
|
||
```
|
||
|
||
同时顺手清理重复赋值 `contentRef.current = found.content;`(第 149-150 行重复)。
|
||
|
||
### 变更 3:useEffect — draft 恢复新建报告(约第 184 行区域)
|
||
|
||
**修改为:**
|
||
与变更 1 类似,将 `stateRef.current` 的赋值提取到 `if (editorRef.current && ...)` 的**外部**:
|
||
```tsx
|
||
if (draft.activeTab) setActiveTab(draft.activeTab);
|
||
stateRef.current = {
|
||
...stateRef.current,
|
||
reportData: draft.reportData,
|
||
videos: draft.videos,
|
||
capturedFrames: draft.capturedFrames,
|
||
loadedTemplateId: draft.loadedTemplateId || ''
|
||
};
|
||
if (editorRef.current && typeof draft.content === 'string' && draft.content.trim().length > 0) {
|
||
// ... 恢复 editor content ...
|
||
}
|
||
```
|
||
|
||
### 变更 4:autoCaptureFrames 引入 flushSync(约第 519-521 行区域)
|
||
|
||
在文件顶部增加导入:
|
||
```tsx
|
||
import { flushSync } from 'react-dom';
|
||
```
|
||
|
||
在 `autoCaptureFrames` 的 for 循环中,每次更新 `capturedFrames` 时使用 `flushSync` 强制同步渲染:
|
||
|
||
**当前代码:**
|
||
```tsx
|
||
setCapturedFrames(accumulatedFrames);
|
||
stateRef.current = { ...stateRef.current, capturedFrames: accumulatedFrames };
|
||
```
|
||
|
||
**修改为:**
|
||
```tsx
|
||
flushSync(() => {
|
||
setCapturedFrames(accumulatedFrames);
|
||
});
|
||
stateRef.current = { ...stateRef.current, capturedFrames: accumulatedFrames };
|
||
```
|
||
|
||
这样每一帧被摘取后都会立即触发 DOM 更新,用户可以在右侧「视频分析」面板中实时看到关键帧逐张出现,同时自动插入逻辑也能按配置延迟后逐张填充 placeholder。
|
||
|
||
## 风险点
|
||
|
||
| 风险 | 级别 | 应对措施 |
|
||
|------|------|---------|
|
||
| `flushSync` 在循环中可能导致轻微渲染卡顿 | 低 | 关键帧数量通常仅 10~20 张,现代浏览器完全可以承受;已在 async 函数中使用,不会阻塞视频 seek 事件 |
|
||
| 移动 `stateRef` 同步位置可能与其他逻辑产生时序影响 | 低 | 仅将同步从 `if` 内部移到外部,逻辑语义完全一致,只是确保在所有路径下都被执行 |
|
||
| `found` 分支中 `contentRef.current` 重复赋值 | 极低 | 顺手清理,不影响行为 |
|
||
|
||
## 回滚策略
|
||
|
||
本次修改仅调整代码执行顺序并引入一个 React 标准 API(`flushSync`),未改变数据结构或接口。如出现异常,可直接 `git revert` 回滚。
|
||
|
||
---
|
||
|
||
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将进入测试方案编写阶段。**
|