docs: add AGENTS.md and experience logs for v1.3

This commit is contained in:
2026-04-19 01:49:30 +08:00
parent 36cfcb116e
commit 1dc3d60248
3 changed files with 2232 additions and 0 deletions

289
AGENTS.md Normal file
View File

@@ -0,0 +1,289 @@
# AGENTS.md
> 本文档面向 AI 编程助手。阅读者应假定对项目一无所知。所有信息均基于项目实际代码和开发历史,不做假设性推断。
---
## 项目概述
**手术图文病历报告系统**(版本 1.3是一款纯前端、单页应用SPA面向医院手术室场景用于
- 通过富文本编辑器撰写结构化手术图文报告
- 上传手术视频并自动/手动抽取关键帧,拖拽插入报告
- 管理报告模板、用户权限、系统设置
- 导出 PDF / JSON 格式的报告和模板
**核心特征**
- **无后端服务器**:所有数据(用户、报告、模板、设置、素材)持久化在浏览器 `localStorage`
- **离线可用**:部署后即为静态文件,无需网络 API
- **A4 打印优先**:编辑器按 A4 尺寸210mm × 297mm排版支持浏览器打印转 PDF
- **角色权限控制**:三级角色 `super`(超级管理员)/`admin`(科室管理员)/`user`(医生)
---
## 技术栈
| 层级 | 技术 |
|------|------|
| 框架 | React 19 + TypeScript 5.8 |
| 构建工具 | Vite 6 |
| 样式 | Tailwind CSS v4使用 `@theme``@import "tailwindcss"` 新语法) |
| 路由 | React Router DOM v7 |
| 图标 | lucide-react |
| 动画 | motion |
| AI SDK | `@google/genai`(依赖已安装,但当前源码中**未实际调用**任何 LLM API |
| 运行时 | 纯浏览器客户端;`express` 仅在依赖列表中,未被源码使用 |
---
## 项目结构
```
├── public/ # 静态资源favicon、logo_square.png
├── src/
│ ├── components/
│ │ └── Sidebar.tsx # 左侧导航栏(角色过滤、自动折叠)
│ ├── pages/
│ │ ├── Login.tsx # 登录页 + 全局初始化(默认用户/模板/字段/素材)
│ │ ├── Dashboard.tsx # 工作台统计卡片、SVG 趋势图)
│ │ ├── ReportEditor.tsx# 核心报告编辑器2,200+ 行,最大文件)
│ │ ├── ReportManage.tsx# 报告列表(搜索、筛选、批量操作、历史回溯)
│ │ ├── ReportView.tsx # 报告只读查看 + 打印
│ │ ├── TemplateManage.tsx # 模板编辑器1,600+ 行,自定义 Undo/Redo
│ │ ├── UserManage.tsx # 用户管理RBAC、签名上传、模板权限
│ │ └── SystemSettings.tsx # 系统设置抽帧配置、AI API、默认模板
│ ├── utils/
│ │ ├── storage.ts # localStorage / sessionStorage 封装
│ │ ├── print.ts # iframe 打印工具A4 样式、@page 边距)
│ │ └── defaultContent.ts # 默认模板 HTML腹腔镜胆囊切除术报告
│ ├── App.tsx # BrowserRouter + 路由表
│ ├── main.tsx # React 根挂载StrictMode
│ ├── types.ts # 核心 TypeScript 类型User/Report/Template/FormField 等)
│ └── index.css # Tailwind 入口 + @theme 变量 + 打印媒体查询
├── Dockerfile # 多阶段构建node builder → nginx alpine
├── docker-compose.yaml # 映射宿主机 4002 → 容器 80
├── nginx.conf # SPA 回退、Gzip、静态缓存
├── vite.config.ts # Vite + Tailwind 插件、GEMINI_API_KEY 注入
├── tsconfig.json # ES2022、react-jsx、路径别名 `@/*`
├── package.json
├── .env.example # GEMINI_API_KEY、APP_URL
├── index.html # 入口 HTML标题 "My Google AI Studio App"
└── 过往经验/ # 开发经验记录(经验记录-1.md / 经验记录-2.md
```
---
## 构建与部署命令
```bash
# 开发
npm run dev # vite --port=3000 --host=0.0.0.0
# 生产构建
npm run build # vite build → dist/
npm run preview # vite preview默认端口 4173
# 清理
npm run clean # rm -rf dist
# 类型检查(唯一 lint 手段,无 ESLint
npm run lint # tsc --noEmit
```
**Docker 部署**
```bash
docker-compose up -d --build
# 宿主机访问 http://localhost:4002
```
**无 Docker 环境部署**Windows 等):
```bash
npm run build
npm run preview
# 或任何静态文件服务器托管 dist/
```
---
## 数据持久化与状态管理
- **无全局状态库**(无 Redux、Zustand、Context API
- 每个页面独立通过 `useState` + `useEffect` 管理状态
- **`localStorage` 即数据库**。关键键名:
- `users` — 用户列表
- `currentUser` — 当前登录用户
- `reports` — 报告列表
- `templates` — 模板列表
- `systemSettings` — 系统设置
- `formFieldsConfig` — 动态字段配置
- `imageAssets` — 系统素材库Base64 图片)
- `reportEditorDraft_${username}` — 每用户报告草稿
- `customTimeFormats` — 用户自定义时间格式缓存
⚠️ **localStorage 容量约 5MB**。关键帧图片采用 Canvas 压缩(最大宽度 800px、JPEG 质量 0.6)以避免超限。`storage.ts` 中的异常已改为 `console.error` 输出,不再静默吞掉。
---
## 核心模块说明
### 1. 富文本编辑器ReportEditor.tsx / TemplateManage.tsx
- **底层**:原生 `contentEditable` + `document.execCommand`
- **智能字段Smart Field**:三层嵌套结构
```html
<span class="smart-field-wrapper" contenteditable="false">
<span class="field-label">标签:</span>
<span class="field-value" contenteditable="true" data-bind="key"></span>
</span>&#8203;
```
- 外层 `contenteditable="false"` 保护标签不被逐字删除
- 输入层 `data-bind` 实现与右侧表单的双向绑定
- 末尾追加 `&#8203;`(零宽空格)防止排版换行异常
- **图片占位符**`<span class="image-placeholder">`,支持 `data-mode="frame|manual"` 分类隔离
- **自定义 Undo/Redo**TemplateManage 中已实现基于 HTML 字符串快照的自定义历史栈(`undoStack`/`redoStack`),取代不可靠的浏览器原生 undo
### 2. 视频分析ReportEditor.tsx
- 上传本地视频 → 生成 object URL
- 自动抽帧:按 `systemSettings.framePositions` 百分比位置逐帧截图
- 手动截图:点击按钮从当前播放时间捕获
- 图片压缩Canvas 等比缩放至最大 800px 宽JPEG 质量 0.6
- 拖拽/一键插入:填充到第一个空置的 `image-placeholder:not([data-mode="manual"])`
- 自动帧插入:非阻塞 `setTimeout` 队列式插入,避免阻塞抽帧循环
### 3. 打印系统src/utils/print.ts
- 创建隐藏 iframe写入带 A4 打印样式的 HTML
- `@page { margin: 15mm 10mm; }` 为**每一页**纸张独立分配边距
- `body { padding: 0 }` — 不可用 body padding 代替 @page margin否则第二页及后续页边距失效
- 打印前临时设置 `document.title` 并注入 iframe `<title>`,确保 PDF 默认文件名正确
- 打印后恢复原始标题
### 4. 角色权限RBAC
| 角色 | 权限 |
|------|------|
| `super` | 全部页面、全部数据 |
| `admin` | 仅管理本科室用户;可管理模板;不能看系统设置中的 AI 配置 |
| `user` | 仅创建/查看/编辑自己的报告;可见被分配模板 |
- `Sidebar.tsx` 根据 `currentUser.role` 过滤导航项
- `UserManage.tsx` 中 admin 只能管理 `department` 与自己相同的用户
---
## 开发规范与约定
### 代码风格
- TypeScript 严格模式未开启;`skipLibCheck: true`
- 路径别名 `@/` 映射到项目根目录(`src/` 同级)
- **无 ESLint、无 Prettier、无格式化配置**
- 所有字符串插值、UI 文案、注释均以**中文**为主
### 关键开发教训(必读)
以下经验来自 `过往经验/` 中的 40+ 条记录,是修改本项目时**最容易踩的坑**
#### A. contentEditable 与 DOM 操作
1. **插入 HTML 必须为紧凑单行**:使用 `document.execCommand('insertHTML', ...)` 或 `Range.insertNode()` 时,多行模板字符串中的缩进/换行会被浏览器解析为额外文本节点,破坏排版和光标行为。
2. **删除字段用 `Range.selectNode + execCommand('delete')`**:直接 `target.remove()` 会绕过浏览器撤销栈,且可能在 WebKit 中误删父级 `<p>`。
3. **任何直接操作 DOM 修改编辑器内容后,必须紧跟**
```ts
contentRef.current = editorRef.current.innerHTML;
```
4. **在表格 `<td>` 内插入复杂 inline 元素时**:优先使用块级 `<div>` 作为外层容器,`execCommand('insertHTML')` 对 `<td>` 内的 inline-flex 嵌套会自动"拍平"结构。
5. **对齐操作弃用 `execCommand('justifyLeft'...)`**:改用直接设置 `block.style.textAlign = align`,避免浏览器对混合排版(文字 + 智能字段/占位符)的肢解。
#### B. 自动保存与 Ref/State 同步
1. **永远不要将 `useRef` 作为自动保存的唯一数据源**React 18 `StrictMode` 的"挂载 → 卸载 → 重挂载"会导致 ref 在首次卸载时仍保持初始空值,从而用空数据覆盖有效的 localStorage draft。
2. **自动保存函数应直接从最新的 React state 和 DOM 读取数据**,通过 `useCallback` + 完整 dependency 数组保证闭包新鲜;或从 `stateRef` / `contentRef` 读取稳定快照,但必须在**所有**数据恢复路径中同步 ref。
3. **`setState` 是异步的**`setCapturedFrames(next); saveDraftToStorage();` 的写法会导致闭包读到旧值。若需即时保存,应在 `setState` 回调中触发保存,或从 ref 读取。
4. **组件卸载时 DOM 可能已失效**`editorRef.current?.innerHTML` 在卸载阶段可能为空,应优先使用 `contentRef.current`(内存引用)。
5. **异步循环中不要 `await` 阻塞主流程**:自动帧插入使用 `setTimeout` 推入事件队列,而非 `await new Promise(...)`。
#### C. 图片占位符体系
1. 占位符涉及**三处必须同步修改**`defaultContent.ts`(静态模板)、`ReportEditor.tsx`(运行时插入/填充/删除恢复)、`TemplateManage.tsx`(模板管理)。
2. 占位符创建时需写入 `max-width` / `max-height`;填充后改为 `width:auto; height:auto`,让图片 shrink-wrap删除恢复时需回读 `maxWidth/maxHeight` 重置尺寸。
3. 提示文字使用 `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center;`,要求父容器带 `position:relative`。
4. 占位符分类隔离:通过 `data-mode="frame|manual"` 区分;自动插入和拖拽填充时必须用 `:not([data-mode="manual"])` 过滤。
#### D. 时间/日期字段格式系统
1. 存储格式与显示格式分离:`YYYY-MM-DD` / `HH:mm` 存储;`YYYY年MM月DD日` / `hh:mm A` 显示。
2. 必须同时实现**正向格式化**(存储 → 显示)和**反向解析**(显示 → 存储),否则编辑器内直接编辑 smart field 会导致数据混乱。
3. 12h/24h 判断使用包含性判断:`field.timeFormat.includes('hh') || field.timeFormat.includes('A')`,避免精确匹配无法覆盖自定义格式。
4. 默认值策略:「固定时间」/`specific` 与「当前时间」/`current`。自动填充必须加「仅当值为空时触发」保护,防止编辑已有报告时覆盖用户数据。
5. 时间格式 token 体系:`YYYY`、`MM`、`DD`、`HH`、`hh`、`mm`、`A`。避免使用简写别名如 `'24h'`、`'12h'` 作为存储值。
#### E. 事件与交互
1. **工具栏/字段库按钮必须加 `onMouseDown={(e) => e.preventDefault()}`**:防止点击时编辑器失焦导致 `Selection/Range` 丢失。
2. **插入操作前恢复 `savedRangeRef`**:作为焦点流失后的兜底保险。
3. **双向联动高亮**:通过 `activeFieldKey` 状态 + `useEffect` 直接操作 DOM style`backgroundColor`、`outline`),避免触发组件重渲染导致光标丢失。点击非字段区域时清空高亮。
4. 打印样式必须通过 `@media print` 强制抹除所有交互高亮内联样式(`outline: none !important; box-shadow: none !important;`)。
#### F. 数据初始化与默认值
1. `Login.tsx` 的 `initData()` 是全局唯一初始化入口默认用户、默认模板、默认字段配置、默认设置、素材预加载logo均应在此处完成。
2. 新增 `localStorage` key 时需提供合理的默认值或降级处理。
3. `resetToDefault` / 恢复出厂设置函数必须**包含所有** `SystemSettings` 字段,不能遗漏新增配置项。
4. 修改 `DEFAULT_FORM_FIELDS` 默认值后,已有用户的 `localStorage` 中旧配置不会自动更新;若变更影响核心功能,应考虑启动时做配置迁移或版本校验。
5. 批量操作后必须同步清理 `selectedIds` 和当前选中状态,避免选中已删除项。
---
## 测试策略
**当前状态:零自动化测试。**
- 无单元测试、无集成测试、无 E2E 测试
- 无 Jest、Vitest、Playwright、Cypress 配置
- 唯一类型检查:`npm run lint``tsc --noEmit`
**建议补充方向**(如用户要求):
- `storage.ts` 的 JSON 序列化/反序列化
- `types.ts` 中日期/时间格式化与解析函数的正反向一致性
- 报告编辑器的草稿保存/恢复逻辑
---
## 安全注意事项
1. **密码明文存储**:用户密码以明文形式保存在 `localStorage` 的 `users` 数组中。这是纯前端架构的固有限制,**不适合生产环境处理真实敏感数据**。
2. **无 HTTPS 强制**Docker 部署默认 HTTP 80 端口。
3. **无 API 鉴权**:无后端,因此无 Token、Session、CSRF 防护概念。
4. **XSS 风险**:报告和模板内容直接以 HTML 字符串存储并在 `innerHTML` 中渲染。当前通过 `contentEditable` 限制输入来源,但若导入外部 JSON 模板/报告,需警惕恶意脚本。
5. **Gemini API Key**:通过 Vite `define` 注入客户端,构建后 key 会暴露在静态 JS 中(当前源码未实际调用)。
---
## 部署环境变量
复制 `.env.example` 为 `.env`
```
GEMINI_API_KEY="YOUR_KEY" # Google Gemini API Key当前未在业务代码中使用
APP_URL="YOUR_APP_URL" # 应用托管 URL
```
Vite 构建时仅将 `GEMINI_API_KEY` 注入 `process.env.GEMINI_API_KEY`,其余变量不自动暴露给客户端。
---
## 默认账号(首次登录或清空数据后)
| 账号 | 密码 | 角色 |
|------|------|------|
| admin | 123456 | super |
| manager | 123456 | admin |
| doctor / 0001 | 123456 | user |
---
## 修改前必读检查清单
在修改任何涉及以下内容的功能前,请先搜索并同步检查所有相关文件:
- [ ] **智能字段结构** → `types.ts`、`defaultContent.ts`、`ReportEditor.tsx`、`TemplateManage.tsx`、`index.css`、`print.ts`
- [ ] **图片占位符(创建/填充/删除恢复)** → `defaultContent.ts`、`ReportEditor.tsx`、`TemplateManage.tsx`
- [ ] **打印样式** → `print.ts`、`index.css``@media print`
- [ ] **时间/日期格式** → `types.ts`、`ReportEditor.tsx`、`TemplateManage.tsx`
- [ ] **数据初始化/默认值** → `Login.tsx`、`SystemSettings.tsx`
- [ ] **自动保存/草稿** → `ReportEditor.tsx` 中的 `saveDraftToStorage`、`stateRef`、`contentRef`