15 KiB
15 KiB
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)
构建与部署命令
# 开发
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 部署:
docker-compose up -d --build
# 宿主机访问 http://localhost:4002
无 Docker 环境部署(Windows 等):
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):三层嵌套结构
<span class="smart-field-wrapper" contenteditable="false"> <span class="field-label">标签:</span> <span class="field-value" contenteditable="true" data-bind="key"></span> </span>​- 外层
contenteditable="false"保护标签不被逐字删除 - 输入层
data-bind实现与右侧表单的双向绑定 - 末尾追加
​(零宽空格)防止排版换行异常
- 外层
- 图片占位符:
<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 操作
- 插入 HTML 必须为紧凑单行:使用
document.execCommand('insertHTML', ...)或Range.insertNode()时,多行模板字符串中的缩进/换行会被浏览器解析为额外文本节点,破坏排版和光标行为。 - 删除字段用
Range.selectNode + execCommand('delete'):直接target.remove()会绕过浏览器撤销栈,且可能在 WebKit 中误删父级<p>。 - 任何直接操作 DOM 修改编辑器内容后,必须紧跟:
contentRef.current = editorRef.current.innerHTML; - 在表格
<td>内插入复杂 inline 元素时:优先使用块级<div>作为外层容器,execCommand('insertHTML')对<td>内的 inline-flex 嵌套会自动"拍平"结构。 - 对齐操作弃用
execCommand('justifyLeft'...):改用直接设置block.style.textAlign = align,避免浏览器对混合排版(文字 + 智能字段/占位符)的肢解。
B. 自动保存与 Ref/State 同步
- 永远不要将
useRef作为自动保存的唯一数据源:React 18StrictMode的"挂载 → 卸载 → 重挂载"会导致 ref 在首次卸载时仍保持初始空值,从而用空数据覆盖有效的 localStorage draft。 - 自动保存函数应直接从最新的 React state 和 DOM 读取数据,通过
useCallback+ 完整 dependency 数组保证闭包新鲜;或从stateRef/contentRef读取稳定快照,但必须在所有数据恢复路径中同步 ref。 setState是异步的:setCapturedFrames(next); saveDraftToStorage();的写法会导致闭包读到旧值。若需即时保存,应在setState回调中触发保存,或从 ref 读取。- 组件卸载时 DOM 可能已失效:
editorRef.current?.innerHTML在卸载阶段可能为空,应优先使用contentRef.current(内存引用)。 - 异步循环中不要
await阻塞主流程:自动帧插入使用setTimeout推入事件队列,而非await new Promise(...)。
C. 图片占位符体系
- 占位符涉及三处必须同步修改:
defaultContent.ts(静态模板)、ReportEditor.tsx(运行时插入/填充/删除恢复)、TemplateManage.tsx(模板管理)。 - 占位符创建时需写入
max-width/max-height;填充后改为width:auto; height:auto,让图片 shrink-wrap;删除恢复时需回读maxWidth/maxHeight重置尺寸。 - 提示文字使用
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center;,要求父容器带position:relative。 - 占位符分类隔离:通过
data-mode="frame|manual"区分;自动插入和拖拽填充时必须用:not([data-mode="manual"])过滤。
D. 时间/日期字段格式系统
- 存储格式与显示格式分离:
YYYY-MM-DD/HH:mm存储;YYYY年MM月DD日/hh:mm A显示。 - 必须同时实现正向格式化(存储 → 显示)和反向解析(显示 → 存储),否则编辑器内直接编辑 smart field 会导致数据混乱。
- 12h/24h 判断使用包含性判断:
field.timeFormat.includes('hh') || field.timeFormat.includes('A'),避免精确匹配无法覆盖自定义格式。 - 默认值策略:「固定时间」/
specific与「当前时间」/current。自动填充必须加「仅当值为空时触发」保护,防止编辑已有报告时覆盖用户数据。 - 时间格式 token 体系:
YYYY、MM、DD、HH、hh、mm、A。避免使用简写别名如'24h'、'12h'作为存储值。
E. 事件与交互
- 工具栏/字段库按钮必须加
onMouseDown={(e) => e.preventDefault()}:防止点击时编辑器失焦导致Selection/Range丢失。 - 插入操作前恢复
savedRangeRef:作为焦点流失后的兜底保险。 - 双向联动高亮:通过
activeFieldKey状态 +useEffect直接操作 DOM style(backgroundColor、outline),避免触发组件重渲染导致光标丢失。点击非字段区域时清空高亮。 - 打印样式必须通过
@media print强制抹除所有交互高亮内联样式(outline: none !important; box-shadow: none !important;)。
F. 数据初始化与默认值
Login.tsx的initData()是全局唯一初始化入口:默认用户、默认模板、默认字段配置、默认设置、素材预加载(logo)均应在此处完成。- 新增
localStoragekey 时需提供合理的默认值或降级处理。 resetToDefault/ 恢复出厂设置函数必须包含所有SystemSettings字段,不能遗漏新增配置项。- 修改
DEFAULT_FORM_FIELDS默认值后,已有用户的localStorage中旧配置不会自动更新;若变更影响核心功能,应考虑启动时做配置迁移或版本校验。 - 批量操作后必须同步清理
selectedIds和当前选中状态,避免选中已删除项。
测试策略
当前状态:零自动化测试。
- 无单元测试、无集成测试、无 E2E 测试
- 无 Jest、Vitest、Playwright、Cypress 配置
- 唯一类型检查:
npm run lint(tsc --noEmit)
建议补充方向(如用户要求):
storage.ts的 JSON 序列化/反序列化types.ts中日期/时间格式化与解析函数的正反向一致性- 报告编辑器的草稿保存/恢复逻辑
安全注意事项
- 密码明文存储:用户密码以明文形式保存在
localStorage的users数组中。这是纯前端架构的固有限制,不适合生产环境处理真实敏感数据。 - 无 HTTPS 强制:Docker 部署默认 HTTP 80 端口。
- 无 API 鉴权:无后端,因此无 Token、Session、CSRF 防护概念。
- XSS 风险:报告和模板内容直接以 HTML 字符串存储并在
innerHTML中渲染。当前通过contentEditable限制输入来源,但若导入外部 JSON 模板/报告,需警惕恶意脚本。 - 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