Bladeren bron

feat (1.26.2 -> 1.27.0): 实现诊断报告模板编辑功能并集成完整管理系统

新增诊断报告模板管理系统,支持模板的创建、编辑、删除和应用功能,包括:
- 模板列表展示,支持常用模板标记和搜索过滤
- 另存为诊断报告模板 功能,从当前报告内容创建模板
- 模板管理对话框,支持增删改查操作
- Redux状态管理,支持模板数据的异步操作
- 用户界面集成,包括确认覆盖提示和错误处理

改动文件:

- docs/实现/诊断报告模板编辑功能设计.md
- src/pages/patient/DiagnosticReport/components/FindingsSection.tsx
- src/pages/patient/DiagnosticReport/components/TemplatePanel.tsx
- src/states/patient/DiagnosticReport/findingsSlice.ts
- src/states/patient/DiagnosticReport/templateSlice.ts
- src/pages/patient/DiagnosticReport/components/TemplateListItem.tsx
- src/pages/patient/DiagnosticReport/components/TemplateManagementModal.tsx
- CHANGELOG.md
- package.json
dengdx 2 weken geleden
bovenliggende
commit
2657c68125

+ 38 - 0
CHANGELOG.md

@@ -2,6 +2,44 @@
 
 本项目的所有重要变更都将记录在此文件中。
 
+## [1.27.0] - 2025-12-25 14:24
+
+### 新增 (Added)
+- **诊断报告模板编辑功能** - 实现完整的诊断报告模板管理系统,包括模板的创建、编辑、删除、应用等功能
+  - 新增诊断报告模板管理对话框,支持模板的增删改查操作
+  - 实现模板列表展示,支持常用模板标记和搜索过滤
+  - 添加"另存为诊断报告模板"功能,从当前报告内容创建模板
+  - 完善Redux状态管理,支持模板数据的异步操作
+  - 集成完整的用户界面和交互流程
+
+**核心功能实现:**
+- 模板管理:创建、编辑、删除、复制诊断报告模板
+- 模板应用:从模板快速填充报告内容,支持内容覆盖确认
+- 常用模板:支持设置和筛选常用诊断报告模板
+- 搜索过滤:按模板名称和标签进行搜索过滤
+- 数据持久化:通过API实现模板数据的后端存储
+
+**技术实现:**
+- 新增TemplateManagementModal组件,实现模板管理对话框
+- 新增TemplateListItem组件,实现模板列表项展示
+- 重构TemplatePanel组件,实现模板面板功能
+- 修改FindingsSection组件,集成"另存为模板"功能
+- 完善templateSlice,实现完整的Redux状态管理
+- 简化findingsSlice,移除不必要的模板保存逻辑
+
+**改动文件:**
+- docs/实现/诊断报告模板编辑功能设计.md (新增)
+- src/pages/patient/DiagnosticReport/components/FindingsSection.tsx (修改)
+- src/pages/patient/DiagnosticReport/components/TemplatePanel.tsx (修改)
+- src/states/patient/DiagnosticReport/findingsSlice.ts (修改)
+- src/states/patient/DiagnosticReport/templateSlice.ts (修改)
+- src/pages/patient/DiagnosticReport/components/TemplateListItem.tsx (新增)
+- src/pages/patient/DiagnosticReport/components/TemplateManagementModal.tsx (新增)
+- CHANGELOG.md
+- package.json (版本更新: 1.26.2 -> 1.27.0)
+
+---
+
 ## [1.26.2] - 2025-12-24 18:18
 
 ### 修复 (Fixed)

+ 1142 - 0
docs/实现/诊断报告模板编辑功能设计.md

@@ -0,0 +1,1142 @@
+# 诊断报告模板编辑功能设计文档
+
+## 📋 功能概述
+
+实现诊断报告模板的完整管理功能,包括诊断报告模板的创建、编辑、删除、应用等操作。用户可以通过诊断报告模板快速填充报告内容,提高工作效率。
+
+## 🎯 详细需求
+
+### 1. 功能需求
+
+#### 1.1 诊断报告模板列表展示(TemplatePanel)
+- **FR-1.1.1** 在侧边栏的"诊断报告模板"卡片中展示诊断报告模板列表
+- **FR-1.1.2** 筛选条功能
+  - `[常用]` 按钮:只显示常用诊断报告模板
+  - `[全部]` 按钮:显示所有诊断报告模板
+  - `[管理]` 按钮:打开诊断报告模板管理对话框
+- **FR-1.1.3** 搜索功能:支持按诊断报告模板名称搜索
+- **FR-1.1.4** 显示常用诊断报告模板标记(星标)
+- **FR-1.1.5** 点击诊断报告模板可以应用到当前报告
+
+#### 1.2 诊断报告模板管理对话框
+- **FR-1.2.1** 显示所有诊断报告模板的列表(表格形式)
+- **FR-1.2.2** 支持创建新的诊断报告模板
+- **FR-1.2.3** 支持编辑现有诊断报告模板
+- **FR-1.2.4** 支持删除诊断报告模板(带确认)
+- **FR-1.2.5** 支持复制现有诊断报告模板(自动添加"- 副本"后缀)
+- **FR-1.2.6** 支持切换常用诊断报告模板状态
+- **FR-1.2.7** 支持按标签过滤诊断报告模板
+
+#### 1.3 诊断报告模板编辑功能
+- **FR-1.3.1** 输入诊断报告模板名称(必填)
+- **FR-1.3.2** 输入影像所见内容(必填)
+- **FR-1.3.3** 输入诊断意见内容(必填)
+- **FR-1.3.4** 输入标签(可选)
+- **FR-1.3.5** 设置是否为常用诊断报告模板(复选框)
+- **FR-1.3.6** 表单验证
+
+#### 1.4 "另存为诊断报告模板"功能
+- **FR-1.4.1** 从"影像所见"区域点击"另存为诊断报告模板"按钮
+- **FR-1.4.2** 自动填充当前的影像所见和诊断意见内容
+- **FR-1.4.3** 打开诊断报告模板编辑区域,用户补充诊断报告模板名称等信息
+
+#### 1.5 应用诊断报告模板功能
+- **FR-1.5.1** 点击诊断报告模板列表中的诊断报告模板
+- **FR-1.5.2** 将诊断报告模板的影像所见和诊断意见内容填充到对应的输入框
+- **FR-1.5.3** 提供确认提示(如果当前已有内容)
+
+### 2. 非功能需求
+
+- **NFR-2.1** 响应时间:所有操作响应时间 < 500ms
+- **NFR-2.2** 用户体验:操作流畅,提示清晰
+- **NFR-2.3** 数据安全:删除操作需要二次确认
+- **NFR-2.4** 错误处理:网络错误、API错误需要友好提示
+
+## 🎨 UI结构设计
+
+### 1. 诊断报告页面-和诊断报告模板相关
+
+```
+DiagnosticReport 页面
+├── ReportHeader(报告头部)
+├── ReportMain(报告主体)
+│   ├── MainContent(左侧,16列)
+│   │   ├── 基本信息卡片
+│   │   ├── 图像卡片
+│   │   ├── 影像所见卡片
+│   │   │   ├── 标题:"影像所见"
+│   │   │   ├── **【"另存为诊断报告模板"按钮】** ← 🎯 入口1
+│   │   │   └── 文本输入框
+│   │   └── 诊断意见卡片
+│   │       ├── 标题:"影像诊断"
+│   │       └── 文本输入框
+│   └── SidePanel(右侧,8列)
+│       ├── 检查过滤卡片
+│       └── **【诊断报告模板卡片】** ← 🎯 主要功能区域
+│           ├── 筛选条:[常用] [全部] [管理] ← 🎯 入口2(管理按钮)
+│           ├── 搜索框
+│           └── 诊断报告模板列表
+│               └── 诊断报告模板项(可点击应用)
+└── ReportFooter(报告底部)
+```
+
+### 2. 诊断报告模板管理对话框(TemplateManagementModal)
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ 诊断报告模板管理                                                    [X] │
+├─────────────────────────────────────────────────────────────────────────┤
+│                                                                         │
+│  左侧:诊断报告模板列表          │  右侧:编辑区域                   │
+│  ┌─────────────────────────────┐  │  ┌─────────────────────────────┐  │
+│  │ [+ 新建诊断报告模板]        │  │  │ 诊断报告模板名称 *          │  │
+│  │ [标签过滤: 全部 ▼]         │  │  │ ┌─────────────────────────┐ │  │
+│  │                             │  │  │ │                         │ │  │
+│  │ ┌─────────────────────────┐ │  │  │ └─────────────────────────┘ │  │
+│  │ │ 名称  │ 标签 │ 常用     │ │  │  │                             │  │
+│  │ ├─────────────────────────┤ │  │  │ 影像所见 *                  │  │
+│  │ │ 模板1 │ 胸部 │ ⭐       │ │  │  │ ┌─────────────────────────┐ │  │
+│  │ │ 模板2 │ 腹部 │          │ │  │  │ │                         │ │  │
+│  │ │ ...   │ ...  │ ...      │ │  │  │ │                         │ │  │
+│  │ └─────────────────────────┘ │  │  │ └─────────────────────────┘ │  │
+│  │                             │  │  │                             │  │
+│  │                             │  │  │ 诊断意见 *                  │  │
+│  │                             │  │  │ ┌─────────────────────────┐ │  │
+│  │                             │  │  │ │                         │ │  │
+│  │                             │  │  │ │                         │ │  │
+│  │                             │  │  │ └─────────────────────────┘ │  │
+│  │                             │  │  │                             │  │
+│  │                             │  │  │ 标签                        │  │
+│  │                             │  │  │ ┌─────────────────────────┐ │  │
+│  │                             │  │  │ │                         │ │  │
+│  │                             │  │  │ └─────────────────────────┘ │  │
+│  │                             │  │  │                             │  │
+│  │                             │  │  │ ☐ 设为常用模板              │  │
+│  │                             │  │  │                             │  │
+│  └─────────────────────────────┘  │  │ [删除] [保存]               │  │
+│                                    │  └─────────────────────────────┘  │
+│                                                                         │
+│                                                        [关闭]           │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+**说明:**
+- **左侧诊断报告模板列表**:
+  - 操作按钮组(根据选中状态动态显示):
+    - `[新建]` 按钮:始终显示,点击后右侧显示空白编辑表单
+    - `[复制]` 按钮:仅在选中模板时显示,点击后复制当前模板(名称自动添加"- 副本"后缀)
+    - `[删除]` 按钮(红色危险按钮):仅在选中模板时显示,点击后删除当前选中的诊断报告模板
+  - 标签搜索框(用于按标签过滤表格中的诊断报告模板,支持手动输入)
+  - 诊断报告模板列表(表格形式):点击某行可在右侧编辑该诊断报告模板
+- **右侧编辑区域**:
+  - 诊断报告模板名称输入框(必填)
+  - 影像所见文本域(必填)
+  - 诊断意见文本域(必填)
+  - 标签输入框(可选)
+  - 常用诊断报告模板复选框
+  - `[删除]` 按钮:仅在编辑现有模板时显示,删除当前选中的诊断报告模板
+  - `[保存]` 按钮:保存编辑内容
+
+### 3. 诊断报告模板面板(TemplatePanel)
+
+```
+┌─────────────────────────────────────┐
+│ 诊断报告模板                    [X] │
+├─────────────────────────────────────┤
+│ 筛选条:                             │
+│ [常用] [全部] [管理]               │
+│                                     │
+│ 搜索:                               │
+│ ┌─────────────────────────────────┐ │
+│ │ 🔍 搜索诊断报告模板...          │ │
+│ └─────────────────────────────────┘ │
+│                                     │
+│ 诊断报告模板列表:                   │
+│ ┌─────────────────────────────────┐ │
+│ │ ⭐ 胸部正位诊断报告模板         │ │
+│ │    标签: 胸部                   │ │
+│ ├─────────────────────────────────┤ │
+│ │ 腹部平扫诊断报告模板            │ │
+│ │    标签: 腹部                   │ │
+│ ├─────────────────────────────────┤ │
+│ │ ...                             │ │
+│ └─────────────────────────────────┘ │
+└─────────────────────────────────────┘
+```
+
+**说明:**
+- **筛选条**:包含三个按钮
+  - `[常用]` - 只显示常用诊断报告模板
+  - `[全部]` - 显示所有诊断报告模板
+  - `[管理]` - 打开诊断报告模板管理对话框
+- **搜索条**:输入框,支持按诊断报告模板名称搜索
+- **诊断报告模板列表**:显示过滤后的诊断报告模板列表,可点击应用
+
+## 👥 参与者列表
+
+### 1. 组件层(Components)
+
+#### 1.1 新建组件
+- **TemplateManagementModal** - 诊断报告模板管理对话框(左右分栏布局)
+  - **左侧:诊断报告模板列表**
+    - "新建诊断报告模板"按钮
+    - 标签搜索框(用于按标签过滤表格,支持手动输入)
+    - 诊断报告模板列表(表格形式)
+    - 点击某行可在右侧编辑
+  - **右侧:编辑区域**
+    - 表单输入(名称、影像所见、诊断意见、标签、常用标记)
+    - 表单验证
+    - 删除/保存按钮
+
+- **TemplateListItem** - 诊断报告模板列表项组件
+  - 显示诊断报告模板名称、标签、常用标记
+  - 点击应用诊断报告模板
+  - 悬停显示预览
+
+#### 1.2 修改组件
+- **TemplatePanel** - 诊断报告模板面板(需要完整实现)
+  - 筛选条:三个按钮([常用] [全部] [管理])
+  - 搜索框(按诊断报告模板名称搜索)
+  - 诊断报告模板列表展示
+  - 应用诊断报告模板功能
+  - 常用诊断报告模板标记显示
+
+- **FindingsSection** - 影像所见区域(需要修改)
+  - "另存为诊断报告模板"按钮功能实现
+
+### 2. 状态管理层(Redux Slices)
+
+#### 2.1 新建/修改 Slices
+- **templateSlice** - 诊断报告模板状态管理(需要完整实现)
+  - State:
+    - `templates: ReportTemplate[]` - 诊断报告模板列表
+    - `loading: boolean` - 加载状态
+    - `error: string | null` - 错误信息
+    - `filterMode: 'all' | 'staple'` - 筛选模式(全部/常用)
+    - `searchKeyword: string` - 搜索关键词
+  - Actions:
+    - `fetchTemplates()` - 获取诊断报告模板列表
+    - `createTemplate(template)` - 创建诊断报告模板
+    - `updateTemplate(id, template)` - 更新诊断报告模板
+    - `deleteTemplate(id)` - 删除诊断报告模板
+    - `setFilterMode(mode)` - 设置筛选模式
+    - `setSearchKeyword(keyword)` - 设置搜索关键词
+  - Thunks:
+    - `fetchTemplatesThunk()` - 异步获取诊断报告模板
+    - `createTemplateThunk(template)` - 异步创建诊断报告模板
+    - `updateTemplateThunk(id, template)` - 异步更新诊断报告模板
+    - `deleteTemplateThunk(id)` - 异步删除诊断报告模板
+
+- **findingsSlice** - 影像所见状态(需要修改)
+  - 实现 `saveTemplate()` action(保存为诊断报告模板)
+
+### 3. API层(已存在)
+
+- **ReportTemplateActions.ts**
+  - `getReportTemplateList(params?)` - 获取诊断报告模板列表
+  - `createReportTemplate(template)` - 创建诊断报告模板
+  - `updateReportTemplate(templateId, template)` - 更新诊断报告模板
+  - `deleteReportTemplate(templateId)` - 删除诊断报告模板
+
+### 4. 类型定义(Types)
+
+- **ReportTemplate** - 诊断报告模板数据结构(已存在)
+  ```typescript
+  interface ReportTemplate {
+    id?: string;
+    name: string;
+    findings: string;
+    impression: string;
+    tag: string;
+    staple: boolean;
+  }
+  ```
+
+## 📝 实现TodoList
+
+### Phase 1: Redux状态管理
+- [ ] 实现诊断报告模板状态管理 `templateSlice.ts`
+  - [ ] 定义 state 结构(诊断报告模板列表、加载状态等)
+  - [ ] 实现同步 actions(设置筛选、搜索等)
+  - [ ] 实现异步 thunks(CRUD操作诊断报告模板)
+  - [ ] 添加到 store
+
+### Phase 2: 核心组件开发
+- [ ] 实现诊断报告模板管理对话框 `TemplateManagementModal` 组件
+  - [ ] 创建组件文件
+  - [ ] 实现左右分栏布局
+  - [ ] 实现左侧诊断报告模板列表(表格)
+  - [ ] 实现右侧编辑表单
+  - [ ] 实现标签过滤
+  - [ ] 实现新建/编辑/删除诊断报告模板操作
+  - [ ] 实现表单验证
+  - [ ] 连接 Redux
+
+- [ ] 实现诊断报告模板列表项 `TemplateListItem` 组件
+  - [ ] 创建组件文件
+  - [ ] 实现列表项布局
+  - [ ] 实现点击应用诊断报告模板功能
+  - [ ] 实现悬停预览
+
+### Phase 3: 集成现有组件
+- [ ] 完善诊断报告模板面板 `TemplatePanel` 组件
+  - [ ] 实现诊断报告模板列表展示
+  - [ ] 添加"管理诊断报告模板"按钮
+  - [ ] 实现标签过滤
+  - [ ] 连接 Redux
+  - [ ] 实现应用诊断报告模板功能
+
+- [ ] 修改影像所见区域 `FindingsSection` 组件
+  - [ ] 实现"另存为诊断报告模板"功能
+  - [ ] 连接诊断报告模板编辑对话框
+
+### Phase 4: 功能完善
+- [ ] 实现应用诊断报告模板确认提示
+- [ ] 实现删除诊断报告模板确认提示
+- [ ] 实现错误处理和提示
+- [ ] 实现加载状态显示
+
+### Phase 5: 测试和优化
+- [ ] 单元测试
+- [ ] 集成测试
+- [ ] 用户体验优化
+- [ ] 性能优化
+
+## 🔄 交互流程(泳道图)
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant TP as TemplatePanel
+    participant TMM as TemplateManagementModal
+    participant Redux as Redux Store
+    participant API as Backend API
+
+    Note over U,API: 场景1: 查看和应用模板
+    U->>TP: 打开诊断报告页面
+    TP->>Redux: dispatch(fetchTemplatesThunk())
+    Redux->>API: getReportTemplateList()
+    API-->>Redux: 返回模板列表
+    Redux-->>TP: 更新模板列表
+    TP->>U: 显示模板列表
+    U->>TP: 点击某个模板
+    TP->>Redux: 获取模板内容
+    TP->>U: 确认是否应用模板?
+    U->>TP: 确认
+    TP->>Redux: dispatch(updateInputValue(findings))
+    TP->>Redux: dispatch(setDiagnosisDescription(impression))
+    Redux-->>U: 更新报告内容
+
+    Note over U,API: 场景2: 管理模板(新建)
+    U->>TP: 点击"编辑"按钮
+    TP->>TMM: 打开模板管理对话框
+    TMM->>U: 显示左侧模板列表
+    U->>TMM: 点击"新建模板"
+    TMM->>TMM: 右侧显示空白编辑表单
+    U->>TMM: 在右侧填写模板信息
+    U->>TMM: 点击"保存"
+    TMM->>TMM: 表单验证
+    TMM->>Redux: dispatch(createTemplateThunk(template))
+    Redux->>API: createReportTemplate(template)
+    API-->>Redux: 返回创建结果
+    Redux-->>TMM: 更新状态
+    TMM->>U: 显示成功提示
+    TMM->>Redux: 刷新左侧模板列表
+    TMM->>U: 更新显示
+
+    Note over U,API: 场景3: 编辑模板
+    U->>TMM: 点击左侧列表中的某个模板
+    TMM->>Redux: 获取模板数据
+    TMM->>TMM: 右侧显示预填充表单
+    TMM->>U: 显示模板详情
+    U->>TMM: 在右侧修改模板信息
+    U->>TMM: 点击"保存"
+    TMM->>Redux: dispatch(updateTemplateThunk(id, template))
+    Redux->>API: updateReportTemplate(id, template)
+    API-->>Redux: 返回更新结果
+    Redux-->>TMM: 更新状态
+    TMM->>U: 显示成功提示
+    TMM->>U: 刷新左侧列表
+
+    Note over U,API: 场景4: 删除模板
+    U->>TMM: 在右侧点击"删除"按钮
+    TMM->>U: 显示确认对话框
+    U->>TMM: 确认删除
+    TMM->>Redux: dispatch(deleteTemplateThunk(id))
+    Redux->>API: deleteReportTemplate(id)
+    API-->>Redux: 返回删除结果
+    Redux-->>TMM: 更新状态
+    TMM->>U: 显示成功提示并刷新列表
+    TMM->>TMM: 右侧清空编辑区域
+
+    Note over U,API: 场景5: 另存为模板
+    U->>U: 填写影像所见和诊断意见
+    U->>TP: 点击"另存为模板"按钮
+    TP->>TMM: 打开模板管理对话框
+    TMM->>Redux: 获取当前报告内容
+    TMM->>TMM: 右侧显示预填充表单
+    TMM->>U: 显示预填充的findings和impression
+    U->>TMM: 补充模板名称等信息
+    U->>TMM: 点击"保存"
+    TMM->>Redux: dispatch(createTemplateThunk(template))
+    Redux->>API: createReportTemplate(template)
+    API-->>Redux: 返回创建结果
+    Redux-->>TMM: 更新状态
+    TMM->>U: 显示成功提示
+    TMM->>U: 刷新左侧模板列表
+```
+
+## 📊 数据流设计
+
+### 1. 数据流向图
+
+```mermaid
+graph TB
+    subgraph "用户界面层"
+        TP[TemplatePanel]
+        TMM[TemplateManagementModal<br/>左侧:列表 右侧:编辑区]
+        FS[FindingsSection]
+        DS[DiagnosisSection]
+    end
+
+    subgraph "状态管理层"
+        TS[templateSlice]
+        FiS[findingsSlice]
+        DiS[diagnosisSlice]
+        Store[Redux Store]
+    end
+
+    subgraph "API层"
+        API[ReportTemplateActions]
+    end
+
+    subgraph "后端"
+        Backend[Backend Server]
+    end
+
+    %% 查询流程
+    TP -->|dispatch fetchTemplatesThunk| TS
+    TMM -->|dispatch fetchTemplatesThunk| TS
+    TS -->|调用| API
+    API -->|HTTP GET| Backend
+    Backend -->|返回数据| API
+    API -->|返回| TS
+    TS -->|更新state| Store
+    Store -->|订阅更新| TP
+    Store -->|订阅更新| TMM
+
+    %% 创建/更新流程
+    TMM -->|dispatch createTemplateThunk| TS
+    TMM -->|dispatch updateTemplateThunk| TS
+    TS -->|调用| API
+    API -->|HTTP POST/PUT| Backend
+    Backend -->|返回结果| API
+    API -->|返回| TS
+    TS -->|更新state| Store
+
+    %% 删除流程
+    TMM -->|dispatch deleteTemplateThunk| TS
+    TS -->|调用| API
+    API -->|HTTP DELETE| Backend
+    Backend -->|返回结果| API
+    API -->|返回| TS
+
+    %% 应用模板流程
+    TP -->|应用模板| FiS
+    TP -->|应用模板| DiS
+    FiS -->|更新| Store
+    DiS -->|更新| Store
+    Store -->|订阅更新| FS
+    Store -->|订阅更新| DS
+
+    %% 另存为模板流程
+    FS -->|获取当前内容| FiS
+    FS -->|获取当前内容| DiS
+    FS -->|打开对话框| TMM
+```
+
+### 2. Redux State 结构
+
+```typescript
+// Redux Store 结构
+{
+  // ... 其他 slices
+  template: {
+    templates: ReportTemplate[],      // 模板列表
+    loading: boolean,                  // 加载状态
+    error: string | null,              // 错误信息
+    selectedTag: string,               // 选中的标签过滤
+  },
+  findings: {
+    diagnosticDescriptionFromImage: string,  // 影像所见内容
+  },
+  diagnosis: {
+    diagnosisDescription: string,      // 诊断意见内容
+  },
+  // ... 其他 slices
+}
+```
+
+## 🗂️ 数据结构定义
+
+### 1. 核心数据结构
+
+```typescript
+// 报告模板
+export interface ReportTemplate {
+  id?: string;           // 模板ID(可选,创建时不需要)
+  name: string;          // 模板名称
+  findings: string;      // 影像所见内容
+  impression: string;    // 诊断意见内容
+  tag: string;           // 标签(用于分类)
+  staple: boolean;       // 是否为常用模板
+}
+
+// 模板查询参数
+export interface ReportTemplateQueryParams {
+  tag?: string;          // 按标签过滤
+  is_staple?: boolean;   // 是否只查询常用模板
+  is_pre_install?: boolean; // 是否只查询预装模板
+}
+
+// API响应结构
+export interface ReportTemplateResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: any;
+}
+
+export interface ReportTemplateListResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: {
+    templates: ReportTemplate[];
+    count?: number;
+  };
+}
+```
+
+### 2. Redux State 类型
+
+```typescript
+// templateSlice state
+export interface TemplateState {
+  templates: ReportTemplate[];
+  loading: boolean;
+  error: string | null;
+  selectedTag: string;
+}
+
+// findingsSlice state
+export interface FindingsState {
+  diagnosticDescriptionFromImage: string;
+}
+
+// diagnosisSlice state
+export interface DiagnosisState {
+  diagnosisDescription: string;
+}
+```
+
+### 3. 组件 Props 类型
+
+```typescript
+// TemplateEditModal Props
+export interface TemplateEditModalProps {
+  visible: boolean;
+  template?: ReportTemplate;  // 编辑时传入,新建时为undefined
+  onSave: (template: ReportTemplate) => void;
+  onCancel: () => void;
+}
+
+// TemplateManagementModal Props
+export interface TemplateManagementModalProps {
+  visible: boolean;
+  onClose: () => void;
+}
+
+// TemplateListItem Props
+export interface TemplateListItemProps {
+  template: ReportTemplate;
+  onClick: (template: ReportTemplate) => void;
+}
+```
+
+## 🚀 执行流程
+
+### 1. 功能起点
+
+#### 起点1: 用户打开诊断报告页面
+```
+用户操作: 从患者列表或其他入口进入诊断报告页面
+↓
+触发: DiagnosticReport 组件挂载
+↓
+执行: useEffect 初始化报告数据
+↓
+执行: TemplatePanel 组件挂载
+↓
+触发: dispatch(fetchTemplatesThunk())
+↓
+执行: 调用 API 获取模板列表
+↓
+结果: 模板列表显示在侧边栏
+```
+
+#### 起点2: 用户点击"管理模板"按钮
+```
+用户操作: 点击 TemplatePanel 中的"管理模板"按钮
+↓
+触发: 打开 TemplateManagementModal
+↓
+执行: 显示模板列表(表格形式)
+↓
+用户可以: 新建/编辑/删除模板
+```
+
+#### 起点3: 用户点击"另存为模板"按钮
+```
+用户操作: 在 FindingsSection 中点击"另存为模板"按钮
+↓
+触发: 获取当前报告内容(findings + impression)
+↓
+触发: 打开 TemplateEditModal
+↓
+执行: 预填充表单(findings 和 impression)
+↓
+用户操作: 补充模板名称等信息并保存
+```
+
+#### 起点4: 用户点击模板列表中的模板
+```
+用户操作: 在 TemplatePanel 中点击某个模板
+↓
+触发: 检查当前报告是否有内容
+↓
+如果有内容: 显示确认对话框
+↓
+用户确认: 应用模板
+↓
+执行: dispatch(updateInputValue(template.findings))
+执行: dispatch(setDiagnosisDescription(template.impression))
+↓
+结果: 报告内容被模板内容替换
+```
+
+### 2. 详细执行流程图
+
+```mermaid
+flowchart TD
+    Start([用户进入诊断报告页面]) --> Init[DiagnosticReport组件初始化]
+    Init --> LoadTemplates[TemplatePanel加载模板列表]
+    LoadTemplates --> FetchAPI[调用fetchTemplatesThunk]
+    FetchAPI --> DisplayList[显示模板列表]
+    
+    DisplayList --> UserAction{用户操作}
+    
+    UserAction -->|点击管理模板| OpenManagement[打开TemplateManagementModal]
+    UserAction -->|点击模板项| ApplyTemplate[应用模板流程]
+    UserAction -->|点击另存为模板| SaveAsTemplate[另存为模板流程]
+    
+    OpenManagement --> ManagementActions{管理操作}
+    ManagementActions -->|新建| CreateNew[打开TemplateEditModal<br/>空表单]
+    ManagementActions -->|编辑| EditExisting[打开TemplateEditModal<br/>预填充数据]
+    ManagementActions -->|删除| DeleteConfirm[显示删除确认]
+    
+    CreateNew --> FillForm[用户填写表单]
+    EditExisting --> FillForm
+    FillForm --> Validate{表单验证}
+    Validate -->|失败| ShowError[显示错误提示]
+    ShowError --> FillForm
+    Validate -->|成功| SaveAPI[调用API保存]
+    SaveAPI --> RefreshList[刷新模板列表]
+    RefreshList --> DisplayList
+    
+    DeleteConfirm --> ConfirmDelete{用户确认?}
+    ConfirmDelete -->|是| CallDeleteAPI[调用deleteTemplateThunk]
+    ConfirmDelete -->|否| DisplayList
+    CallDeleteAPI --> RefreshList
+    
+    ApplyTemplate --> CheckContent{当前有内容?}
+    CheckContent -->|是| ConfirmApply[显示确认对话框]
+    CheckContent -->|否| ApplyDirectly[直接应用]
+    ConfirmApply --> UserConfirm{用户确认?}
+    UserConfirm -->|是| ApplyDirectly
+    UserConfirm -->|否| DisplayList
+    ApplyDirectly --> UpdateFindings[更新影像所见]
+    UpdateFindings --> UpdateDiagnosis[更新诊断意见]
+    UpdateDiagnosis --> DisplayList
+    
+    SaveAsTemplate --> GetCurrentContent[获取当前报告内容]
+    GetCurrentContent --> OpenEditModal[打开TemplateEditModal<br/>预填充findings和impression]
+    OpenEditModal --> FillForm
+```
+
+## 🧪 测试方案
+
+### 1. 功能测试场景
+
+#### 场景1: 模板列表加载
+**测试步骤:**
+1. 打开诊断报告页面
+2. 观察右侧模板面板
+
+**预期结果:**
+- 模板列表正确显示
+- 常用模板显示星标
+- 标签正确显示
+- 加载状态正确显示
+
+**测试数据:**
+- 至少3个不同标签的模板
+- 至少1个常用模板
+
+#### 场景2: 应用模板(无内容)
+**测试步骤:**
+1. 打开诊断报告页面(空白报告)
+2. 点击模板列表中的某个模板
+
+**预期结果:**
+- 影像所见输入框填充模板的findings内容
+- 诊断意见输入框填充模板的impression内容
+- 无确认对话框
+
+#### 场景3: 应用模板(有内容)
+**测试步骤:**
+1. 打开诊断报告页面
+2. 在影像所见和诊断意见中输入一些内容
+3. 点击模板列表中的某个模板
+
+**预期结果:**
+- 显示确认对话框:"当前报告已有内容,是否替换为模板内容?"
+- 点击确认后,内容被替换
+- 点击取消后,内容保持不变
+
+#### 场景4: 创建新模板
+**测试步骤:**
+1. 点击"管理模板"按钮
+2. 点击"新建模板"按钮
+3. 填写表单:
+   - 模板名称:测试模板
+   - 影像所见:测试所见内容
+   - 诊断意见:测试诊断内容
+   - 标签:测试
+   - 勾选"设为常用模板"
+4. 点击"保存"
+
+**预期结果:**
+- 表单验证通过
+- API调用成功
+- 显示成功提示
+- 模板列表刷新,新模板出现
+- 新模板显示星标(常用)
+
+#### 场景5: 编辑模板
+**测试步骤:**
+1. 点击"管理模板"按钮
+2. 点击某个模板的"编辑"按钮
+3. 修改模板名称
+4. 点击"保存"
+
+**预期结果:**
+- 表单预填充原有数据
+- 修改后保存成功
+- 模板列表刷新,显示修改后的名称
+
+#### 场景6: 删除模板
+**测试步骤:**
+1. 点击"管理模板"按钮
+2. 点击某个模板的"删除"按钮
+3. 在确认对话框中点击"确认"
+
+**预期结果:**
+- 显示确认对话框:"确定要删除此模板吗?"
+- 点击确认后,模板被删除
+- 模板列表刷新,该模板消失
+- 显示成功提示
+
+#### 场景7: 另存为模板
+**测试步骤:**
+1. 在影像所见中输入:"胸部正位片显示..."
+2. 在诊断意见中输入:"未见明显异常"
+3. 点击"另存为模板"按钮
+4. 在弹出的对话框中:
+   - 输入模板名称:"胸部正常模板"
+   - 输入标签:"胸部"
+   - 勾选"设为常用模板"
+5. 点击"保存"
+
+**预期结果:**
+- 对话框自动填充影像所见和诊断意见
+- 保存成功
+- 模板列表刷新,新模板出现
+
+#### 场景8: 标签过滤
+**测试步骤:**
+1. 在模板面板中选择标签过滤:"胸部"
+2. 观察模板列表
+
+**预期结果:**
+- 只显示标签为"胸部"的模板
+- 其他标签的模板被隐藏
+
+#### 场景9: 复制诊断报告模板
+**测试步骤:**
+1. 点击"管理诊断报告模板"按钮
+2. 点击选择一个已有的诊断报告模板
+3. 点击"复制"按钮
+4. 修改模板名称(自动添加"- 副本"后缀)
+5. 点击"保存"
+
+**预期结果:**
+- 复制按钮仅在选中模板时显示
+- 右侧表单预填充选中模板的所有内容
+- 模板名称自动添加"- 副本"后缀
+- 保存成功后,列表中出现新的复制模板
+- 原模板不受影响
+
+#### 场景10: 表单验证
+**测试步骤:**
+1. 点击"管理模板"→"新建模板"
+2. 不填写任何内容,直接点击"保存"
+
+**预期结果:**
+- 显示验证错误提示
+- 必填字段标红
+- 无法保存
+
+### 2. 边界情况测试
+
+#### 测试用例1: 空模板列表
+**场景:** 后端返回空模板列表
+**预期:** 显示"暂无模板"提示
+
+#### 测试用例2: API错误
+**场景:** 网络错误或API返回错误
+**预期:** 显示友好的错误提示,不影响其他功能
+
+#### 测试用例3: 长文本内容
+**场景:** 模板内容超过1000字
+**预期:** 正确保存和显示,文本框支持滚动
+
+#### 测试用例4: 特殊字符
+**场景:** 模板名称包含特殊字符(如:<>、&、")
+**预期:** 正确转义和显示
+
+#### 测试用例5: 并发操作
+**场景:** 快速连续点击保存按钮
+**预期:** 防抖处理,只发送一次请求
+
+### 3. 性能测试
+
+#### 测试用例1: 大量模板
+**场景:** 模板列表包含100+个模板
+**预期:** 
+- 列表渲染流畅
+- 滚动无卡顿
+- 考虑虚拟滚动优化
+
+#### 测试用例2: 响应时间
+**场景:** 所有操作
+**预期:** 响应时间 < 500ms
+
+### 4. 用户体验测试
+
+#### 测试用例1: 加载状态
+**场景:** API调用期间
+**预期:** 显示加载指示器(Spin)
+
+#### 测试用例2: 成功提示
+**场景:** 创建/编辑/删除成功
+**预期:** 显示Toast提示,3秒后自动消失
+
+#### 测试用例3: 错误提示
+**场景:** 操作失败
+**预期:** 显示清晰的错误信息和解决建议
+
+## 🐛 潜在问题分析
+
+### 1. 数据一致性问题
+
+**问题:** 多个用户同时编辑同一个模板
+**影响:** 数据覆盖,后保存的覆盖先保存的
+**解决方案:**
+- 实现乐观锁机制(版本号)
+- 保存前检查版本号
+- 如果版本号不匹配,提示用户刷新后再编辑
+
+### 2. 网络错误处理
+
+**问题:** API调用失败(网络断开、超时等)
+**影响:** 用户操作失败,体验差
+**解决方案:**
+- 实现重试机制(最多3次)
+- 显示友好的错误提示
+- 提供手动重试按钮
+- 离线状态检测
+
+### 3. 内存泄漏
+
+**问题:** 组件卸载时未清理订阅
+**影响:** 内存占用增加,性能下降
+**解决方案:**
+- 使用useEffect清理函数
+- 取消未完成的API请求
+- 清理事件监听器
+
+### 4. 表单状态管理
+
+**问题:** 表单数据与Redux状态不同步
+**影响:** 数据不一致,保存失败
+**解决方案:**
+- 使用受控组件
+- 统一状态管理
+- 表单验证在提交前进行
+
+### 5. 性能问题
+
+**问题:** 大量模板导致列表渲染慢
+**影响:** 页面卡顿,用户体验差
+**解决方案:**
+- 实现虚拟滚动(react-window)
+- 分页加载
+- 懒加载
+- 防抖和节流
+
+### 6. 并发问题
+
+**问题:** 用户快速连续点击保存按钮
+**影响:** 发送多次重复请求
+**解决方案:**
+- 按钮防抖处理
+- 保存期间禁用按钮
+- 显示加载状态
+
+### 7. 数据验证
+
+**问题:** 前端验证不足,后端返回错误
+**影响:** 用户体验差
+**解决方案:**
+- 完善前端验证规则
+- 与后端验证规则保持一致
+- 显示清晰的错误提示
+
+### 8. 浏览器兼容性
+
+**问题:** 某些浏览器不支持特定API
+**影响:** 功能异常
+**解决方案:**
+- 使用Polyfill
+- 特性检测
+- 降级方案
+
+## 📐 组件架构图
+
+```mermaid
+graph TB
+    subgraph "DiagnosticReport Page"
+        DR[DiagnosticReport]
+        RH[ReportHeader]
+        RM[ReportMain]
+        RF[ReportFooter]
+    end
+    
+    subgraph "ReportMain"
+        MC[MainContent]
+        SP[SidePanel]
+    end
+    
+    subgraph "MainContent"
+        BI[BaseInfo]
+        IL[ImageList]
+        FS[FindingsSection]
+        DS[DiagnosisSection]
+    end
+    
+    subgraph "SidePanel"
+        SF[StudyFilter]
+        TP[TemplatePanel]
+    end
+    
+    subgraph "TemplatePanel"
+        TPList[模板列表]
+        TPFilter[标签过滤]
+        TPManage[管理模板按钮]
+    end
+    
+    subgraph "Modals"
+        TMM[TemplateManagementModal<br/>左:列表 右:编辑]
+    end
+    
+    DR --> RH
+    DR --> RM
+    DR --> RF
+    RM --> MC
+    RM --> SP
+    MC --> BI
+    MC --> IL
+    MC --> FS
+    MC --> DS
+    SP --> SF
+    SP --> TP
+    TP --> TPList
+    TP --> TPFilter
+    TP --> TPManage
+    TPManage -.打开.-> TMM
+    FS -.打开.-> TMM
+    TPList -.应用模板.-> FS
+    TPList -.应用模板.-> DS
+```
+
+## 🔧 技术实现细节
+
+### 1. Redux Thunk 实现示例
+
+```typescript
+// templateSlice.ts
+export const fetchTemplatesThunk = createAsyncThunk(
+  'template/fetchTemplates',
+  async (params?: ReportTemplateQueryParams, { rejectWithValue }) => {
+    try {
+      const response = await getReportTemplateList(params);
+      return response.data.templates;
+    } catch (error: any) {
+      return rejectWithValue(error.message);
+    }
+  }
+);
+
+export const createTemplateThunk = createAsyncThunk(
+  'template/createTemplate',
+  async (template: ReportTemplate, { rejectWithValue }) => {
+    try {
+      const response = await createReportTemplate(template);
+      return response.data;
+    } catch (error: any) {
+      return rejectWithValue(error.message);
+    }
+  }
+);
+```
+
+### 2. 表单验证规则
+
+```typescript
+const validateTemplate = (template: ReportTemplate): string[] => {
+  const errors: string[] = [];
+  
+  if (!template.name || template.name.trim() === '') {
+    errors.push('模板名称不能为空');
+  }
+  
+  if (template.name && template.name.length > 50) {
+    errors.push('模板名称不能超过50个字符');
+  }
+  
+  if (!template.findings || template.findings.trim() === '') {
+    errors.push('影像所见不能为空');
+  }
+  
+  if (!template.impression || template.impression.trim() === '') {
+    errors.push('诊断意见不能为空');
+  }
+  
+  return errors;
+};
+```
+
+### 3. 应用模板确认逻辑
+
+```typescript
+const handleApplyTemplate = (template: ReportTemplate) => {
+  const hasContent = 
+    findings.diagnosticDescriptionFromImage.trim() !== '' ||
+    diagnosis.diagnosisDescription.trim() !== '';
+  
+  if (hasContent) {
+    Modal.confirm({
+      title: '确认应用模板',
+      content: '当前报告已有内容,是否替换为模板内容?',
+      onOk: () => {
+        dispatch(updateInputValue(template.findings));
+        dispatch(setDiagnosisDescription(template.impression));
+        message.success('模板已应用');
+      },
+    });
+  } else {
+    dispatch(updateInputValue(template.findings));
+    dispatch(setDiagnosisDescription(template.impression));
+    message.success('模板已应用');
+  }
+};
+```
+
+## 📱 响应式设计考虑
+
+虽然当前主要针对桌面端,但需要考虑:
+
+1. **对话框尺寸**
+   - 大屏:宽度800px
+   - 中屏:宽度90%
+   - 小屏:全屏显示
+
+2. **模板列表**
+   - 大屏:显示完整信息
+   - 小屏:简化显示,点击展开详情
+
+3. **表单布局**
+   - 大屏:两列布局
+   - 小屏:单列布局
+
+## 🎯 实现优先级
+
+### P0 - 核心功能(必须实现)
+1. ✅ Redux templateSlice 实现
+2. ✅ TemplateEditModal 组件
+3. ✅ TemplatePanel 基本功能
+4. ✅ 应用模板功能
+5. ✅ 创建/编辑/删除模板
+
+### P1 - 重要功能(应该实现)
+1. TemplateManagementModal 组件
+2. 标签过滤功能
+3. 常用模板标记
+4. 另存为模板功能
+5. 确认对话框
+
+### P2 - 增强功能(可以实现)
+1. 模板预览
+2. 模板搜索
+3. 模板排序
+4. 批量操作
+5. 模板导入/导出
+
+## 📚 参考文档
+
+- [Ant Design Modal](https://ant.design/components/modal-cn/)
+- [Ant Design Form](https://ant.design/components/form-cn/)
+- [Ant Design Table](https://ant.design/components/table-cn/)
+- [Redux Toolkit Async Thunks](https://redux-toolkit.js.org/api/createAsyncThunk)
+- [React Hooks](https://react.dev/reference/react)
+
+## 📝 总结
+
+本文档详细描述了诊断报告模板编辑功能的完整设计方案,包括:
+
+1. **需求分析** - 明确了功能需求和非功能需求
+2. **UI设计** - 提供了详细的界面布局和交互设计
+3. **架构设计** - 定义了组件结构、状态管理和数据流
+4. **实现计划** - 制定了分阶段的实现TodoList
+5. **测试方案** - 覆盖了功能测试、边界测试和性能测试
+6. **风险分析** - 识别了潜在问题并提供了解决方案
+
+通过本文档,开发团队可以清晰地了解功能的全貌,按照计划有序地实现各个模块,确保功能的质量和用户体验。
+
+---
+
+**文档版本:** v1.0  
+**创建日期:** 2025-12-25  
+**最后更新:** 2025-12-25  
+**作者:** Cline AI Assistant

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.26.2",
+  "version": "1.27.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 27 - 11
src/pages/patient/DiagnosticReport/components/FindingsSection.tsx

@@ -1,16 +1,21 @@
-import React from 'react';
-import { useDispatch } from 'react-redux';
+/* eslint-disable */
+import React, { useState } from 'react';
 import { Button, Input } from 'antd';
-import {
-  saveTemplate,
-  updateInputValue,
-} from '@/states/patient/DiagnosticReport/findingsSlice';
+import { useAppDispatch, useAppSelector } from '@/states/store';
+import { updateInputValue } from '@/states/patient/DiagnosticReport/findingsSlice';
+import { TemplateManagementModal } from './TemplateManagementModal';
 
 export const FindingsSection: React.FC = () => {
-  const dispatch = useDispatch();
+  const dispatch = useAppDispatch();
+  const { diagnosticDescriptionFromImage } = useAppSelector(
+    (state) => state.findings
+  );
+  const { diagnosisDescription } = useAppSelector((state) => state.diagnosis);
+
+  const [templateModalVisible, setTemplateModalVisible] = useState(false);
 
-  const handleSaveTemplate = () => {
-    dispatch(saveTemplate());
+  const handleSaveAsTemplate = () => {
+    setTemplateModalVisible(true);
   };
 
   const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -21,15 +26,26 @@ export const FindingsSection: React.FC = () => {
     <div className="flex flex-col">
       <div className="flex justify-between items-center">
         <span>影像所见</span>
-        <Button type="primary" onClick={handleSaveTemplate}>
-          另存为模板
+        <Button type="primary" onClick={handleSaveAsTemplate}>
+          另存为诊断报告模板
         </Button>
       </div>
       <Input.TextArea
         placeholder="由用户输入"
         className="mt-2.5 w-full h-24"
+        value={diagnosticDescriptionFromImage}
         onChange={handleInputChange}
       />
+
+      {/* 诊断报告模板管理对话框 */}
+      <TemplateManagementModal
+        visible={templateModalVisible}
+        onClose={() => setTemplateModalVisible(false)}
+        initialTemplate={{
+          findings: diagnosticDescriptionFromImage,
+          impression: diagnosisDescription,
+        }}
+      />
     </div>
   );
 };

+ 41 - 0
src/pages/patient/DiagnosticReport/components/TemplateListItem.tsx

@@ -0,0 +1,41 @@
+/* eslint-disable */
+import React from 'react';
+import { Card } from 'antd';
+import { StarFilled } from '@ant-design/icons';
+import { ReportTemplate } from '@/API/report/ReportTemplateActions';
+
+interface TemplateListItemProps {
+  template: ReportTemplate;
+  onClick: (template: ReportTemplate) => void;
+}
+
+export const TemplateListItem: React.FC<TemplateListItemProps> = ({
+  template,
+  onClick,
+}) => {
+  return (
+    <Card
+      size="small"
+      hoverable
+      onClick={() => onClick(template)}
+      className="mb-2 cursor-pointer"
+      bodyStyle={{ padding: '8px 12px' }}
+    >
+      <div className="flex items-start justify-between">
+        <div className="flex-1">
+          <div className="flex items-center gap-2">
+            {template.staple && (
+              <StarFilled style={{ color: '#faad14', fontSize: '14px' }} />
+            )}
+            <span className="font-medium text-sm">{template.name}</span>
+          </div>
+          {template.tag && (
+            <div className="text-xs text-gray-500 mt-1">
+              标签: {template.tag}
+            </div>
+          )}
+        </div>
+      </div>
+    </Card>
+  );
+};

+ 309 - 0
src/pages/patient/DiagnosticReport/components/TemplateManagementModal.tsx

@@ -0,0 +1,309 @@
+/* eslint-disable */
+import React, { useState, useEffect } from 'react';
+import { Modal, Button, Table, Select, Input, Checkbox, message, Form, Space, Row, Col } from 'antd';
+import { useAppDispatch, useAppSelector } from '@/states/store';
+import { CloseCircleOutlined } from '@ant-design/icons';
+
+import {
+  fetchTemplatesThunk,
+  createTemplateThunk,
+  updateTemplateThunk,
+  deleteTemplateThunk,
+  setSelectedTag,
+} from '@/states/patient/DiagnosticReport/templateSlice';
+import { ReportTemplate } from '@/API/report/ReportTemplateActions';
+
+interface TemplateManagementModalProps {
+  visible: boolean;
+  onClose: () => void;
+  initialTemplate?: Partial<ReportTemplate>; // 用于"另存为模板"功能
+}
+
+export const TemplateManagementModal: React.FC<TemplateManagementModalProps> = ({
+  visible,
+  onClose,
+  initialTemplate,
+}) => {
+  const dispatch = useAppDispatch();
+  const [form] = Form.useForm();
+  const { templates, loading, selectedTag } = useAppSelector((state) => state.template);
+
+  const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
+  const [isNewTemplate, setIsNewTemplate] = useState(false);
+
+  // 获取所有唯一的标签
+  const allTags = Array.from(new Set(templates.map((t) => t.tag).filter(Boolean)));
+
+  // 过滤后的模板列表
+  const filteredTemplates = selectedTag
+    ? templates.filter((t) => t.tag === selectedTag)
+    : templates;
+
+  // 当前选中的模板
+  const currentTemplate = templates.find((t) => t.id === selectedTemplateId);
+
+  useEffect(() => {
+    if (visible) {
+      dispatch(fetchTemplatesThunk(undefined));
+
+      // 如果有初始模板(来自"另存为模板"),则自动进入新建模式
+      if (initialTemplate) {
+        handleNewTemplate();
+        form.setFieldsValue({
+          name: initialTemplate.name || '',
+          findings: initialTemplate.findings || '',
+          impression: initialTemplate.impression || '',
+          tag: initialTemplate.tag || '',
+          staple: initialTemplate.staple || false,
+        });
+      }
+    }
+  }, [visible, dispatch, initialTemplate]);
+
+  // 新建模板
+  const handleNewTemplate = () => {
+    setIsNewTemplate(true);
+    setSelectedTemplateId(null);
+    form.resetFields();
+  };
+
+  // 复制模板
+  const handleCopyTemplate = () => {
+    if (!currentTemplate) {
+      return;
+    }
+
+    setIsNewTemplate(true);
+    setSelectedTemplateId(null);
+    form.setFieldsValue({
+      name: `${currentTemplate.name} - 副本`,
+      findings: currentTemplate.findings,
+      impression: currentTemplate.impression,
+      tag: currentTemplate.tag,
+      staple: currentTemplate.staple,
+    });
+  };
+
+  // 选择模板
+  const handleSelectTemplate = (template: ReportTemplate) => {
+    setIsNewTemplate(false);
+    setSelectedTemplateId(template.id || null);
+    form.setFieldsValue({
+      name: template.name,
+      findings: template.findings,
+      impression: template.impression,
+      tag: template.tag,
+      staple: template.staple,
+    });
+  };
+
+  // 保存模板
+  const handleSave = async () => {
+    try {
+      const values = await form.validateFields();
+
+      if (isNewTemplate) {
+        // 创建新模板
+        await dispatch(createTemplateThunk(values)).unwrap();
+        message.success('诊断报告模板创建成功');
+      } else if (selectedTemplateId) {
+        // 更新现有模板
+        await dispatch(
+          updateTemplateThunk({
+            id: selectedTemplateId,
+            template: values,
+          })
+        ).unwrap();
+        message.success('诊断报告模板更新成功');
+      }
+
+      // 刷新列表
+      await dispatch(fetchTemplatesThunk(undefined));
+
+      // 清空表单
+      form.resetFields();
+      setIsNewTemplate(false);
+      setSelectedTemplateId(null);
+    } catch (error: any) {
+      message.error(error || '保存失败');
+    }
+  };
+
+  // 删除模板
+  const handleDelete = async () => {
+    if (!selectedTemplateId) {
+      return;
+    }
+
+    Modal.confirm({
+      title: '确认删除',
+      content: '确定要删除此诊断报告模板吗?',
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dispatch(deleteTemplateThunk(selectedTemplateId)).unwrap();
+          message.success('诊断报告模板删除成功');
+
+          // 刷新列表
+          await dispatch(fetchTemplatesThunk(undefined));
+
+          // 清空表单
+          form.resetFields();
+          setSelectedTemplateId(null);
+          setIsNewTemplate(false);
+        } catch (error: any) {
+          message.error(error || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: '40%',
+    },
+    {
+      title: '标签',
+      dataIndex: 'tag',
+      key: 'tag',
+      width: '30%',
+    },
+    {
+      title: '常用',
+      dataIndex: 'staple',
+      key: 'staple',
+      width: '30%',
+      render: (staple: boolean) => (staple ? '⭐' : ''),
+    },
+  ];
+
+  return (
+    <Modal
+      title="诊断报告模板管理"
+      open={visible}
+      onCancel={onClose}
+      width={1000}
+      footer={[
+        <Button key="close" onClick={onClose}>
+          关闭
+        </Button>,
+      ]}
+    >
+      <Row gutter={16}>
+        <Col span={12}>
+          {/* 左侧:诊断报告模板列表 */}
+          <div className="flex flex-col">
+            <Space>
+              <Button type="primary" onClick={handleNewTemplate}>
+                新建
+              </Button>
+              {!isNewTemplate && selectedTemplateId && (
+                <>
+                  <Button onClick={handleCopyTemplate}>
+                    复制
+                  </Button>
+                  <Button danger onClick={handleDelete}>
+                    删除
+                  </Button>
+                </>
+              )}
+            </Space>
+
+            <Space>
+              <Input
+                placeholder="🔍 按标签搜索..."
+                value={selectedTag}
+                onChange={(e) => dispatch(setSelectedTag(e.target.value))}
+                size="small"
+                style={{ transition: 'none' }}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<CloseCircleOutlined />}
+                onClick={() => dispatch(setSelectedTag(''))}
+                style={{ visibility: selectedTag ? 'visible' : 'hidden', transition: 'none' }}
+              />
+            </Space>
+
+            <Table
+              columns={columns}
+              dataSource={filteredTemplates}
+              rowKey="id"
+              loading={loading}
+              pagination={false}
+              scroll={{ y: 380 }}
+              onRow={(record) => ({
+                onClick: () => handleSelectTemplate(record),
+                style: {
+                  cursor: 'pointer',
+                  backgroundColor:
+                    record.id === selectedTemplateId ? '#e6f7ff' : undefined,
+                },
+              })}
+              size="small"
+            />
+          </div>
+        </Col>
+
+        <Col span={12}>
+          {/* 右侧:编辑区域 */}
+          <div className="flex flex-col">
+            <Form form={form} layout="vertical">
+              <Form.Item
+                label="诊断报告模板名称"
+                name="name"
+                rules={[
+                  { required: true, message: '请输入诊断报告模板名称' },
+                  { max: 50, message: '名称不能超过50个字符' },
+                ]}
+              >
+                <Input placeholder="请输入诊断报告模板名称" />
+              </Form.Item>
+
+              <Form.Item
+                label="影像所见"
+                name="findings"
+                rules={[{ required: true, message: '请输入影像所见内容' }]}
+              >
+                <Input.TextArea rows={6} placeholder="请输入影像所见内容" />
+              </Form.Item>
+
+              <Form.Item
+                label="诊断意见"
+                name="impression"
+                rules={[{ required: true, message: '请输入诊断意见内容' }]}
+              >
+                <Input.TextArea rows={6} placeholder="请输入诊断意见内容" />
+              </Form.Item>
+
+              <Form.Item label="标签" name="tag">
+                <Input placeholder="请输入标签(可选)" />
+              </Form.Item>
+
+              <Form.Item name="staple" valuePropName="checked">
+                <Checkbox>设为常用诊断报告模板</Checkbox>
+              </Form.Item>
+            </Form>
+
+            <Space>
+              {!isNewTemplate && selectedTemplateId && (
+                <Button danger onClick={handleDelete}>
+                  删除
+                </Button>
+              )}
+              <Button type="primary" onClick={handleSave} loading={loading}>
+                保存
+              </Button>
+            </Space>
+          </div>
+        </Col>
+      </Row>
+    </Modal>
+  );
+};

+ 145 - 2
src/pages/patient/DiagnosticReport/components/TemplatePanel.tsx

@@ -1,2 +1,145 @@
-import React from 'react';
-export const TemplatePanel: React.FC = () => <div>TemplatePanel TODO</div>;
+/* eslint-disable */
+import React, { useState, useEffect, useMemo } from 'react';
+import { Input, Button, Modal, message, Spin, Empty } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import { useAppDispatch, useAppSelector } from '@/states/store';
+import {
+  fetchTemplatesThunk,
+  setFilterMode,
+  setSearchKeyword,
+} from '@/states/patient/DiagnosticReport/templateSlice';
+import { updateInputValue } from '@/states/patient/DiagnosticReport/findingsSlice';
+import { setDiagnosisDescription } from '@/states/patient/DiagnosticReport/diagnosisSlice';
+import { ReportTemplate } from '@/API/report/ReportTemplateActions';
+import { TemplateListItem } from './TemplateListItem';
+import { TemplateManagementModal } from './TemplateManagementModal';
+
+export const TemplatePanel: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const { templates, loading, filterMode, searchKeyword } = useAppSelector(
+    (state) => state.template
+  );
+  const { diagnosticDescriptionFromImage } = useAppSelector(
+    (state) => state.findings
+  );
+  const { diagnosisDescription } = useAppSelector((state) => state.diagnosis);
+
+  const [managementModalVisible, setManagementModalVisible] = useState(false);
+
+  useEffect(() => {
+    // 加载诊断报告模板列表
+    dispatch(fetchTemplatesThunk(undefined));
+  }, [dispatch]);
+
+  // 过滤和搜索诊断报告模板
+  const filteredTemplates = useMemo(() => {
+    let result = templates;
+
+    // 根据filterMode过滤
+    if (filterMode === 'staple') {
+      result = result.filter((t) => t.staple);
+    }
+
+    // 根据searchKeyword搜索
+    if (searchKeyword.trim()) {
+      result = result.filter((t) =>
+        t.name.toLowerCase().includes(searchKeyword.toLowerCase())
+      );
+    }
+
+    return result;
+  }, [templates, filterMode, searchKeyword]);
+
+  // 应用诊断报告模板
+  const handleApplyTemplate = (template: ReportTemplate) => {
+    const hasContent =
+      diagnosticDescriptionFromImage.trim() !== '' ||
+      diagnosisDescription.trim() !== '';
+
+    if (hasContent) {
+      Modal.confirm({
+        title: '确认应用诊断报告模板',
+        content: '当前报告已有内容,是否替换为诊断报告模板内容?',
+        okText: '确定',
+        cancelText: '取消',
+        onOk: () => {
+          dispatch(updateInputValue(template.findings));
+          dispatch(setDiagnosisDescription(template.impression));
+          message.success('诊断报告模板已应用');
+        },
+      });
+    } else {
+      dispatch(updateInputValue(template.findings));
+      dispatch(setDiagnosisDescription(template.impression));
+      message.success('诊断报告模板已应用');
+    }
+  };
+
+  return (
+    <div className="flex flex-col bg-white rounded">
+
+      {/* 筛选条 */}
+      <div className="flex pb-[10px]">
+        <Button
+          size="small"
+          type={filterMode === 'staple' ? 'primary' : 'default'}
+          onClick={() => dispatch(setFilterMode('staple'))}
+        >
+          常用
+        </Button>
+        <Button
+          size="small"
+          type={filterMode === 'all' ? 'primary' : 'default'}
+          onClick={() => dispatch(setFilterMode('all'))}
+        >
+          全部
+        </Button>
+        <Button
+          size="small"
+          onClick={() => setManagementModalVisible(true)}
+        >
+          管理
+        </Button>
+      </div>
+
+      {/* 搜索框 */}
+      <Input
+        placeholder="🔍 搜索诊断报告模板..."
+        prefix={<SearchOutlined />}
+        value={searchKeyword}
+        onChange={(e) => dispatch(setSearchKeyword(e.target.value))}
+        allowClear
+        size="small"
+        className="mb-2"
+      />
+
+      {/* 诊断报告模板列表 */}
+      <div className="overflow-y-auto max-h-96">
+        {loading ? (
+          <div className="flex justify-center items-center h-full">
+            <Spin tip="加载中..." />
+          </div>
+        ) : filteredTemplates.length === 0 ? (
+          <Empty
+            description="暂无诊断报告模板"
+            image={Empty.PRESENTED_IMAGE_SIMPLE}
+          />
+        ) : (
+          filteredTemplates.map((template) => (
+            <TemplateListItem
+              key={template.id}
+              template={template}
+              onClick={handleApplyTemplate}
+            />
+          ))
+        )}
+      </div>
+
+      {/* 诊断报告模板管理对话框 */}
+      <TemplateManagementModal
+        visible={managementModalVisible}
+        onClose={() => setManagementModalVisible(false)}
+      />
+    </div>
+  );
+};

+ 1 - 4
src/states/patient/DiagnosticReport/findingsSlice.ts

@@ -10,14 +10,11 @@ const findingsSlice = createSlice({
     diagnosticDescriptionFromImage: '',
   },
   reducers: {
-    saveTemplate: (state) => {
-      // Implement the logic to save the template
-    },
     updateInputValue: (state, action) => {
       state.diagnosticDescriptionFromImage = action.payload;
     },
   },
 });
 
-export const { saveTemplate, updateInputValue } = findingsSlice.actions;
+export const { updateInputValue } = findingsSlice.actions;
 export default findingsSlice.reducer;

+ 225 - 5
src/states/patient/DiagnosticReport/templateSlice.ts

@@ -1,14 +1,234 @@
 /* eslint-disable */
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import {
+  getReportTemplateList,
+  createReportTemplate,
+  updateReportTemplate,
+  deleteReportTemplate,
+  ReportTemplate,
+  ReportTemplateQueryParams,
+} from '../../../API/report/ReportTemplateActions';
+
+// State 类型定义
+export interface TemplateState {
+  templates: ReportTemplate[]; // 诊断报告模板列表
+  loading: boolean; // 加载状态
+  error: string | null; // 错误信息
+  filterMode: 'all' | 'staple'; // 筛选模式(全部/常用)
+  searchKeyword: string; // 搜索关键词
+  selectedTag: string; // 选中的标签过滤
+}
+
+// 初始状态
+const initialState: TemplateState = {
+  templates: [],
+  loading: false,
+  error: null,
+  filterMode: 'all',
+  searchKeyword: '',
+  selectedTag: '',
+};
+
+// ============================================================
+// Async Thunks - 异步操作
+// ============================================================
+
+/**
+ * 获取诊断报告模板列表
+ */
+export const fetchTemplatesThunk = createAsyncThunk(
+  'template/fetchTemplates',
+  async (params: ReportTemplateQueryParams | undefined, { rejectWithValue }) => {
+    try {
+      const response = await getReportTemplateList(params);
+      return response.data.templates;
+    } catch (error: any) {
+      return rejectWithValue(error.message || '获取诊断报告模板列表失败');
+    }
+  }
+);
+
+/**
+ * 创建诊断报告模板
+ */
+export const createTemplateThunk = createAsyncThunk(
+  'template/createTemplate',
+  async (template: ReportTemplate, { rejectWithValue }) => {
+    try {
+      const response = await createReportTemplate(template);
+      return response.data;
+    } catch (error: any) {
+      return rejectWithValue(error.message || '创建诊断报告模板失败');
+    }
+  }
+);
+
+/**
+ * 更新诊断报告模板
+ */
+export const updateTemplateThunk = createAsyncThunk(
+  'template/updateTemplate',
+  async (
+    { id, template }: { id: string; template: Partial<ReportTemplate> },
+    { rejectWithValue }
+  ) => {
+    try {
+      const response = await updateReportTemplate(id, template);
+      return { id, template: response.data };
+    } catch (error: any) {
+      return rejectWithValue(error.message || '更新诊断报告模板失败');
+    }
+  }
+);
+
+/**
+ * 删除诊断报告模板
+ */
+export const deleteTemplateThunk = createAsyncThunk(
+  'template/deleteTemplate',
+  async (id: string, { rejectWithValue }) => {
+    try {
+      await deleteReportTemplate(id);
+      return id;
+    } catch (error: any) {
+      return rejectWithValue(error.message || '删除诊断报告模板失败');
+    }
+  }
+);
+
+// ============================================================
+// Slice 定义
+// ============================================================
 
 const templateSlice = createSlice({
   name: 'template',
-  initialState: {},
-   
+  initialState,
+
   reducers: {
-    // Implement template reducers
+    /**
+     * 设置筛选模式(全部/常用)
+     */
+    setFilterMode: (state, action: PayloadAction<'all' | 'staple'>) => {
+      state.filterMode = action.payload;
+    },
+
+    /**
+     * 设置搜索关键词
+     */
+    setSearchKeyword: (state, action: PayloadAction<string>) => {
+      state.searchKeyword = action.payload;
+    },
+
+    /**
+     * 设置选中的标签过滤
+     */
+    setSelectedTag: (state, action: PayloadAction<string>) => {
+      state.selectedTag = action.payload;
+    },
+
+    /**
+     * 清除错误信息
+     */
+    clearError: (state) => {
+      state.error = null;
+    },
+
+    /**
+     * 重置状态
+     */
+    resetTemplateState: () => initialState,
+  },
+
+  extraReducers: (builder) => {
+    // ============================================================
+    // 获取诊断报告模板列表
+    // ============================================================
+    builder.addCase(fetchTemplatesThunk.pending, (state) => {
+      state.loading = true;
+      state.error = null;
+    });
+    builder.addCase(fetchTemplatesThunk.fulfilled, (state, action) => {
+      state.loading = false;
+      state.templates = action.payload;
+      state.error = null;
+    });
+    builder.addCase(fetchTemplatesThunk.rejected, (state, action) => {
+      state.loading = false;
+      state.error = action.payload as string;
+    });
+
+    // ============================================================
+    // 创建诊断报告模板
+    // ============================================================
+    builder.addCase(createTemplateThunk.pending, (state) => {
+      state.loading = true;
+      state.error = null;
+    });
+    builder.addCase(createTemplateThunk.fulfilled, (state, action) => {
+      state.loading = false;
+      // 将新创建的模板添加到列表
+      state.templates.push(action.payload);
+      state.error = null;
+    });
+    builder.addCase(createTemplateThunk.rejected, (state, action) => {
+      state.loading = false;
+      state.error = action.payload as string;
+    });
+
+    // ============================================================
+    // 更新诊断报告模板
+    // ============================================================
+    builder.addCase(updateTemplateThunk.pending, (state) => {
+      state.loading = true;
+      state.error = null;
+    });
+    builder.addCase(updateTemplateThunk.fulfilled, (state, action) => {
+      state.loading = false;
+      // 更新列表中的模板
+      const index = state.templates.findIndex((t) => t.id === action.payload.id);
+      if (index !== -1) {
+        state.templates[index] = {
+          ...state.templates[index],
+          ...action.payload.template,
+        };
+      }
+      state.error = null;
+    });
+    builder.addCase(updateTemplateThunk.rejected, (state, action) => {
+      state.loading = false;
+      state.error = action.payload as string;
+    });
+
+    // ============================================================
+    // 删除诊断报告模板
+    // ============================================================
+    builder.addCase(deleteTemplateThunk.pending, (state) => {
+      state.loading = true;
+      state.error = null;
+    });
+    builder.addCase(deleteTemplateThunk.fulfilled, (state, action) => {
+      state.loading = false;
+      // 从列表中移除已删除的模板
+      state.templates = state.templates.filter((t) => t.id !== action.payload);
+      state.error = null;
+    });
+    builder.addCase(deleteTemplateThunk.rejected, (state, action) => {
+      state.loading = false;
+      state.error = action.payload as string;
+    });
   },
 });
 
-export const {} = templateSlice.actions;
+// ============================================================
+// 导出 Actions 和 Reducer
+// ============================================================
+
+export const {
+  setFilterMode,
+  setSearchKeyword,
+  setSelectedTag,
+  clearError,
+  resetTemplateState,
+} = templateSlice.actions;
+
 export default templateSlice.reducer;