Files
Mdeical_Sur_Report/工程分析/经验记录.md

18 KiB
Raw Blame History

经验记录


记录 1report-editor 新建报告时显示空白模板

A. 具体问题 超级管理员进入 /report-editor(新建报告)时,编辑区域为纯白色空白,顶部模板选择器显示"无",但 system-settings 中已配置了默认模板。

B. 产生问题原因

  1. ReportEditor.tsx 在组件卸载如页面切换时会自动将当前编辑器内容保存为草稿draft。即使用户未输入任何内容保存的 content 也是空字符串 ""
  2. 初始化 effect 中判断草稿是否有效的条件仅使用了 typeof draft.content === 'string',空字符串满足该条件,导致编辑器被填充为空白 HTML并将 contentLoadedRef.current 设为 true
  3. 由于 contentLoadedRef.current 已被置为 true,后续加载 settings.defaultTemplate 的默认模板分支被完全跳过,从而永远显示空白。
  4. 此外,草稿中未保存 loadedTemplateId,即使内容非空时恢复草稿,模板选择器也会因缺少状态而显示"无"。

C. 解决问题方案

  1. saveDraftToStorage 中将当前 loadedTemplateId 一并存入 draft。
  2. 将四处草稿恢复的判断条件从 typeof draft.content === 'string' 收紧为 typeof draft.content === 'string' && draft.content.trim().length > 0,使空白草稿不再拦截默认模板加载。
  3. 恢复草稿时同步执行 setLoadedTemplateId(draft.loadedTemplateId || ''),确保模板选择器名称正确。

D. 后续如何避免问题

  • 在前端使用 contentEditable 的自动保存机制时,保存和恢复草稿都应增加对空/仅空白内容的过滤。
  • 若草稿与某个业务状态(如当前模板 ID强关联应确保两者一并持久化和恢复避免状态不一致。
  • 对兜底初始化逻辑(如默认模板加载)增加更严格的防护,防止被无效中间状态提前截断。

记录 2关键帧一键插入占位符功能实现

A. 具体问题 用户希望视频分析面板中的关键帧截图除了拖拽插入外,还能通过点击 "插入" 按钮一键自动填充到编辑器中第一个空置的 image-placeholder

B. 产生问题原因 原先仅支持拖拽方式将关键帧放入占位符。当关键帧数量多或占位符位置较远时,操作不便。且 handleDrop 中的填充逻辑未抽离,无法被其他交互方式复用。

C. 解决问题方案

  1. handleDrop 中的 HTML 填充逻辑抽离为 fillPlaceholder(placeholder, frame) 公共函数。
  2. 新增 insertFrameToPlaceholder(frame) 函数:通过 editorRef.current.querySelector('.image-placeholder:not(.has-image)') 查找第一个空置占位符,找到则调用 fillPlaceholder,未找到则 alert('没有可插入图片的空位')
  3. 在关键帧卡片底部的 timeFormatted 与 "可拖拽" 之间新增 "插入" 按钮,使用 opacity-0 group-hover:opacity-100 transition-opacity 与 "可拖拽" 保持一致的显隐行为,并通过 e.stopPropagation() 避免触发卡片的视频跳转 onClick

D. 后续如何避免问题

  • 当同一交互效果(如填充占位符)需要支持多种触发方式(拖拽、按钮点击、快捷键等)时,应将核心逻辑抽离为独立函数,避免重复代码。
  • 在可点击子元素上务必注意事件冒泡控制,防止触发父级不必要的副作用(如此处的视频跳转)。
  • UI 提示文字(如 "插入"、"可拖拽")的显隐样式应尽量保持一致,减少用户认知成本。

记录 3关键帧 "插入" 按钮位置与样式优化

A. 具体问题 用户对已实现的 "插入" 按钮位置和样式提出优化:希望按钮位于图片中央、做成实体按钮样式、颜色与 "可拖拽" 的蓝色有明显区分。

B. 产生问题原因 初次实现时将 "插入" 按钮放在了卡片底部文字区域,采用纯文字链接样式(text-accent),视觉上不够醒目,且与 "可拖拽" 提示颜色重叠,辨识度低。

C. 解决问题方案

  1. 将 "插入" 按钮从底部文字行移到图片层的 <div className="relative"> 容器内,使用 absolute inset-0 m-auto w-fit h-fit 实现水平和垂直居中。
  2. 将按钮样式改为实体胶囊按钮:px-3 py-1.5 bg-emerald-500 text-white rounded-full shadow-mdhover 时加深为 bg-emerald-600
  3. 底部文字区域只保留 timeFormatted 和 "可拖拽" 提示,"插入" 按钮不再与它们并列。

D. 后续如何避免问题

  • 对于图片卡片上的核心操作按钮,优先考虑覆盖在图片中央或显著位置,比在底部小字中放置链接更符合用户直觉。
  • 同一卡片上的多个 hover 提示元素应保持显隐动画一致(opacity-0 group-hover:opacity-100 transition-opacity),但颜色上要有区分,避免用户混淆不同功能。
  • 使用 absolute inset-0 m-auto w-fit h-fit 是一种在 Tailwind 中不依赖 flex/grid 的居中技巧,适合在 relative 容器内居中不定宽高的元素。

记录 4关键帧 "插入" 按钮位置微调(从图片中央移回底部)

A. 具体问题 用户反馈将 "插入" 按钮放在图片正中央会遮挡图片内容,希望移回卡片底部,但仍保留实体按钮样式和蓝色。

B. 产生问题原因 按钮以 absolute 层覆盖在图片中央时,确实会遮挡部分图片内容,对于医学影像类截图可能影响用户预览。

C. 解决问题方案

  1. 将 "插入" 按钮从图片层的 absolute 覆盖层移回卡片底部的文字行,放置在 timeFormatted 与 "可拖拽" 之间。
  2. 按钮颜色恢复为蓝色(bg-accent text-white),与 "可拖拽" 蓝色保持一致,视觉上统一。
  3. 保留实体胶囊按钮样式:px-2 py-0.5 rounded-full shadow-sm,不再是纯文字链接。
  4. 显隐行为仍通过 opacity-0 group-hover:opacity-100 transition-opacity 与 "可拖拽" 同步。

D. 后续如何避免问题

  • 对于图片/截图类卡片上的操作按钮,应优先考虑不遮挡核心图片内容的区域(如底部、角落),避免影响预览。
  • 在 UI 微调过程中,可以通过小步迭代快速验证用户意图,减少一次性大改导致的方向偏差。
  • 实体按钮比纯文字链接具有更高的可点击性和辨识度,在微小空间中也能提供良好的交互体验。

记录 5路由切换后视频分析图片丢失

A. 具体问题/report-editor 中上传视频、自动摘取关键帧、手动截图或拖拽截图到 image-placeholder 后,切换到 /report-manage 等其他页面再返回 /report-editor,右侧「视频分析」面板中的所有截图和关键帧全部消失;编辑器中已拖拽到 placeholder 的图片也不可见。

B. 产生问题原因

  1. ReportEditor.tsx 在组件卸载时通过 stateRef.current 保存草稿到 localStorage
  2. 初始化 useEffectuseLayoutEffect 从 draft 或已保存报告恢复数据时,仅通过 setState 更新了 React statevideoscapturedFrames),但 没有同步更新 stateRef.current
  3. 用户首次进入页面时数据正确显示;离开页面时,stateRef.current 仍保存着初始值(空数组),导致 saveDraftToStorage() 用空数组覆盖了 localStorage 中的 draft。
  4. 再次返回页面时,系统优先读取被污染后的 draft从而丢失了所有视频分析数据。

C. 解决问题方案ReportEditor.tsx 的 6 个数据恢复入口(初始化 useEffect 的 3 个分支 + useLayoutEffect 安全网的 3 个分支)中,恢复 reportDatavideoscapturedFrames 后立即同步赋值给 stateRef.current,确保后续草稿保存时数据完整。

D. 后续如何避免问题

  • 当使用 useRef 作为「自动保存」的数据快照时,任何从持久化存储恢复数据到 React state 的操作,必须同步更新对应的 ref,否则 ref 将始终保存陈旧值。
  • 在涉及草稿/自动保存的功能中,应定期审查所有数据恢复路径(初始化 effect、安全网 effect、手动导入等确保 ref 与 state 的一致性。
  • 对于复杂单文件组件,可考虑将「持久化 ↔ 状态同步」逻辑抽离为统一的数据恢复函数,集中处理 ref 同步,减少遗漏点。

记录 6路由切换后报告内容、基本信息、视频分析全部丢失 + 自动帧插入 UI 延迟刷新

A. 具体问题

  1. /report-editor 中编辑报告后,切换到 /report-manage 再返回 /report-editor报告内容变空、基本信息清空、视频分析数据全部丢失
  2. 开启「自动帧插入」后,自动关键帧摘取过程中右侧关键帧列表和 placeholder 中的图片不会逐张实时更新,而是等所有帧全部处理完后一次性批量出现。

B. 产生问题原因

  1. 数据丢失原因:在初始化 useEffect 中,将 stateRef.current 的同步赋值放在了 if (editorRef.current && draft.content.trim().length > 0) 条件块的内部。当组件首次渲染时 editorRef 尚未挂载,或 draft.content 为空(新建报告常见场景),stateRef.current 就得不到同步,始终保存着初始空值。组件卸载时,空值被保存为 draft覆盖了用户已有的数据。
  2. UI 延迟原因autoCaptureFrames 是一个 async 函数,内部循环中连续调用 setCapturedFrames。由于 React 18 的自动批处理机制在异步函数中连续的状态更新会被合并DOM 重渲染被推迟到整个循环结束后才执行一次,导致用户看不到逐帧实时更新的效果。

C. 解决问题方案

  1. 修复数据丢失:在 ReportEditor.tsx 初始化 useEffect 的 3 个数据恢复分支draft 恢复已有报告、found 恢复已有报告、draft 恢复新建报告)中,将 stateRef.current 的同步赋值移到 editorRef.current/content 判断条件的外部,确保无论编辑器 DOM 是否已挂载、content 是否为空,reportDatavideoscapturedFrames 都会立即写入 stateRef.current
  2. 清理重复代码:顺带移除了 found 恢复分支中 contentRef.current = found.content; 的重复赋值。
  3. 修复 UI 延迟:在 autoCaptureFrames 的 for 循环中,将 setCapturedFrames 包裹在 flushSync(() => { ... }) 中,强制每一帧被摘取后立即触发 DOM 更新,实现逐张实时显示和逐张插入 placeholder。

D. 后续如何避免问题

  • 当使用 useRef 作为自动保存的数据快照时,ref 的同步赋值绝对不能依赖于任何与 UI 渲染相关的条件判断(如 editorRef.current 是否存在、content 是否非空),否则在组件挂载前或内容为空时会导致数据丢失。
  • 在异步函数中需要让用户看到实时状态更新时,应使用 flushSync 强制同步渲染,避免被 React 自动批处理延迟。
  • 对于复杂单文件组件中的「恢复数据」逻辑,建议将所有 setState 和对应的 ref 同步集中在一个统一的恢复函数中处理,减少遗漏点和条件嵌套。

记录 7重新部署应用Vite 生产构建 + Vite Preview

A. 具体问题 用户要求将最新代码重新部署到生产环境,但当前运行环境中未安装 Docker无法使用项目自带的 docker-compose.yaml 进行容器化部署。

B. 产生问题原因

  1. 当前 Windows 环境缺少 Docker 和 docker-compose CLI
  2. 项目本身是基于 Vite 的前端应用,可通过 npm run build 生成静态文件后,使用 vite preview 或任意静态文件服务器进行部署;
  3. 系统中已存在旧版本的 vite preview 进程在运行,需要先停止旧服务再启动新服务。

C. 解决问题方案

  1. 使用 PowerShell 查询并强制停止所有属于当前项目目录的旧 vite preview 进程;
  2. 执行 npm run build 重新构建生产包;
  3. 使用 cmd /c "start /B npm run preview" 在后台启动新的 Vite 预览服务器;
  4. 通过 Invoke-WebRequest 访问 http://localhost:4173/ 验证服务返回 HTTP 200确认部署成功。

D. 后续如何避免问题

  • 在无法使用 Docker 的环境中,可将 npm run build && npm run preview 作为标准部署脚本;
  • 重新部署前务必先清理旧的同类型进程,避免端口冲突或多版本服务同时运行导致访问混乱;
  • 如需固定端口,可在 package.jsonpreview 脚本中增加 --port 参数(如 vite preview --port 8080)。

记录 8路由切换后所有内容仍然丢失——彻底重构自动保存机制

A. 具体问题/report-editor 中编辑报告(填写基本信息、上传视频、自动/手动截取关键帧、拖拽图片到 placeholder切换到 /report-manage 再返回 /report-editor,报告编辑器内容、基本信息、视频列表、关键帧截图全部丢失

B. 产生问题原因

  1. 自动保存机制过度依赖 stateRefcontentRef 作为"数据快照"。
  2. React 18 StrictMode 在开发/预览环境下会执行"挂载 → 立即卸载 → 重新挂载"。在首次模拟卸载时,stateRef.current 仍然是组件创建时的初始空值(videos: []capturedFrames: []、默认 reportData)。
  3. 组件卸载cleanup时调用保存用这个空值覆盖了 localStorage 中已有的正确 draft
  4. 重新挂载后,系统读取了被清空的 draft导致所有数据全部丢失。
  5. 此前两次修复仅把 stateRef.current 同步移到了更多恢复分支中,但没有从根本上消除对 ref 的依赖,因此 StrictMode 下的首次卸载仍会覆盖有效 draft。

C. 解决问题方案

  1. 彻底重构 saveDraftToStorage:不再读取 contentRef.currentstateRef.current,而是直接从最新的 React state 和 editorRef.current?.innerHTML 获取数据。useCallback 的 dependency 数组包含 reportDatavideoscapturedFramesactiveTabloadedTemplateIdreportId,确保闭包永远绑定当前渲染周期的最新 state。
  2. 重构自动保存 effect:将 beforeunloadvisibilitychange 事件处理器直接绑定到 saveDraftToStorageeffect 的 dependency 改为 [saveDraftToStorage]。这样即使 StrictMode 导致组件在首次挂载后立即卸载cleanup 中调用的 saveDraftToStorage 也指向最新数据的闭包,不会用空值覆盖已有 draft。
  3. useLayoutEffect 安全网添加 [] 依赖:防止每次渲染后重复执行,避免潜在的意外覆盖。

D. 后续如何避免问题

  • 永远不要将 useRef 作为自动保存的唯一数据源。ref 在 React 18 StrictMode 的模拟卸载阶段仍然保持初始值,会导致用空数据覆盖有效持久化数据。
  • 自动保存函数应直接从最新的 React state 和 DOM 读取数据,通过 useCallback + 完整的 dependency 数组保证闭包始终新鲜。
  • 在开发阶段应始终开启 StrictMode 测试,因为它能暴露 ref-based 状态同步在卸载/重挂载时的隐藏 bug。
  • 对于大型表单/编辑器组件,应将自动保存逻辑与业务状态彻底解耦,统一通过 hook 的最新状态闭包来持久化。

记录 9编辑器内容和关键帧在路由切换后仍然丢失——从 Ref 读取避免闭包陷阱和 DOM 失效

A. 具体问题/report-editor 中编辑报告(输入文字、上传视频、自动/手动摘取关键帧、拖拽图片到 placeholder切换到 /report-manage 再返回 /report-editor

  • class="editor-content-wrapper print-wrapper" 中的报告内容全部丢失;
  • 视频分析面板中的自动关键帧和手动截图全部丢失。

B. 产生问题原因

  1. 闭包陷阱:之前为修复 stateRef 不同步的问题,将 saveDraftToStorage 改为直接从 React statecapturedFramesvideos)读取。但代码中大量存在 setCapturedFrames(nextFrames); saveDraftToStorage(); 的写法。由于 setState 是异步的,saveDraftToStorage 闭包中读到的 capturedFrames 仍然是旧值(空数组),导致旧值覆盖了 localStorage 中的有效 draft。
  2. 卸载时 DOM 失效:组件卸载时 React 开始销毁 DOM 树,editorRef.current 可能已经变为 null 或其 innerHTML 已为空。content: editorRef.current?.innerHTML || '' 会把空字符串保存到 draft 中,导致报告内容丢失。
  3. contentRef 更新遗漏:在 handleEditorClick 中通过 document.execCommand('delete') 删除 placeholder 后,直接调用了 saveDraftToStorage(),但没有先更新 contentRef.current,进一步加剧了内容不一致。

C. 解决问题方案

  1. 重构 saveDraftToStorage 从 Ref 读取
    • content 优先读取 contentRef.current(内存引用,卸载时仍稳定存在),回退到 editorRef.current?.innerHTML
    • reportDatavideoscapturedFramesactiveTabloadedTemplateId 全部从 stateRef.current 读取,彻底避开 React state 的闭包陷阱。
    • useCallback 的 dependency 仅保留 [reportId],避免因 state 变化产生陈旧闭包。
  2. 补齐 contentRef 遗漏:在 handleEditorClickdocument.execCommand('delete') 分支后,增加 if (editorRef.current) contentRef.current = editorRef.current.innerHTML;,确保 DOM 修改后 contentRef 及时同步。

D. 后续如何避免问题

  • 对于需要在异步操作或组件卸载时读取的"最新状态"应优先使用 useRef 作为稳定的数据快照,而不是依赖 React state 的闭包。
  • 自动保存函数的 useCallback dependency 应尽量精简(如只保留 reportId),避免因 state 变化导致闭包更新不同步。
  • 任何直接操作 DOM 修改编辑器内容的代码,都必须紧跟一行 contentRef.current = editorRef.current.innerHTML,确保内存中的内容快照与 DOM 保持一致。
  • 在开发阶段应定期测试「组件卸载 → 重新挂载」的场景React 18 StrictMode 会自动模拟),提前暴露闭包和 ref 同步问题。