Files
Mdeical_Sur_Report/过往经验/经验记录-2.md

46 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 同步问题。

记录 10自动帧插入阻塞关键帧摘取——改为 setTimeout 非阻塞异步插入

A. 具体问题 开启「自动帧插入」后,点击「自动关键帧摘取」时,系统不是快速完成所有关键帧的摘取,而是每摘取一张就停下来等待插入延迟(如 2 秒),插入完成后才继续摘取下一张。整体过程非常缓慢,用户体验卡顿。

B. 产生问题原因 autoCaptureFramesfor 循环内部,自动插入逻辑使用了 await new Promise<void>(r => setTimeout(...))

if ((settings.autoInsertDelay || 0) > 0) {
  await new Promise<void>(r => setTimeout(r, (settings.autoInsertDelay || 0) * 1000));
}

await 会暂停整个 for 循环的执行,导致关键帧摘取和插入变成了串行阻塞流程:必须等插入完成才能摘取下一张。

C. 解决问题方案

  1. await new Promise(...) 替换为 setTimeout(...),把插入操作推入事件队列异步执行,for 循环不再被阻塞,可以全速完成所有关键帧的摘取。
  2. 实现延迟叠加(顺序插入):通过 settings.autoInsertFrameIndices.indexOf(i) 计算当前帧是第几个需要插入的,延迟时间为 baseDelay * (insertOrderIndex + 1),避免所有图片在同一时刻同时插入。
  3. setTimeout 回调中实时查询 .image-placeholder:not(.has-image),找到则插入,并同步更新 contentRef.current 和调用 saveDraftToStorage()

D. 后续如何避免问题

  • 在异步循环中,如果某个操作不需要依赖前一步的完成结果,绝对不要使用 await 阻塞主循环,应改用 setTimeoutPromise.all 实现并行/异步解耦。
  • 当多个定时任务需要按顺序执行时,可以通过索引计算累积延迟(delay * (index + 1)),实现简单的"队列式"顺序触发,而不需要阻塞主流程。
  • setTimeout 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。

记录 125 项交互与默认值优化(占位符尺寸、签名状态、素材预加载)

A. 具体问题 用户提出 5 个 UI/UX 改进需求:

  1. 插入图片占位符时两次 prompt 弹窗合并为一次,用英文逗号分隔宽高;
  2. 占位符未指定尺寸时默认显示为 200×200px,且样式直接使用 width/height 而非 max-width/max-height
  3. 系统重置后的默认设置中增加 autoInsertFrames: trueautoInsertDelay: 1autoInsertFrameIndices: [0,1,2,3,4,5]
  4. 用户管理表格在「部门」与「状态」之间新增「签名状态」列,根据 user.signature 显示「已上传」/「未上传」;
  5. 修复系统重置后 ReportEditor 的素材库为空的问题,将 logo 预加载逻辑从 TemplateManage.tsx 前置到 Login.tsxinitData() 中。

B. 产生问题原因

  1. insertImage() 在两个编辑器(TemplateManageReportEditor)中均使用两次独立的 prompt(),操作冗余且中断感强。
  2. 旧占位符样式使用 max-width/max-height,当内容区域大于占位符时,边框和背景不会收缩到指定尺寸,视觉尺寸不可控;且未指定时的 padding:8px 16px 导致占位符尺寸随文字变化,不统一。
  3. Login.tsx 初始化 systemSettings 时遗漏了自动帧插入相关的 3 个字段,导致新系统首次进入 /system-settings 时相关开关为空。
  4. UserManage.tsx 表格缺少签名可视化列,管理员无法一眼辨别哪些医生已上传电子签名。
  5. imageAssets 的预加载仅在 TemplateManage.tsxuseEffect 中执行。若用户首次登录后直接进入 ReportEditor,素材库为空,图片选择器无法使用系统默认 logo。

C. 解决问题方案

  1. 合并 prompt
    const input = prompt('请输入占位符的最大宽度和高度(px),用英文逗号分隔(如: 100,50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
    const parts = input.split(',').map(s => s.trim());
    
    按逗号分割,第一部分为宽度,第二部分为高度。留空或单侧留空时,另一侧自动回退到 200
  2. 固定尺寸样式
    • 移除 max-width/max-height,改用 width:${width}px; / height:${height}px;
    • 默认值逻辑:!widthStr && !heightStr200×200widthStr && !heightStr → 宽自定义、高 200!widthStr && heightStr → 宽 200、高自定义。
  3. 默认设置补全:在 Login.tsxdefaultSettings 中显式加入:
    autoInsertFrames: true,
    autoInsertDelay: 1,
    autoInsertFrameIndices: [0, 1, 2, 3, 4, 5]
    
  4. 签名状态列:在 UserManage.tsx 表格的 <th><td> 中,于「部门」之后、「状态」之前插入:
    <span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
      user.signature ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'
    }`}>
      {user.signature ? '已上传' : '未上传'}
    </span>
    
  5. 素材预加载前置:将 fetch('/logo_square.png') → FileReader → storage.set('imageAssets', [...]) 的逻辑从 TemplateManage.tsx 迁移到 Login.tsxinitData() 中,并增加 savedAssets.length === 0 的判空保护,避免覆盖用户后续上传的素材。

D. 后续如何避免问题

  • 对于成对的数值输入(如宽高、行列),优先考虑单输入框 + 分隔符,减少弹窗次数;同时做好格式解析和容错(空值、单侧空值、非数字)。
  • 使用 width/height 代替 max-width/max-height 能确保占位符尺寸严格可控,避免 inline-flex 内容撑大容器。
  • 任何需要在多个页面共享的初始化数据(如素材库、默认配置),应放在全局初始化入口(如登录页的 initData),而不是分散在各个页面的 useEffect 中。
  • 表格字段变更时,注意保持 <thead><tbody> 的列顺序严格一致,避免列错位。

记录 136 项交互优化placeholder 虚线框、删除按钮、签名尺寸、多选重构)

A. 具体问题 用户提出 6 个 UI/UX 改进需求:

  1. 图片插入占位符后虚线框残留——内联 border:1px dashed #cbd5e1 优先级高于 .has-image CSS class
  2. insertImage 生成的 placeholder 中 overflow:hidden 裁切了绝对定位的删除按钮(×
  3. 占位符尺寸输入从逗号分隔改为星号(*)分隔,格式错误时提示重新输入;
  4. 默认模板中「手术者签名」占位符固定为 200×40px
  5. 删除「手术者签名确认」字段及相关的弱阻断确认弹窗;
  6. 多选组件从 tag 形态重构为纯文本拼接形态,支持多种标点符号拆分并自动保存新选项。

B. 产生问题原因

  1. fillPlaceholderSrc 仅添加了 has-image class但内联 style="border:..." 的优先级永远高于外部 CSS导致虚线框无法消除。
  2. insertImagestyleStr 中硬编码了 overflow:hidden;,而删除按钮使用 position:absolute; top:-8px; right:-8px 之类的定位,必然被父级裁切。
  3. 英文逗号分隔容易与用户输入的千位分隔符或中文逗号混淆。
  4. 默认模板中签名占位符使用 min-width:80px;min-height:24px,尺寸过小且不一致。
  5. isSigned 字段与签名图片是两个独立的状态,造成医生需要多点一次确认,流程冗余。
  6. 原多选使用 tag 胶囊形式,每个 tag 带背景色和删除按钮,占用空间大,且无法直接复制粘贴整段文本。

C. 解决问题方案

  1. 清除内联样式:在 ReportEditor.tsxTemplateManage.tsxfillPlaceholderSrc 中增加:
    placeholder.style.border = 'none';
    placeholder.style.background = 'transparent';
    
    同时统一 defaultContent.ts 中所有 8 个 placeholder 为 <span style="display:inline-flex;..."> 格式,表格中的 6 个也统一使用 width:100%;height:150px;
  2. 移除 overflow:hidden:从两个 insertImagestyleStr 中删除 overflow:hidden;,保留在 placeholder-text 子元素上(文字截断仍可用)。
  3. 星号分隔 + 校验循环
    while (true) {
      const input = prompt('...用 * 分隔...', '');
      if (input === null) return;
      const trimmed = input.trim();
      if (trimmed === '') break;
      const parts = trimmed.split('*').map(s => s.trim());
      if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
        width = parseInt(parts[0]); height = parseInt(parts[1]); break;
      }
      alert('格式错误...');
    }
    
  4. 签名占位符尺寸defaultContent.ts 中改为 width:200px;height:40px;
  5. 移除 isSigned
    • types.tsDEFAULT_FORM_FIELDS 中删除;
    • ReportEditor.tsx 的初始 reportData 中删除;
    • saveReport 的完成确认逻辑中删除 isSigned 判断;
    • smart field 同步逻辑中删除 isSigned 判断,只要有 signatureData 就直接显示签名图。
  6. 多选重构为文本拼接
    • displayText = currentValues.join(', ')
    • input 使用 value={displayText} 受控组件;
    • onChange 实时解析并更新 reportDataparseMultiInput(text)/[,;;、]/ 正则拆分、去重;
    • onBlur / Enter 时调用 handleMultiCommit,将拆分出的新选项保存到 multiSelectOptionsformFieldsConfig
    • 下拉选择时追加 , opt 到现有文本。

D. 后续如何避免问题

  • 当使用内联样式设置边框/背景时,如需在特定状态下移除,必须在内联层面重置style.border = 'none'),不能仅依赖 CSS class 覆盖。
  • overflow:hidden 与绝对定位子元素互斥,若需要裁切文字但保留溢出按钮,应将 overflow:hidden 限制在文字子元素上,而非父容器。
  • 用户输入的格式校验应使用 while 循环 + alert 重试,避免静默容错导致不可预期的行为。
  • 删除字段时务必全局搜索(grep -r 'isSigned'),确保初始化状态、表单验证、模板绑定等所有引用点都被清理。
  • 将「标签胶囊」改为「纯文本拼接」时,注意保持 reportData 的数据结构仍为数组UI 层只做 join/split 转换。

记录 11关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志

A. 具体问题 报告编辑器内容和视频列表在路由切换后能正常保留,但视频分析面板中的自动摘取关键帧和手动截图全部丢失。

B. 产生问题原因

  1. LocalStorage 5MB 容量限制:当前抽帧逻辑使用视频原始分辨率 + JPEG 质量 0.9
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const dataUrl = canvas.toDataURL('image/jpeg', 0.9);
    
    对于 1080p/4K 视频,单张 Base64 图片可达 300KB~1MB十几张关键帧即可超过 5MB。
  2. 静默失败storage.ts 中的 set 方法捕获了 QuotaExceededError 但没有任何日志:
    } catch {
      // ignore quota exceeded
    }
    
    saveDraftToStorage() 尝试保存大量关键帧时,localStorage.setItem 抛出异常draft 无法更新,但用户和开发者都感知不到错误。最终返回 /report-editor 时,只能读取到"有视频、无关键帧"的旧 draft。

C. 解决问题方案

  1. 压缩关键帧分辨率与质量

    • captureFrame()(手动截图)和 autoCaptureFrames()(自动抽帧)中,增加 Canvas 等比缩放:
      const MAX_WIDTH = 800;
      const scale = Math.min(1, MAX_WIDTH / video.videoWidth);
      canvas.width = video.videoWidth * scale;
      canvas.height = video.videoHeight * scale;
      
    • 将 JPEG 导出质量从 0.9 降到 0.6
    • 这样单张图片体积可从 500KB 降至 30KB~80KB有效避免 LocalStorage 超限。
  2. 增加存储错误可见性

    • storage.tssetsetSession 中,将静默 catch 改为输出 console.error
      } catch (e) {
        console.error('Storage save failed (possibly quota exceeded):', e);
      }
      

D. 后续如何避免问题

  • 任何将 Base64 图片持久化到 localStorage 的场景,都必须预估数据体积并对图片进行适当的分辨率/质量压缩。
  • 存储层的异常捕获绝不应静默吞掉,至少要输出日志,必要时还应弹出用户提示。
  • 对于需要存储大量图片的医疗/图文报告系统,应将 localStorage 逐步迁移到 IndexedDB,从根本上解除 5MB 容量瓶颈。
  • 在开发测试阶段,应使用高分辨率视频和大批量关键帧进行压力测试,提前暴露存储容量问题。

记录 145 项交互修复虚线框恢复、prompt 文案、删除按钮、多选输入、label 提示)

A. 具体问题 用户提出 5 个修复需求:

  1. 删除 image-placeholder 中的图片后,虚线框消失——fillPlaceholderSrc 中设置了 border='none',但删除图片的代码没有恢复;
  2. ReportEditor.tsxinsertImage prompt 文案仍显示旧版 "用英文逗号分隔",未同步修改;
  3. 新生成的 image-placeholder 右上角红色 × 显示不完全——ReportEditor.tsxinsertImageoverflow:hidden 未移除;
  4. 多选框无法输入 ,; 等分隔符——onChange 实时调用 parseMultiInput + filter(Boolean),末尾的分隔符被瞬间吃掉;
  5. 多选框 label 缺少 "(可多选)" 提示。

B. 产生问题原因

  1. 上批次修改时,ReportEditor.tsxinsertImage 替换未成功匹配(旧字符串与文件实际内容有微小差异),导致该函数保留了旧代码。删除图片逻辑同样缺少 border/background 恢复。
  2. overflow:hidden 仅在新版 TemplateManage.tsx 中被移除,ReportEditor.tsx 中仍保留。
  3. 多选框使用受控 value={displayText} + onChange={handleMultiChange},每次输入都会触发 split(/[,;;、]/)filter(Boolean)。当用户输入一个逗号时split 产生空字符串filter 将其过滤,输入框值立即回退到无逗号状态。
  4. label 渲染时直接使用 {field.label},未追加多选提示。

C. 解决问题方案

  1. 恢复虚线框:在 ReportEditor.tsxTemplateManage.tsx 的删除图片分支中,增加:
    placeholder.style.border = '1px dashed #cbd5e1';
    placeholder.style.background = '#f8fafc';
    
  2. 修正 prompt 文案:将 ReportEditor.tsxinsertImage 重写为与 TemplateManage.tsx 一致的新版逻辑(* 分隔 + while 循环校验)。
  3. 移除 overflow:hidden:从 ReportEditor.tsxstyleStr 中删除 overflow:hidden;
  4. 多选输入解耦:引入本地状态 multiInputText: Record<string, string>
    • onChange 仅更新 multiInputText,不触发拆分;
    • onBlurEnter 时才调用 handleMultiCommit 执行 split + filter(Boolean) + 保存新选项;
    • 输入框 value 优先读取 multiInputText[field.key],无本地缓存时回退到 displayText
  5. label 追加提示{field.label}(可多选)

D. 后续如何避免问题

  • 同类型函数在多个文件中存在时,务必逐个文件 grep 确认修改结果,不能假设一次替换就能覆盖所有实例。
  • 任何 "实时解析输入" 的逻辑都必须警惕 filter(Boolean) 对空字符串的过滤效应——如果允许用户输入分隔符应使用独立状态缓存原始输入仅在确认时blur/enter执行解析。
  • StrReplaceFile 的批量替换若返回 "Applied N edit(s) with M total replacement(s)" 且 M < N应立即检查未匹配的文件避免遗漏。

记录 15时间/日期字段格式配置与撰写时间动态字段

A. 具体问题 用户提出 2 个需求:

  1. TemplateManage 字段管理中,时间/日期字段增加配置date 可选 YYYY-MM-DD / YYYY年MM月DD日 显示格式time 可选 24h / 12h 显示格式;两者均可选「当前时间」或「手动选择」作为默认值策略。
  2. 默认模板底部写死的「年 月 日」改为动态「撰写时间」智能字段,自动取当前日期。

B. 产生问题原因

  1. FormField 数据结构缺少格式和默认值配置字段。
  2. ReportEditor 中 time 字段的表单渲染仅支持 startTime/endTime 且固定为 24 小时制smart field 同步时直接显示原始值,不做任何格式转换。
  3. 模板底部「年 月 日」是纯静态 HTML 文本,没有数据绑定能力。

C. 解决问题方案

  1. 扩展数据结构FormField 增加 timeFormat?: stringtimeDefault?: 'current' | 'specific'。现有字段补充默认值(surgeryDateYYYY-MM-DD+specificstartTime/endTime24h+specific);新增系统字段 reportDateYYYY年MM月DD日+current)。
  2. TemplateManage UI 增强
    • 新增字段表单category 为「时间」时显示「默认值」select手动选择/当前时间和「显示格式」selectdate 提供两种日期格式time 提供 24h/12h
    • 字段编辑面板:点击已有时间字段进入编辑模式时,可修改上述两项配置。
  3. ReportEditor 自动填充:新增 useEffect 监听 formFields,对 timeDefault === 'current' 且值为空的字段,自动填充系统当前日期/时间。
  4. ReportEditor 表单渲染重构
    • startTime/endTime:根据 timeFormat 选择 hour select 的选项范围24h: 00-2312h: 01-1212h 时额外增加 AM/PM select。存储仍保持 24hstartHour/startMinute),转换函数 to24h/from24h 处理 12h↔24h。
    • 通用 time 字段(非 startTime/endTime新增 hour+minute select 渲染,值统一存储为 HH:MM 字符串。
  5. smart field 同步格式化:同步 useEffect 中,根据字段定义调用 formatDateDisplay/formatTimeDisplay,将原始值转换为配置格式后写入编辑器。
  6. 编辑器反向编辑解析handleEditorInput 中,当用户直接在编辑器内修改 date/time smart field 时,通过正则解析格式化文本(如 2026年04月17日2026-04-1702:30 下午14:30),转回原始值后存入 reportData
  7. 默认模板更新defaultContent.ts 底部静态「年 月 日」替换为 ${smartField('reportDate')}

D. 后续如何避免问题

  • 当为字段增加新的配置属性时,务必在 DEFAULT_FORM_FIELDS 中为所有已有字段提供合理的默认值,保证向后兼容。
  • 显示格式与存储格式分离时,必须同时实现「正向格式化」(存储→显示)和「反向解析」(显示→存储),否则用户在编辑器中直接编辑格式化后的值会导致数据格式混乱。
  • 12h/24h 转换要覆盖所有边界情况12AM→00、12PM→12、1PM→13建议用独立纯函数to24h/from24h)集中处理,避免在 JSX 中内联复杂计算。
  • 自动填充当前时间必须增加「仅当值为空时触发」的保护,防止编辑已有报告时覆盖用户数据。

记录 16时间字段增强——自定义格式、固定时间默认值、系统锁定标签

A. 具体问题 用户提出 4 个改进需求:

  1. 默认模板底部「撰写时间」文字前缀与 smartField 占位符重复,需删除前缀仅保留占位符;
  2. 多选类和时间类字段在 TemplateManage 字段管理中仍可修改名称,应锁定为系统字段;
  3. 「手动选择」文案歧义,应改为「固定时间」;
  4. 时间格式应从固定下拉选项改为支持自定义格式输入(类似单选新增选项策略),并支持为「固定时间」设置默认值。

B. 产生问题原因

  1. defaultContent.ts 中底部 HTML 写死了 撰写时间:${smartField('reportDate')},导致编辑器中显示重复文字。
  2. DEFAULT_FORM_FIELDSsurgeryDatestartTimeendTimesurgeon 等字段的 isSystemLockedfalse,字段库允许修改 label。
  3. 早期实现时默认将时间默认值策略命名为「手动选择」,语义不够精确。
  4. 日期/时间格式仅通过固定 <select> 提供预设选项(如 YYYY-MM-DD24h),无法覆盖用户自定义需求(如 YYYY/MM/DDhh:mm A 等)。
  5. 当默认值策略为「固定时间」时,系统无法自动填充用户指定的固定值到报告表单中。

C. 解决问题方案

  1. 删除前缀defaultContent.ts 中将底部 HTML 从 撰写时间:${smartField('reportDate')} 改为仅 ${smartField('reportDate')}
  2. 系统锁定types.tsDEFAULT_FORM_FIELDSsurgeryDatestartTimeendTimereportDatesurgeonassistantanesthesiologist 全部改为 isSystemLocked: true
  3. 文案修改TemplateManage.tsx 中所有「手动选择」改为「固定时间」。
  4. 自定义格式输入
    • types.tsFormField 增加 fixedTimeValue?: string
    • TemplateManage.tsx 的时间格式 UI 改为「下拉 + 自定义输入」双模式:
      • formatInputMode: 'select' | 'custom',默认 select
      • 选择「自定义」时显示 <input>,用户可自由输入格式字符串;回车后将输入值加入候选列表并设为当前值。
      • 预设候选包含常用格式:YYYY-MM-DDYYYY年MM月DD日YYYY/MM/DD24h12hhh:mm AHH:mm
    • 通用化显示函数:
      const formatDateDisplay = (isoDate: string, fmt?: string): string => {
        if (!isoDate || !fmt) return isoDate || '';
        const [y, m, d] = isoDate.split('-');
        return fmt.replace(/YYYY/g, y || '').replace(/MM/g, m || '').replace(/DD/g, d || '');
      };
      const formatTimeDisplay = (timeStr: string, fmt?: string): string => {
        if (!timeStr || !fmt) return timeStr || '';
        const [h24str, mstr] = timeStr.split(':');
        const h24 = parseInt(h24str) || 0;
        const isPM = h24 >= 12;
        let h12 = h24 % 12; if (h12 === 0) h12 = 12;
        return fmt.replace(/HH/g, String(h24).padStart(2, '0'))
          .replace(/mm/g, mstr || '00')
          .replace(/hh/g, String(h12).padStart(2, '0'))
          .replace(/A/g, isPM ? '下午' : '上午');
      };
      
  5. 通用化反向解析:新增 parseDateFromFormat / parseTimeFromFormat,从格式化文本中通过数字正则提取原始值,确保用户在编辑器中直接编辑格式化后的 smart field 后能正确回存。
  6. 固定时间默认值自动填充ReportEditor.tsx 的自动填充 useEffect 中增加 timeDefault === 'specific' 分支,若字段配置了 fixedTimeValue 且当前值为空,则自动填入固定值。

D. 后续如何避免问题

  • 自定义格式输入必须同时提供「正向格式化」和「反向解析」函数,否则编辑器双向同步会断裂。
  • 使用占位符替换(如 fmt.replace(/YYYY/g, y))实现通用格式化时,要确保所有可能的 token 都覆盖到,且替换顺序不会相互干扰。
  • 当某个字段被标记为 isSystemLocked: true 后,需在 UI 层面同时禁用 label 输入框,否则用户会困惑「为何修改无效」。
  • 时间/日期字段的默认值策略文案应直接体现业务含义(如「固定时间」「当前时间」),避免使用技术词汇(如「手动选择」)。
  • 对于 startTime/endTime 这类拆分存储(startHour+startMinute)的遗留字段,在通用化处理时需保留特殊分支,避免破坏现有数据结构。

记录 17时间字段联动修复——默认格式、固定时间自动填充、12/24h 动态切换

A. 具体问题 用户发现 3 个时间字段配置与报告编辑器的联动断层:

  1. 模板管理中新建日期字段时默认格式为 YYYY-MM-DD,缺少中文格式 YYYY年MM月DD日;新建时间字段时默认格式为不可解析的 '24h'
  2. 在模板管理中将时间字段设为「固定时间」并填写固定值后,进入报告编辑器新建报告时,该固定值未自动填充到表单中。
  3. 在模板管理中将 startTime 格式改为 hh:mm A12小时制报告编辑器中的手术开始时间表单仍显示为 24 小时制下拉框,未联动切换。

B. 产生问题原因

  1. 默认格式错误TemplateManage.tsxnewFieldForm.typeonChange 将时间字段默认值硬编码为 '24h',而实际通用格式化函数 formatTimeDisplay 使用的是 HHhhmmA 等 token '24h' 无法被正确解析。
  2. 固定时间未注入ReportEditor.tsx 初始 reportData 和切换模板时的 nextReportData 中,surgeryDate 被强制赋值为 new Date().toISOString().split('T')[0],导致后续「仅当值为空时才填充固定时间」的判断被跳过(因为已有值了)。切换模板时也未遍历 formFields 读取字段的 timeDefault/fixedTimeValue 配置来注入默认值。
  3. 12h 判断写死ReportEditor.tsxconst is12h = field.timeFormat === '12h'; 仅匹配精确的 '12h' 字符串。当用户在模板管理中选择了 hh:mm A 或自定义了其他包含 hh/A 的格式时,判断失败,表单始终渲染为 24 小时制。

C. 解决问题方案

  1. 修正默认格式
    • TemplateManage.tsx 中新建字段的默认格式改为:
      setNewFieldTimeFormat(t === 'date' ? 'YYYY年MM月DD日' : 'HH:mm');
      
    • 重置表单时的默认值同步修正。
  2. 注入固定时间默认值
    • ReportEditor.tsx 初始 reportDatasurgeryDatenew Date() 改为空字符串 ''
    • 切换模板的 useEffect 中,在构建 nextReportData 后增加遍历 formFields 的逻辑:
      formFields.forEach(field => {
        if (field.category === '时间') {
          if (field.timeDefault === 'specific' && field.fixedTimeValue) {
            // 按 field.type 和 field.key 注入固定值
          } else if (field.timeDefault === 'current') {
            // 注入当前系统时间
          }
        }
      });
      if (!nextReportData.surgeryDate) {
        nextReportData.surgeryDate = new Date().toISOString().split('T')[0];
      }
      
  3. 通用化 12h 判断
    • ReportEditor.tsx 中:
      const is12h = field.timeFormat ? (field.timeFormat.includes('hh') || field.timeFormat.includes('A')) : false;
      
    • 这样无论格式是 12hhh:mm Ahh:mm 还是用户自定义的 hh时mm分 A,只要包含 hhA 就自动切换为 12 小时制表单。

D. 后续如何避免问题

  • 时间/日期格式的默认值必须与通用格式化函数的 token 体系保持一致,不能使用简写别名(如 '24h''12h')作为存储值,除非格式化函数也能识别这些别名。
  • 当字段配置了「固定默认值」或「自动填充当前值」时,必须在所有「创建新数据」的入口(初始 state、切换模板、重置表单等中显式遍历字段配置并注入不能依赖单个 useEffect 来兜底——因为 useEffect 的触发条件可能与数据创建时机不一致。
  • 对于「格式→UI 形态」的联动判断,应使用包含性判断includes)而非精确匹配,以兼容用户自定义格式。如果判断逻辑较为复杂,建议抽离为独立工具函数(如 is12HourFormat(fmt: string): boolean)。
  • 当某个字段在初始化时被赋予了「看似合理的默认值」(如 surgeryDate: new Date()),必须评估这是否会拦截后续基于字段配置的自动填充逻辑。若会拦截,应改为空值并在最后做兜底赋值。