| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
- import { View as BodyPosition } from '../../API/patient/viewActions';
- import { Work } from './examWorksCacheSlice';
- import { dview } from '@/domain/dview';
- import emitter from '@/utils/eventEmitter';
- import store from '@/states/store';
- import { MqttMessage } from '@/domain/mqttService';
- import { fetchViewDetail } from '@/API/patient/viewActions';
- import { judgeImage } from '@/API/exam/judgeImage';
- import { executeSaveImageAs } from '@/domain/exam/saveImageAs';
- /**
- * ExtendedBodyPosition即包含体位模块信息,又包含体位实例信息,也包括study(work)信息
- */
- export interface ExtendedBodyPosition extends BodyPosition {
- patient_name: string;
- patient_id: string;
- registration_number: string;
- study_description: string;
- body_position_image: string;
- collimator_length: number | string;
- collimator_width: number | string;
- sid: string;
- work: Work;
- sop_instance_uid: string;
- series_instance_uid?: string;
- study_instance_uid?: string;
- secondary_sop_uid?: string;
- study_id?: string;
- dview: dview;
- }
- interface BodyPositionListState {
- bodyPositions: ExtendedBodyPosition[];
- selectedBodyPosition: ExtendedBodyPosition | null;
- exposureStatus: 'Half Exposed' | 'Fully Exposed' | 'Not Exposed' | null;
- loading: boolean;
- error: string | null;
- }
- const initialState: BodyPositionListState = {
- bodyPositions: [],
- selectedBodyPosition: null,
- exposureStatus: null,
- loading: false,
- error: null,
- };
- /**
- * 图像判断状态类型
- */
- type ImageJudgedStatus = 'Accept' | 'Reject' | '';
- /**
- * 判断图像 Thunk
- * 调用 API 接受或拒绝图像,成功后更新本地状态
- */
- 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') as ImageJudgedStatus,
- };
- }
- );
- /**
- * 图像另存为 Thunk
- * 调用 domain 层业务逻辑执行另存为,成功后更新体位列表
- */
- export const saveImageAsThunk = createAsyncThunk(
- 'bodyPositionList/saveImageAs',
- async (payload: { sopInstanceUid: string; studyId: string }) => {
- const result = await executeSaveImageAs(
- payload.sopInstanceUid,
- payload.studyId
- );
-
- if (!result.success) {
- throw new Error(result.error || '图像另存为失败');
- }
-
- return {
- updatedBodyPositions: result.updatedBodyPositions,
- };
- }
- );
- emitter.on('TASK_SUCCESS', (message: MqttMessage) => {
- console.log(
- `[bodyPositionListSlice] TASK_SUCCESS received with message:`,
- message
- );
- const { sop } = message;
- if (sop) {
- store.dispatch(
- bodyPositionListSlice.actions.updateBodyPositionDview({
- sopInstanceUid: sop,
- msg: message,
- })
- );
- }
- });
- export const transformWorksToBodyPositions = async (
- works: Work[]
- ): Promise<ExtendedBodyPosition[]> => {
- return await Promise.all(
- works.flatMap((work) =>
- work.Views.map(async (view) => {
- const viewDetail = await fetchViewDetail(view.view_id);
- return {
- ...viewDetail,
- collimator_length:
- viewDetail.config_object.DX?.CollimatorSizeLength ||
- viewDetail.config_object?.Common?.CollimatorSizeLength ||
- 0,
- // viewDetail.config_object.Common.CollimatorSizeLength || 0, 用于动物
- collimator_width:
- viewDetail.config_object.DX?.CollimatorSizeWidth ||
- viewDetail.config_object?.Common?.CollimatorSizeWidth ||
- 0,
- // viewDetail.config_object.Common.CollimatorSizeWidth || 0, 用于动物
- sid: '', // todo 这里还缺少值的来源
- patient_name: work.PatientName,
- patient_id: work.PatientID,
- registration_number: work.AccessionNumber,
- study_description: work.StudyDescription,
- body_position_image: viewDetail.view_icon_name,
- work: work,
- study_instance_uid: work.StudyInstanceUID,
- sop_instance_uid: view.PrimarySopUID,
- study_id:work.StudyID,
- dview: view,
- };
- })
- )
- );
- };
- const bodyPositionListSlice = createSlice({
- name: 'bodyPositionList',
- initialState,
- reducers: {
- setExposureStatus: (
- state,
- action: PayloadAction<
- 'Half Exposed' | 'Fully Exposed' | 'Not Exposed' | null
- >
- ) => {
- state.exposureStatus = action.payload;
- },
- addBodyPosition: (state, action: PayloadAction<ExtendedBodyPosition>) => {
- state.bodyPositions.push(action.payload);
- },
- setBodyPositions: (
- state,
- action: PayloadAction<ExtendedBodyPosition[]>
- ) => {
- state.bodyPositions = action.payload;
- },
- setSelectedBodyPosition: (
- state,
- action: PayloadAction<ExtendedBodyPosition | null>
- ) => {
- state.selectedBodyPosition = action.payload;
- console.log(
- `[bodyPositionListSlice] setSelectedBodyPosition:`,
- action.payload
- );
- },
- setByIndex: (state, action: PayloadAction<number>) => {
- if (action.payload >= 0 && action.payload < state.bodyPositions.length) {
- state.selectedBodyPosition = state.bodyPositions[action.payload];
- }
- },
- addViewsToBodyPosition: (
- state,
- action: PayloadAction<{ instanceUid: string; dviews: dview[] }>
- ) => {
- const [dview] = action.payload.dviews;
- if (dview && state.selectedBodyPosition) {
- const newBodyPosition: ExtendedBodyPosition = {
- ...state.selectedBodyPosition,
- work: {
- ...state.selectedBodyPosition.work,
- Views: [...state.selectedBodyPosition.work.Views, dview],
- },
- sop_instance_uid: action.payload.instanceUid,
- };
- state.bodyPositions.push(newBodyPosition);
- }
- },
- removeBodyPositionBySopInstanceUid: (
- state,
- action: PayloadAction<string>
- ) => {
- state.bodyPositions = state.bodyPositions.filter(
- (bodyPosition) => bodyPosition.sop_instance_uid !== action.payload
- );
- },
- updateBodyPositionDview: (
- state,
- action: PayloadAction<{ sopInstanceUid: string; msg: MqttMessage }>
- ) => {
- const { sopInstanceUid, msg } = action.payload;
- const index = state.bodyPositions.findIndex(
- (bp) => bp.sop_instance_uid === sopInstanceUid
- );
- if (index === -1) {
- console.warn(
- `[bodyPositionListSlice] updateBodyPositionDview: No body position found for SOP Instance UID ${sopInstanceUid}`
- );
- return;
- }
- console.log(
- `[bodyPositionListSlice] updateBodyPositionDview: Updating body position for SOP Instance UID ${sopInstanceUid} with message ${JSON.stringify(msg)} `
- );
- const updatedBodyPosition = {
- ...state.bodyPositions[index],
- sop_instance_uid: sopInstanceUid,
- dview: {
- ...state.bodyPositions[index].dview,
- PrimarySopUID: sopInstanceUid,
- thumbnail_file: msg.thumbnail ?? '',
- image_file: msg.dcm ?? '',
- expose_status: 'Exposed',
- judged_status: state.bodyPositions[index].dview.judged_status || '',
- } satisfies dview,
- };
- state.bodyPositions[index] = updatedBodyPosition;
- console.log(
- `[bodyPositionListSlice] updateBodyPositionDview: Updated body position:`,
- updatedBodyPosition
- );
- // Ensure the selectedBodyPosition is updated if it matches the updated bodyPosition
- if (state.selectedBodyPosition?.sop_instance_uid === sopInstanceUid) {
- state.selectedBodyPosition = updatedBodyPosition;
- }
- },
- },
- extraReducers: (builder) => {
- builder
- // 处理 judgeImageThunk
- .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;
- }
- // 同步更新 selectedBodyPosition
- if (
- state.selectedBodyPosition?.sop_instance_uid ===
- action.payload.sopInstanceUid &&
- state.selectedBodyPosition.dview
- ) {
- state.selectedBodyPosition.dview.judged_status =
- action.payload.judgedStatus;
- }
- })
- .addCase(judgeImageThunk.rejected, (state, action) => {
- state.loading = false;
- state.error = action.error.message || 'Failed to judge image';
- })
- // 处理 saveImageAsThunk
- .addCase(saveImageAsThunk.pending, (state) => {
- state.loading = true;
- state.error = null;
- })
- .addCase(saveImageAsThunk.fulfilled, (state, action) => {
- state.loading = false;
- // 更新体位列表
- state.bodyPositions = action.payload.updatedBodyPositions;
- console.log(
- `[bodyPositionListSlice] Save as completed, updated ${action.payload.updatedBodyPositions.length} body positions`
- );
- })
- .addCase(saveImageAsThunk.rejected, (state, action) => {
- state.loading = false;
- state.error = action.error.message || '图像另存为失败';
- })
- // 处理曝光状态变化
- .addMatcher(
- (action) =>
- action.type === setBodyPositions.type ||
- action.type === addViewsToBodyPosition.type ||
- action.type === removeBodyPositionBySopInstanceUid.type ||
- action.type === updateBodyPositionDview.type,
- (state) => {
- const newExposureStatus = determineExposureStatus(state.bodyPositions);
- state.exposureStatus = newExposureStatus;
- // bodyPositionListSlice.caseReducers.setExposureStatus(state, {
- // payload: newExposureStatus,
- // type: 'setExposureStatus',
- // });
- }
- );
- },
- });
- const determineExposureStatus = (
- bodyPositions: ExtendedBodyPosition[]
- ): 'Half Exposed' | 'Fully Exposed' | 'Not Exposed' | null => {
- if (bodyPositions.length === 0) return null;
- const allExposed = bodyPositions.every(
- (bp) => bp.dview.expose_status === 'Exposed'
- );
- const allUnExposed = bodyPositions.every(
- (bp) => bp.dview.expose_status === 'Unexposed'
- );
- if (allExposed) {
- return 'Fully Exposed';
- } else if (allUnExposed) {
- return 'Not Exposed';
- } else {
- return 'Half Exposed';
- }
- };
- export const {
- addBodyPosition,
- setBodyPositions,
- setSelectedBodyPosition,
- setByIndex,
- addViewsToBodyPosition,
- removeBodyPositionBySopInstanceUid,
- updateBodyPositionDview,
- } = bodyPositionListSlice.actions;
- export default bodyPositionListSlice.reducer;
|