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

444 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 经验记录
> 本文档为项目统一知识库,记录开发过程中遇到的关键问题及解决方案。每次执行修改前必须阅读,防止重复踩坑。
> 记录格式A. 具体问题 → B. 产生问题原因 → C. 解决问题方案 → D. 后续如何避免问题
---
## 记录 1report-editor 新建报告时显示空白模板
**A. 具体问题**
超级管理员进入 `/report-editor`(新建报告)时,编辑区域为纯白色空白,顶部模板选择器显示"无",但 system-settings 中已配置了默认模板。
**B. 产生问题原因**
1. `ReportEditor.tsx` 在组件卸载时会自动将当前编辑器内容保存为草稿。即使用户未输入任何内容,保存的 `content` 也是空字符串 `""`
2. 初始化 effect 中判断草稿是否有效的条件仅使用了 `typeof draft.content === 'string'`,空字符串满足该条件,导致编辑器被填充为空白 HTML并将 `contentLoadedRef.current` 设为 `true`
3. 由于 `contentLoadedRef.current` 已被置为 `true`,后续加载 `settings.defaultTemplate` 的默认模板分支被完全跳过。
**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)')` 查找第一个空置占位符。
3. 在关键帧卡片底部新增 "插入" 按钮,使用 `opacity-0 group-hover:opacity-100 transition-opacity`,并通过 `e.stopPropagation()` 避免触发卡片的视频跳转 `onClick`
**D. 后续如何避免问题**
- 当同一交互效果需要支持多种触发方式时,应将核心逻辑抽离为独立函数,避免重复代码。
- 在可点击子元素上务必注意事件冒泡控制,防止触发父级不必要的副作用。
---
## 记录 3路由切换后视频分析图片丢失
**A. 具体问题**
`/report-editor` 中上传视频、自动摘取关键帧后,切换到 `/report-manage` 再返回 `/report-editor`,右侧「视频分析」面板中的所有截图和关键帧全部消失。
**B. 产生问题原因**
1. `ReportEditor.tsx` 在组件卸载时通过 `stateRef.current` 保存草稿到 `localStorage`
2. 初始化 `useEffect` 从 draft 恢复数据时,仅通过 `setState` 更新了 React state**没有同步更新 `stateRef.current`**
3. 离开页面时,`stateRef.current` 仍保存着初始值(空数组),导致 `saveDraftToStorage()` 用空数组覆盖了 localStorage 中的 draft。
**C. 解决问题方案**
`ReportEditor.tsx` 的所有数据恢复入口中,恢复 `reportData``videos``capturedFrames` 后立即同步赋值给 `stateRef.current`
**D. 后续如何避免问题**
- 当使用 `useRef` 作为「自动保存」的数据快照时,**任何从持久化存储恢复数据到 React state 的操作,必须同步更新对应的 ref**。
- 在涉及草稿/自动保存的功能中,应定期审查所有数据恢复路径,确保 ref 与 state 的一致性。
---
## 记录 4路由切换后报告内容、基本信息、视频分析全部丢失 + 自动帧插入 UI 延迟刷新
**A. 具体问题**
1.`/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回,**报告内容变空、基本信息清空、视频分析数据全部丢失**。
2. 开启「自动帧插入」后,自动关键帧摘取过程中右侧关键帧列表和 placeholder 中的图片**不会逐张实时更新**。
**B. 产生问题原因**
1. **数据丢失**:在初始化 `useEffect` 中,将 `stateRef.current` 的同步赋值放在了 `if (editorRef.current && draft.content.trim().length > 0)` 条件块内部。当 `editorRef` 尚未挂载或 `draft.content` 为空时,`stateRef.current` 就得不到同步。
2. **UI 延迟**`autoCaptureFrames` 是 async 函数,内部循环中连续调用 `setCapturedFrames`。React 18 的自动批处理机制在异步函数中会合并状态更新DOM 重渲染被推迟到整个循环结束后。
**C. 解决问题方案**
1.`stateRef.current` 的同步赋值**移到 `editorRef.current/content` 判断条件的外部**。
2.`autoCaptureFrames` 的 for 循环中,将 `setCapturedFrames` 包裹在 `flushSync(() => { ... })` 中,强制每一帧被摘取后立即触发 DOM 更新。
**D. 后续如何避免问题**
- ref 的同步赋值绝对不能依赖于任何与 UI 渲染相关的条件判断。
- 在异步函数中需要让用户看到实时状态更新时,应使用 `flushSync` 强制同步渲染。
---
## 记录 5路由切换后所有内容仍然丢失——彻底重构自动保存机制
**A. 具体问题**
`/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回,报告编辑器内容、基本信息、视频列表、关键帧截图**全部丢失**。
**B. 产生问题原因**
1. 自动保存机制过度依赖 `stateRef``contentRef` 作为"数据快照"。
2. **React 18 `StrictMode`** 在开发/预览环境下会执行"挂载 → 立即卸载 → 重新挂载"。在首次模拟卸载时,`stateRef.current` 仍然是组件创建时的初始空值。
3. 组件卸载cleanup时调用保存用这个空值**覆盖了 localStorage 中已有的正确 draft**。
**C. 解决问题方案**
1. **彻底重构 `saveDraftToStorage`**:不再读取 `contentRef.current``stateRef.current`,而是直接从最新的 React state 和 `editorRef.current?.innerHTML` 获取数据。`useCallback` 的 dependency 数组包含所有相关 state确保闭包永远绑定当前渲染周期的最新 state。
2.`beforeunload``visibilitychange` 事件处理器直接绑定到 `saveDraftToStorage`effect 的 dependency 改为 `[saveDraftToStorage]`
**D. 后续如何避免问题**
- **永远不要将 `useRef` 作为自动保存的唯一数据源**。ref 在 React 18 `StrictMode` 的模拟卸载阶段仍然保持初始值,会导致用空数据覆盖有效持久化数据。
- 自动保存函数应直接从最新的 React state 和 DOM 读取数据,通过 `useCallback` + 完整的 dependency 数组保证闭包始终新鲜。
---
## 记录 6编辑器内容和关键帧在路由切换后仍然丢失——从 Ref 读取避免闭包陷阱和 DOM 失效
**A. 具体问题**
`/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回:报告内容全部丢失;视频分析面板中的自动关键帧和手动截图全部丢失。
**B. 产生问题原因**
1. **闭包陷阱**`saveDraftToStorage` 直接从 React state 读取,但代码中存在 `setCapturedFrames(nextFrames); saveDraftToStorage();` 的写法。由于 `setState` 是异步的,`saveDraftToStorage` 闭包中读到的 `capturedFrames` 仍然是旧值。
2. **卸载时 DOM 失效**:组件卸载时 React 开始销毁 DOM 树,`editorRef.current` 可能已经变为 `null``content: editorRef.current?.innerHTML || ''` 会把空字符串保存到 draft 中。
3. **`contentRef` 更新遗漏**:在 `handleEditorClick` 中删除 placeholder 后,直接调用了 `saveDraftToStorage()`,但没有先更新 `contentRef.current`
**C. 解决问题方案**
1. **重构 `saveDraftToStorage` 从 Ref 读取**`content` 优先读取 `contentRef.current`(内存引用,卸载时仍稳定存在);`reportData``videos``capturedFrames` 全部从 `stateRef.current` 读取。
2. **补齐 `contentRef` 遗漏**:在 `handleEditorClick``document.execCommand('delete')` 分支后,增加 `if (editorRef.current) contentRef.current = editorRef.current.innerHTML;`
**D. 后续如何避免问题**
- 对于需要在异步操作或组件卸载时读取的"最新状态"**应优先使用 `useRef` 作为稳定的数据快照**,而不是依赖 React state 的闭包。
- 任何直接操作 DOM 修改编辑器内容的代码,都必须**紧跟一行 `contentRef.current = editorRef.current.innerHTML`**。
---
## 记录 7自动帧插入阻塞关键帧摘取——改为 setTimeout 非阻塞异步插入
**A. 具体问题**
开启「自动帧插入」后,点击「自动关键帧摘取」时,系统不是快速完成所有关键帧的摘取,而是每摘取一张就停下来等待插入延迟,整体过程非常缓慢。
**B. 产生问题原因**
`autoCaptureFrames``for` 循环内部,自动插入逻辑使用了 `await new Promise<void>(r => setTimeout(...))``await` 会暂停整个 `for` 循环的执行。
**C. 解决问题方案**
1.`await new Promise(...)` 替换为 `setTimeout(...)`,把插入操作推入事件队列异步执行。
2. 实现延迟叠加(顺序插入):通过 `settings.autoInsertFrameIndices.indexOf(i)` 计算当前帧是第几个需要插入的,延迟时间为 `baseDelay * (insertOrderIndex + 1)`
3. `setTimeout` 回调中实时查询 `.image-placeholder:not(.has-image)`,找到则插入,并同步更新 `contentRef.current` 和调用 `saveDraftToStorage()`
**D. 后续如何避免问题**
- 在异步循环中,如果某个操作不需要依赖前一步的完成结果,**绝对不要使用 `await` 阻塞主循环**,应改用 `setTimeout``Promise.all` 实现并行/异步解耦。
-`setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用。
---
## 记录 8关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志
**A. 具体问题**
报告编辑器内容和视频列表在路由切换后能正常保留,但视频分析面板中的自动摘取关键帧和手动截图全部丢失。
**B. 产生问题原因**
1. **LocalStorage 5MB 容量限制**:当前抽帧逻辑使用视频原始分辨率 + JPEG 质量 0.9,对于 1080p/4K 视频,单张 Base64 图片可达 300KB~1MB十几张关键帧即可超过 5MB。
2. **静默失败**`storage.ts` 中的 `set` 方法捕获了 `QuotaExceededError` 但没有任何日志,导致用户和开发者都感知不到错误。
**C. 解决问题方案**
1. **压缩关键帧分辨率与质量**Canvas 等比缩放至最大 800px 宽JPEG 导出质量从 `0.9` 降到 `0.6`。单张图片体积可从 500KB 降至 30KB~80KB。
2. **增加存储错误可见性**:将静默 `catch` 改为输出 `console.error`
**D. 后续如何避免问题**
- 任何将 Base64 图片持久化到 `localStorage` 的场景,都必须**预估数据体积**并对图片进行适当的分辨率/质量压缩。
- 存储层的异常捕获**绝不应静默吞掉**,至少要输出日志,必要时还应弹出用户提示。
---
## 记录 9contentEditable 中实现标签锁定与输入方格的双向绑定
**A. 具体问题**
需要在富文本编辑器中插入"标签锁定、内容可调"的智能占位控件,使"姓名:"等固定文本不会被用户误删,同时方格内的输入能与右侧表单双向联动。
**B. 产生问题原因**
原生 `contentEditable` 区域内所有文本节点对用户都是可编辑的,无法直接保护某一段固定标签不被单独删除或篡改。
**C. 解决问题方案**
采用三层嵌套 HTML 结构:
1. **外层** `<span class="smart-field-wrapper" contenteditable="false">`
2. **标签层** `<span class="field-label">`
3. **输入层** `<span class="field-value" contenteditable="true" data-bind="patientName">`
双向绑定逻辑:富文本 → 表单通过 `handleEditorInput``e.target.hasAttribute('data-bind')` 判断;表单 → 富文本通过 `useEffect` 监听 `reportData` 变化,仅当 `el.innerText !== newValue` 时才重写 DOM。
**D. 后续如何避免问题**
- 对于需要在富文本中保护的固定文本,优先采用 `contenteditable="false"` 的包装器。
-`State -> DOM` 的同步中务必加入差异判断,避免不必要的 DOM 重写导致输入焦点异常。
---
## 记录 10智能字段插入间距修复与 Backspace 防误删
**A. 具体问题**
1. 插入智能字段后,字段后方会出现一个可见的空格(由 `&nbsp;` 和多行模板字符串中的换行/缩进空白引起)。
2. 光标位于 `<p>` 行首且后紧跟 `.smart-field-wrapper` 时按 BackspaceWebKit 内核会直接删除整段 `<p>` 而不是仅删除字段节点。
**B. 产生问题原因**
1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 `&nbsp;`
2. `contenteditable="false"` 的 inline 元素处于行边界时WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除。
**C. 解决问题方案**
1. **压缩 HTML 字符串**:将 `insertSmartField``defaultContent.ts``smartField` 输出改为单行 HTML移除所有无意义的换行和缩进并去掉尾部的 `&nbsp;`
2. **防止内部折行**:给 `.smart-field-wrapper` 增加 `white-space: nowrap;`
3. **拦截 Backspace/Delete**:在编辑器上增加 `keydown` 事件监听capture 阶段)。当光标位于文本节点起始位置且前一个兄弟节点是 `.smart-field-wrapper` 时按 Backspace主动 `preventDefault()` 并手动移除该字段节点。
**D. 后续如何避免问题**
-`contentEditable` 中使用 `document.execCommand('insertHTML', ...)` 插入 HTML 时,**传入的字符串必须是无多余空白的紧凑单行**。
- 对于 `contenteditable="false"` 的内联控件,若放置在块级边界,务必增加键盘事件拦截。
---
## 记录 11撤销栈修复、字段删除交互优化与签名字段闭环
**A. 具体问题**
1. 删除智能字段后浏览器撤销栈Undo失效点击"撤销"按钮无法恢复。
2. 插入字段后,字段框有时会跳到下一行。
3. Backspace 键无法删除字段Delete 键会误删字段前面的大段文本。
**B. 产生问题原因**
1. 删除字段时使用了 `target.remove()` 直接操作 DOM绕过了浏览器的原生撤销栈。
2. 插入的 `smart-field-wrapper``inline-block` 元素,但其后缺少行内锚点文本节点,浏览器容易将其挤到新行。
3. `keydown` 拦截逻辑中 `target.remove()` 同样会误删父级块节点。
**C. 解决问题方案**
1. **撤销栈修复**:将点击红 × 删除和键盘 Backspace/Delete 删除全部改为 `Range.selectNode(target)` + `document.execCommand('delete')`
2. **防换行**:在 `insertSmartField``defaultContent.ts``smartField()` 生成的 HTML 末尾增加 `&#8203;`(零宽空格),作为稳定的行内锚点。
3. **精准键盘删除**:配合 `Range.selectNode` + `execCommand('delete')`,不再直接 `remove()` DOM 节点。
**D. 后续如何避免问题**
-`contentEditable` 中删除元素时,**优先使用 `Range.selectNode` + `execCommand('delete')`** 而非直接 `remove()`,以确保撤销/重做等原生编辑行为正常工作。
- 插入 `inline-block``inline-flex` 控件时,可在其后追加 `&#8203;` 零宽空格,为浏览器提供稳定的行内文本锚点。
---
## 记录 12TemplateManage 自定义 Undo/Redo 与插入字段光标定位修复
**A. 具体问题**
1. 删除智能字段后,点击工具栏的"撤销"按钮无法恢复字段,"重做"也失效。
2. 点击右侧字段库按钮插入字段时,字段经常跳到下一行或文档末尾。
**B. 产生问题原因**
1. 即使将删除逻辑改为 `execCommand('delete')`,浏览器原生的 undo stack 在 `contentEditable` 中结合 React 状态更新时仍然非常脆弱,容易被清空。
2. 点击侧边栏按钮会导致编辑器 `blur`浏览器内部的光标位置Selection/Range丢失再次 `focus()` 后光标被重置,导致 `insertHTML` 插入位置错误。
**C. 解决问题方案**
1. **自定义 Undo/Redo 栈**:引入 `undoStack``redoStack` 两个 `useRef<string[]>([])`。实现 `pushHistory()`,在执行任何结构性变更前将当前 `editorRef.current.innerHTML` 推入 undo 栈。
2. **阻止焦点流失**:在所有工具栏按钮和字段库插入按钮上增加 `onMouseDown={(e) => e.preventDefault()}`,阻止 mousedown 默认行为导致编辑器失去焦点。
3. **光标位置记忆与恢复**:利用 `savedRangeRef`,实现 `saveSelection()``restoreSelection()`。在编辑器 `<div>` 上绑定 `onBlur={saveSelection}``onMouseUp={saveSelection}``onKeyUp={saveSelection}`
**D. 后续如何避免问题**
- 对于 `contentEditable` 编辑器中的结构性变更,如果原生 undo 不可靠,应尽早实现自定义历史栈(基于 HTML 字符串快照),完全接管撤销/重做逻辑。
- 侧边栏/工具栏按钮与编辑器共存时,**必须**通过 `onMouseDown={e => e.preventDefault()}` 阻止焦点流失。
---
## 记录 13时间/日期字段格式配置与撰写时间动态字段
**A. 具体问题**
1. 时间/日期字段缺少配置date 可选显示格式time 可选 24h / 12h 显示格式;两者均可选「当前时间」或「固定时间」作为默认值策略。
2. 默认模板底部写死的「年 月 日」改为动态「撰写时间」智能字段,自动取当前日期。
**B. 产生问题原因**
1. `FormField` 数据结构缺少格式和默认值配置字段。
2. `ReportEditor` 中 time 字段的表单渲染仅支持 `startTime/endTime` 且固定为 24 小时制smart field 同步时直接显示原始值,不做任何格式转换。
**C. 解决问题方案**
1. **扩展数据结构**`FormField` 增加 `timeFormat?: string``timeDefault?: 'current' | 'specific'`
2. **ReportEditor 表单渲染重构**`startTime/endTime` 根据 `timeFormat` 选择 hour select 的选项范围12h 时额外增加 AM/PM select。
3. **smart field 同步格式化**:同步 useEffect 中,根据字段定义调用 `formatDateDisplay`/`formatTimeDisplay`
4. **编辑器反向编辑解析**`handleEditorInput` 中,通过正则解析格式化文本,转回原始值后存入 `reportData`
**D. 后续如何避免问题**
- 显示格式与存储格式分离时,**必须同时实现「正向格式化」(存储→显示)和「反向解析」(显示→存储)**,否则用户在编辑器中直接编辑格式化后的值会导致数据格式混乱。
- 自动填充当前时间必须增加「仅当值为空时触发」的保护,防止编辑已有报告时覆盖用户数据。
---
## 记录 14时间字段联动修复——默认格式、固定时间自动填充、12/24h 动态切换
**A. 具体问题**
1. 新建日期字段时默认格式为 `YYYY-MM-DD`,缺少中文格式;新建时间字段时默认格式为不可解析的 `'24h'`
2. 时间字段设为「固定时间」后,进入报告编辑器新建报告时,该固定值未自动填充到表单中。
3. `startTime` 格式改为 `hh:mm A`12小时制报告编辑器中的表单仍显示为 24 小时制下拉框。
**B. 产生问题原因**
1. **默认格式错误**`TemplateManage.tsx``newFieldForm.type``onChange` 将时间字段默认值硬编码为 `'24h'`,而实际通用格式化函数使用的是 `HH``hh``mm``A` 等 token。
2. **固定时间未注入**`ReportEditor.tsx` 初始 `reportData``surgeryDate` 被强制赋值为 `new Date().toISOString().split('T')[0]`,导致后续「仅当值为空时才填充固定时间」的判断被跳过。
3. **12h 判断写死**`const is12h = field.timeFormat === '12h';` 仅匹配精确的 `'12h'` 字符串。
**C. 解决问题方案**
1. 默认格式改为:`t === 'date' ? 'YYYY年MM月DD日' : 'HH:mm'`
2. `surgeryDate` 初始值从 `new Date()` 改为空字符串 `''`;切换模板时显式遍历 `formFields` 注入固定值/当前值。
3. 12h 判断改为包含性判断:`field.timeFormat.includes('hh') || field.timeFormat.includes('A')`
**D. 后续如何避免问题**
- 时间/日期格式的默认值必须与通用格式化函数的 token 体系保持一致,不能使用简写别名(如 `'24h'``'12h'`)作为存储值。
- 当字段配置了「固定默认值」或「自动填充当前值」时,必须在所有「创建新数据」的入口中显式遍历字段配置并注入。
- 对于「格式→UI 形态」的联动判断,应使用**包含性判断**`includes`)而非**精确匹配**。
---
## 记录 15打印分页边距失效
**A. 具体问题**
`report-editor` / `report-view` 打印多页报告时,第二页及后续页面的上下边距几乎为 0内容紧贴纸张边缘。
**B. 产生问题原因**
`@page { margin: 0 }` 将物理纸张边距设为 0`body { padding: 10mm }` 只在整个 HTML 文档的顶部和底部各生效一次。当内容跨页时,浏览器在分页切断处不会保留 `body` 的 padding。
**C. 解决问题方案**
`print.ts` 中:
- `@page { margin: 15mm 10mm; }` 让打印引擎为每一页物理纸张独立分配边距
- `body { padding: 0; }` 清除 body padding
- `.content { width: 100%; }` 让内容自然撑满可用区域
**D. 后续如何避免问题**
- 打印样式的边距控制**必须使用 `@page { margin: ... }` 而非 `body { padding: ... }`**,前者会让打印引擎为每一页物理纸张独立分配边距,后者只在文档首尾生效一次。
---
## 记录 16表格内 execCommand 插入破坏结构
**A. 具体问题**
`template-manage` 编辑器表格中点击"插入图片占位符"后HTML 结构被破坏——外层 `<span class="image-placeholder">` 丢失,仅剩内部子元素散落为 `<td>` 的直接子元素。
**B. 产生问题原因**
`document.execCommand('insertHTML')``<td>` 内处理复杂的 `inline-flex` 嵌套 `<span>`WebKit/Blink 会将其自动"拍平"或重新排列。外层 `contenteditable="false"` 的 inline 容器被浏览器移除。
**C. 解决问题方案**
`insertImage` 中通过 `window.getSelection().anchorNode` 向上遍历检测是否在 `<td>` / `<th>` 内:
- 若在表格内:不弹出 prompt使用 `<div>` 块级容器 + `width:100%;height:100%;`
- 若不在表格内:保持现有 `<span>` 行内容器
**D. 后续如何避免问题**
- `document.execCommand('insertHTML')` 对块级元素边界(尤其是 `<td>` 内)的自动修正行为不可控;在表格等复杂容器内插入 HTML 时,应优先使用块级标签(如 `<div>`)作为外层容器。
---
## 记录 17图片占位符体系重构与双端统一
**A. 具体问题**
1. `template-manage` 的"插入字段"中仍存在"图片"分类手术者签名、医院Logo
2. 插入图片占位符时无法自定义默认宽高,且使用 `<div>` 导致强制换行。
3. 占位符框太小时"插入/点击放置图片"文字显示不全。
**B. 产生问题原因**
1. `DEFAULT_FORM_FIELDS` 仍包含 `surgeonSignature``hospitalLogo`
2. 两端编辑器的 `insertImage()` 使用块级 `<div>` 插入,未提供尺寸 prompt。
3. 占位符提示文本固定为长文本,未根据容器宽度做缩写适配。
**C. 解决问题方案**
1.`DEFAULT_FORM_FIELDS``types.ts` 中移除 `surgeonSignature``hospitalLogo`;在 `TemplateManage.tsx` 中彻底移除"图片"分类。
2. 改造 `insertImage()`:插入前通过 `prompt` 获取最大宽度/高度px生成带 `max-width/max-height``<span>` 行内占位符。
3. 根据 prompt 输入的宽度决定提示文字:宽度 < 80px 时显示"插入图片",否则显示"插入/点击放置图片"。
**D. 后续如何避免问题**
- 当从字段体系中彻底移除某一分类时,需要同时清理:`DEFAULT_FORM_FIELDS`、UI 渲染数组、新增表单 options、以及可能残留的分类判断逻辑。
-`contentEditable` 中实现"同行插入"必须使用行内元素(`<span>`)并显式设置 `display:inline-flex` + `vertical-align:middle`
---
## 记录 18默认模板中 image-placeholder 缺少 data-mode 导致来源隔离失效
**A. 具体问题**
默认模板 `defaultContent.ts` 中的 8 个 `.image-placeholder` 使用的是旧版 HTML 结构,缺少 `data-mode="frame|manual"` 属性。新建报告加载默认模板后,签名和 Logo 区域可被关键帧拖拽误填充。
**B. 产生问题原因**
此前对「插入图片占位符」进行弹窗改造时,仅在运行时插入逻辑中新增了 `data-mode` 属性,但未同步回刷默认模板 `defaultContent.ts`
**C. 解决问题方案**
`defaultContent.ts` 中对 8 个占位符做最小化修补:
1. 医院 Logo65×65和手术者签名200×40添加 `data-mode="manual"`
2. 表格内 6 个术中影像占位符100%×150添加 `data-mode="frame"`
3. 所有占位符的 `width/height/margin/display` 等布局属性绝对保持不变。
**D. 后续如何避免问题**
- 当为 `image-placeholder` 引入新的核心属性(如 `data-mode``data-allow-source`)时,**必须同步检索 `defaultContent.ts` 和任何预置模板文件**,确保静态模板中的占位符结构与运行时插入逻辑保持一致。
---
## 记录 195 项交互修复虚线框恢复、prompt 文案、删除按钮、多选输入、label 提示)
**A. 具体问题**
1. 删除 `image-placeholder` 中的图片后,虚线框消失。
2. `ReportEditor.tsx``insertImage` prompt 文案仍显示旧版 "用英文逗号分隔",未同步修改。
3. 新生成的 `image-placeholder` 右上角红色 `×` 显示不完全——`overflow:hidden` 未移除。
4. 多选框无法输入 `,``;````、` 等分隔符——`onChange` 实时调用 `parseMultiInput` + `filter(Boolean)`,末尾的分隔符被瞬间吃掉。
5. 多选框 label 缺少 "(可多选)" 提示。
**B. 产生问题原因**
1. `fillPlaceholderSrc` 设置了 `border='none'`,但删除图片的代码没有恢复。
2. `overflow:hidden` 仅在新版 `TemplateManage.tsx` 中被移除,`ReportEditor.tsx` 中仍保留。
3. 多选框使用受控 `value={displayText}` + `onChange={handleMultiChange}`,每次输入都会触发 `split(/[,;;、]/)``filter(Boolean)`。当用户输入一个逗号时split 产生空字符串filter 将其过滤,输入框值立即回退。
**C. 解决问题方案**
1. 在删除图片分支中增加:`placeholder.style.border = '1px dashed #cbd5e1'; placeholder.style.background = '#f8fafc';`
2.`ReportEditor.tsx``insertImage` 重写为与 `TemplateManage.tsx` 一致的新版逻辑(`*` 分隔 + while 循环校验)。
3.`ReportEditor.tsx``styleStr` 中删除 `overflow:hidden;`
4. **多选输入解耦**:引入本地状态 `multiInputText: Record<string, string>``onChange` 仅更新 `multiInputText`,不触发拆分;`onBlur``Enter` 时才调用 `handleMultiCommit` 执行拆分。
5. label 追加 `(可多选)` 提示。
**D. 后续如何避免问题**
- 同类型函数在多个文件中存在时,务必逐个文件 grep 确认修改结果,不能假设一次替换就能覆盖所有实例。
- 任何 "实时解析输入" 的逻辑都必须警惕 `filter(Boolean)` 对空字符串的过滤效应——如果允许用户输入分隔符应使用独立状态缓存原始输入仅在确认时blur/enter执行解析。
---
## 记录 20图片占位符填充后高度自适应
**A. 具体问题**
图片占位符填充后仍保留固定高度(如 200px导致图片下方出现大片空白。
**B. 产生问题原因**
此前仅将 `height` 改为 `auto`,未同步处理 `width`,也未利用 `max-width`/`max-height` 作为硬限制来实现等比例缩放。
**C. 解决问题方案**
1. **插入时**:为 inline-block 占位符追加 `max-width:${w}px;max-height:${h}px;`
2. **填充时**:统一执行以下步骤:
- 读取 `placeholder.style.maxWidth || placeholder.style.width``placeholder.style.maxHeight || placeholder.style.height` 作为硬限制值 `mw` / `mh`
-`<img>` 的 style 设为 `max-width:${mw};max-height:${mh};display:block;object-fit:contain;object-position:left top;`
- 将占位符外壳设为 `width:auto;height:auto;line-height:normal;max-width:${mw};max-height:${mh};`
**D. 后续如何避免问题**
- `image-placeholder` 的尺寸逻辑涉及「创建时预设」和「填充后自适应」两个阶段,修改时必须同时考虑:创建时是否写入了 `max-width`/`max-height`;填充时是否同步清除了固定宽高并保留了硬限制。
---
## 记录 21重新部署应用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. 使用 `Start-Process` 以独立 Windows 进程启动 `npm run preview -- --host`(避免后台任务超时杀死服务)。
4. 通过 `Invoke-WebRequest` 访问 `http://localhost:4173/` 验证服务返回 HTTP 200。
**D. 后续如何避免问题**
- 在无法使用 Docker 的环境中,可将 `npm run build && npm run preview -- --host` 作为标准部署脚本。
- 重新部署前务必先清理旧的同类型进程,避免端口冲突或多版本服务同时运行导致访问混乱。
- **切勿使用 Shell 后台任务(`run_in_background=true`)长时间运行 `npm run preview`**,因为任务超时机制(默认 60s会强制终止 preview 进程,导致服务中断。