在检查流程(exam 模块)中实现"恢复/拒绝/保存参数"功能,允许用户对已曝光的图像进行判断操作,对未曝光的体位进行参数保存。
实现日期: 2025-01-14
版本: v1.0.0
拒绝按钮
恢复按钮
保存参数按钮
删除按钮可见性调整
体位状态 | expose_status | judged_status | 拒绝按钮 | 恢复按钮 | 保存参数 | 删除按钮 |
---|---|---|---|---|---|---|
未曝光 | Unexposed | - | 隐藏 | 隐藏 | 显示 | 显示 |
已曝光-接受 | Exposed | Accept | 显示 | 隐藏 | 隐藏 | 隐藏 |
已曝光-拒绝 | Exposed | Reject | 隐藏 | 显示 | 隐藏 | 隐藏 |
已曝光-未判断 | Exposed | '' | 显示 | 隐藏 | 隐藏 | 隐藏 |
ContentAreaLarge.tsx - 主容器组件,包含所有控制按钮
handleReject()
, handleRestore()
, handleSaveParams()
BodyPositionList.tsx - 体位列表组件
selectedBodyPosition
- 当前选中的体位judgeImageThunk
- 判断图像的异步 Thunkloading
- 加载状态
关键 Reducer:
judgeImageThunk.fulfilled
- 更新 judged_statusjudgeImageThunk.rejected
- 处理错误
aprSlice.ts - APR 参数状态管理
aprConfig
- 当前 APR 参数配置(用于保存参数功能)judgeImage.ts - 图像判断 API
judgeImage(instanceUid, accept)
- 接受/拒绝图像POST /auth/task/inspection/judge
请求参数:
{
instance_uid: string; // SOP 实例 UID
accept: boolean; // true: 接受, false: 拒绝
}
typescript
interface ExtendedBodyPosition {
sop_instance_uid: string;
dview: {
expose_status: 'Exposed' | 'Unexposed';
judged_status: 'Accept' | 'Reject' | '';
// ... 其他字段
};
// ... 其他字段
}
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: 显示"保存参数功能开发中"
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
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
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;
}
// 在 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;
// 请求
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;
};
}
flowchart TD
Start([用户进入检查流程]) --> LoadPositions[加载体位列表]
LoadPositions --> AutoSelect[自动选中第一个未曝光体位]
AutoSelect --> GetStatus[获取体位状态信息]
GetStatus --> CalcVisibility[计算按钮可见性]
CalcVisibility --> RenderUI[渲染 UI]
RenderUI --> WaitUser[等待用户操作]
flowchart TD
Click([用户点击体位]) --> UpdateSelected[更新 selectedBodyPosition]
UpdateSelected --> GetNewStatus[获取新体位状态]
GetNewStatus --> RecalcVisibility[重新计算按钮可见性]
RecalcVisibility --> ReRender[重新渲染按钮区域]
ReRender --> Complete([完成])
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
与拒绝流程类似,但参数 accept: true
,结果是 judged_status = 'Accept'
flowchart TD
Start([用户点击保存参数按钮]) --> Check{selectedBodyPosition 存在?}
Check -->|否| Warning[显示警告消息]
Check -->|是| ReadAPR[读取当前 APR 配置]
ReadAPR --> LogInfo[记录日志]
LogInfo --> TODO[TODO: 调用保存参数 API]
TODO --> ShowInfo[显示: 保存参数功能开发中]
Warning --> End([结束])
ShowInfo --> End
新增导入:
import {
removeBodyPositionBySopInstanceUid,
setByIndex,
judgeImageThunk, // 新增
} from '../../states/exam/bodyPositionListSlice';
新增处理函数:
handleReject() - 拒绝图像
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('拒绝图像失败');
}
};
try { await dispatch(
judgeImageThunk({
sopInstanceUid: selectedBodyPosition.sop_instance_uid,
accept: true,
})
).unwrap();
message.success('图像已恢复'); } catch (err) { console.error('恢复图像失败:', err); message.error('恢复图像失败'); } }; ```
handleSaveParams() - 保存参数(待实现)
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('保存参数失败');
}
};
新增状态订阅:
// 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;
修改的按钮渲染:
删除按钮 - 添加条件渲染
{showDeleteButton && (
<Tooltip title="删除选择的体位">
<Button ... />
</Tooltip>
)}
typescript
{showSaveParamsButton && (
<Tooltip title="保存参数">
<Button onClick={handleSaveParams} ... />
</Tooltip>
)}
拒绝按钮 - 添加条件渲染、loading 状态和点击处理
{showRejectButton && (
<Tooltip title="拒绝">
<Button
loading={loading}
onClick={handleReject}
...
/>
</Tooltip>
)}
typescript
{showRestoreButton && (
<Tooltip title="恢复">
<Button
loading={loading}
onClick={handleRestore}
icon={<Icon name="btn_RestoreImage" ... />}
/>
</Tooltip>
)}
以下文件已经具备所需功能,无需修改:
judgeImageThunk
已存在用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
TC-001 | 未曝光体位 | 选中未曝光体位 | 查看按钮区域 | 显示:保存参数、删除 隐藏:拒绝、恢复 |
TC-002 | 已曝光-接受 | 选中已曝光且接受的体位 | 查看按钮区域 | 显示:拒绝 隐藏:恢复、保存参数、删除 |
TC-003 | 已曝光-拒绝 | 选中已曝光且拒绝的体位 | 查看按钮区域 | 显示:恢复 隐藏:拒绝、保存参数、删除 |
TC-004 | 已曝光-未判断 | 选中已曝光但未判断的体位 | 查看按钮区域 | 显示:拒绝 隐藏:恢复、保存参数、删除 |
TC-005 | 无选中体位 | 清空选中状态 | 查看按钮区域 | 所有判断相关按钮隐藏 |
用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
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) |
用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
TC-201 | 恢复成功 | 选中已曝光-拒绝体位 | 1. 点击恢复按钮 2. 等待响应 |
1. 按钮显示 loading 2. API 调用成功 3. judged_status 更新为 'Accept' 4. 显示拒绝按钮 5. 显示"图像已恢复" |
TC-202 | 恢复失败 | 选中已拒绝体位,模拟 API 错误 | 点击恢复按钮 | 1. 显示错误消息 2. 状态不变 |
TC-203 | 拒绝后恢复 | 选中已曝光-接受体位 | 1. 点击拒绝 2. 等待成功 3. 点击恢复 |
最终状态恢复为接受 |
用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
TC-301 | 点击保存参数 | 选中未曝光体位 | 点击保存参数按钮 | 1. 控制台输出 APR 配置 2. 显示"保存参数功能开发中" |
TC-302 | 无选中体位 | 未选中任何体位 | 点击保存参数按钮(如果可见) | 显示警告"请先选择一个体位" |
用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
TC-401 | 未曝光体位 | 选中未曝光体位 | 查看删除按钮 | 删除按钮可见 |
TC-402 | 已曝光体位 | 选中已曝光体位 | 查看删除按钮 | 删除按钮隐藏 |
用例编号 | 测试场景 | 前置条件 | 操作步骤 | 期望结果 |
---|---|---|---|---|
TC-501 | 未曝光→已曝光 | 当前选中未曝光体位 | 点击已曝光-接受体位 | 按钮更新:显示拒绝,隐藏保存参数和删除 |
TC-502 | 已曝光接受→已曝光拒绝 | 当前选中已曝光-接受体位 | 点击已曝光-拒绝体位 | 按钮更新:显示恢复,隐藏拒绝 |
TC-503 | 已曝光拒绝→未曝光 | 当前选中已曝光-拒绝体位 | 点击未曝光体位 | 按钮更新:显示保存参数和删除,隐藏恢复 |
进入检查流程
→ 选中未曝光体位(显示保存参数、删除)
→ 进行曝光(MQTT 更新状态)
→ 体位变为已曝光-未判断(显示拒绝,隐藏保存参数、删除)
→ 点击拒绝(显示恢复,隐藏拒绝)
→ 点击恢复(显示拒绝,隐藏恢复)
→ 退出
进入检查流程
→ 体位列表包含:2个未曝光,2个已曝光-接受,1个已曝光-拒绝
→ 依次点击每个体位
→ 验证每个体位的按钮显示正确
→ 对已曝光体位进行拒绝/恢复操作
→ 验证状态更新正确
测试场景 | 操作 | 性能指标 |
---|---|---|
按钮切换响应 | 快速点击不同体位 | UI 更新延迟 < 100ms |
API 响应时间 | 执行拒绝/恢复操作 | 总耗时 < 2s |
并发操作 | 在 loading 期间尝试其他操作 | 按钮正确禁用,无重复请求 |
selectedBodyPosition.dview
会导致运行时错误解决方案:
const exposeStatus = selectedBodyPosition?.dview.expose_status;
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
使用可选链操作符 ?.
和默认值 || ''
judged_status
字段typescript
const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
提供空字符串作为默认值
处理方案:
try {
await dispatch(judgeImageThunk(...)).unwrap();
message.success('操作成功');
} catch (err) {
console.error('操作失败:', err);
message.error('操作失败,请检查网络连接');
}
response.code !== '0x000000'
judgeImageThunk.fulfilled
确保 API 成功后才更新状态typescript
<Button loading={loading} ... />
loading
属性禁用按钮实现:
const loading = useSelector(
(state: RootState) => state.bodyPositionList.loading
);
message.success('图像已拒绝')
message.error('拒绝图像失败')
message.warning('请先选择一个体位')
useSelector
订阅特定状态useMemo
selectedBodyPosition
存在性typescript
if (!selectedBodyPosition) {
message.warning('请先选择一个体位');
return;
}
judgedStatus || ''
待实现内容:
定义保存参数 API
// src/API/exam/saveParams.ts
const saveBodyPositionParams = async (
sopInstanceUid: string,
params: AprConfig
): Promise<void> => {
await axiosInstance.post('/auth/exam/save-params', {
sop_instance_uid: sopInstanceUid,
apr_config: params
});
};
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;
}
);
更新 handleSaveParams
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('保存参数失败');
}
};
对于某些关键操作,可以考虑添加确认对话框:
// 拒绝操作确认(可选)
const handleRejectWithConfirm = () => {
Modal.confirm({
title: '确认拒绝',
content: '确定要拒绝这张图像吗?',
onOk: handleReject,
});
};
未来可能需要支持批量拒绝/恢复:
// 批量拒绝
const handleBatchReject = async (sopInstanceUids: string[]) => {
for (const uid of sopInstanceUids) {
await dispatch(judgeImageThunk({ sopInstanceUid: uid, accept: false }));
}
};
记录用户的判断操作历史:
interface JudgeHistory {
sopInstanceUid: string;
action: 'reject' | 'restore';
timestamp: number;
operator: string;
}
为常用操作添加快捷键:
R
- 拒绝图像A
- 恢复图像(Accept)S
- 保存参数考虑实现 WebSocket 或轮询机制,确保多客户端状态同步:
// 定期检查状态
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
- 体位选择逻辑POST /auth/task/inspection/judge
- 接受/拒绝图像POST /auth/exam/save-params
- 保存参数(待实现)实施前检查:
实施中检查:
实施后检查:
本次实现成功为检查流程添加了图像状态控制功能,包括:
createAsyncThunk
处理异步操作文档版本: v1.0.0
最后更新: 2025-01-14
维护者: 开发团队