# 检查流程中的图像状态控制功能实现文档
## 📋 功能概述
在检查流程(exam 模块)中实现"恢复/拒绝/保存参数"功能,允许用户对已曝光的图像进行判断操作,对未曝光的体位进行参数保存。
**实现日期**: 2025-01-14
**版本**: v1.0.0
## 🎯 需求分析
### 功能需求
1. **拒绝按钮**
- 将已曝光且状态为接受的图像标记为拒绝状态
- 仅在选中体位已曝光且 judged_status 为 'Accept' 或 '' 时显示
2. **恢复按钮**
- 将已拒绝的图像恢复为接受状态
- 仅在选中体位已曝光且 judged_status 为 'Reject' 时显示
- 与拒绝按钮占据同一位置(互斥显示)
3. **保存参数按钮**
- 保存当前 APR 参数到未曝光的体位
- 仅在选中体位未曝光时显示
- **注意**: 当前 API 暂未提供,使用 TODO 标记
4. **删除按钮可见性调整**
- 当选中体位已曝光时,隐藏"删除选择的体位"按钮
- 防止误删已曝光的图像
### 显示逻辑表
| 体位状态 | expose_status | judged_status | 拒绝按钮 | 恢复按钮 | 保存参数 | 删除按钮 |
|---------|---------------|---------------|---------|---------|---------|---------|
| 未曝光 | Unexposed | - | 隐藏 | 隐藏 | **显示** | 显示 |
| 已曝光-接受 | Exposed | Accept | **显示** | 隐藏 | 隐藏 | **隐藏** |
| 已曝光-拒绝 | Exposed | Reject | 隐藏 | **显示** | 隐藏 | **隐藏** |
| 已曝光-未判断 | Exposed | '' | **显示** | 隐藏 | 隐藏 | **隐藏** |
## 🏗️ 架构设计
### 参与者列表
#### 1. UI 层
- **ContentAreaLarge.tsx** - 主容器组件,包含所有控制按钮
- 添加处理函数:`handleReject()`, `handleRestore()`, `handleSaveParams()`
- 计算按钮可见性逻辑
- 管理按钮的显示/隐藏
- **BodyPositionList.tsx** - 体位列表组件
- 提供选中体位信息
- 显示体位的曝光和判断状态徽章
#### 2. 状态管理层
- **bodyPositionListSlice.ts** - 体位列表状态管理
- `selectedBodyPosition` - 当前选中的体位
- `judgeImageThunk` - 判断图像的异步 Thunk
- `loading` - 加载状态
关键 Reducer:
- `judgeImageThunk.fulfilled` - 更新 judged_status
- `judgeImageThunk.rejected` - 处理错误
- **aprSlice.ts** - APR 参数状态管理
- `aprConfig` - 当前 APR 参数配置(用于保存参数功能)
#### 3. 业务逻辑层
- **paraSettingCoordinator.ts** - APR 参数协调器
- 协调设备参数的增减操作
- (保存参数功能待实现时会用到)
#### 4. API 层
- **judgeImage.ts** - 图像判断 API
- `judgeImage(instanceUid, accept)` - 接受/拒绝图像
- 端点: `POST /auth/task/inspection/judge`
- 请求参数:
```typescript
{
instance_uid: string; // SOP 实例 UID
accept: boolean; // true: 接受, false: 拒绝
}
```
#### 5. 数据模型
- **ExtendedBodyPosition** - 扩展体位信息
```typescript
interface ExtendedBodyPosition {
sop_instance_uid: string;
dview: {
expose_status: 'Exposed' | 'Unexposed';
judged_status: 'Accept' | 'Reject' | '';
// ... 其他字段
};
// ... 其他字段
}
```
### 组件交互序列图
```mermaid
sequenceDiagram
participant User as 👤 用户
participant UI as ContentAreaLarge
participant Redux as Redux Store
participant API as judgeImage API
Note over User,API: 场景1: 拒绝图像
User->>UI: 点击拒绝按钮
UI->>UI: 检查 selectedBodyPosition 是否存在
UI->>Redux: dispatch(judgeImageThunk({sopInstanceUid, accept: false}))
Redux->>Redux: judgeImageThunk.pending
Redux->>UI: loading = true
UI->>User: 按钮显示 loading 状态
Redux->>API: POST /auth/task/inspection/judge
API-->>Redux: 返回成功 {code: '0x000000'}
Redux->>Redux: judgeImageThunk.fulfilled
Redux->>Redux: 更新 bodyPositions[i].dview.judged_status = 'Reject'
Redux->>Redux: 更新 selectedBodyPosition.dview.judged_status = 'Reject'
Redux-->>UI: 状态更新, loading = false
UI->>UI: 重新计算按钮可见性
UI-->>User: 显示恢复按钮(隐藏拒绝按钮)
UI-->>User: 显示成功消息
Note over User,API: 场景2: 恢复图像
User->>UI: 点击恢复按钮
UI->>Redux: dispatch(judgeImageThunk({sopInstanceUid, accept: true}))
Redux->>API: POST /auth/task/inspection/judge
API-->>Redux: 返回成功
Redux->>Redux: 更新 judged_status = 'Accept'
Redux-->>UI: 状态更新
UI-->>User: 显示拒绝按钮(隐藏恢复按钮)
UI-->>User: 显示成功消息
Note over User,API: 场景3: 保存参数(待实现)
User->>UI: 点击保存参数按钮
UI->>UI: 读取当前 APR 参数
UI->>UI: TODO: 调用保存参数 API
UI-->>User: 显示"保存参数功能开发中"
```
### 数据流图
```mermaid
flowchart TD
Start[用户操作] --> Check{检查操作类型}
Check -->|拒绝| Reject[handleReject]
Check -->|恢复| Restore[handleRestore]
Check -->|保存参数| Save[handleSaveParams]
Reject --> ValidateReject{检查体位存在?}
ValidateReject -->|否| WarnReject[显示警告: 请先选择一个体位]
ValidateReject -->|是| DispatchReject[dispatch judgeImageThunk accept=false]
Restore --> ValidateRestore{检查体位存在?}
ValidateRestore -->|否| WarnRestore[显示警告: 请先选择一个体位]
ValidateRestore -->|是| DispatchRestore[dispatch judgeImageThunk accept=true]
DispatchReject --> APICall[调用 judgeImage API]
DispatchRestore --> APICall
APICall --> APIResult{API 返回}
APIResult -->|成功| UpdateState[更新 Redux 状态]
APIResult -->|失败| ShowError[显示错误消息]
UpdateState --> UpdatePositions[更新 bodyPositions 中的 judged_status]
UpdatePositions --> UpdateSelected[更新 selectedBodyPosition.judged_status]
UpdateSelected --> Rerender[触发 UI 重新渲染]
Rerender --> CalcVisibility[重新计算按钮可见性]
CalcVisibility --> UpdateButtons[更新按钮显示/隐藏]
UpdateButtons --> ShowSuccess[显示成功消息]
Save --> ValidateSave{检查体位存在?}
ValidateSave -->|否| WarnSave[显示警告: 请先选择一个体位]
ValidateSave -->|是| TODOSave[TODO: 调用保存参数 API]
TODOSave --> InfoSave[显示: 保存参数功能开发中]
WarnReject --> End[结束]
WarnRestore --> End
WarnSave --> End
ShowError --> End
ShowSuccess --> End
InfoSave --> End
```
### 按钮可见性计算流程
```mermaid
flowchart TD
Start[获取 selectedBodyPosition] --> CheckNull{体位是否存在?}
CheckNull -->|否| HideAll[隐藏所有按钮]
CheckNull -->|是| GetStatus[获取 expose_status 和 judged_status]
GetStatus --> CheckExpose{expose_status?}
CheckExpose -->|Unexposed| ShowUnexposed[显示: 保存参数, 删除]
ShowUnexposed --> HideUnexposed[隐藏: 拒绝, 恢复]
CheckExpose -->|Exposed| CheckJudged{judged_status?}
CheckJudged -->|'Reject'| ShowRestore[显示: 恢复]
ShowRestore --> HideRestore[隐藏: 拒绝, 保存参数, 删除]
CheckJudged -->|'Accept' 或 ''| ShowReject[显示: 拒绝]
ShowReject --> HideReject[隐藏: 恢复, 保存参数, 删除]
HideAll --> End[结束]
HideUnexposed --> End
HideRestore --> End
HideReject --> End
```
## 📊 数据结构
### ExtendedBodyPosition 接口
```typescript
interface ExtendedBodyPosition {
// 基本信息
patient_name: string;
patient_id: string;
registration_number: string;
study_description: string;
// 体位信息
view_name: string;
view_description: string;
view_icon_name: string;
body_position_image: string;
// 唯一标识符
sop_instance_uid: string;
series_instance_uid?: string;
study_instance_uid?: string;
study_id?: string;
// 核心状态对象
dview: {
expose_status: 'Exposed' | 'Unexposed'; // 曝光状态
judged_status: 'Accept' | 'Reject' | ''; // 判断状态
PrimarySopUID: string;
thumbnail_file: string;
image_file: string;
// ... 其他字段
};
// 其他配置
collimator_length: number | string;
collimator_width: number | string;
sid: string;
work: Work;
}
```
### 按钮可见性状态
```typescript
// 在 ContentAreaLarge 组件中计算
const exposeStatus = selectedBodyPosition?.dview.expose_status;
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
const isExposed = exposeStatus === 'Exposed';
const isUnexposed = exposeStatus === 'Unexposed';
// 按钮可见性
const showRejectButton = isExposed && judgedStatus !== 'Reject';
const showRestoreButton = isExposed && judgedStatus === 'Reject';
const showSaveParamsButton = isUnexposed;
const showDeleteButton = isUnexposed;
```
### API 请求/响应结构
```typescript
// 请求
interface JudgeImageRequest {
instance_uid: string; // SOP 实例 UID
accept: boolean; // true: 接受, false: 拒绝
}
// 响应
interface JudgeImageResponse {
code: string; // '0x000000' 表示成功
description: string;
solution: string;
data: {
'@type': string;
value: object;
};
}
```
## 🔄 执行流程
### 用户操作起点
#### 1. 进入检查流程
```mermaid
flowchart TD
Start([用户进入检查流程]) --> LoadPositions[加载体位列表]
LoadPositions --> AutoSelect[自动选中第一个未曝光体位]
AutoSelect --> GetStatus[获取体位状态信息]
GetStatus --> CalcVisibility[计算按钮可见性]
CalcVisibility --> RenderUI[渲染 UI]
RenderUI --> WaitUser[等待用户操作]
```
#### 2. 切换体位
```mermaid
flowchart TD
Click([用户点击体位]) --> UpdateSelected[更新 selectedBodyPosition]
UpdateSelected --> GetNewStatus[获取新体位状态]
GetNewStatus --> RecalcVisibility[重新计算按钮可见性]
RecalcVisibility --> ReRender[重新渲染按钮区域]
ReRender --> Complete([完成])
```
#### 3. 拒绝图像完整流程
```mermaid
flowchart TD
Start([用户点击拒绝按钮]) --> Check{selectedBodyPosition 存在?}
Check -->|否| Warning[显示警告消息]
Check -->|是| Dispatch[dispatch judgeImageThunk]
Dispatch --> Pending[judgeImageThunk.pending]
Pending --> SetLoading[设置 loading = true]
SetLoading --> ShowLoading[按钮显示 loading 状态]
ShowLoading --> CallAPI[调用 POST /auth/task/inspection/judge]
CallAPI --> CheckResponse{API 响应}
CheckResponse -->|成功| Fulfilled[judgeImageThunk.fulfilled]
CheckResponse -->|失败| Rejected[judgeImageThunk.rejected]
Fulfilled --> UpdateJudged[更新 judged_status = 'Reject']
UpdateJudged --> UpdateList[更新 bodyPositions 列表]
UpdateList --> UpdateSelection[更新 selectedBodyPosition]
UpdateSelection --> ClearLoading[设置 loading = false]
ClearLoading --> Rerender[触发 UI 重新渲染]
Rerender --> HideReject[隐藏拒绝按钮]
HideReject --> ShowRestore[显示恢复按钮]
ShowRestore --> Success[显示成功消息]
Rejected --> SetError[设置 error 信息]
SetError --> ClearLoadingError[设置 loading = false]
ClearLoadingError --> ErrorMsg[显示错误消息]
Warning --> End([结束])
Success --> End
ErrorMsg --> End
```
#### 4. 恢复图像流程
与拒绝流程类似,但参数 `accept: true`,结果是 `judged_status = 'Accept'`
#### 5. 保存参数流程(待实现)
```mermaid
flowchart TD
Start([用户点击保存参数按钮]) --> Check{selectedBodyPosition 存在?}
Check -->|否| Warning[显示警告消息]
Check -->|是| ReadAPR[读取当前 APR 配置]
ReadAPR --> LogInfo[记录日志]
LogInfo --> TODO[TODO: 调用保存参数 API]
TODO --> ShowInfo[显示: 保存参数功能开发中]
Warning --> End([结束])
ShowInfo --> End
```
## 📝 实现清单
### 已修改的文件
#### 1. src/pages/exam/ContentAreaLarge.tsx
**新增导入**:
```typescript
import {
removeBodyPositionBySopInstanceUid,
setByIndex,
judgeImageThunk, // 新增
} from '../../states/exam/bodyPositionListSlice';
```
**新增处理函数**:
1. **handleReject()** - 拒绝图像
```typescript
const handleReject = async () => {
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
try {
await dispatch(
judgeImageThunk({
sopInstanceUid: selectedBodyPosition.sop_instance_uid,
accept: false,
})
).unwrap();
message.success('图像已拒绝');
} catch (err) {
console.error('拒绝图像失败:', err);
message.error('拒绝图像失败');
}
};
```
2. **handleRestore()** - 恢复图像
```typescript
const handleRestore = async () => {
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
try {
await dispatch(
judgeImageThunk({
sopInstanceUid: selectedBodyPosition.sop_instance_uid,
accept: true,
})
).unwrap();
message.success('图像已恢复');
} catch (err) {
console.error('恢复图像失败:', err);
message.error('恢复图像失败');
}
};
```
3. **handleSaveParams()** - 保存参数(待实现)
```typescript
const handleSaveParams = async () => {
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
try {
// TODO: 实现保存参数 API 调用
console.log('保存参数功能待实现,当前 APR 配置:', aprConfig);
console.log('选中体位:', selectedBodyPosition.sop_instance_uid);
message.info('保存参数功能开发中');
} catch (err) {
console.error('保存参数失败:', err);
message.error('保存参数失败');
}
};
```
**新增状态订阅**:
```typescript
// 4. 计算按钮可见性
const loading = useSelector(
(state: RootState) => state.bodyPositionList.loading
);
const exposeStatus = selectedBodyPosition?.dview.expose_status;
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
const isExposed = exposeStatus === 'Exposed';
const isUnexposed = exposeStatus === 'Unexposed';
// 按钮可见性逻辑
const showRejectButton = isExposed && judgedStatus !== 'Reject';
const showRestoreButton = isExposed && judgedStatus === 'Reject';
const showSaveParamsButton = isUnexposed;
const showDeleteButton = isUnexposed;
```
**修改的按钮渲染**:
1. 删除按钮 - 添加条件渲染
```typescript
{showDeleteButton && (
)}
```
2. 保存参数按钮 - 添加条件渲染和点击处理
```typescript
{showSaveParamsButton && (
)}
```
3. 拒绝按钮 - 添加条件渲染、loading 状态和点击处理
```typescript
{showRejectButton && (
)}
```
4. 恢复按钮 - 新增(与拒绝按钮互斥)
```typescript
{showRestoreButton && (
}
/>
)}
```
### 未修改的文件
以下文件已经具备所需功能,无需修改:
- **src/states/exam/bodyPositionListSlice.ts** - `judgeImageThunk` 已存在
- **src/API/exam/judgeImage.ts** - API 已就绪
- **src/pages/exam/components/BodyPositionList.tsx** - 体位列表无需修改
- **src/domain/exam/bodyPositionSelection.ts** - 体位选择逻辑无需修改
## 🧪 测试方案
### 功能测试用例
#### 1. 按钮显示逻辑测试
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-001 | 未曝光体位 | 选中未曝光体位 | 查看按钮区域 | 显示:保存参数、删除
隐藏:拒绝、恢复 |
| TC-002 | 已曝光-接受 | 选中已曝光且接受的体位 | 查看按钮区域 | 显示:拒绝
隐藏:恢复、保存参数、删除 |
| TC-003 | 已曝光-拒绝 | 选中已曝光且拒绝的体位 | 查看按钮区域 | 显示:恢复
隐藏:拒绝、保存参数、删除 |
| TC-004 | 已曝光-未判断 | 选中已曝光但未判断的体位 | 查看按钮区域 | 显示:拒绝
隐藏:恢复、保存参数、删除 |
| TC-005 | 无选中体位 | 清空选中状态 | 查看按钮区域 | 所有判断相关按钮隐藏 |
#### 2. 拒绝功能测试
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-101 | 拒绝成功 | 选中已曝光-接受体位 | 1. 点击拒绝按钮
2. 等待响应 | 1. 按钮显示 loading
2. API 调用成功
3. judged_status 更新为 'Reject'
4. 显示恢复按钮
5. 显示"图像已拒绝" |
| TC-102 | 拒绝失败 | 选中已曝光体位,模拟 API 错误 | 点击拒绝按钮 | 1. 显示错误消息
2. 状态不变
3. 按钮显示不变 |
| TC-103 | 无选中体位 | 未选中任何体位 | 点击拒绝按钮(如果可见) | 显示警告"请先选择一个体位" |
| TC-104 | 加载状态 | 选中已曝光体位 | 点击拒绝按钮,立即再次点击 | 第二次点击无效(按钮 disabled) |
#### 3. 恢复功能测试
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-201 | 恢复成功 | 选中已曝光-拒绝体位 | 1. 点击恢复按钮
2. 等待响应 | 1. 按钮显示 loading
2. API 调用成功
3. judged_status 更新为 'Accept'
4. 显示拒绝按钮
5. 显示"图像已恢复" |
| TC-202 | 恢复失败 | 选中已拒绝体位,模拟 API 错误 | 点击恢复按钮 | 1. 显示错误消息
2. 状态不变 |
| TC-203 | 拒绝后恢复 | 选中已曝光-接受体位 | 1. 点击拒绝
2. 等待成功
3. 点击恢复 | 最终状态恢复为接受 |
#### 4. 保存参数功能测试(当前版本)
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-301 | 点击保存参数 | 选中未曝光体位 | 点击保存参数按钮 | 1. 控制台输出 APR 配置
2. 显示"保存参数功能开发中" |
| TC-302 | 无选中体位 | 未选中任何体位 | 点击保存参数按钮(如果可见) | 显示警告"请先选择一个体位" |
#### 5. 删除按钮可见性测试
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-401 | 未曝光体位 | 选中未曝光体位 | 查看删除按钮 | 删除按钮可见 |
| TC-402 | 已曝光体位 | 选中已曝光体位 | 查看删除按钮 | 删除按钮隐藏 |
#### 6. 体位切换测试
| 用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
|---------|---------|---------|---------|---------|
| TC-501 | 未曝光→已曝光 | 当前选中未曝光体位 | 点击已曝光-接受体位 | 按钮更新:显示拒绝,隐藏保存参数和删除 |
| TC-502 | 已曝光接受→已曝光拒绝 | 当前选中已曝光-接受体位 | 点击已曝光-拒绝体位 | 按钮更新:显示恢复,隐藏拒绝 |
| TC-503 | 已曝光拒绝→未曝光 | 当前选中已曝光-拒绝体位 | 点击未曝光体位 | 按钮更新:显示保存参数和删除,隐藏恢复 |
### 集成测试场景
#### 场景 1: 完整工作流
```
进入检查流程
→ 选中未曝光体位(显示保存参数、删除)
→ 进行曝光(MQTT 更新状态)
→ 体位变为已曝光-未判断(显示拒绝,隐藏保存参数、删除)
→ 点击拒绝(显示恢复,隐藏拒绝)
→ 点击恢复(显示拒绝,隐藏恢复)
→ 退出
```
#### 场景 2: 多体位混合状态
```
进入检查流程
→ 体位列表包含:2个未曝光,2个已曝光-接受,1个已曝光-拒绝
→ 依次点击每个体位
→ 验证每个体位的按钮显示正确
→ 对已曝光体位进行拒绝/恢复操作
→ 验证状态更新正确
```
#### 场景 3: 边界条件
- 只有1个体位(未曝光)
- 只有1个体位(已曝光)
- 所有体位已曝光
- 所有体位未曝光
### 性能测试
| 测试场景 | 操作 | 性能指标 |
|---------|------|---------|
| 按钮切换响应 | 快速点击不同体位 | UI 更新延迟 < 100ms |
| API 响应时间 | 执行拒绝/恢复操作 | 总耗时 < 2s |
| 并发操作 | 在 loading 期间尝试其他操作 | 按钮正确禁用,无重复请求 |
## 🐛 潜在问题与异常处理
### 1. 边界情况处理
#### 问题 1: selectedBodyPosition 为 null
- **场景**: 体位列表为空或未选中任何体位
- **影响**: 访问 `selectedBodyPosition.dview` 会导致运行时错误
- **解决方案**:
```typescript
const exposeStatus = selectedBodyPosition?.dview.expose_status;
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
```
使用可选链操作符 `?.` 和默认值 `|| ''`
#### 问题 2: judged_status 未定义
- **场景**: 旧数据或新创建的体位可能没有 `judged_status` 字段
- **影响**: 按钮可见性判断可能出错
- **解决方案**:
```typescript
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
```
提供空字符串作为默认值
#### 问题 3: 只有一个体位且已曝光
- **场景**: 列表中只有一个已曝光的体位
- **影响**: 用户无法删除该体位(删除按钮被隐藏)
- **处理**: 这是预期行为,已曝光的体位不应被删除,保护已采集的数据
#### 问题 4: expose_status 状态不一致
- **场景**: 前端状态与后端状态不同步
- **影响**: 按钮显示可能不正确
- **解决方案**: 依赖 MQTT 事件自动更新状态,确保状态同步
### 2. API 调用异常处理
#### 网络错误
- **错误类型**:
- 连接超时
- 网络中断
- DNS 解析失败
- **处理方案**:
```typescript
try {
await dispatch(judgeImageThunk(...)).unwrap();
message.success('操作成功');
} catch (err) {
console.error('操作失败:', err);
message.error('操作失败,请检查网络连接');
}
```
- **用户反馈**: 显示友好的错误消息,建议用户检查网络
#### 服务器错误
- **错误类型**:
- 500 Internal Server Error
- 503 Service Unavailable
- **处理方案**:
- 捕获错误并显示通用错误消息
- 记录详细错误日志供调试
- 不修改当前 UI 状态
#### API 返回非成功码
- **场景**: `response.code !== '0x000000'`
- **处理**: judgeImage API 内部已处理,会抛出错误
- **结果**: 在 catch 块中捕获并显示错误消息
### 3. 状态一致性问题
#### Redux 状态与后端不同步
- **场景**:
1. API 调用成功但 Redux 状态未更新
2. 多个客户端同时操作同一体位
- **解决方案**:
1. 使用 `judgeImageThunk.fulfilled` 确保 API 成功后才更新状态
2. 失败时保持原状态不变
3. 考虑添加刷新功能重新拉取最新数据
#### 并发操作冲突
- **场景**: 用户在 loading 期间快速点击其他按钮
- **解决方案**:
```typescript
```
- 使用 `loading` 属性禁用按钮
- 防止重复提交请求
### 4. 用户体验优化
#### Loading 状态管理
- **实现**:
```typescript
const loading = useSelector(
(state: RootState) => state.bodyPositionList.loading
);
```
- **效果**: 按钮显示加载动画,用户清楚操作正在进行
#### 操作反馈
- **成功**: `message.success('图像已拒绝')`
- **失败**: `message.error('拒绝图像失败')`
- **警告**: `message.warning('请先选择一个体位')`
#### 按钮互斥显示
- **设计**: 拒绝和恢复按钮同一时间只显示一个
- **好处**: 界面简洁,避免用户混淆
### 5. 性能考虑
#### 状态更新优化
- **当前实现**: React 自动优化,使用 `useSelector` 订阅特定状态
- **优化点**:
- 按钮可见性计算逻辑简单,不需要额外的 `useMemo`
- Redux 的 selector 已经做了浅比较优化
#### 图标加载
- **处理**: Icon 组件内部处理图标缓存
- **性能**: 首次加载后图标被缓存,后续渲染快速
#### 频繁切换体位
- **场景**: 用户快速点击不同体位
- **影响**: 触发多次重渲染
- **优化**: React 的批量更新机制自动优化,实际性能影响小
### 6. 数据完整性
#### SOP Instance UID 验证
- **处理**: 在调用 API 前检查 `selectedBodyPosition` 存在性
- **代码**:
```typescript
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
```
#### 状态字段缺失
- **处理**: 使用可选链和默认值
- **示例**: `judgedStatus || ''`
## 🔧 后续优化建议
### 1. 保存参数功能完整实现
**待实现内容**:
1. 定义保存参数 API
```typescript
// src/API/exam/saveParams.ts
const saveBodyPositionParams = async (
sopInstanceUid: string,
params: AprConfig
): Promise => {
await axiosInstance.post('/auth/exam/save-params', {
sop_instance_uid: sopInstanceUid,
apr_config: params
});
};
```
2. 创建 Redux Thunk
```typescript
// src/states/exam/bodyPositionListSlice.ts
export const saveParamsThunk = createAsyncThunk(
'bodyPositionList/saveParams',
async (payload: { sopInstanceUid: string; aprConfig: AprConfig }) => {
await saveBodyPositionParams(
payload.sopInstanceUid,
payload.aprConfig
);
return payload;
}
);
```
3. 更新 handleSaveParams
```typescript
const handleSaveParams = async () => {
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
try {
await dispatch(
saveParamsThunk({
sopInstanceUid: selectedBodyPosition.sop_instance_uid,
aprConfig: aprConfig
})
).unwrap();
message.success('参数保存成功');
} catch (err) {
console.error('保存参数失败:', err);
message.error('保存参数失败');
}
};
```
### 2. 添加操作确认对话框
对于某些关键操作,可以考虑添加确认对话框:
```typescript
// 拒绝操作确认(可选)
const handleRejectWithConfirm = () => {
Modal.confirm({
title: '确认拒绝',
content: '确定要拒绝这张图像吗?',
onOk: handleReject,
});
};
```
### 3. 批量操作支持
未来可能需要支持批量拒绝/恢复:
```typescript
// 批量拒绝
const handleBatchReject = async (sopInstanceUids: string[]) => {
for (const uid of sopInstanceUids) {
await dispatch(judgeImageThunk({ sopInstanceUid: uid, accept: false }));
}
};
```
### 4. 操作历史记录
记录用户的判断操作历史:
```typescript
interface JudgeHistory {
sopInstanceUid: string;
action: 'reject' | 'restore';
timestamp: number;
operator: string;
}
```
### 5. 快捷键支持
为常用操作添加快捷键:
- `R` - 拒绝图像
- `A` - 恢复图像(Accept)
- `S` - 保存参数
### 6. 状态同步机制
考虑实现 WebSocket 或轮询机制,确保多客户端状态同步:
```typescript
// 定期检查状态
useEffect(() => {
const interval = setInterval(async () => {
// 重新获取选中体位的最新状态
await refreshBodyPositionStatus(selectedBodyPosition?.sop_instance_uid);
}, 30000); // 每30秒
return () => clearInterval(interval);
}, [selectedBodyPosition]);
```
## 📚 参考资料
### 相关文件
- `src/pages/view/components/ImageStateControl.tsx` - 类似功能的参考实现
- `src/states/exam/bodyPositionListSlice.ts` - 状态管理
- `src/API/exam/judgeImage.ts` - API 定义
- `src/domain/exam/bodyPositionSelection.ts` - 体位选择逻辑
### 技术栈
- React + TypeScript
- Redux Toolkit (状态管理)
- Ant Design (UI 组件)
- Axios (HTTP 请求)
### API 端点
- `POST /auth/task/inspection/judge` - 接受/拒绝图像
- `POST /auth/exam/save-params` - 保存参数(待实现)
## 📋 检查清单
实施前检查:
- [x] 理解业务需求和显示逻辑
- [x] 确认 API 可用性(judgeImage 已就绪)
- [x] 设计组件交互流程
- [x] 规划错误处理策略
实施中检查:
- [x] 导入必要的依赖
- [x] 实现处理函数
- [x] 添加状态订阅
- [x] 更新按钮渲染逻辑
- [x] 添加 loading 状态
- [x] 实现错误处理
实施后检查:
- [ ] 手动测试所有按钮显示逻辑
- [ ] 测试拒绝/恢复功能
- [ ] 测试边界情况
- [ ] 测试错误处理
- [ ] 性能测试
- [ ] 代码审查
- [ ] 文档更新
## 🎯 总结
本次实现成功为检查流程添加了图像状态控制功能,包括:
1. **拒绝按钮** - 将已曝光图像标记为拒绝状态
2. **恢复按钮** - 将已拒绝图像恢复为接受状态
3. **保存参数按钮** - 预留保存 APR 参数功能(待 API 就绪)
4. **删除按钮控制** - 根据曝光状态控制删除按钮的显示
### 关键特性
- ✅ 按钮互斥显示(拒绝/恢复)
- ✅ 基于体位状态动态显示
- ✅ 完整的错误处理
- ✅ Loading 状态反馈
- ✅ 用户友好的提示信息
- ✅ 边界情况处理
### 技术亮点
- 使用 Redux Toolkit 的 `createAsyncThunk` 处理异步操作
- 可选链操作符确保代码健壮性
- 条件渲染优化 UI 显示
- 统一的错误处理策略
### 后续工作
- 实现保存参数 API 并集成
- 添加更多测试用例
- 考虑添加快捷键支持
- 优化用户体验细节
---
**文档版本**: v1.0.0
**最后更新**: 2025-01-14
**维护者**: 开发团队