在图像处理界面(view 模式)的 OperationPanel 底部,实现图像状态控制功能,包括:
judged_status
OperationPanel (src/pages/view/components/OperationPanel.tsx
)
ImageStateControl (src/pages/view/components/ImageStateControl.tsx
) - 新建
viewerContainerSlice (src/states/view/viewerContainerSlice.ts
)
selectGridLayout
, selectSelectedViewers
bodyPositionListSlice (src/states/exam/bodyPositionListSlice.ts
) - 需修改
judgeImageThunk
异步操作src/API/exam/judgeImage.ts
)
flowchart TD
A[用户点击拒绝/恢复按钮] --> B[ImageStateControl 组件]
B --> C{获取状态}
C --> D[viewerContainerSlice<br/>selectedViewers, gridLayout]
C --> E[bodyPositionList<br/>judged_status, sopInstanceUid]
B --> F[dispatch judgeImageThunk]
F --> G[bodyPositionListSlice]
G --> H[调用 judgeImage API]
H --> I{API 结果}
I -->|成功| J[extraReducers.fulfilled<br/>更新 judged_status]
I -->|失败| K[extraReducers.rejected<br/>设置 error]
J --> L[Redux 通知状态变更]
K --> L
L --> B
B --> M[重新渲染按钮]
sequenceDiagram
participant User as 用户
participant UI as ImageStateControl
participant VCS as viewerContainerSlice
participant BPL as bodyPositionListSlice
participant API as judgeImage API
Note over UI: 组件初始化
UI->>VCS: selectSelectedViewers()
VCS-->>UI: [imageUrl]
UI->>VCS: selectGridLayout()
VCS-->>UI: '1x1'
UI->>BPL: 读取 bodyPositions
BPL-->>UI: {judged_status: '', sopInstanceUid}
Note over UI: 决定按钮可见性
alt gridLayout === '1x1'
alt judged_status === 'Reject'
UI->>UI: 渲染"恢复"按钮
else
UI->>UI: 渲染"拒绝"按钮
end
else
UI->>UI: 不渲染拒绝/恢复按钮
end
Note over User: 用户点击"拒绝"按钮
User->>UI: onClick
UI->>BPL: dispatch judgeImageThunk({sopInstanceUid, accept: false})
BPL->>BPL: pending → loading = true
BPL->>API: judgeImage(sopInstanceUid, false)
API-->>BPL: 成功
BPL->>BPL: fulfilled → 更新 judged_status = 'Reject'
BPL->>BPL: loading = false
BPL-->>UI: 状态变更通知
UI->>User: 显示成功提示
UI->>UI: 重新渲染,显示"恢复"按钮
/**
* 图像判断状态
* - 'Accept': 已接受
* - 'Reject': 已拒绝
* - '': 未判断
*/
type ImageJudgedStatus = 'Accept' | 'Reject' | '';
interface BodyPositionListState {
bodyPositions: BodyPosition[];
selectedBodyPosition: BodyPosition | null;
loading: boolean; // 新增
error: string | null; // 新增
}
interface JudgeImagePayload {
sopInstanceUid: string;
accept: boolean;
}
interface JudgeImageResult {
sopInstanceUid: string;
judgedStatus: ImageJudgedStatus;
}
文件: src/pages/view/components/ImageStateControl.tsx
核心逻辑:
const ImageStateControl = () => {
const dispatch = useDispatch();
// 获取状态
const selectedViewers = useSelector(selectSelectedViewers);
const gridLayout = useSelector(selectGridLayout);
const bodyPositionList = useSelector((state: RootState) =>
state.bodyPositionList.bodyPositions
);
const loading = useSelector((state: RootState) =>
state.bodyPositionList.loading
);
// 查找当前选中的图像
const selectedImage = useMemo(() => {
if (selectedViewers.length === 0) return null;
const imageUrl = selectedViewers[0];
return bodyPositionList.find(
bp => getDcmImageUrl(bp.sop_instance_uid) === imageUrl
);
}, [selectedViewers, bodyPositionList]);
// 判断按钮可见性
const isSingleGrid = gridLayout === '1x1';
const judgedStatus = selectedImage?.dview?.judged_status || '';
const showRejectButton = isSingleGrid && judgedStatus !== 'Reject';
const showRestoreButton = isSingleGrid && judgedStatus === 'Reject';
// 按钮处理函数
const handleReject = async () => {
if (!selectedImage) return;
try {
await dispatch(judgeImageThunk({
sopInstanceUid: selectedImage.sop_instance_uid,
accept: false
})).unwrap();
message.success('图像已拒绝');
} catch (err) {
message.error('拒绝图像失败');
}
};
const handleRestore = async () => {
if (!selectedImage) return;
try {
await dispatch(judgeImageThunk({
sopInstanceUid: selectedImage.sop_instance_uid,
accept: true
})).unwrap();
message.success('图像已恢复');
} catch (err) {
message.error('恢复图像失败');
}
};
return (
<Flex gap="small" align="center">
{showRejectButton && (
<Button
onClick={handleReject}
loading={loading}
icon={<Icon module="module-process" name="reject" userId="base" theme="default" size="2x" state="normal" />}
style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
title="拒绝"
/>
)}
{showRestoreButton && (
<Button
onClick={handleRestore}
loading={loading}
icon={<Icon module="module-process" name="restore" userId="base" theme="default" size="2x" state="normal" />}
style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
title="恢复"
/>
)}
<Button
disabled
icon={<Icon module="module-process" name="saveas" userId="base" theme="default" size="2x" state="normal" />}
style={{ width: '1.5rem', height: '1.5rem', padding: 0 }}
title="另存为"
/>
</Flex>
);
};
文件: src/states/exam/bodyPositionListSlice.ts
新增 thunk:
import { judgeImage } from '@/API/exam/judgeImage';
export const judgeImageThunk = createAsyncThunk(
'bodyPositionList/judgeImage',
async (payload: { sopInstanceUid: string; accept: boolean }) => {
await judgeImage(payload.sopInstanceUid, payload.accept);
return {
sopInstanceUid: payload.sopInstanceUid,
judgedStatus: payload.accept ? 'Accept' : 'Reject'
};
}
);
extraReducers:
extraReducers: (builder) => {
builder
.addCase(judgeImageThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(judgeImageThunk.fulfilled, (state, action) => {
state.loading = false;
const position = state.bodyPositions.find(
bp => bp.sop_instance_uid === action.payload.sopInstanceUid
);
if (position?.dview) {
position.dview.judged_status = action.payload.judgedStatus;
}
})
.addCase(judgeImageThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to judge image';
});
}
场景 1: 拒绝图像完整流程
场景 2: 恢复图像完整流程
场景 3: 多分格场景
场景 4: API 失败处理
describe('图像状态控制 E2E', () => {
it('完整拒绝和恢复流程', () => {
cy.navigateToView();
cy.get('[data-testid="layout-1x1"]').click();
cy.get('[data-testid="viewer-0"]').click();
cy.get('[title="拒绝"]').click();
cy.contains('图像已拒绝').should('be.visible');
cy.get('[title="恢复"]').should('be.visible');
cy.get('[title="恢复"]').click();
cy.contains('图像已恢复').should('be.visible');
cy.get('[title="拒绝"]').should('be.visible');
});
});
selectedViewers[0]
<Button loading={loading}>
需要提供以下图标文件(放在 src/assets/imgs/module-process/base/default/2x/
目录下):
docs/实现/图像状态控制功能.md
- 实现文档src/pages/view/components/ImageStateControl.tsx
- 组件src/pages/view/components/OperationPanel.tsx
- 替换占位符src/states/exam/bodyPositionListSlice.ts
- 添加 judgeImageThunkclassDiagram
class OperationPanel {
+renderPanel() React.ReactNode
}
class ImageStateControl {
-selectedImage: BodyPosition | null
-judgedStatus: ImageJudgedStatus
-isSingleGrid: boolean
-loading: boolean
-showRejectButton: boolean
-showRestoreButton: boolean
+handleReject() Promise~void~
+handleRestore() Promise~void~
+render() React.ReactNode
}
class viewerContainerSlice {
+gridLayout: GridLayout
+selectedViewers: string[]
+selectGridLayout() GridLayout
+selectSelectedViewers() string[]
}
class bodyPositionListSlice {
+bodyPositions: BodyPosition[]
+loading: boolean
+error: string | null
+judgeImageThunk(payload) AsyncThunk
}
class judgeImage_API {
+judgeImage(instanceUid, accept) Promise
}
class dview {
+PrimarySopUID: string
+judged_status: ImageJudgedStatus
+expose_status: string
}
OperationPanel --> ImageStateControl : 包含
ImageStateControl --> viewerContainerSlice : 读取状态
ImageStateControl --> bodyPositionListSlice : 调用 thunk
bodyPositionListSlice --> judgeImage_API : 调用
bodyPositionListSlice --> dview : 更新
本功能实现了图像处理界面的状态控制,主要特点:
实施完成后,用户可以在单分格模式下方便地拒绝或恢复图像,提升工作效率。