# 经验记录 --- ## 记录 1:report-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. 将 "插入" 按钮从底部文字行移到图片层的 `
` 等块元素)的场景。 3. `ReportManage` 的设计只支持单条查看/编辑/删除,没有设计多选状态和导出逻辑。 **C. 解决问题方案** 1. **唯一性校验**:在 `insertSmartField` 中通过 `editorRef.current?.querySelector([data-bind="..."])` 预检查,若已存在则 `alert` 并终止插入。 2. **删除按钮**:给 `.smart-field-wrapper` 内部增加一个红色圆形的 `×`,点击即可删除整个字段节点。同时在 `index.css` 和 `print` 媒体查询中分别定义显示/隐藏样式。 3. **键盘删除增强**:重写 `keydown` 处理器,同时处理 `startContainer` 为 `TEXT_NODE` 和 `ELEMENT_NODE` 两种情况。当光标位于块级父节点的子节点边界时,通过 `el.childNodes[offset - 1]` 或 `el.childNodes[offset]` 定位字段节点并安全删除。 4. **报告批量操作**: - 在 `ReportManage.tsx` 中引入 `selectedIds` 状态,表格每行增加 Checkbox,表头支持全选/反选。 - 增加浮动批量操作栏,支持"批量删除"、"批量导出 PDF"、"批量导出 JSON"、"取消选择"。 - 单报告操作列增加"导出"按钮,点击弹出模态框选择 PDF 或 JSON。 - PDF 导出复用现有的 `printDocument(content)`;JSON 导出通过 `Blob` + `URL.createObjectURL` 实现下载,数据结构包含 `meta`(报告元信息)和 `fields`(所有 `DEFAULT_FORM_FIELDS` 对应值)。 - 批量 PDF 将多份报告的 HTML 用 `
` 拼接后统一打印。 - 批量 JSON 将多份报告导出为数组形式的单个 `.json` 文件。 **D. 后续如何避免问题** - 在 `contentEditable` 中插入的任何可复用控件,都应考虑增加唯一性校验和明确的删除入口(可视化按钮 + 键盘事件拦截)。 - 键盘事件处理不能假设 `startContainer` 一定是文本节点,必须覆盖块级元素边界的情况。 - 当列表页需要增加批量操作时,建议将"选择状态"和"批量动作"封装为独立逻辑,保持单条操作按钮的可维护性。 - 导出功能应尽量复用现有的 `printDocument` 等工具函数,减少新依赖引入。 --- ## 记录 17:字段聚焦高亮、删除按钮显隐隔离与 multi_select 脏数据崩溃修复 **A. 具体问题** 1. `TemplateManage` 中编辑智能字段时缺少视觉焦点反馈,用户体验不够直观。 2. 红色 × 删除按钮始终显示在字段内部左侧,且在任何包含 `smart-field-wrapper` 的页面(包括 `ReportEditor`)都会显示。 3. `ReportEditor` 加载某些历史报告时崩溃,报错 `(y[x.key] || []).map is not a function`。 **B. 产生问题原因** 1. 之前没有为 `.field-value` 定义 `:focus` 状态的 CSS 样式。 2. `delete-btn` 使用 `display: inline-flex` 默认常驻显示,且没有针对页面做显隐隔离。 3. `multi_select` 字段(如 `surgeon`、`assistant`)的渲染直接对值调用 `.map()`,但旧数据或异常存储可能将其保存为字符串(如 `"张医生"` 而非 `["张医生"]`),导致 `.map` 在字符串上调用时抛出 `TypeError`。 **C. 解决问题方案** 1. **聚焦高亮**:在 `index.css` 中为 `.smart-field-wrapper .field-value:focus` 增加背景色加深(`#e2e8f0`)、边框变深(`#94a3b8`)和蓝色外发光(`box-shadow: 0 0 0 2px rgba(59,130,246,0.25)`)的样式,配合 `transition` 实现平滑反馈。 2. **删除按钮定位与显隐隔离**: - 将 `delete-btn` 从字段内部移到 `.field-value` 之后,并给 `.smart-field-wrapper` 增加 `position:relative`,使 `delete-btn` 可绝对定位到右上角(`top: -8px; right: -8px`)。 - 默认 `display: none`;在 `TemplateManage` 的编辑器容器上增加 `template-editor-mode` class,通过 `.template-editor-mode .smart-field-wrapper:hover .delete-btn` 和 `:focus-within .delete-btn` 控制仅在 TemplateManage 中悬浮/聚焦时显示。 - `ReportEditor` 的编辑器容器没有 `template-editor-mode`,因此删除按钮不会显示。 3. **类型安全修复**:在 `ReportEditor.tsx` 的 `multi_select` 渲染分支中,增加 `Array.isArray` 检查: ```ts const rawValue = (reportData as any)[field.key]; const tags = Array.isArray(rawValue) ? rawValue : (rawValue ? [String(rawValue)] : []); ``` 确保无论旧数据是数组、字符串还是空值,都能安全渲染为标签列表。 **D. 后续如何避免问题** - 任何需要在不同页面显隐不同的 UI 元素,应通过容器级 class 做样式隔离,而不是依赖全局显示/隐藏。 - `contentEditable` 控件的焦点状态必须有明确的视觉反馈(背景/边框/阴影变化),否则用户难以感知当前编辑位置。 - 对从持久化存储读取的数组类型数据,在 React 渲染前务必做 `Array.isArray` 校验,防止历史脏数据导致整页崩溃。 --- ## 记录 18:字段悬浮高亮、电子签上传与手术者签名联动 **A. 具体问题** 1. `TemplateManage` 中右侧字段库按钮与编辑器中的字段缺乏视觉关联,用户难以快速定位字段位置。 2. `UserManage` 缺少电子签名上传功能,无法为医生绑定个人签名图。 3. 模板中缺少"手术者签名"字段,报告编辑时无法自动带入医生签名。 4. 签名图片若直接放入 `.field-value` 中,容易撑大行高,影响排版和打印效果。 **B. 产生问题原因** 1. 字段库按钮没有任何与编辑器 DOM 联动的交互反馈机制。 2. 早期设计未考虑医疗文书中的电子签需求,`User` 模型和 `DEFAULT_FORM_FIELDS` 均缺少签名相关定义。 3. 没有针对签名图片设计专门的 CSS 尺寸约束,导致浏览器按原图尺寸渲染,破坏行高。 **C. 解决问题方案** 1. **悬浮高亮**:在 `TemplateManage.tsx` 的字段库按钮上增加 `onMouseEnter` / `onMouseLeave`,直接操作编辑器中对应 `data-bind` 的 `.field-value` 的 `style.boxShadow` 和 `style.backgroundColor`,实现蓝色外发光/背景变浅蓝色的即时高亮反馈。 2. **电子签上传与压缩**: - 在 `UserManage.tsx` 中增加 `compressImage(file, maxSize=500)` 工具函数,利用 Canvas 等比例缩放并填充白色背景,输出 JPEG base64(质量 0.8)。 - 在用户编辑/新增弹窗中增加"电子签名"区块:预览图、上传按钮、清除按钮。 - 编辑当前登录用户时同步更新 `storage.set('currentUser', ...)`,确保 ReportEditor 能读取最新签名。 3. **手术者签名字段**: - `types.ts` 中 `User` 增加 `signature?: string`;`FieldType` 增加 `'signature'`;`DEFAULT_FORM_FIELDS` 追加 `surgeonSignature`(分类"图片",系统锁定)。 - `TemplateManage` 插入字段分类增加"图片",`surgeonSignature` 自动出现在该分类下。 - `ReportEditor` 的"表单 → 编辑器"同步 `useEffect` 中,对 `fieldKey === 'surgeonSignature'` 做特殊分支:有签名则填充 `` 的问题。
4. **签名尺寸与字段管理**:
- `types.ts` 中将 `surgeonSignature` 改为 `visibleInForm: true, isSystemLocked: false`,使其出现在字段管理和右侧表单中。
- 新增 `isSigned` 字段(单选:已签字 / 未签字,默认"未签字")。
- 签名图片样式改为 `max-width: 120px; max-height: 40px; object-fit: contain;`,并在打印样式和 `print.ts` 中同步。
5. **签名同步逻辑重构**:`ReportEditor` 中 `surgeonSignature` 的渲染由 `isSigned` 控制:
- `已签字` 且 `currentUser.signature` 存在 → 显示签名图片。
- `已签字` 但无签名图 → 显示 "【请上传电子签】"。
- `未签字` → 显示 "【未签字】"。
6. **完成报告签名校验**:`saveReport('completed')` 中,若模板包含 `surgeonSignature`:
- 未选择"已签字" → `confirm` 弱阻断提示。
- 已选择"已签字"但无签名图 → `confirm` 弱阻断提示。
- 用户点击"取消"则中断保存,点击"确定"仍可继续保存。
**D. 后续如何避免问题**
- 在 `contentEditable` 中删除元素时,**优先使用 `Range.selectNode` + `execCommand('delete')`** 而非直接 `remove()`,以确保撤销/重做等原生编辑行为正常工作。
- 插入 `inline-block` 或 `inline-flex` 控件时,可在其后追加 `` 零宽空格,为浏览器提供稳定的行内文本锚点,减少排版异常。
- 任何需要从"不可见"改为"可见/可配置"的字段,应在 `DEFAULT_FORM_FIELDS`、`Report 类型`、`reportData 初始值` 三处同步更新,防止表单渲染遗漏。
- 对于图片类嵌入内容,应使用 `max-width`/`max-height` + `object-fit: contain` 做硬约束,避免不同来源图片破坏页面布局。
---
## 记录 20:TemplateManage 自定义 Undo/Redo 与插入字段光标定位修复
**A. 具体问题**
1. `TemplateManage` 中删除智能字段(通过红 × 或 Backspace/Delete)后,点击工具栏的"撤销"按钮无法恢复字段,"重做"也失效。
2. 点击右侧字段库按钮插入字段时,字段经常跳到下一行或文档末尾。
**B. 产生问题原因**
1. 即使将删除逻辑改为 `execCommand('delete')`,浏览器原生的 undo stack 在 `contentEditable` 中结合 React 状态更新时仍然非常脆弱,容易被清空。
2. 点击侧边栏按钮会导致编辑器 `blur`,浏览器内部的光标位置(Selection/Range)丢失;再次 `focus()` 后光标被重置,导致 `insertHTML` 插入位置错误。
**C. 解决问题方案**
1. **自定义 Undo/Redo 栈**:
- 在 `TemplateManage.tsx` 中引入 `undoStack` 和 `redoStack` 两个 `useRef ` 行首且后紧跟 `.smart-field-wrapper` 时按 Backspace,WebKit 内核会直接删除整段 ` ` 而不是仅删除字段节点。
3. `defaultContent.ts` 中的 `smartField` 辅助函数同样存在多行缩进导致的模板 HTML 中夹杂空白文本节点问题。
**B. 产生问题原因**
1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 ` `,导致插入后字段与后续文字之间总有一个不必要的空格。
2. `contenteditable="false"` 的 inline 元素处于行边界时,WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除,而不是只删除该不可编辑元素。
3. `defaultContent.ts` 中的 `smartField` 为了可读性也使用了多行缩进模板字面量,导致默认模板里每个 `smartField` 调用前后都引入了额外的空白文本节点。
**C. 解决问题方案**
1. **压缩 HTML 字符串**:将 `insertSmartField` 和 `defaultContent.ts` 的 `smartField` 输出改为单行 HTML,移除所有无意义的换行和缩进,并去掉尾部的 ` `。
2. **防止内部折行**:给 `.smart-field-wrapper` 增加 `white-space: nowrap;`(内联样式 + CSS 类双保险),确保标签和输入框不会在行中间被拆开。
3. **拦截 Backspace/Delete**:在 `TemplateManage.tsx` 的编辑器上增加 `keydown` 事件监听(capture 阶段)。当光标位于文本节点起始位置且前一个兄弟节点是 `.smart-field-wrapper` 时按 Backspace,或光标在文本节点末尾且后一个兄弟节点是 `.smart-field-wrapper` 时按 Delete,主动 `preventDefault()` 并手动移除该字段节点,随后同步更新 `localStorage` 中的模板内容。
**D. 后续如何避免问题**
- 在 `contentEditable` 中使用 `document.execCommand('insertHTML', ...)` 插入 HTML 时,**传入的字符串必须是无多余空白的紧凑单行**,否则浏览器会将其中的换行符解析为额外的文本节点,破坏排版和光标行为。
- 对于 `contenteditable="false"` 的内联控件,若放置在块级边界(如 ` ` 开头/结尾),务必增加键盘事件拦截,防止浏览器默认行为误删父级块。
- 默认模板或任何通过代码生成的 HTML,应避免为了代码可读性而牺牲运行时 DOM 的纯净性;必要时在生成后对字符串进行 `.replace(/\s+/g, ' ').trim()` 处理。
---
## 记录 21:TemplateManage 快捷键 Undo/Redo 与字段插入排版修复
**A. 具体问题**
1. TemplateManage 中删除 smart-field-wrapper 后按键盘 Ctrl+Z 无法撤销,但点击工具栏撤销按钮可以恢复。
2. 当目标段落以 外部)。
**B. 问题产生原因**
1. keydown 事件监听器只拦截了 Backspace/Delete,未拦截 Ctrl+Z/Ctrl+Y,导致浏览器原生 undo 与自定义 undoStack/
edoStack 完全脱节。
2. insertSmartField 使用 document.execCommand('insertHTML'),WebKit/Blink 在块级元素末尾存在
结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行( 跑到了
时,会自动将插入的 inline 修正到块级元素外部,造成排版错位。
**C. 解决问题方法**
1. **快捷键拦截**:在 keydown 监听的最开头增加 Ctrl+Z / Cmd+Z / Ctrl+Shift+Z / Ctrl+Y 的拦截,调用 e.preventDefault() 后路由到 handleUndo() 或 handleRedo()。
2. **精确 Range 插入**:将 insertSmartField 的插入方式从 execCommand('insertHTML') 替换为手动 Range.insertNode():
-
estoreSelection() 恢复光标;
-
ange.deleteContents() 清空当前选区;
- 将 HTML 字符串转为 DocumentFragment;
-
ange.insertNode(fragment) 精确插入到 Range 位置;
- setStartAfter(lastNode) 把光标移动到插入内容末尾。
**D. 经验与教训总结**
- 在 contentEditable 中实现自定义撤销栈时,必须**同时拦截界面按钮和键盘快捷键**的 undo/redo,否则两套历史机制会互相冲突。
- document.execCommand('insertHTML') 对块级元素边界(尤其是
结尾)的自动修正行为不可控;需要精确插入时,应优先使用 Range.insertNode() 手动操作 DOM。
- 任何对 contentEditable 的 DOM 修改后,都应同步保存内容(saveTemplateContent),确保 localStorage 中的模板数据与编辑器状态一致。
---
## 记录 22:TemplateManage 字段体系升级与双向交互联动
**A. 具体问题**
1. 新增字段时单选/多选分类仍显示"文本"选项,联动逻辑错误。
2. 默认模板中存在大量静态灰色占位文本(术前诊断、术后诊断等),无法与右侧表单双向绑定。
3. 字段管理列表平铺展示,无分组折叠,系统字段选项不可修改。
4. 图片占位符只能通过本地上传填充,无法使用签名图或系统素材。
5. 编辑器中的智能字段与右侧侧边栏完全无联动。
**B. 问题产生原因**
1. `newFieldForm.category` onChange 时未正确过滤 type select 的 options。
2. `DEFAULT_FORM_FIELDS` 缺少术前/术后诊断等临床字段,导致 `defaultContent.ts` 只能写死占位文本。
3. 字段管理 UI 未按 category 分组,也未提供编辑系统字段选项的入口。
4. `ReportEditor.tsx` 中图片占位符点击后直接调用 `input.click()`,缺少多渠道选择机制。
5. `TemplateManage.tsx` 的 `handleEditorClick` 仅处理了删除逻辑,未处理点击高亮/导航。
**C. 解决问题方法**
1. **类型联动修复**:category onChange 时强制设置对应 type(单选→single_select、多选→multi_select、图片→image);type select 使用条件渲染,只显示当前 category 支持的选项。
2. **扩展默认字段**:在 `types.ts` 追加 `preoperativeDiagnosis`、`postoperativeDiagnosis`、`postOpCondition`、`specimenDescription`、`pathologyCheck`、`frozenPathology`、`hospitalLogo` 等系统字段,全部 `isSystemLocked: true`。
3. **替换模板占位文本**:在 `defaultContent.ts` 中将所有灰色占位文本替换为 `smartField(...)`,Logo 替换为带 `data-bind="hospitalLogo"` 的 `image-placeholder`。
4. **字段管理折叠与编辑**:新增 `expandedCategories` 状态实现折叠面板;新增 `editingFieldKey` 等状态实现点击编辑(系统字段 label 只读、选项可编辑)。
5. **素材库与图片字段**:`FieldType` 扩展 `'image'`;初始化时自动将 Logo 转 Base64 存入 `imageAssets`;`insertSmartField` 对图片类型插入 `image-placeholder`。
6. **图片来源选择弹窗**:`ReportEditor.tsx` 点击图片占位符弹出 Modal,支持本地上传、我的签名、系统素材三选一。
7. **编辑器-侧边栏双向联动**:点击 `smart-field-wrapper` 时读取 `data-bind`,高亮并滚动定位到右侧对应字段,自动展开分组。
**D. 经验与教训总结**
- category→type 的联动应在 state 变更层强制收敛,而不是仅依赖 JSX 条件渲染。
- 升级静态占位文本为字段时,必须同步修改 `DEFAULT_FORM_FIELDS`、`defaultContent.ts` 和 `formFieldsConfig`。
- 图片字段与普通文本字段的 DOM 结构差异大,插入逻辑需要按 type 分支。
- 编辑器与侧边栏联动建议使用 `scrollIntoView` + 临时 CSS 类,避免复杂的状态同步。
- 新增 localStorage key 时应提供合理的默认值或降级处理。
---
## 记录 23:图片占位符体系重构与双端统一
**A. 具体问题**
1. `template-manage` 的"插入字段"中仍存在"图片"分类(手术者签名、医院Logo),用户认为不再需要。
2. 插入图片占位符时无法自定义默认宽高,且使用 `` 增加 `max-width:100%; max-height:100%; object-fit:contain;`,避免撑破设置了固定尺寸的占位符。
**D. 经验与教训总结**
- 当从字段体系中彻底移除某一分类时,需要同时清理:`DEFAULT_FORM_FIELDS`、UI 渲染数组、新增表单 options、以及可能残留的分类判断逻辑(如编辑字段时显示 options 输入框的条件)。
- 在 `contentEditable` 中实现"同行插入"必须使用行内元素(``)并显式设置 `display:inline-flex` + `vertical-align:middle`;块级 `