diff --git a/.agents/skills/medical-report-dev-workflow/SKILL.md b/.agents/skills/medical-report-dev-workflow/SKILL.md index 01f7978..7a835e1 100644 --- a/.agents/skills/medical-report-dev-workflow/SKILL.md +++ b/.agents/skills/medical-report-dev-workflow/SKILL.md @@ -1,48 +1,57 @@ --- name: medical-report-dev-workflow -description: 手术图文病历报告系统(Gemini-图文报告系统)的项目修改需求专用工作流。当用户提出任何与该项目相关的代码修改、功能开发、Bug 修复、性能优化、UI 调整、需求变更等任务时,必须严格按此工作流执行。不适用于纯问答、查询信息、或不涉及代码修改的任务。 +description: | + 手术图文病历报告系统(Gemini-图文报告系统)的强制性项目修改工作流。 + 当用户提出任何与该项目相关的代码修改、功能开发、Bug 修复、性能优化、UI 调整、 + 需求变更等任务时,**必须严格按此工作流逐步执行,严禁跳过或合并步骤**。 + 不适用于纯问答、查询信息、阅读文档或不涉及代码/文件修改的任务。 --- -# 手术图文病历报告系统 — 项目修改工作流 +# 手术图文病历报告系统 — 项目修改工作流(v2.0) -> 本文档是 AI 编码代理执行项目修改需求时的强制性规范。任何涉及代码变更的任务必须按以下步骤逐一执行,严禁跳过或合并步骤。 +> 本文档是 AI 编码代理执行项目修改需求时的**强制性规范**。任何涉及代码变更的任务必须按以下 7 个步骤逐一执行,每一步有明确的停止/确认点,严禁跳过。 + +--- ## 前置检查 -- **确认任务类型**:如果用户请求不涉及代码/配置/文件修改(如纯提问、查资料、解释概念),则无需使用此工作流。 -- **确认当前工作目录**:所有路径均相对于 `C:\Users\Administrator\Downloads\Gemini-图文报告系统-V1.2`(项目根目录)。 +- **确认任务类型**:如果用户请求不涉及代码/配置/文件修改(如纯提问、查资料、解释概念),则**不启用**此工作流。 +- **确认当前工作目录**:所有路径均相对于项目根目录 `C:\Users\Administrator\Downloads\Gemini-图文报告系统-V1.2`。 --- ## Step 0:记录时间戳 -**任务开始后的第一件事**:获取当前时间并格式化为 `YYYY-MM-DD-HH-MM-SS`(如 `2026-04-16-21-30-00`)。 +**任务开始后的第一件事**:获取当前系统时间并格式化为 `YYYY-MM-DD-HH-MM-SS`(示例:`2026-04-17-22-53-01`)。 -- 将该时间戳保存到工作上下文中(如变量 `workflowTimestamp`)。 -- 所有后续文档的文件名必须以此时间戳结尾。 +- 将该时间戳保存到工作上下文中(变量名建议:`workflowTimestamp`)。 +- **所有后续产出的文档文件名必须以此时间戳结尾**。 --- -## Step 1:创建工程分析目录 +## Step 1:工程分析目录 -在**项目根目录**下创建文件夹: -``` -.\工程分析\ -``` - -如果该目录已存在,则无需重复创建。 +1. 检查并确认项目根目录下存在文件夹: + ``` + .\工程分析\ + ``` +2. 若不存在,则立即创建。 +3. 同时检查该目录下是否存在 `经验记录.md`;若不存在,创建一个空文件并在顶部写入 `# 经验记录`。 --- -## Step 2:需求分析 +## Step 2:需求分析文档 -1. 仔细阅读用户提出的需求,拆解成功能点与非功能点。 -2. 分析影响范围(修改哪些文件、是否有风险点)。 -3. 将分析结果写入文件: +1. 仔细阅读用户提出的原始需求,将其拆解为: + - **功能点**:具体要实现什么 + - **非功能点**:性能、兼容性、向后兼容、安全性等要求 + - **影响范围**:预估需要修改的模块/文件及影响程度 + - **待确认问题**:如有需求模糊之处,列出请用户澄清 +2. 将分析结果写入文件: ``` .\工程分析\需求分析-{workflowTimestamp}.md ``` -4. 文档结构模板: +3. 文档模板: ```markdown # 需求分析 — {workflowTimestamp} @@ -52,12 +61,13 @@ description: 手术图文病历报告系统(Gemini-图文报告系统)的项 ## 需求拆解 ### 功能点 - - + - F1:... + - F2:... ### 非功能点 - - ## 影响范围预估 + ## 影响范围 | 模块 | 影响程度 | 说明 | |------|---------|------| @@ -69,43 +79,45 @@ description: 手术图文病历报告系统(Gemini-图文报告系统)的项 --- -## Step 3:实现方案 +## Step 3:实现方案文档 1. 基于需求分析,设计详细的实现方案: - - 根因分析(如果是 Bug 修复) - - 修改文件清单 - - 具体代码变更(含前后对比) - - 风险点与应对措施 - - 回滚策略 + - **根因分析**(如果是 Bug 修复,必须说明根因) + - **修改文件清单**(精确到文件路径) + - **具体代码变更**(含关键代码的前后对比或伪代码) + - **风险点与应对措施** + - **回滚策略** 2. 将方案写入文件: ``` .\工程分析\实现方案-{workflowTimestamp}.md ``` -3. **必须停止执行,向用户展示实现方案文档内容,并请求人工审核确认。** - - 提示语:"实现方案已完成,请审核 `.\工程分析\实现方案-{workflowTimestamp}.md`。确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。" -4. **在得到用户明确确认(如回复"确认"、"同意"、"OK")之前,禁止进入下一步。** +3. **🛑 强制停止点**:向用户展示实现方案文档内容,并请求人工审核确认。 + - 标准提示语: + > 实现方案已完成,请审核 `.\工程分析\实现方案-{workflowTimestamp}.md`。确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。 + - **在得到用户明确确认(如回复"确认"、"同意"、"OK")之前,禁止进入下一步。** --- -## Step 4:测试方案 +## Step 4:测试方案文档 -1. 基于实现方案设计测试用例: - - 测试目标 - - 测试环境 - - 测试用例(步骤、操作、预期结果) - - 验收标准(勾选列表) - - 测试方式(手工/自动化) +1. 基于已审核通过的实现方案,设计测试方案: + - **测试目标** + - **测试环境** + - **测试用例**(步骤、操作、预期结果,以表格形式呈现) + - **验收标准**(勾选列表 `\- [ ]` 形式) + - **测试方式**(手工 / 自动化,本项目目前无自动化测试框架,通常为手工验证) 2. 将方案写入文件: ``` .\工程分析\测试方案-{workflowTimestamp}.md ``` -3. **必须停止执行,向用户展示测试方案文档内容,并请求人工审核确认。** - - 提示语:"测试方案已完成,请审核 `.\工程分析\测试方案-{workflowTimestamp}.md`。确认无误后回复「确认」或提出修改意见,我将进入最终执行阶段。" -4. **在得到用户明确确认之前,禁止进入下一步。** +3. **🛑 强制停止点**:向用户展示测试方案文档内容,并请求人工审核确认。 + - 标准提示语: + > 测试方案已完成,请审核 `.\工程分析\测试方案-{workflowTimestamp}.md`。确认无误后回复「确认」或提出修改意见,我将进入最终执行阶段。 + - **在得到用户明确确认之前,禁止进入下一步。** --- -## Step 5:执行修改 +## Step 5:执行修改 & 经验记录 ### 5.1 执行前必读 @@ -114,72 +126,94 @@ description: 手术图文病历报告系统(Gemini-图文报告系统)的项 .\工程分析\经验记录.md ``` -将该文档中的关键经验教训纳入本次执行的注意事项中,防止重复犯错。 +- 将该文档中的关键经验教训纳入本次执行的注意事项。 +- 尤其关注与本次修改相关的历史踩坑点,防止重复犯错。 ### 5.2 执行修改 1. 严格按照已审核通过的实现方案修改代码。 -2. 每次修改后应进行必要的验证(如 `npm run lint`、本地编译检查、关键路径手工验证)。 -3. 如果在执行过程中遇到**任何未在实现方案中预料到的问题**(包括修改范围扩大、发现新的 Bug、遇到环境异常、实现方案中的假设不成立等),必须: +2. 每次修改后应进行必要的验证(如 `npm run lint`、本地编译检查)。 +3. 如果在执行过程中遇到**任何未在实现方案中预料到的问题**(包括修改范围扩大、发现新的 Bug、环境异常、方案假设不成立等),必须: - 先解决问题 - - 然后在 `经验记录.md` 中记录 + - 然后将问题及解决方案记录到 `经验记录.md` 中(见 5.3) ### 5.3 更新经验记录 -执行完成后,检查本次过程中是否出现了值得记录的关键问题。如果有,在 `.\工程分析\经验记录.md` 中追加记录,使用**统一的四段式格式**: +执行完成后,检查本次过程中是否出现了值得记录的关键问题。如果有,在 `.\工程分析\经验记录.md` 中**追加**记录,使用**统一的四段式格式**: ```markdown --- -## 记录 N:标题 +## 记录 N:标题(简短概括) **A. 具体问题** -(清晰描述现象) +(清晰描述现象,包括复现步骤) **B. 产生问题原因** -(根因分析,可多条) +(根因分析,可多条,避免表面归因) **C. 解决问题方案** -(具体修改了什么) +(具体修改了哪些文件、哪些代码、什么逻辑) **D. 后续如何避免问题** -(可执行的建议) +(可执行的建议,供未来需求参考) ``` -如果 `经验记录.md` 不存在,则创建它,并在顶部加入 `# 经验记录` 标题。 +- 记录编号按顺序递增(如记录 1、记录 2……)。 +- 如果 `经验记录.md` 原本为空或不存在,创建后顶部加 `# 经验记录`,然后写入第一条记录。 --- ## Step 6:Gitea 备份 -1. 在项目根目录执行以下 Git 操作: - ```bash - git add .\工程分析\ - git commit -m "{workflowTimestamp} - {本次修改的简要描述}" - git push origin main - ``` - - 简要描述应概括本次修改的核心内容(如 "修复路由切换后关键帧丢失问题"、"新增自动帧插入非阻塞优化" 等)。 - - 如果只修改了工程分析文档而没有改代码(理论上不应发生),也应提交这些文档。 -2. 备份完成后,**必须向用户明确提醒**: - > "本次工作流相关文档已备份到 Gitea,commit 信息为:`{workflowTimestamp} - {简要描述}`。" +### 6.1 首次备份(仓库未初始化时) + +如果项目根目录下**没有 `.git` 目录**或没有配置 remote,执行: +```bash +git init +git checkout -b main +git add README.md +# 若 README.md 不存在,先创建:echo "# Medical Sur Report" > README.md +git commit -m "first commit" +git remote add origin http://192.168.31.5:5002/admin/Mdeical_Sur_Report.git +git push -u origin main +``` + +### 6.2 日常备份(已有仓库时) + +执行以下 Git 操作,将本次工作流产出的文档(工程分析目录)备份到 Gitea: +```bash +git add .\工程分析\ +git commit -m "{workflowTimestamp} - {本次修改的简要描述}" +git push origin main +``` + +- **简要描述**应概括本次修改的核心内容(如 "修复路由切换后关键帧丢失问题"、"新增自动帧插入非阻塞优化" 等),控制在 50 字以内。 +- 如果同时修改了源代码,也应将源码变更一并 `git add` 并提交。 + +### 6.3 备份完成提醒 + +备份完成后,**必须向用户明确提醒**: +> 本次工作流相关文档(及代码修改)已备份到 Gitea,commit 信息为:`{workflowTimestamp} - {简要描述}`。 --- ## 禁忌清单(严禁事项) -- ❌ 严禁跳过实现方案审核直接进入代码修改。 -- ❌ 严禁跳过测试方案审核直接执行测试。 -- ❌ 严禁未阅读 `经验记录.md` 就动手改代码。 -- ❌ 严禁执行完成后不更新 `经验记录.md`(如有新问题)。 -- ❌ 严禁执行完成后不进行 Gitea 备份。 -- ❌ 严禁在需求分析、实现方案、测试方案文档中使用不明确的描述,应具体到文件路径和函数名。 +- ❌ **严禁跳过实现方案审核直接进入代码修改。** +- ❌ **严禁跳过测试方案审核直接执行测试或部署。** +- ❌ **严禁未阅读 `经验记录.md` 就动手改代码。** +- ❌ **严禁执行完成后不更新 `经验记录.md`(如有新问题)。** +- ❌ **严禁执行完成后不进行 Gitea 备份。** +- ❌ **严禁在需求分析、实现方案、测试方案文档中使用不明确的描述**,应具体到文件路径、函数名、CSS 类名。 +- ❌ **严禁在 Step 3 / Step 4 的用户确认阶段擅自推进**,必须等待用户明确回复。 --- ## 快速参考:常用命令 ```bash -# 类型检查 +# 类型检查(修改后必须执行) npm run lint # 生产构建 @@ -188,7 +222,7 @@ npm run build # 启动预览服务 npm run preview -- --host 0.0.0.0 -# Git 备份 +# Gitea 日常备份 git add .\工程分析\ git commit -m "{timestamp} - {描述}" git push origin main diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index fb1b001..69983a1 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -1,5 +1,8 @@ # 经验记录 +> 本文档为项目统一知识库,记录开发过程中遇到的关键问题及解决方案。每次执行修改前必须阅读,防止重复踩坑。 +> 记录格式:A. 具体问题 → B. 产生问题原因 → C. 解决问题方案 → D. 后续如何避免问题 + --- ## 记录 1:report-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` 中,光标位于 `

` 行首且后紧跟 `.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()` 处理。 + +--- + +## 记录 15:5 项交互与默认值优化(占位符尺寸、签名状态、素材预加载) + +**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` 表格的 `` 和 `` 中,于「部门」之后、「状态」之前插入签名状态标签。 +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` 中。 +- 表格字段变更时,注意保持 `` 与 `` 的列顺序严格一致,避免列错位。 + +--- + ## 记录 16:模板字段唯一性、删除按钮与报告批量导出 **A. 具体问题** @@ -521,62 +585,32 @@ if ((settings.autoInsertDelay || 0) > 0) { - 侧边栏/工具栏按钮与编辑器共存时,**必须**通过 `onMouseDown={e => e.preventDefault()}` 或等价手段阻止焦点流失,这是保证光标位置不丢失的最简单有效方案。 - 插入操作前恢复 `savedRangeRef` 可以作为焦点流失后的兜底保险,两者结合使用效果最佳。 ---- - -## 记录 14:智能字段插入间距修复与 Backspace 防误删 - -**A. 具体问题** -1. `TemplateManage.tsx` 中使用 `insertSmartField` 插入智能字段后,字段后方会出现一个可见的空格(由 ` ` 和多行模板字符串中的换行/缩进空白引起)。 -2. 在 `contenteditable` 中,光标位于 `

` 行首且后紧跟 `.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. 当目标段落以
结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行( 跑到了

外部)。 +2. 当目标段落以 `
` 结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行(`` 跑到了 `

` 外部)。 **B. 问题产生原因** -1. keydown 事件监听器只拦截了 Backspace/Delete,未拦截 Ctrl+Z/Ctrl+Y,导致浏览器原生 undo 与自定义 undoStack/ -edoStack 完全脱节。 -2. insertSmartField 使用 document.execCommand('insertHTML'),WebKit/Blink 在块级元素末尾存在
时,会自动将插入的 inline 修正到块级元素外部,造成排版错位。 +1. keydown 事件监听器只拦截了 Backspace/Delete,未拦截 Ctrl+Z/Ctrl+Y,导致浏览器原生 undo 与自定义 undoStack/redoStack 完全脱节。 +2. insertSmartField 使用 document.execCommand('insertHTML'),WebKit/Blink 在块级元素末尾存在 `
` 时,会自动将插入的 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() 清空当前选区; + - restoreSelection() 恢复光标; + - Range.deleteContents() 清空当前选区; - 将 HTML 字符串转为 DocumentFragment; - - -ange.insertNode(fragment) 精确插入到 Range 位置; + - Range.insertNode(fragment) 精确插入到 Range 位置; - setStartAfter(lastNode) 把光标移动到插入内容末尾。 **D. 经验与教训总结** - 在 contentEditable 中实现自定义撤销栈时,必须**同时拦截界面按钮和键盘快捷键**的 undo/redo,否则两套历史机制会互相冲突。 -- document.execCommand('insertHTML') 对块级元素边界(尤其是
结尾)的自动修正行为不可控;需要精确插入时,应优先使用 Range.insertNode() 手动操作 DOM。 +- document.execCommand('insertHTML') 对块级元素边界(尤其是 `
` 结尾)的自动修正行为不可控;需要精确插入时,应优先使用 Range.insertNode() 手动操作 DOM。 - 任何对 contentEditable 的 DOM 修改后,都应同步保存内容(saveTemplateContent),确保 localStorage 中的模板数据与编辑器状态一致。 - --- ## 记录 22:TemplateManage 字段体系升级与双向交互联动 @@ -611,7 +645,6 @@ ange.insertNode(fragment) 精确插入到 Range 位置; - 编辑器与侧边栏联动建议使用 `scrollIntoView` + 临时 CSS 类,避免复杂的状态同步。 - 新增 localStorage key 时应提供合理的默认值或降级处理。 - --- ## 记录 23:图片占位符体系重构与双端统一 @@ -642,3 +675,145 @@ ange.insertNode(fragment) 精确插入到 Range 位置; - 在 `contentEditable` 中实现"同行插入"必须使用行内元素(``)并显式设置 `display:inline-flex` + `vertical-align:middle`;块级 `

` 即使通过 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(手动选择/当前时间)和「显示格式」select(date 提供两种日期格式,time 提供 24h/12h)。 + - 字段编辑面板:点击已有时间字段进入编辑模式时,可修改上述两项配置。 +3. **ReportEditor 自动填充**:新增 `useEffect` 监听 `formFields`,对 `timeDefault === 'current'` 且值为空的字段,自动填充系统当前日期/时间。 +4. **ReportEditor 表单渲染重构**: + - `startTime/endTime`:根据 `timeFormat` 选择 hour select 的选项范围(24h: 00-23,12h: 01-12),12h 时额外增加 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. 日期/时间格式仅通过固定 ``,用户可自由输入格式字符串;回车后将输入值加入候选列表并设为当前值。 + - 预设候选包含常用格式:`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()`),必须评估这是否会拦截后续基于字段配置的自动填充逻辑。若会拦截,应改为空值并在最后做兜底赋值。