Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-16-17-40-20.md
2026-04-16 17:55:13 +08:00

7.4 KiB
Raw Blame History

实现方案 — 2026-04-16-17-40-20

技术思路

本次需求包含三个相互关联的修复/优化点:

  1. 自动抽帧图片在页面切换后丢失:根因在于 saveDraftToStorage 依赖的 stateRef.currentsetCapturedFrames 异步更新后未及时同步,导致组件卸载前保存的 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 — 扩展类型

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 修复 autoCaptureFramescapturedFrames 不同步问题

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.currentdraft 中的 loadedTemplateId 将始终是最新的。


风险点及应对策略

风险 影响 应对策略
autoInsertFrames 开启时循环内频繁写 DOM 性能轻微下降 循环内只修改 DOM不调用 saveDraftToStorage,统一在循环结束后保存
stateRef.current 手动同步遗漏其他 setState 点 其他状态仍可能丢失 本次重点修复 capturedFramesvideosloadedTemplateId 三个关键字段;reportDataactiveTab 在现有代码中已在事件处理内手动同步到 stateRef
Check 图标与现有 % 符号/删除按钮重叠 布局错乱 将 Check 放在 input 框内右下角 (bottom-0.5 right-1),不与居中 % 符号重叠
systemSettings 没有 autoInsertFrames 字段 读取 undefined 所有访问点使用 `settings.autoInsertFrames

改动范围总结

  • 三个文件修改,不引入新依赖。
  • 自动帧插入默认关闭,不影响现有用户体验。