7.4 KiB
7.4 KiB
实现方案 — 2026-04-16-17-40-20
技术思路
本次需求包含三个相互关联的修复/优化点:
- 自动抽帧图片在页面切换后丢失:根因在于
saveDraftToStorage依赖的stateRef.current在setCapturedFrames异步更新后未及时同步,导致组件卸载前保存的 draft 中缺失自动帧。 - 编辑后切换界面模板选择器显示"无":根因是
loadedTemplateId未纳入stateRef.current,且saveDraftToStorage因 stale closure 捕获了过时的loadedTemplateId值(空字符串)。 - 自动帧插入开关:在
SystemSettings中新增autoInsertFrames布尔开关,并在autoCaptureFrames中支持自动将新生成的帧填充到第一个空置占位符。
修改文件清单
| 文件 | 变更类型 | 说明 |
|---|---|---|
src/types.ts |
修改 | SystemSettings 增加 autoInsertFrames?: boolean |
src/pages/SystemSettings.tsx |
修改 | 新增"开启自动帧插入"开关及抽帧位置对勾标记 |
src/pages/ReportEditor.tsx |
修改 | 修复 stateRef 同步逻辑、自动帧插入实现 |
关键代码变更说明
1. src/types.ts — 扩展类型
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 列表上方)增加开关:
<div className="flex items-center gap-2">
<input
type="checkbox"
id="autoInsertFrames"
checked={settings.autoInsertFrames || false}
onChange={(e) => setSettings({ ...settings, autoInsertFrames: e.target.checked })}
className="w-4 h-4 accent-accent cursor-pointer"
/>
<label htmlFor="autoInsertFrames" className="text-sm text-text-main cursor-pointer">开启自动帧插入</label>
</div>
<p className="text-[11px] text-text-muted">开启后,视频自动抽帧的每一帧将按顺序自动插入到报告的空置图片占位符中,插满后不再提示。</p>
在 settings.framePositions 的每个 input 框中,当 autoInsertFrames 开启时显示绿色对勾(Check 图标):
{settings.autoInsertFrames && (
<span className="absolute bottom-0.5 right-1 text-green-500 pointer-events-none">
<Check size={10} />
</span>
)}
注意:需确保 Check 图标已从 lucide-react 导入(当前页面已导入)。
3. src/pages/ReportEditor.tsx — 修复状态同步与自动插入
3.1 将 loadedTemplateId 纳入 stateRef
修改第 72 行附近的 ref 定义:
const stateRef = useRef({ reportData, videos, capturedFrames, activeTab, loadedTemplateId });
// ...
stateRef.current = { reportData, videos, capturedFrames, activeTab, loadedTemplateId };
3.2 精简 saveDraftToStorage
移除独立的 loadedTemplateId 字段(已由 stateRef.current 包含),并加入 syncStateToRef:
const saveDraftToStorage = React.useCallback(() => {
stateRef.current = { reportData, videos, capturedFrames, activeTab, loadedTemplateId };
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, loadedTemplateId]);
保留
loadedTemplateId在依赖数组中无实际副作用,但可确保 lint 不报警。
3.3 修复 autoCaptureFrames 中 capturedFrames 不同步问题
在 autoCaptureFrames 函数中,生成所有帧后、调用 saveDraftToStorage 前,手动将最新帧列表同步到 stateRef.current:
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):
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 = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${newFrame.dataUrl}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" draggable="false">
`;
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 中状态同步
const nextFrames = [...capturedFrames, newFrame].sort((a, b) => a.time - b.time);
setCapturedFrames(nextFrames);
stateRef.current = { ...stateRef.current, capturedFrames: nextFrames };
saveDraftToStorage();
3.5 修复 removeVideo 中状态同步
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 中状态同步
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 |
改动范围总结
- 三个文件修改,不引入新依赖。
- 自动帧插入默认关闭,不影响现有用户体验。