2026-04-17-22-53-01 - 新建项目修改工作流Skill并创建统一经验记录

This commit is contained in:
2026-04-17 23:10:57 +08:00
parent 8e7079e6a9
commit 1a766edb90
2 changed files with 325 additions and 116 deletions

View File

@@ -1,5 +1,8 @@
# 经验记录
> 本文档为项目统一知识库,记录开发过程中遇到的关键问题及解决方案。每次执行修改前必须阅读,防止重复踩坑。
> 记录格式A. 具体问题 → B. 产生问题原因 → C. 解决问题方案 → D. 后续如何避免问题
---
## 记录 1report-editor 新建报告时显示空白模板
@@ -84,7 +87,6 @@
- 在 UI 微调过程中,可以通过小步迭代快速验证用户意图,减少一次性大改导致的方向偏差。
- 实体按钮比纯文字链接具有更高的可点击性和辨识度,在微小空间中也能提供良好的交互体验。
---
## 记录 5路由切换后视频分析图片丢失
@@ -106,7 +108,6 @@
- 在涉及草稿/自动保存的功能中,应定期审查所有数据恢复路径(初始化 effect、安全网 effect、手动导入等确保 ref 与 state 的一致性。
- 对于复杂单文件组件,可考虑将「持久化 ↔ 状态同步」逻辑抽离为统一的数据恢复函数,集中处理 ref 同步,减少遗漏点。
---
## 记录 6路由切换后报告内容、基本信息、视频分析全部丢失 + 自动帧插入 UI 延迟刷新
@@ -129,7 +130,6 @@
- 在异步函数中需要让用户看到实时状态更新时,应使用 `flushSync` 强制同步渲染,避免被 React 自动批处理延迟。
- 对于复杂单文件组件中的「恢复数据」逻辑,建议将所有 `setState` 和对应的 `ref` 同步集中在一个统一的恢复函数中处理,减少遗漏点和条件嵌套。
---
## 记录 7重新部署应用Vite 生产构建 + Vite Preview
@@ -153,7 +153,6 @@
- 重新部署前务必先清理旧的同类型进程,避免端口冲突或多版本服务同时运行导致访问混乱;
- 如需固定端口,可在 `package.json``preview` 脚本中增加 `--port` 参数(如 `vite preview --port 8080`)。
---
## 记录 8路由切换后所有内容仍然丢失——彻底重构自动保存机制
@@ -179,7 +178,6 @@
- 在开发阶段应始终开启 `StrictMode` 测试,因为它能暴露 ref-based 状态同步在卸载/重挂载时的隐藏 bug。
- 对于大型表单/编辑器组件,应将自动保存逻辑与业务状态彻底解耦,统一通过 hook 的最新状态闭包来持久化。
---
## 记录 9编辑器内容和关键帧在路由切换后仍然丢失——从 Ref 读取避免闭包陷阱和 DOM 失效
@@ -207,7 +205,6 @@
- 任何直接操作 DOM 修改编辑器内容的代码,都必须**紧跟一行 `contentRef.current = editorRef.current.innerHTML`**,确保内存中的内容快照与 DOM 保持一致。
- 在开发阶段应定期测试「组件卸载 → 重新挂载」的场景React 18 `StrictMode` 会自动模拟),提前暴露闭包和 ref 同步问题。
---
## 记录 10自动帧插入阻塞关键帧摘取——改为 setTimeout 非阻塞异步插入
@@ -235,7 +232,6 @@ if ((settings.autoInsertDelay || 0) > 0) {
- 当多个定时任务需要按顺序执行时,可以通过索引计算累积延迟(`delay * (index + 1)`),实现简单的"队列式"顺序触发,而不需要阻塞主流程。
-`setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。
---
## 记录 11关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志
@@ -345,6 +341,74 @@ if ((settings.autoInsertDelay || 0) > 0) {
---
## 记录 14智能字段插入间距修复与 Backspace 防误删
**A. 具体问题**
1. `TemplateManage.tsx` 中使用 `insertSmartField` 插入智能字段后,字段后方会出现一个可见的空格(由 ` ` 和多行模板字符串中的换行/缩进空白引起)。
2. 在 `contenteditable` 中,光标位于 `<p>` 行首且后紧跟 `.smart-field-wrapper` 时按 BackspaceWebKit 内核会直接删除整段 `<p>` 而不是仅删除字段节点。
3. `defaultContent.ts` 中的 `smartField` 辅助函数同样存在多行缩进导致的模板 HTML 中夹杂空白文本节点问题。
**B. 产生问题原因**
1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 `&nbsp;`,导致插入后字段与后续文字之间总有一个不必要的空格。
2. `contenteditable="false"` 的 inline 元素处于行边界时WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除,而不是只删除该不可编辑元素。
3. `defaultContent.ts` 中的 `smartField` 为了可读性也使用了多行缩进模板字面量,导致默认模板里每个 `smartField` 调用前后都引入了额外的空白文本节点。
**C. 解决问题方案**
1. **压缩 HTML 字符串**:将 `insertSmartField` 和 `defaultContent.ts` 的 `smartField` 输出改为单行 HTML移除所有无意义的换行和缩进并去掉尾部的 `&nbsp;`。
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"` 的内联控件,若放置在块级边界(如 `<p>` 开头/结尾),务必增加键盘事件拦截,防止浏览器默认行为误删父级块。
- 默认模板或任何通过代码生成的 HTML应避免为了代码可读性而牺牲运行时 DOM 的纯净性;必要时在生成后对字符串进行 `.replace(/\s+/g, ' ').trim()` 处理。
---
## 记录 155 项交互与默认值优化(占位符尺寸、签名状态、素材预加载)
**A. 具体问题**
用户提出 5 个 UI/UX 改进需求:
1. 插入图片占位符时两次 `prompt` 弹窗合并为一次,用英文逗号分隔宽高;
2. 占位符未指定尺寸时默认显示为 `200×200px`,且样式直接使用 `width`/`height` 而非 `max-width`/`max-height`
3. 系统重置后的默认设置中增加 `autoInsertFrames: true`、`autoInsertDelay: 1`、`autoInsertFrameIndices: [0,1,2,3,4,5]`
4. 用户管理表格在「部门」与「状态」之间新增「签名状态」列,根据 `user.signature` 显示「已上传」/「未上传」;
5. 修复系统重置后 `ReportEditor` 的素材库为空的问题,将 logo 预加载逻辑从 `TemplateManage.tsx` 前置到 `Login.tsx` 的 `initData()` 中。
**B. 产生问题原因**
1. `insertImage()` 在两个编辑器(`TemplateManage`、`ReportEditor`)中均使用两次独立的 `prompt()`,操作冗余且中断感强。
2. 旧占位符样式使用 `max-width`/`max-height`,当内容区域大于占位符时,边框和背景不会收缩到指定尺寸,视觉尺寸不可控;且未指定时的 `padding:8px 16px` 导致占位符尺寸随文字变化,不统一。
3. `Login.tsx` 初始化 `systemSettings` 时遗漏了自动帧插入相关的 3 个字段,导致新系统首次进入 `/system-settings` 时相关开关为空。
4. `UserManage.tsx` 表格缺少签名可视化列,管理员无法一眼辨别哪些医生已上传电子签名。
5. `imageAssets` 的预加载仅在 `TemplateManage.tsx` 的 `useEffect` 中执行。若用户首次登录后直接进入 `ReportEditor`,素材库为空,图片选择器无法使用系统默认 logo。
**C. 解决问题方案**
1. **合并 prompt**
```ts
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 && !heightStr` → `200×200``widthStr && !heightStr` → 宽自定义、高 `200``!widthStr && heightStr` → 宽 `200`、高自定义。
3. **默认设置补全**:在 `Login.tsx` 的 `defaultSettings` 中显式加入:
```ts
autoInsertFrames: true,
autoInsertDelay: 1,
autoInsertFrameIndices: [0, 1, 2, 3, 4, 5]
```
4. **签名状态列**:在 `UserManage.tsx` 表格的 `<th>` 和 `<td>` 中,于「部门」之后、「状态」之前插入签名状态标签。
5. **素材预加载前置**:将 `fetch('/logo_square.png') → FileReader → storage.set('imageAssets', [...])` 的逻辑从 `TemplateManage.tsx` 迁移到 `Login.tsx` 的 `initData()` 中,并增加 `savedAssets.length === 0` 的判空保护,避免覆盖用户后续上传的素材。
**D. 后续如何避免问题**
- 对于成对的数值输入(如宽高、行列),优先考虑单输入框 + 分隔符,减少弹窗次数;同时做好格式解析和容错(空值、单侧空值、非数字)。
- 使用 `width`/`height` 代替 `max-width`/`max-height` 能确保占位符尺寸严格可控,避免 `inline-flex` 内容撑大容器。
- 任何需要在多个页面共享的初始化数据(如素材库、默认配置),应放在全局初始化入口(如登录页的 `initData`),而不是分散在各个页面的 `useEffect` 中。
- 表格字段变更时,注意保持 `<thead>` 与 `<tbody>` 的列顺序严格一致,避免列错位。
---
## 记录 16模板字段唯一性、删除按钮与报告批量导出
**A. 具体问题**
@@ -521,62 +585,32 @@ if ((settings.autoInsertDelay || 0) > 0) {
- 侧边栏/工具栏按钮与编辑器共存时,**必须**通过 `onMouseDown={e => e.preventDefault()}` 或等价手段阻止焦点流失,这是保证光标位置不丢失的最简单有效方案。
- 插入操作前恢复 `savedRangeRef` 可以作为焦点流失后的兜底保险,两者结合使用效果最佳。
---
## 记录 14智能字段插入间距修复与 Backspace 防误删
**A. 具体问题**
1. `TemplateManage.tsx` 中使用 `insertSmartField` 插入智能字段后,字段后方会出现一个可见的空格(由 `&nbsp;` 和多行模板字符串中的换行/缩进空白引起)。
2. 在 `contenteditable` 中,光标位于 `<p>` 行首且后紧跟 `.smart-field-wrapper` 时按 BackspaceWebKit 内核会直接删除整段 `<p>` 而不是仅删除字段节点。
3. `defaultContent.ts` 中的 `smartField` 辅助函数同样存在多行缩进导致的模板 HTML 中夹杂空白文本节点问题。
**B. 产生问题原因**
1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 `&nbsp;`,导致插入后字段与后续文字之间总有一个不必要的空格。
2. `contenteditable="false"` 的 inline 元素处于行边界时WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除,而不是只删除该不可编辑元素。
3. `defaultContent.ts` 中的 `smartField` 为了可读性也使用了多行缩进模板字面量,导致默认模板里每个 `smartField` 调用前后都引入了额外的空白文本节点。
**C. 解决问题方案**
1. **压缩 HTML 字符串**:将 `insertSmartField` 和 `defaultContent.ts` 的 `smartField` 输出改为单行 HTML移除所有无意义的换行和缩进并去掉尾部的 `&nbsp;`。
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"` 的内联控件,若放置在块级边界(如 `<p>` 开头/结尾),务必增加键盘事件拦截,防止浏览器默认行为误删父级块。
- 默认模板或任何通过代码生成的 HTML应避免为了代码可读性而牺牲运行时 DOM 的纯净性;必要时在生成后对字符串进行 `.replace(/\s+/g, ' ').trim()` 处理。
---
## 记录 21TemplateManage 快捷键 Undo/Redo 与字段插入排版修复
**A. 具体问题**
1. TemplateManage 中删除 smart-field-wrapper 后按键盘 Ctrl+Z 无法撤销,但点击工具栏撤销按钮可以恢复。
2. 当目标段落以 <br> 结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行(<span> 跑到了 <p> 外部)。
2. 当目标段落以 `<br>` 结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行(`<span>` 跑到了 `<p>` 外部)。
**B. 问题产生原因**
1. keydown 事件监听器只拦截了 Backspace/Delete未拦截 Ctrl+Z/Ctrl+Y导致浏览器原生 undo 与自定义 undoStack/
edoStack 完全脱节
2. insertSmartField 使用 document.execCommand('insertHTML')WebKit/Blink 在块级元素末尾存在 <br> 时,会自动将插入的 inline <span> 修正到块级元素外部,造成排版错位。
1. keydown 事件监听器只拦截了 Backspace/Delete未拦截 Ctrl+Z/Ctrl+Y导致浏览器原生 undo 与自定义 undoStack/redoStack 完全脱节。
2. insertSmartField 使用 document.execCommand('insertHTML')WebKit/Blink 在块级元素末尾存在 `<br>` 时,会自动将插入的 inline `<span>` 修正到块级元素外部,造成排版错位
**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() 清空当前选区;
- restoreSelection() 恢复光标;
- Range.deleteContents() 清空当前选区
- 将 HTML 字符串转为 DocumentFragment
-
ange.insertNode(fragment) 精确插入到 Range 位置;
- Range.insertNode(fragment) 精确插入到 Range 位置;
- setStartAfter(lastNode) 把光标移动到插入内容末尾。
**D. 经验与教训总结**
- 在 contentEditable 中实现自定义撤销栈时,必须**同时拦截界面按钮和键盘快捷键**的 undo/redo否则两套历史机制会互相冲突。
- document.execCommand('insertHTML') 对块级元素边界(尤其是 <br> 结尾)的自动修正行为不可控;需要精确插入时,应优先使用 Range.insertNode() 手动操作 DOM。
- document.execCommand('insertHTML') 对块级元素边界(尤其是 `<br>` 结尾)的自动修正行为不可控;需要精确插入时,应优先使用 Range.insertNode() 手动操作 DOM。
- 任何对 contentEditable 的 DOM 修改后都应同步保存内容saveTemplateContent确保 localStorage 中的模板数据与编辑器状态一致。
---
## 记录 22TemplateManage 字段体系升级与双向交互联动
@@ -611,7 +645,6 @@ ange.insertNode(fragment) 精确插入到 Range 位置;
- 编辑器与侧边栏联动建议使用 `scrollIntoView` + 临时 CSS 类,避免复杂的状态同步。
- 新增 localStorage key 时应提供合理的默认值或降级处理。
---
## 记录 23图片占位符体系重构与双端统一
@@ -642,3 +675,145 @@ ange.insertNode(fragment) 精确插入到 Range 位置;
- 在 `contentEditable` 中实现"同行插入"必须使用行内元素(`<span>`)并显式设置 `display:inline-flex` + `vertical-align:middle`;块级 `<div>` 即使通过 CSS 改 display 也可能因浏览器 execCommand 修正导致换行。
- 跨页面/跨编辑器的一致交互(如图片选择弹窗)应抽取为可复用逻辑或至少保持代码结构一致,避免用户在不同页面产生认知割裂。
- `prompt` 虽不是最优雅的用户交互,但在工具栏快捷操作中是一种零依赖、快速落地的方案;若后续需要更复杂交互,可再替换为 Modal 组件。
---
## 记录 24时间/日期字段格式配置与撰写时间动态字段
**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?: string` 和 `timeDefault?: 'current' | 'specific'`。现有字段补充默认值(`surgeryDate` → `YYYY-MM-DD`+`specific``startTime/endTime` → `24h`+`specific`);新增系统字段 `reportDate``YYYY年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。存储仍保持 24h`startHour/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-17`、`02:30 下午` → `14:30`),转回原始值后存入 `reportData`。
7. **默认模板更新**`defaultContent.ts` 底部静态「年 月 日」替换为 `${smartField('reportDate')}`。
**D. 后续如何避免问题**
- 当为字段增加新的配置属性时,务必在 `DEFAULT_FORM_FIELDS` 中为所有已有字段提供合理的默认值,保证向后兼容。
- 显示格式与存储格式分离时,必须同时实现「正向格式化」(存储→显示)和「反向解析」(显示→存储),否则用户在编辑器中直接编辑格式化后的值会导致数据格式混乱。
- 12h/24h 转换要覆盖所有边界情况12AM→00、12PM→12、1PM→13建议用独立纯函数`to24h`/`from24h`)集中处理,避免在 JSX 中内联复杂计算。
- 自动填充当前时间必须增加「仅当值为空时触发」的保护,防止编辑已有报告时覆盖用户数据。
---
## 记录 25时间字段增强——自定义格式、固定时间默认值、系统锁定标签
**A. 具体问题**
用户提出 4 个改进需求:
1. 默认模板底部「撰写时间」文字前缀与 smartField 占位符重复,需删除前缀仅保留占位符;
2. 多选类和时间类字段在 TemplateManage 字段管理中仍可修改名称,应锁定为系统字段;
3. 「手动选择」文案歧义,应改为「固定时间」;
4. 时间格式应从固定下拉选项改为支持自定义格式输入(类似单选新增选项策略),并支持为「固定时间」设置默认值。
**B. 产生问题原因**
1. `defaultContent.ts` 中底部 HTML 写死了 `撰写时间:${smartField('reportDate')}`,导致编辑器中显示重复文字。
2. `DEFAULT_FORM_FIELDS` 中 `surgeryDate`、`startTime`、`endTime`、`surgeon` 等字段的 `isSystemLocked` 为 `false`,字段库允许修改 label。
3. 早期实现时默认将时间默认值策略命名为「手动选择」,语义不够精确。
4. 日期/时间格式仅通过固定 `<select>` 提供预设选项(如 `YYYY-MM-DD`、`24h`),无法覆盖用户自定义需求(如 `YYYY/MM/DD`、`hh:mm A` 等)。
5. 当默认值策略为「固定时间」时,系统无法自动填充用户指定的固定值到报告表单中。
**C. 解决问题方案**
1. **删除前缀**`defaultContent.ts` 中将底部 HTML 从 `撰写时间:${smartField('reportDate')}` 改为仅 `${smartField('reportDate')}`。
2. **系统锁定**`types.ts` 中 `DEFAULT_FORM_FIELDS` 的 `surgeryDate`、`startTime`、`endTime`、`reportDate`、`surgeon`、`assistant`、`anesthesiologist` 全部改为 `isSystemLocked: true`。
3. **文案修改**`TemplateManage.tsx` 中所有「手动选择」改为「固定时间」。
4. **自定义格式输入**
- `types.ts` 的 `FormField` 增加 `fixedTimeValue?: string`。
- `TemplateManage.tsx` 的时间格式 UI 改为「下拉 + 自定义输入」双模式:
- `formatInputMode: 'select' | 'custom'`,默认 `select`。
- 选择「自定义」时显示 `<input>`,用户可自由输入格式字符串;回车后将输入值加入候选列表并设为当前值。
- 预设候选包含常用格式:`YYYY-MM-DD`、`YYYY年MM月DD日`、`YYYY/MM/DD`、`24h`、`12h`、`hh:mm A`、`HH:mm`。
- 通用化显示函数:
```ts
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`)的遗留字段,在通用化处理时需保留特殊分支,避免破坏现有数据结构。
---
## 记录 26时间字段联动修复——默认格式、固定时间自动填充、12/24h 动态切换
**A. 具体问题**
用户发现 3 个时间字段配置与报告编辑器的联动断层:
1. 模板管理中新建日期字段时默认格式为 `YYYY-MM-DD`,缺少中文格式 `YYYY年MM月DD日`;新建时间字段时默认格式为不可解析的 `'24h'`。
2. 在模板管理中将时间字段设为「固定时间」并填写固定值后,进入报告编辑器新建报告时,该固定值未自动填充到表单中。
3. 在模板管理中将 `startTime` 格式改为 `hh:mm A`12小时制报告编辑器中的手术开始时间表单仍显示为 24 小时制下拉框,未联动切换。
**B. 产生问题原因**
1. **默认格式错误**`TemplateManage.tsx` 中 `newFieldForm.type` 的 `onChange` 将时间字段默认值硬编码为 `'24h'`,而实际通用格式化函数 `formatTimeDisplay` 使用的是 `HH`、`hh`、`mm`、`A` 等 token `'24h'` 无法被正确解析。
2. **固定时间未注入**`ReportEditor.tsx` 初始 `reportData` 和切换模板时的 `nextReportData` 中,`surgeryDate` 被强制赋值为 `new Date().toISOString().split('T')[0]`,导致后续「仅当值为空时才填充固定时间」的判断被跳过(因为已有值了)。切换模板时也未遍历 `formFields` 读取字段的 `timeDefault`/`fixedTimeValue` 配置来注入默认值。
3. **12h 判断写死**`ReportEditor.tsx` 中 `const is12h = field.timeFormat === '12h';` 仅匹配精确的 `'12h'` 字符串。当用户在模板管理中选择了 `hh:mm A` 或自定义了其他包含 `hh`/`A` 的格式时,判断失败,表单始终渲染为 24 小时制。
**C. 解决问题方案**
1. **修正默认格式**
- `TemplateManage.tsx` 中新建字段的默认格式改为:
```ts
setNewFieldTimeFormat(t === 'date' ? 'YYYY年MM月DD日' : 'HH:mm');
```
- 重置表单时的默认值同步修正。
2. **注入固定时间默认值**
- `ReportEditor.tsx` 初始 `reportData` 中 `surgeryDate` 从 `new Date()` 改为空字符串 `''`。
- 切换模板的 `useEffect` 中,在构建 `nextReportData` 后增加遍历 `formFields` 的逻辑:
```ts
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` 中:
```ts
const is12h = field.timeFormat ? (field.timeFormat.includes('hh') || field.timeFormat.includes('A')) : false;
```
- 这样无论格式是 `12h`、`hh:mm A`、`hh:mm` 还是用户自定义的 `hh时mm分 A`,只要包含 `hh` 或 `A` 就自动切换为 12 小时制表单。
**D. 后续如何避免问题**
- 时间/日期格式的默认值必须与通用格式化函数的 token 体系保持一致,不能使用简写别名(如 `'24h'`、`'12h'`)作为存储值,除非格式化函数也能识别这些别名。
- 当字段配置了「固定默认值」或「自动填充当前值」时,必须在所有「创建新数据」的入口(初始 state、切换模板、重置表单等中显式遍历字段配置并注入不能依赖单个 `useEffect` 来兜底——因为 `useEffect` 的触发条件可能与数据创建时机不一致。
- 对于「格式→UI 形态」的联动判断,应使用**包含性判断**`includes`)而非**精确匹配**,以兼容用户自定义格式。如果判断逻辑较为复杂,建议抽离为独立工具函数(如 `is12HourFormat(fmt: string): boolean`)。
- 当某个字段在初始化时被赋予了「看似合理的默认值」(如 `surgeryDate: new Date()`),必须评估这是否会拦截后续基于字段配置的自动填充逻辑。若会拦截,应改为空值并在最后做兜底赋值。