# 实现方案 — 2026-04-16-17-40-20
## 技术思路
本次需求包含三个相互关联的修复/优化点:
1. **自动抽帧图片在页面切换后丢失**:根因在于 `saveDraftToStorage` 依赖的 `stateRef.current` 在 `setCapturedFrames` 异步更新后未及时同步,导致组件卸载前保存的 draft 中缺失自动帧。
2. **编辑后切换界面模板选择器显示"无"**:根因是 `loadedTemplateId` 未纳入 `stateRef.current`,且 `saveDraftToStorage` 因 stale closure 捕获了过时的 `loadedTemplateId` 值(空字符串)。
3. **自动帧插入开关**:在 `SystemSettings` 中新增 `autoInsertFrames` 布尔开关,并在 `autoCaptureFrames` 中支持自动将新生成的帧填充到第一个空置占位符。
---
## 修改文件清单
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `src/types.ts` | 修改 | `SystemSettings` 增加 `autoInsertFrames?: boolean` |
| `src/pages/SystemSettings.tsx` | 修改 | 新增"开启自动帧插入"开关及抽帧位置对勾标记 |
| `src/pages/ReportEditor.tsx` | 修改 | 修复 `stateRef` 同步逻辑、自动帧插入实现 |
---
## 关键代码变更说明
### 1. `src/types.ts` — 扩展类型
```tsx
export interface SystemSettings {
frameCount: number;
framePositions: number[];
apiEndpoint: string;
apiKey: string;
defaultTemplate?: string;
frameMode?: 'uniform' | 'keep';
autoInsertFrames?: boolean; // 新增
}
```
### 2. `src/pages/SystemSettings.tsx` — 新增开关与 UI 标记
在"视频抽帧配置"区域(`framePositions` 列表上方)增加开关:
```tsx
setSettings({ ...settings, autoInsertFrames: e.target.checked })}
className="w-4 h-4 accent-accent cursor-pointer"
/>
开启后,视频自动抽帧的每一帧将按顺序自动插入到报告的空置图片占位符中,插满后不再提示。
```
在 `settings.framePositions` 的每个 input 框中,当 `autoInsertFrames` 开启时显示绿色对勾(`Check` 图标):
```tsx
{settings.autoInsertFrames && (
)}
```
注意:需确保 `Check` 图标已从 `lucide-react` 导入(当前页面已导入)。
### 3. `src/pages/ReportEditor.tsx` — 修复状态同步与自动插入
#### 3.1 将 `loadedTemplateId` 纳入 `stateRef`
修改第 72 行附近的 ref 定义:
```tsx
const stateRef = useRef({ reportData, videos, capturedFrames, activeTab, loadedTemplateId });
// ...
stateRef.current = { reportData, videos, capturedFrames, activeTab, loadedTemplateId };
```
#### 3.2 精简 `saveDraftToStorage`
移除独立的 `loadedTemplateId` 字段(已由 `stateRef.current` 包含),并加入 `syncStateToRef`:
```tsx
const saveDraftToStorage = React.useCallback(() => {
stateRef.current = { reportData, videos, capturedFrames, activeTab, loadedTemplateId };
const user = storage.get('currentUser', null);
const key = user ? `reportEditorDraft_${user.username}` : '';
if (key) {
storage.set(key, {
content: contentRef.current,
draftReportId: reportId || null,
...stateRef.current
});
}
}, [reportId, loadedTemplateId]);
```
> 保留 `loadedTemplateId` 在依赖数组中无实际副作用,但可确保 lint 不报警。
#### 3.3 修复 `autoCaptureFrames` 中 `capturedFrames` 不同步问题
在 `autoCaptureFrames` 函数中,生成所有帧后、调用 `saveDraftToStorage` 前,手动将最新帧列表同步到 `stateRef.current`:
```tsx
const nextFrames = [...capturedFrames, ...newFrames].sort((a, b) => a.time - b.time);
setCapturedFrames(nextFrames);
stateRef.current = { ...stateRef.current, capturedFrames: nextFrames };
if (wasPlaying) video.play();
saveDraftToStorage();
```
同时,若开启了 `autoInsertFrames`,在循环中每生成一帧立即尝试插入第一个空置占位符(不触发 save):
```tsx
for (const pos of positions) {
... // 生成 frame
newFrames.push(newFrame);
if (settings.autoInsertFrames && editorRef.current) {
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
if (emptyPlaceholder) {
emptyPlaceholder.innerHTML = `
×
`;
emptyPlaceholder.classList.add('has-image');
}
}
}
// 循环结束后统一更新 contentRef 与 save
if (settings.autoInsertFrames && editorRef.current) {
contentRef.current = editorRef.current.innerHTML;
}
const nextFrames = [...capturedFrames, ...newFrames].sort((a, b) => a.time - b.time);
setCapturedFrames(nextFrames);
stateRef.current = { ...stateRef.current, capturedFrames: nextFrames };
if (wasPlaying) video.play();
saveDraftToStorage();
```
#### 3.4 修复 `captureFrame` 中状态同步
```tsx
const nextFrames = [...capturedFrames, newFrame].sort((a, b) => a.time - b.time);
setCapturedFrames(nextFrames);
stateRef.current = { ...stateRef.current, capturedFrames: nextFrames };
saveDraftToStorage();
```
#### 3.5 修复 `removeVideo` 中状态同步
```tsx
setVideos(updated);
stateRef.current = { ...stateRef.current, videos: updated };
// ... 调整 currentVideoIndex 后
const nextCaptured = prev => prev.filter(...).map(...).sort(...);
setCapturedFrames(nextCaptured);
stateRef.current = { ...stateRef.current, capturedFrames: nextCaptured };
saveDraftToStorage();
```
#### 3.6 修复 `handleVideoUpload` 中状态同步
```tsx
setVideos(combined);
stateRef.current = { ...stateRef.current, videos: combined };
setCurrentVideoIndex(videos.length);
if (videoInputRef.current) videoInputRef.current.value = '';
saveDraftToStorage();
```
#### 3.7 恢复 draft 时 `loadedTemplateId` 读取调整
由于 `loadedTemplateId` 现在已通过 `stateRef.current` 保存到 draft 中(键名为 `loadedTemplateId`),恢复逻辑中的 `setLoadedTemplateId(draft.loadedTemplateId || '')` 保持不变即可。但因为 `saveDraftToStorage` 每次调用前都会同步 `stateRef.current`,draft 中的 `loadedTemplateId` 将始终是最新的。
---
## 风险点及应对策略
| 风险 | 影响 | 应对策略 |
|------|------|----------|
| `autoInsertFrames` 开启时循环内频繁写 DOM | 性能轻微下降 | 循环内只修改 DOM,不调用 `saveDraftToStorage`,统一在循环结束后保存 |
| `stateRef.current` 手动同步遗漏其他 setState 点 | 其他状态仍可能丢失 | 本次重点修复 `capturedFrames`、`videos`、`loadedTemplateId` 三个关键字段;`reportData` 和 `activeTab` 在现有代码中已在事件处理内手动同步到 `stateRef` |
| `Check` 图标与现有 % 符号/删除按钮重叠 | 布局错乱 | 将 Check 放在 input 框内右下角 (`bottom-0.5 right-1`),不与居中 % 符号重叠 |
| 旧 `systemSettings` 没有 `autoInsertFrames` 字段 | 读取 undefined | 所有访问点使用 `settings.autoInsertFrames || false` 兜底 |
---
## 改动范围总结
- 三个文件修改,不引入新依赖。
- 自动帧插入默认关闭,不影响现有用户体验。