From f0eda59c633e69337d257e4b9e615758f88602d0 Mon Sep 17 00:00:00 2001
From: admin <572701190@qq.com>
Date: Mon, 20 Apr 2026 02:54:50 +0800
Subject: [PATCH] =?UTF-8?q?2026-04-20-02-53-00=20-=20=E5=88=9D=E5=A7=8B?=
=?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BC=96=E7=BA=82=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=EF=BC=9A=E5=88=9B=E5=BB=BA=E5=B7=A5=E7=A8=8B=E5=88=86?=
=?UTF-8?q?=E6=9E=90=E7=9B=AE=E5=BD=95=E3=80=81=E6=96=87=E6=A1=A3=E6=A8=A1?=
=?UTF-8?q?=E6=9D=BF=E3=80=81=E7=BB=8F=E9=AA=8C=E8=AE=B0=E5=BD=95=EF=BC=8C?=
=?UTF-8?q?=E5=B9=B6=E5=B0=86=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A7=84=E8=8C=83?=
=?UTF-8?q?=E5=86=99=E5=85=A5AGENTS.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 305 +++++++++
工程分析/00-工作流总览.md | 136 ++++
工程分析/实现方案-模板.md | 74 ++
工程分析/测试方案-模板.md | 76 +++
工程分析/经验记录.md | 32 +
工程分析/需求分析-模板.md | 49 ++
过往经验/经验记录-1.md | 1360 +++++++++++++++++++++++++++++++++++++
过往经验/经验记录-2.md | 583 ++++++++++++++++
过往经验/经验记录-3.md | 916 +++++++++++++++++++++++++
9 files changed, 3531 insertions(+)
create mode 100644 AGENTS.md
create mode 100644 工程分析/00-工作流总览.md
create mode 100644 工程分析/实现方案-模板.md
create mode 100644 工程分析/测试方案-模板.md
create mode 100644 工程分析/经验记录.md
create mode 100644 工程分析/需求分析-模板.md
create mode 100644 过往经验/经验记录-1.md
create mode 100644 过往经验/经验记录-2.md
create mode 100644 过往经验/经验记录-3.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..56b7044
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,305 @@
+# AGENTS.md — 手术图文病历报告系统(AI手术图文报告系统 V2.0)
+
+> 本文档面向 AI 编程助手。如果你正在阅读本文件,说明你对本项目一无所知。以下内容全部基于项目实际代码与配置,请勿假设任何未在本文中显式说明的内容。
+
+---
+
+## 1. 项目概述
+
+本项目是一个**纯前端**的医疗图文报告管理应用,面向医院手术记录场景。核心能力包括:
+
+- 通过富文本编辑器撰写手术图文报告
+- 上传手术视频并自动/手动截取关键帧插入报告
+- 模板管理(创建、维护标准模板,新建报告时自动加载默认模板)
+- 基于角色的权限控制(超级管理员 / 管理员 / 普通用户)
+- AI 辅助撰写(支持 Kimi、DeepSeek、OpenAI 及自定义兼容接口)
+- 报告批量操作(删除、导出 PDF / JSON)
+- 讯飞语音输入(WebSocket 实时转写)
+
+**重要前提**:本项目没有任何后端服务。所有数据(用户、报告、模板、视频帧、系统配置)全部持久化在浏览器 `localStorage` 中。认证与授权逻辑完全在客户端执行。
+
+默认测试账号:
+- `admin` / `123456`
+- `manager` / `123456`
+- `doctor` / `123456`
+
+---
+
+## 2. 技术栈
+
+| 层级 | 技术 |
+|------|------|
+| 框架 | React 19 + TypeScript ~5.8.2 |
+| 构建工具 | Vite 6.2.0(`@vitejs/plugin-react`) |
+| 路由 | React Router DOM v7(`BrowserRouter`) |
+| 样式 | Tailwind CSS v4.1.14(通过 `@tailwindcss/vite` 插件引入,非 PostCSS) |
+| 图标 | `lucide-react` |
+| 动画 | `motion`(Framer Motion 继任者) |
+| AI SDK | `@google/genai`(依赖列表中,实际主要使用 OpenAI 兼容 API) |
+| 其他关键依赖 | `diff`(字符级差异比对)、`crypto-js`(讯飞 HMAC-SHA256 鉴权) |
+
+`package.json` 中虽然声明了 `express` 与 `@types/express`,但源码中未见服务端代码,推测为历史遗留或未使用的依赖。
+
+---
+
+## 3. 项目结构
+
+```
+src/
+├── App.tsx # 根组件,定义所有路由
+├── main.tsx # 应用入口(React 19 + StrictMode)
+├── types.ts # 全局类型定义与默认值常量
+├── index.css # Tailwind v4 引入 + 自定义编辑器/打印/A4 样式
+├── components/
+│ └── Sidebar.tsx # 全局侧边导航栏(带角色过滤、自动收起)
+├── pages/ # 所有页面均为大而整的单文件组件
+│ ├── Login.tsx # 登录 + 初始数据种子写入
+│ ├── Dashboard.tsx # 统计看板、SVG 趋势图、快捷入口
+│ ├── ReportEditor.tsx # 核心编辑器(~2900 行):富文本、视频、AI、Diff、表单
+│ ├── ReportManage.tsx # 报告列表、检索、批量操作、历史版本恢复
+│ ├── ReportView.tsx # 只读报告展示 + 打印触发
+│ ├── TemplateManage.tsx # 模板编辑器(~1700 行)+ 字段库侧边栏
+│ ├── UserManage.tsx # RBAC 用户 CRUD + 电子签名
+│ └── SystemSettings.tsx # 抽帧配置、AI 服务商、讯飞语音参数
+└── utils/
+ ├── storage.ts # localStorage/sessionStorage 封装(含 XOR 加密)
+ ├── print.ts # iframe 静默渲染 + window.print() 实现 A4 打印
+ └── defaultContent.ts # 默认手术报告 HTML 模板(含 data-bind 占位)
+```
+
+**架构特点**:
+- 无外部状态管理库,全部使用 React 原生 `useState` / `useRef` / `useEffect`
+- 无组件拆分策略,页面级组件体积巨大(`ReportEditor.tsx`、`TemplateManage.tsx` 均超 1500 行)
+- 无懒加载,所有页面在 `App.tsx` 中直接静态导入
+- 共享布局仅有 `Sidebar.tsx` 一处
+
+---
+
+## 4. 构建与运行命令
+
+```bash
+# 安装依赖
+npm install
+
+# 本地开发(端口 3000,监听 0.0.0.0)
+npm run dev
+
+# 类型检查(无单元测试)
+npm run lint # 等价于 tsc --noEmit
+
+# 生产构建
+npm run build
+
+# 预览生产构建(端口 4173)
+npm run preview
+
+# 清理构建产物
+npm run clean # rm -rf dist
+```
+
+**环境变量**(复制 `.env.example` 为 `.env.local`):
+- `GEMINI_API_KEY`:Gemini AI API 密钥(构建时通过 Vite `define` 注入为 `process.env.GEMINI_API_KEY`)
+- `APP_URL`:部署后的访问地址,用于自引用链接
+
+**Windows 特别注意**:重新部署前若使用 `npm run preview`,需手动终止旧的 `vite preview` 进程,避免端口冲突。
+
+---
+
+## 5. 开发规范与代码风格
+
+以下规范全部来自 `过往经验/` 中的踩坑记录,**必须遵守**:
+
+### 5.1 contentEditable 编辑器规范
+- **插入 HTML 时,模板字符串必须为紧凑单行**,禁止多行缩进/换行,否则浏览器会解析出额外文本节点破坏排版。
+- **删除特殊节点**优先使用 `Range.selectNode(target) + document.execCommand('delete')`,而非直接 `target.remove()`,以保留浏览器原生撤销栈。
+- **所有工具栏/侧边栏按钮**必须加 `onMouseDown={(e) => e.preventDefault()}`,防止点击时编辑器失焦导致光标位置丢失。
+- **自定义 Undo/Redo**:结构性变更(插入/删除智能字段、表格等)应基于 `innerHTML` 快照实现自定义历史栈(`undoStack` / `redoStack`),并在操作前执行 `pushHistory()`。
+- **键盘事件拦截**:对 `contenteditable="false"` 的内联控件,若处于块级边界,必须拦截 Backspace/Delete(以及 Ctrl+Z/Ctrl+Y),防止浏览器误删父级 `
`。
+- **防换行技巧**:在 `inline-block` / `inline-flex` 控件后追加 ``(零宽空格)作为行内锚点。
+
+### 5.2 自动保存与草稿机制(高频踩坑区)
+- **禁止单独依赖 `useRef` 作为自动保存的唯一数据源**。React 18 `StrictMode` 的模拟卸载会导致 ref 仍保持初始空值,从而用空数据覆盖有效的 `localStorage` 草稿。
+- 在 `ReportEditor.tsx` 中新增任何 `useState` 时,必须三问:
+ 1. 是否需要持久化到 draft?
+ 2. 是否已加入 `stateRef` 初始化?
+ 3. 是否已在 `saveDraftToStorage`、所有恢复分支、以及 state→ref 同步 effect 中补齐?
+- `contentRef.current = editorRef.current.innerHTML` 必须紧跟在任何直接操作 DOM 修改编辑器内容的代码之后。
+
+### 5.3 图片与视频处理
+- **LocalStorage 容量预警**:视频抽帧必须使用 Canvas 等比压缩(最大宽度 800px,JPEG 质量 0.6),单张控制在 30KB~80KB,避免 5MB 上限静默失败。
+- **存储层异常捕获绝不应静默吞掉**,至少输出 `console.error`。
+- **占位符体系**:所有 `.image-placeholder` 必须携带 `data-mode="frame|manual"` 属性。修改占位符的创建/填充/删除逻辑时,必须同步检索所有入口:`fillPlaceholderSrc`、`fillPlaceholder`、`autoCaptureFrames`、拖拽填充、`TemplateManage.tsx`,以及 `defaultContent.ts` 中的预置模板。
+- 填充图片后需将占位符外壳的固定 `width/height` 改为 `auto`,同时保留 `max-width/max-height` 作为硬限制。
+
+### 5.4 智能字段与动态表单
+- **DOM 三层结构**:`` → `` → ``。
+- **双向绑定**:富文本→表单通过 `handleEditorInput` 中 `e.target.hasAttribute('data-bind')` 实时更新;表单→富文本通过 `useEffect` 监听 `reportData`,仅在 `el.innerText !== newValue` 时才重写 DOM,防止光标跳动。
+- **时间/日期字段**:显示格式与存储格式必须分离,且同时实现正向格式化与反向解析;12h/24h 判断使用 `includes('hh') || includes('A')` 等包含性判断,兼容自定义格式。
+
+### 5.5 AI 功能开发规范
+- **模型兼容性**:调用 OpenAI 兼容 API 时,必须根据「是否有图片附件」动态决定 `content` 类型(`string` vs `array`),纯文本模型发送数组会 400。
+- **System Prompt 设计**:
+ - 条件应只依赖用户意图开关(如 `aiModifyEnabled`),不应依赖具体 UI 状态。
+ - 向大模型发送局部修改请求时,必须提供全局上下文,但同时设置**严格的内容边界(Fencing)**,明确声明"全局参考仅供理解,严禁输出"其他模块内容。
+ - 对关键输出字段(如 `updatedHtml`)使用「绝对强制」「绝对不允许」等最强措辞,并在前端增加缺失校验兜底。
+- **降级机制**:目标区域注入必须配备降级方案(如光标处 `execCommand('insertHTML')`);修改模式开启后应自动兜底锁定第一个可用 AI 区域。
+- **DOM 结构修复**:在读取 `.ai-content` 数据前,必须将因用户回车而溢出为兄弟节点的 `` 重新移回容器内。
+- **样式与撤销**:AI 内容注入必须使用 `Range.selectNodeContents + execCommand('insertHTML')` 以保留撤销栈;返回的 HTML 需在后处理阶段统一注入内联字体样式(`font-family: SimSun; font-size: 12pt;`);注入前需清洗(去掉 `
`、段落间空白)。
+- **Diff 对比**:使用 `diff` 库做字符级差异比对,并在注入前去掉 diff 高亮标签。
+
+### 5.6 数据迁移与类型安全
+- 任何涉及 `localStorage` 数据结构变更的重构,必须在初始化入口提供**自动迁移逻辑**,使用 `(obj as any).oldField` 安全访问旧字段,避免类型污染和配置丢失。
+- 从持久化存储读取的数组类型数据,渲染前务必做 `Array.isArray` 校验,防止历史脏数据导致整页崩溃。
+- 当将格式简写迁移为标准 token 时,必须全局搜索所有硬编码默认值,并在初始化时建立无效值清理机制。
+
+### 5.7 UI/UX 通用规范
+- **禁用原生 `prompt`/`confirm`/`alert`**,统一使用项目风格的自定义 Modal 组件。
+- **URL 拼接**:必须对基础路径做尾部斜杠移除 `.replace(/\/+$/, '')`。
+- **打印边距**:使用 `@page { margin: ... }` 为每一页物理纸张独立分配边距,切勿依赖 `body { padding: ... }`。
+- **滚动容器内展开**:点击展开/折叠的交互组件,应考虑增加 `scrollIntoView` 兜底,防止布局突变导致点击失效。
+- **表格列变更**:保持 `` 与 `
` 列顺序严格一致。
+- **大文件修改策略**:对于超过 1500 行的单文件组件,采用「Grep 定位 → 精确读取 → 最小化替换」的三段式策略,避免盲目滚动。
+
+---
+
+## 6. 测试说明
+
+**本项目没有任何测试框架或测试用例**。
+
+- `npm run lint`(`tsc --noEmit`)是唯一的自动化质量门禁。
+- 确保 `tsconfig.json` 已正确 `exclude` 非源码目录(如 `参考信息`、`dist`、`node_modules`),否则参考文件会导致类型检查失败。
+- 由于无后端、无测试,所有功能验证依赖手动端到端测试。
+
+---
+
+## 7. 部署流程
+
+### 7.1 Docker 部署(推荐)
+
+```bash
+docker-compose up -d --build
+```
+
+- 构建阶段:`node:20-alpine` 执行 `npm ci` + `npm run build`
+- 运行阶段:`nginx:alpine` 托管静态产物,暴露容器端口 `80`
+- 宿主机端口映射:`4002` → `80`
+- 服务名:`tuwen_system`,重启策略:`unless-stopped`
+
+### 7.2 无 Docker 静态部署
+
+```bash
+npm run build
+npm run preview
+```
+
+构建产物输出到 `dist/` 目录,可直接作为静态站点托管到任意 Web 服务器。
+
+### 7.3 Nginx 配置要点(`nginx.conf`)
+
+- **SPA 路由**:`try_files $uri $uri/ /index.html`,确保客户端路由正常工作。
+- **Gzip 压缩**:对文本、CSS、JS、XML、JSON 启用。
+- **静态资源缓存**:JS、CSS、图片、字体等设置 `immutable` 1 年缓存。
+
+---
+
+## 8. 安全注意事项
+
+**本项目为纯前端应用,不存在服务端安全层。以下风险必须在部署前被充分理解:**
+
+1. **用户密码以明文形式保存在浏览器 `localStorage` 中**,无后端哈希处理。
+2. **API 密钥与凭证通过源码混淆存放**(字符码数组、XOR 加密),但仍在客户端可还原。
+3. **所有认证与授权逻辑在客户端执行**,可通过开发者工具绕过。
+4. **若用于生产环境,必须部署在内网或受信任环境中**,严禁直接暴露于公网。
+5. `storage.ts` 中对 `systemSettings` 使用 XOR 加密,密钥硬编码于源码,仅提供最低限度的防窥视保护。
+
+---
+
+## 9. 关键架构决策
+
+| 决策 | 说明 |
+|------|------|
+| 纯前端 / 无后端 | 所有数据存 `localStorage`,零服务端依赖,开箱即用 |
+| 原生 `contentEditable` | 不使用第三方富文本库,通过 `document.execCommand` 和自定义 DOM 操作实现 |
+| 单文件大组件 | 页面逻辑、状态、副作用全部内聚在同一文件中,无细粒度组件拆分 |
+| localStorage 即数据库 | 用户、报告、模板、配置、视频帧全部序列化后存入浏览器本地存储 |
+| iframe 打印 | 通过隐藏 iframe 独立渲染 A4 尺寸 HTML,隔离打印样式与交互样式 |
+| OpenAI 兼容 API | AI 功能不绑定特定厂商,通过统一接口支持多服务商切换 |
+
+---
+
+*最后更新:基于当前代码库实际内容生成。若项目结构或依赖发生变更,请同步更新本文件。*
+
+
+---
+
+## 10. 代码编纂工作流(强制执行)
+
+> 以下工作流由项目所有者定义,**所有涉及项目代码修改的需求必须严格执行**,不允许跳过任何步骤。
+
+### 工作流触发条件
+- 用户提出任何与项目修改相关的需求(功能新增、Bug 修复、重构、优化等)
+- 用户明确说"按照工作流执行"或类似指令
+
+### 工作流 7 步骤
+
+#### Step 0:记录开始时间
+在对话开头记录时间戳:`{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}`。
+
+#### Step 1:阅读工程分析
+- 阅读 `.\工程分析\` 文件夹下所有已有文档(需求分析、实现方案、测试方案、经验记录)。
+- 若文件夹为空,基于当前代码库做一次整体工程分析并写入 `.\工程分析\` 留存。
+
+#### Step 2:整理需求
+- 将用户原始需求结构化拆解。
+- 写入 `.\工程分析\需求分析-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`。
+- 模板见 `.\工程分析\需求分析-模板.md`。
+
+#### Step 3:制定实现方案
+- 基于需求分析和代码理解,制定详细实现方案。
+- 写入 `.\工程分析\实现方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`。
+- 模板见 `.\工程分析\实现方案-模板.md`。
+- **⚠️ 写完后必须停止,等待用户二次人工审核确认,严禁擅自继续。**
+
+#### Step 4:制定测试方案
+- 基于实现方案,制定可执行的测试验证方案。
+- 写入 `.\工程分析\测试方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`。
+- 模板见 `.\工程分析\测试方案-模板.md`。
+- **⚠️ 写完后必须停止,等待用户二次人工审核确认,严禁擅自继续。**
+
+#### Step 5:执行前检查 + 经验沉淀
+1. **执行前必读** `.\工程分析\经验记录.md`,避免重复踩坑。
+2. 严格按照已审核的实现方案执行代码修改。
+3. 按已审核的测试方案执行验证。
+4. 若执行过程中遇到任何问题,在 `.\工程分析\经验记录.md` 中以四段式追加:
+ - **A. 具体问题**
+ - **B. 产生问题原因**
+ - **C. 解决问题方案**
+ - **D. 后续如何避免问题**
+
+#### Step 6:Git 备份
+```bash
+git add .
+git commit -m "{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec} - 本次修改简要描述"
+git push origin main
+```
+- 完成后向用户报告:"已完成对文档的备份 commit。"
+
+#### Step 7:重新部署
+```bash
+npm install # 如需
+npm run lint
+npm run build
+npm run preview # 如需预览
+```
+- 确保构建成功无错误,向用户报告部署完成。
+
+### 不可跳过的硬性要求
+
+| 要求 | 说明 |
+|------|------|
+| 时间戳贯穿 | 同一需求的所有文档使用同一时间戳 |
+| 用户审核卡点 | 实现方案、测试方案文档写完后必须人工确认 |
+| 经验沉淀 | 遇到问题必须写入经验记录.md,四段式格式 |
+| Git 备份 | 每次修改完成后必须 commit + push |
+| 构建验证 | 必须执行 `npm run build` 并确保成功 |
+| 规范兼容 | 实现方案必须自检 AGENTS.md 第 5 章规范清单 |
diff --git a/工程分析/00-工作流总览.md b/工程分析/00-工作流总览.md
new file mode 100644
index 0000000..1d96689
--- /dev/null
+++ b/工程分析/00-工作流总览.md
@@ -0,0 +1,136 @@
+# 代码编纂工作流总览
+
+> 本文档定义了所有项目修改需求的标准执行流程。
+> 每次接到修改需求后,严格按照以下 7 个步骤执行。
+
+---
+
+## 执行前准备
+
+- 记录当前时间戳:`{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}`
+- 时间戳将作为本次需求的唯一标识,贯穿所有文档命名
+
+---
+
+## Step 0:记录开始时间
+
+在对话开头明确记录:
+```
+[工作流开始] {Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}
+```
+
+---
+
+## Step 1:阅读工程分析
+
+- 阅读 `.\工程分析\` 文件夹下所有已有文档
+- 了解项目当前状态、历史修改、已知踩坑点
+- 若文件夹为空(首次执行),则基于当前代码库做一次整体工程分析并留存
+
+---
+
+## Step 2:整理需求
+
+- 将用户提出的修改需求整理成结构化文档
+- 写入 `.\工程分析\需求分析-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+- 文档结构见 [需求分析-模板.md](./需求分析-模板.md)
+
+---
+
+## Step 3:制定实现方案
+
+- 基于需求分析和代码理解,制定详细的实现方案
+- 写入 `.\工程分析\实现方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+- 文档结构见 [实现方案-模板.md](./实现方案-模板.md)
+- **⚠️ 必须经用户二次人工审核确认后方可继续**
+
+---
+
+## Step 4:制定测试方案
+
+- 基于实现方案,制定可执行的测试验证方案
+- 写入 `.\工程分析\测试方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+- 文档结构见 [测试方案-模板.md](./测试方案-模板.md)
+- **⚠️ 必须经用户二次人工审核确认后方可继续**
+
+---
+
+## Step 5:执行前检查 + 经验沉淀
+
+### 5.1 执行前必读
+- 阅读 `.\工程分析\经验记录.md`
+- 避免重复踩坑
+
+### 5.2 执行修改
+- 严格按照已审核的实现方案执行代码修改
+- 按测试方案执行验证
+
+### 5.3 执行后记录
+- 若执行过程中遇到任何问题,在 `.\工程分析\经验记录.md` 中以以下四段式追加:
+
+```markdown
+## [{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}] 问题简述
+
+### A. 具体问题
+(描述遇到的现象或错误)
+
+### B. 产生问题原因
+(根因分析)
+
+### C. 解决问题方案
+(具体的解决步骤或代码改动)
+
+### D. 后续如何避免问题
+(预防措施、规范建议、检查清单项)
+```
+
+---
+
+## Step 6:Git 备份
+
+使用 gitea 进行备份提交:
+
+```bash
+# 添加所有变更(含新增的需求/方案文档)
+git add .
+
+# 提交,消息格式:"{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec} - 本次修改简要描述"
+git commit -m "2026-04-20-02-46-43 - 示例:修复报告编辑器自动保存失效问题"
+
+# 推送至远程主分支
+git push origin main
+```
+
+- 完成后向用户报告:"已完成对文档的备份 commit"
+
+---
+
+## Step 7:重新部署
+
+执行项目构建并部署:
+
+```bash
+# 安装依赖(如 node_modules 缺失)
+npm install
+
+# 类型检查
+npm run lint
+
+# 生产构建
+npm run build
+
+# 如需预览
+npm run preview
+```
+
+- 确保构建成功无错误
+- 向用户报告部署完成
+
+---
+
+## 附件
+
+- [需求分析-模板.md](./需求分析-模板.md)
+- [实现方案-模板.md](./实现方案-模板.md)
+- [测试方案-模板.md](./测试方案-模板.md)
+- [经验记录.md](./经验记录.md)
diff --git a/工程分析/实现方案-模板.md b/工程分析/实现方案-模板.md
new file mode 100644
index 0000000..a355ada
--- /dev/null
+++ b/工程分析/实现方案-模板.md
@@ -0,0 +1,74 @@
+# 实现方案
+
+> 时间戳:{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}
+> 对应需求分析:`需求分析-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+
+---
+
+## 1. 方案概述
+
+(用 2-3 句话概括整体实现思路)
+
+---
+
+## 2. 详细改动清单
+
+### 2.1 文件:xxx.tsx
+
+| 行号范围 | 改动类型 | 说明 |
+|----------|----------|------|
+| | 新增/修改/删除 | |
+
+**具体代码逻辑**:
+```typescript
+// 关键代码片段或伪代码
+```
+
+### 2.2 文件:yyy.ts
+
+(同上格式)
+
+---
+
+## 3. 关键设计决策
+
+| 决策点 | 选择方案 | 备选方案 | 不选备选的原因 |
+|--------|----------|----------|----------------|
+| | | | |
+
+---
+
+## 4. 与现有规范的兼容性检查
+
+- [ ] contentEditable 规范(5.1)
+- [ ] 自动保存与草稿机制(5.2)
+- [ ] 图片与视频处理(5.3)
+- [ ] 智能字段与动态表单(5.4)
+- [ ] AI 功能开发规范(5.5)
+- [ ] 数据迁移与类型安全(5.6)
+- [ ] UI/UX 通用规范(5.7)
+
+---
+
+## 5. 回滚策略
+
+若方案执行失败或验证不通过,如何快速回滚:
+
+```bash
+# 回滚命令(示例)
+git reset --hard HEAD~1
+```
+
+---
+
+## 6. 预估工作量
+
+| 阶段 | 预估时间 |
+|------|----------|
+| 代码修改 | |
+| 测试验证 | |
+| 文档更新 | |
+
+---
+
+**状态**:□ 待审核 | □ 已确认 | □ 已驳回(原因:)
diff --git a/工程分析/测试方案-模板.md b/工程分析/测试方案-模板.md
new file mode 100644
index 0000000..6c16df3
--- /dev/null
+++ b/工程分析/测试方案-模板.md
@@ -0,0 +1,76 @@
+# 测试方案
+
+> 时间戳:{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}
+> 对应需求分析:`需求分析-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+> 对应实现方案:`实现方案-{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}.md`
+
+---
+
+## 1. 测试环境
+
+- 浏览器:
+- Node 版本:
+- 是否使用默认测试数据:□ 是 / □ 否
+
+---
+
+## 2. 功能测试用例
+
+| 用例ID | 测试场景 | 操作步骤 | 预期结果 | 实际结果 | 是否通过 |
+|--------|----------|----------|----------|----------|----------|
+| TC-01 | | 1.
2. | | | □ |
+| TC-02 | | 1.
2. | | | □ |
+
+---
+
+## 3. 边界测试
+
+| 场景 | 测试内容 | 预期表现 |
+|------|----------|----------|
+| 空数据 | | |
+| 大数据量 | | |
+| 异常输入 | | |
+
+---
+
+## 4. 回归测试范围
+
+(列出可能受影响的已有功能,需验证未被破坏)
+
+- [ ] 功能 A
+- [ ] 功能 B
+
+---
+
+## 5. 类型检查
+
+```bash
+npm run lint
+```
+
+- 预期结果:`tsc --noEmit` 无错误
+
+---
+
+## 6. 构建验证
+
+```bash
+npm run build
+```
+
+- 预期结果:构建成功,无报错,产物输出至 `dist/`
+
+---
+
+## 7. 测试结论
+
+| 项目 | 结果 |
+|------|------|
+| 功能测试 | □ 通过 / □ 部分通过 / □ 未通过 |
+| 类型检查 | □ 通过 / □ 未通过 |
+| 构建验证 | □ 通过 / □ 未通过 |
+| 整体结论 | □ 可交付 / □ 需修复后复测 |
+
+---
+
+**状态**:□ 待审核 | □ 已确认 | □ 已驳回(原因:)
diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md
new file mode 100644
index 0000000..e2e8fd7
--- /dev/null
+++ b/工程分析/经验记录.md
@@ -0,0 +1,32 @@
+# 经验记录
+
+> 本文档用于沉淀项目修改过程中遇到的关键问题及解决方案。
+> 每次执行工作流 Step 5 后,按四段式追加新记录。
+> 执行修改前必须阅读本文档,避免重复踩坑。
+
+---
+
+## 记录格式
+
+```markdown
+## [{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}] 问题简述
+
+### A. 具体问题
+(描述遇到的现象或错误)
+
+### B. 产生问题原因
+(根因分析)
+
+### C. 解决问题方案
+(具体的解决步骤或代码改动)
+
+### D. 后续如何避免问题
+(预防措施、规范建议、检查清单项)
+```
+
+---
+
+## 历史记录
+
+(以下为实际踩坑记录,按时间倒序排列)
+
diff --git a/工程分析/需求分析-模板.md b/工程分析/需求分析-模板.md
new file mode 100644
index 0000000..2d49e3b
--- /dev/null
+++ b/工程分析/需求分析-模板.md
@@ -0,0 +1,49 @@
+# 需求分析
+
+> 时间戳:{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec}
+
+---
+
+## 1. 原始需求描述
+
+(用户原始表述,尽量保持原话)
+
+---
+
+## 2. 需求拆解
+
+| 序号 | 子需求 | 涉及页面/模块 | 优先级 |
+|------|--------|---------------|--------|
+| 1 | | | |
+| 2 | | | |
+| 3 | | | |
+
+---
+
+## 3. 影响范围评估
+
+### 3.1 需要修改的文件
+- `src/...`
+- `src/...`
+
+### 3.2 潜在风险点
+-
+-
+
+### 3.3 是否涉及数据迁移
+- [ ] 否
+- [ ] 是,说明:
+
+---
+
+## 4. 验收标准
+
+- [ ] 标准1:
+- [ ] 标准2:
+- [ ] 标准3:
+
+---
+
+## 5. 关联历史需求
+
+(如有相似或相关历史修改,在此引用对应的需求分析文档)
diff --git a/过往经验/经验记录-1.md b/过往经验/经验记录-1.md
new file mode 100644
index 0000000..d0ec5d6
--- /dev/null
+++ b/过往经验/经验记录-1.md
@@ -0,0 +1,1360 @@
+# 经验记录
+
+> 本文档为项目统一知识库,记录开发过程中遇到的关键问题及解决方案。每次执行修改前必须阅读,防止重复踩坑。
+> 记录格式:A. 具体问题 → B. 产生问题原因 → C. 解决问题方案 → D. 后续如何避免问题
+
+---
+
+## 记录 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. 将 "插入" 按钮从底部文字行移到图片层的 `` 容器内,使用 `absolute inset-0 m-auto w-fit h-fit` 实现水平和垂直居中。
+2. 将按钮样式改为实体胶囊按钮:`px-3 py-1.5 bg-emerald-500 text-white rounded-full shadow-md`,hover 时加深为 `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. 初始化 `useEffect` 和 `useLayoutEffect` 从 draft 或已保存报告恢复数据时,仅通过 `setState` 更新了 React state(`videos`、`capturedFrames`),但 **没有同步更新 `stateRef.current`**。
+3. 用户首次进入页面时数据正确显示;离开页面时,`stateRef.current` 仍保存着初始值(空数组),导致 `saveDraftToStorage()` 用空数组覆盖了 localStorage 中的 draft。
+4. 再次返回页面时,系统优先读取被污染后的 draft,从而丢失了所有视频分析数据。
+
+**C. 解决问题方案**
+在 `ReportEditor.tsx` 的 6 个数据恢复入口(初始化 `useEffect` 的 3 个分支 + `useLayoutEffect` 安全网的 3 个分支)中,恢复 `reportData`、`videos`、`capturedFrames` 后立即同步赋值给 `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 恢复已有报告、draf t 恢复新建报告)中,将 `stateRef.current` 的同步赋值**移到 `editorRef.current/content` 判断条件的外部**,确保无论编辑器 DOM 是否已挂载、`content` 是否为空,`reportData`、`videos`、`capturedFrames` 都会立即写入 `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.json` 的 `preview` 脚本中增加 `--port` 参数(如 `vite preview --port 8080`)。
+
+---
+
+## 记录 8:路由切换后所有内容仍然丢失——彻底重构自动保存机制
+
+**A. 具体问题**
+在 `/report-editor` 中编辑报告(填写基本信息、上传视频、自动/手动截取关键帧、拖拽图片到 placeholder)后,切换到 `/report-manage` 再返回 `/report-editor`,报告编辑器内容、基本信息、视频列表、关键帧截图**全部丢失**。
+
+**B. 产生问题原因**
+1. 自动保存机制过度依赖 `stateRef` 和 `contentRef` 作为"数据快照"。
+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.current` 和 `stateRef.current`,而是直接从最新的 React state 和 `editorRef.current?.innerHTML` 获取数据。`useCallback` 的 dependency 数组包含 `reportData`、`videos`、`capturedFrames`、`activeTab`、`loadedTemplateId`、`reportId`,确保闭包永远绑定当前渲染周期的最新 state。
+2. **重构自动保存 effect**:将 `beforeunload` 和 `visibilitychange` 事件处理器直接绑定到 `saveDraftToStorage`,effect 的 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 state(如 `capturedFrames`、`videos`)读取。但代码中大量存在 `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`。
+ - `reportData`、`videos`、`capturedFrames`、`activeTab`、`loadedTemplateId` 全部从 `stateRef.current` 读取,彻底避开 React state 的闭包陷阱。
+ - `useCallback` 的 dependency 仅保留 `[reportId]`,避免因 state 变化产生陈旧闭包。
+2. **补齐 `contentRef` 遗漏**:在 `handleEditorClick` 的 `document.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. 产生问题原因**
+`autoCaptureFrames` 的 `for` 循环内部,自动插入逻辑使用了 `await new Promise
(r => setTimeout(...))`:
+```tsx
+if ((settings.autoInsertDelay || 0) > 0) {
+ await new Promise(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` 阻塞主循环**,应改用 `setTimeout` 或 `Promise.all` 实现并行/异步解耦。
+- 当多个定时任务需要按顺序执行时,可以通过索引计算累积延迟(`delay * (index + 1)`),实现简单的"队列式"顺序触发,而不需要阻塞主流程。
+- 在 `setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。
+
+---
+
+## 记录 11:关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志
+
+**A. 具体问题**
+报告编辑器内容和视频列表在路由切换后能正常保留,但视频分析面板中的自动摘取关键帧和手动截图全部丢失。
+
+**B. 产生问题原因**
+1. **LocalStorage 5MB 容量限制**:当前抽帧逻辑使用视频原始分辨率 + JPEG 质量 0.9:
+ ```tsx
+ 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` 但没有任何日志:
+ ```typescript
+ } catch {
+ // ignore quota exceeded
+ }
+ ```
+ 当 `saveDraftToStorage()` 尝试保存大量关键帧时,`localStorage.setItem` 抛出异常,draft 无法更新,但用户和开发者都感知不到错误。最终返回 `/report-editor` 时,只能读取到"有视频、无关键帧"的旧 draft。
+
+**C. 解决问题方案**
+1. **压缩关键帧分辨率与质量**:
+ - 在 `captureFrame()`(手动截图)和 `autoCaptureFrames()`(自动抽帧)中,增加 Canvas 等比缩放:
+ ```tsx
+ 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.ts` 的 `set` 和 `setSession` 中,将静默 `catch` 改为输出 `console.error`:
+ ```typescript
+ } catch (e) {
+ console.error('Storage save failed (possibly quota exceeded):', e);
+ }
+ ```
+
+**D. 后续如何避免问题**
+- 任何将 Base64 图片持久化到 `localStorage` 的场景,都必须**预估数据体积**并对图片进行适当的分辨率/质量压缩。
+- 存储层的异常捕获**绝不应静默吞掉**,至少要输出日志,必要时还应弹出用户提示。
+- 对于需要存储大量图片的医疗/图文报告系统,应将 `localStorage` 逐步迁移到 `IndexedDB`,从根本上解除 5MB 容量瓶颈。
+- 在开发测试阶段,应使用高分辨率视频和大批量关键帧进行压力测试,提前暴露存储容量问题。
+
+---
+
+## 记录 12:contentEditable 中实现标签锁定与输入方格的双向绑定
+
+**A. 具体问题**
+需要在 `ReportEditor` 和 `TemplateManage` 的富文本编辑器中插入"标签锁定、内容可调"的智能占位控件,使"姓名:"等固定文本不会被用户误删,同时方格内的输入能与右侧【基本信息】表单双向联动。
+
+**B. 产生问题原因**
+原生 `contentEditable` 区域内所有文本节点对用户都是可编辑的,无法直接保护某一段固定标签不被单独删除或篡改。若仅用样式区分的普通 ``,用户仍可通过退格键将"姓名:"删掉一半或改乱。
+
+**C. 解决问题方案**
+采用三层嵌套 HTML 结构:
+1. **外层** ``:作为不可编辑的框架,确保整个控件不会被内部逐字删除。
+2. **标签层** ``:显示固定文本如"姓名:",受外层保护。
+3. **输入层** ``:允许用户输入,并通过 `data-bind` 属性建立与 `reportData` 的映射关系。
+
+双向绑定逻辑:
+- **富文本 → 表单**:在 `handleEditorInput` 中通过 `e.target.hasAttribute('data-bind')` 判断输入源,实时更新 `reportData`。
+- **表单 → 富文本**:在 `useEffect` 中监听 `reportData` 变化,仅当 `el.innerText !== newValue` 时才重写 DOM,防止光标跳动。
+
+**D. 后续如何避免问题**
+- 对于需要在富文本中保护的固定文本,优先采用 `contenteditable="false"` 的包装器,而不是仅靠样式区分的普通 ``。
+- 在 `State -> DOM` 的同步中务必加入差异判断,避免不必要的 DOM 重写导致输入焦点异常。
+- 数组类型字段(如 `surgeon`)在同步到方格前应先 `join(', ')` 转换为字符串,保持显示一致性。
+
+---
+
+## 记录 13:手术时间方框化、动态字段分类体系与 UI 紧凑化
+
+**A. 具体问题**
+1. 手术开始/终止时间在模板中是纯文本"时 分",无法与右侧表单联动。
+2. `TemplateManage` 的字段库是静态列表,无法按医院需求自定义字段;`ReportEditor` 的右侧表单全部硬编码,每新增一个字段就要改代码。
+3. `field-value` 方格使用了 `min-width: 60px` 和上下 `padding`,导致行间距被撑大,排版松散。
+
+**B. 产生问题原因**
+1. 时间字段在 `defaultContent.ts` 中没有使用 `data-bind` 智能控件,且右侧表单将时间拆分为 `startHour`/`startMinute` 两个独立字段,缺少与方格的双向转换层。
+2. 早期设计采用了"硬编码表单"思路,字段名、类型、选项全部写死在 `ReportEditor.tsx` 的 JSX 中,不具备扩展性。
+3. `inline-block` 元素自带上下 `padding` 和 `border`,超出了默认行高,浏览器不得不增大整行高度以容纳它。
+
+**C. 解决问题方案**
+1. **时间方框联动**:
+ - 在 `defaultContent.ts` 中替换为 `data-bind="startTime"` 和 `data-bind="endTime"` 的方格。
+ - 在 `ReportEditor.tsx` 的 `handleEditorInput` 中,对 `startTime`/`endTime` 使用 `split(':')` 解析,反向更新 `startHour`/`startMinute`;在 `useEffect(reportData)` 中拼接 `HH:mm` 同步回方格。
+2. **动态字段体系**:
+ - 在 `types.ts` 中新增 `FieldType`、`FormField`、`DEFAULT_FORM_FIELDS`,定义字段的 key/label/分类/类型/显隐/锁定状态/选项。
+ - 使用 `localStorage` 的 `formFieldsConfig` 持久化字段配置。
+ - `TemplateManage.tsx` 右侧字段库重构为 Tab 结构:【插入字段】按"填空/单选/多选/时间"分组;【字段管理】支持新增、删除(非锁定字段)、显隐开关。
+ - `ReportEditor.tsx` 右侧基本信息表单改为遍历 `formFieldsConfig`、按 `type` switch-case 动态渲染(文本框/下拉框/多选标签/时间拆分下拉框)。
+3. **UI 紧凑化**:
+ - 将 `min-width` 从 `60px` 缩至 `32px`。
+ - 去除上下 `padding`,使用 `line-height: 1.2`、`font-size: inherit`、`vertical-align: text-bottom`。
+ - 背景色改为 `#f8fafc`(编辑态更明显),打印时恢复透明并只保留下划线。
+
+**D. 后续如何避免问题**
+- 对于需要将多个子字段映射到单一 UI 控件的场景,应在事件处理器和 `useEffect` 中各维护一层"拼接/解析"转换逻辑,保持底层数据结构不变。
+- 当表单字段超过 5 个且存在频繁变更需求时,应尽早从硬编码 JSX 转向"配置驱动渲染"(Config-Driven UI),降低后续维护成本。
+- 在 `contentEditable` 中插入 `inline-block` 元素时,务必通过 `line-height`、`vertical-align` 和最小化 `padding` 控制其对行高的影响,避免破坏段落排版的紧凑性。
+
+---
+
+## 记录 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. 具体问题**
+1. `TemplateManage` 中智能字段可以重复插入多次,导致模板混乱。
+2. 智能字段在某些边界位置(如段落开头/结尾)无法通过 Backspace/Delete 删除。
+3. `ReportManage` 缺少报告导出功能和批量操作能力。
+
+**B. 产生问题原因**
+1. `insertSmartField` 没有检测 DOM 中是否已存在相同 `data-bind` 的字段节点。
+2. 之前的 `keydown` 拦截逻辑只处理了光标在文本节点内的情况,没有处理光标直接在块级父节点边界(`startContainer` 为 `` 等块元素)的场景。
+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. **签名排版优化**:
+ - 在 `index.css` 和 `print.ts` 中定义 `.report-signature-img`:
+ ```css
+ .report-signature-img {
+ height: 2.4em;
+ width: auto;
+ vertical-align: middle;
+ display: inline-block;
+ margin: -0.3em 0;
+ }
+ ```
+ - 打印媒体查询中同步使用 `!important` 确保打印输出也保持同样尺寸。
+
+**D. 后续如何避免问题**
+- 当需要在 React 之外直接操作 DOM 样式实现即时反馈时,优先使用原生事件 + inline style(避免触发组件重渲染导致光标丢失)。
+- 任何新增的持久化字段,应在类型定义(TypeScript interface)、默认值(DEFAULT_xxx)、以及所有相关读写逻辑中同步补齐,防止类型不一致。
+- 在 `contentEditable` 中插入图片时,务必通过 CSS 对 `height`/`width`/`vertical-align` 做严格约束,避免原图尺寸破坏文本流。
+- 涉及打印的样式必须在 iframe 打印模板和 `@media print` 中双端同步,防止打印效果与屏幕预览不一致。
+
+---
+
+## 记录 19:撤销栈修复、字段删除交互优化与签名字段闭环
+
+**A. 具体问题**
+1. `TemplateManage` 中通过红色 × 或键盘删除智能字段后,浏览器撤销栈(Undo)失效,点击"撤销"按钮无法恢复。
+2. 插入"手术日期"、"手术者签名"等字段后,字段框有时会跳到下一行。
+3. Backspace 键无法删除字段;Delete 键会误删字段前面的大段文本(如"手术步骤、术中出现的情况及处理:")。
+4. 签名图片没有最大尺寸限制;"手术者签名"字段不在 ReportEditor 表单中显示,无法受控管理签字状态。
+5. 点击"完成报告"时缺少对签名状态的确认提示。
+
+**B. 产生问题原因**
+1. 删除字段时使用了 `target.remove()` 直接操作 DOM,绕过了浏览器的原生撤销栈(`undo stack`)。
+2. 插入的 `smart-field-wrapper` 是 `inline-block` 元素,但其后缺少行内锚点文本节点,浏览器在特定光标位置插入时容易将其挤到新行。
+3. `keydown` 拦截逻辑中 `target.remove()` 同样会误删父级块节点(WebKit 在边界处对 `contenteditable="false"` inline 元素的处理缺陷)。
+4. `surgeonSignature` 字段原先 `visibleInForm: false`,且签名图片样式仅用 `height: 2.4em` 约束,没有 `max-width/max-height` 的硬限制。
+5. 完成报告逻辑中缺少针对签名字段的业务校验。
+
+**C. 解决问题方案**
+1. **撤销栈修复**:将点击红 × 删除和键盘 Backspace/Delete 删除全部改为 `Range.selectNode(target)` + `document.execCommand('delete')`。这样浏览器会将删除操作记录到撤销栈中,`execCommand('undo')` 可以正确恢复。
+2. **防换行**:在 `insertSmartField` 和 `defaultContent.ts` 的 `smartField()` 生成的 HTML 末尾增加 ``(零宽空格),作为稳定的行内锚点,防止字段被浏览器排到新行。
+3. **精准键盘删除**:配合 `Range.selectNode` + `execCommand('delete')`,不再直接 `remove()` DOM 节点,彻底避免误删父级 `` 的问题。
+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([])`。
+ - 实现 `pushHistory()`,在执行任何结构性变更(删除字段、插入字段、插入表格/图片、格式化命令)前将当前 `editorRef.current.innerHTML` 推入 undo 栈并清空 redo 栈。
+ - 实现 `handleUndo()` / `handleRedo()`,直接替换工具栏按钮的 `execCmd('undo')` / `execCmd('redo')` 调用。从栈中取出历史 HTML 字符串并赋值给 `editorRef.current.innerHTML`,再调用 `saveTemplateContent()` 同步到 React state 和 `localStorage`。
+2. **阻止焦点流失**:
+ - 在所有工具栏按钮和字段库插入按钮上增加 `onMouseDown={(e) => e.preventDefault()}`,阻止 mousedown 默认行为导致编辑器失去焦点。
+3. **光标位置记忆与恢复**:
+ - 利用已有的 `savedRangeRef`,实现 `saveSelection()` 和 `restoreSelection()`。
+ - 在编辑器 `` 上绑定 `onBlur={saveSelection}`、`onMouseUp={saveSelection}`、`onKeyUp={saveSelection}`,持续记录光标位置。
+ - 在 `insertSmartField` 和 `insertImage` 中,执行 `insertHTML` 前先调用 `restoreSelection()` 恢复光标,确保字段插入到正确的位置。
+
+**D. 后续如何避免问题**
+- 对于 `contentEditable` 编辑器中的结构性变更(插入/删除特殊节点),如果原生 undo 不可靠,应尽早实现自定义历史栈(基于 HTML 字符串快照),完全接管撤销/重做逻辑。
+- 侧边栏/工具栏按钮与编辑器共存时,**必须**通过 `onMouseDown={e => e.preventDefault()}` 或等价手段阻止焦点流失,这是保证光标位置不丢失的最简单有效方案。
+- 插入操作前恢复 `savedRangeRef` 可以作为焦点流失后的兜底保险,两者结合使用效果最佳。
+
+---
+
+## 记录 21:TemplateManage 快捷键 Undo/Redo 与字段插入排版修复
+
+**A. 具体问题**
+1. TemplateManage 中删除 smart-field-wrapper 后按键盘 Ctrl+Z 无法撤销,但点击工具栏撤销按钮可以恢复。
+2. 当目标段落以 `
` 结尾时,从字段库插入 smart-field-wrapper 会被拆到下一行(`
` 跑到了 `` 外部)。
+
+**B. 问题产生原因**
+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():
+ - restoreSelection() 恢复光标;
+ - Range.deleteContents() 清空当前选区;
+ - 将 HTML 字符串转为 DocumentFragment;
+ - Range.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. 插入图片占位符时无法自定义默认宽高,且使用 `` 导致强制换行。
+3. 占位符框太小时"插入/点击放置图片"文字显示不全。
+4. 默认模板中签名和 Logo 的结构不统一(一个是 `smartField`,一个是 `div.image-placeholder`)。
+5. `template-manage` 点击图片占位符直接调起本地文件选择器,与 `report-editor` 的三选一弹窗行为不一致。
+
+**B. 问题产生原因**
+1. `DEFAULT_FORM_FIELDS` 仍包含 `surgeonSignature` 和 `hospitalLogo`。
+2. 两端编辑器的 `insertImage()` 使用块级 `
` 插入,未提供尺寸 prompt。
+3. 占位符提示文本固定为长文本,未根据容器宽度做缩写适配。
+4. `TemplateManage` 的 placeholder 点击事件直接调用 `triggerPlaceholderUpload()`,缺少与 `ReportEditor` 一致的弹窗组件。
+
+**C. 解决问题方法**
+1. **清理图片字段**:从 `DEFAULT_FORM_FIELDS` 和 `types.ts` 中移除 `surgeonSignature` 和 `hospitalLogo`;在 `TemplateManage.tsx` 的插入字段/字段管理/新增字段表单中彻底移除"图片"分类。
+2. **统一默认模板**:在 `defaultContent.ts` 中将 Logo 和签名均替换为 `
`。
+3. **改造 insertImage()**:在 `TemplateManage.tsx` 和 `ReportEditor.tsx` 中,插入前通过 `prompt` 获取最大宽度/高度(px),生成带 `max-width/max-height` 的 `` 行内占位符;提示文字中附加"正文一行文字高度约为 20 像素左右"。
+4. **文本自适应**:根据 prompt 输入的宽度决定提示文字:宽度 < 80px 时显示"插入图片",否则显示"插入/点击放置图片"。
+5. **统一弹窗行为**:将 `ReportEditor` 的 `imagePickerOpen` / `imagePickerTarget` / `fillPlaceholderSrc` 逻辑完整移植到 `TemplateManage`;删除旧的 `triggerPlaceholderUpload` 直接上传逻辑;两端点击图片占位符均弹出"本地上传 / 我的签名 / 系统素材"三选一弹窗。
+6. **优化填充样式**:`fillPlaceholderSrc` 中给 `
` 增加 `max-width:100%; max-height:100%; object-fit:contain;`,避免撑破设置了固定尺寸的占位符。
+
+**D. 经验与教训总结**
+- 当从字段体系中彻底移除某一分类时,需要同时清理:`DEFAULT_FORM_FIELDS`、UI 渲染数组、新增表单 options、以及可能残留的分类判断逻辑(如编辑字段时显示 options 输入框的条件)。
+- 在 `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. 日期/时间格式仅通过固定 `