|
@@ -0,0 +1,217 @@
|
|
|
+import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
|
+import { View, fetchViews } from '@/API/patient/viewActions';
|
|
|
+import {
|
|
|
+ appendBodyPositionToStudy,
|
|
|
+ AppendViewRequest,
|
|
|
+} from '@/API/exam/appendBodyPosition';
|
|
|
+import { ExtendedBodyPosition, addBodyPosition } from './bodyPositionListSlice';
|
|
|
+import { fetchViewDetail } from '@/API/patient/viewActions';
|
|
|
+import { dview } from '@/domain/dview';
|
|
|
+import { Work } from './examWorksCacheSlice';
|
|
|
+
|
|
|
+interface AppendViewState {
|
|
|
+ availableViews: View[];
|
|
|
+ selectedViews: View[];
|
|
|
+ isModalOpen: boolean;
|
|
|
+ loading: boolean;
|
|
|
+ error: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+const initialState: AppendViewState = {
|
|
|
+ availableViews: [],
|
|
|
+ selectedViews: [],
|
|
|
+ isModalOpen: false,
|
|
|
+ loading: false,
|
|
|
+ error: null,
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取可用体位列表
|
|
|
+ */
|
|
|
+export const fetchAvailableViewsThunk = createAsyncThunk(
|
|
|
+ 'appendView/fetchAvailableViews',
|
|
|
+ async (
|
|
|
+ params: {
|
|
|
+ patient_type: string;
|
|
|
+ body_part_id: string;
|
|
|
+ },
|
|
|
+ { rejectWithValue }
|
|
|
+ ) => {
|
|
|
+ try {
|
|
|
+ const response = await fetchViews(
|
|
|
+ params.patient_type,
|
|
|
+ params.body_part_id,
|
|
|
+ true,
|
|
|
+ null
|
|
|
+ );
|
|
|
+ return response.data.views;
|
|
|
+ } catch (error) {
|
|
|
+ return rejectWithValue(
|
|
|
+ error.message || 'Failed to fetch available views'
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 追加体位到Study并更新bodyPositionList
|
|
|
+ */
|
|
|
+export const appendViewsThunk = createAsyncThunk(
|
|
|
+ 'appendView/appendViews',
|
|
|
+ async (
|
|
|
+ params: {
|
|
|
+ study_id: string;
|
|
|
+ views: View[];
|
|
|
+ currentWork: Work;
|
|
|
+ },
|
|
|
+ { dispatch, rejectWithValue }
|
|
|
+ ) => {
|
|
|
+ try {
|
|
|
+ // 1. 构建请求数据
|
|
|
+ const request: AppendViewRequest = {
|
|
|
+ study_id: params.study_id,
|
|
|
+ views: params.views.map((view) => ({
|
|
|
+ view_id: view.view_id,
|
|
|
+ procedure_id: view.procedure_id || '',
|
|
|
+ })),
|
|
|
+ };
|
|
|
+
|
|
|
+ // 2. 调用API追加体位
|
|
|
+ const response = await appendBodyPositionToStudy(request);
|
|
|
+
|
|
|
+ // 3. 将返回的 Series.images 转换为 ExtendedBodyPosition
|
|
|
+ const newBodyPositions: ExtendedBodyPosition[] = [];
|
|
|
+
|
|
|
+ for (const series of response.data.series) {
|
|
|
+ for (const image of series.images) {
|
|
|
+ // 获取完整的 view 详情
|
|
|
+ const viewDetail = await fetchViewDetail(image.view_id);
|
|
|
+
|
|
|
+ const dviewData: dview = {
|
|
|
+ view_id: image.view_id,
|
|
|
+ series_instance_uid: image.series_instance_uid,
|
|
|
+ study_instance_uid: image.study_instance_uid,
|
|
|
+ study_id: image.study_id,
|
|
|
+ procedure_id: series.procedure_id,
|
|
|
+ view_description: image.view_description,
|
|
|
+ view_type: image.view_id,
|
|
|
+ PrimarySopUID: image.sop_instance_uid,
|
|
|
+ thumbnail_file: image.image_file_path,
|
|
|
+ image_file: image.image_file_path,
|
|
|
+ image_file_path: image.image_file_path,
|
|
|
+ expose_status: 'Unexposed',
|
|
|
+ };
|
|
|
+
|
|
|
+ const extendedBodyPosition: ExtendedBodyPosition = {
|
|
|
+ ...viewDetail,
|
|
|
+ collimator_length:
|
|
|
+ viewDetail.config_object.DX?.CollimatorSizeLength ||
|
|
|
+ viewDetail.config_object?.Common?.CollimatorSizeLength ||
|
|
|
+ 0,
|
|
|
+ collimator_width:
|
|
|
+ viewDetail.config_object.DX?.CollimatorSizeWidth ||
|
|
|
+ viewDetail.config_object?.Common?.CollimatorSizeWidth ||
|
|
|
+ 0,
|
|
|
+ sid: '',
|
|
|
+ patient_name: params.currentWork.PatientName,
|
|
|
+ patient_id: params.currentWork.PatientID,
|
|
|
+ registration_number: params.currentWork.AccessionNumber,
|
|
|
+ study_description: params.currentWork.StudyDescription,
|
|
|
+ body_position_image: viewDetail.view_icon_name,
|
|
|
+ work: params.currentWork,
|
|
|
+ study_instance_uid: params.currentWork.StudyInstanceUID,
|
|
|
+ sop_instance_uid: image.sop_instance_uid,
|
|
|
+ series_instance_uid: image.series_instance_uid,
|
|
|
+ secondary_sop_uid: image.secondary_sop_uid,
|
|
|
+ study_id: image.study_id,
|
|
|
+ dview: dviewData,
|
|
|
+ };
|
|
|
+
|
|
|
+ newBodyPositions.push(extendedBodyPosition);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 将新体位添加到 bodyPositionList
|
|
|
+ for (const bodyPosition of newBodyPositions) {
|
|
|
+ dispatch(addBodyPosition(bodyPosition));
|
|
|
+ }
|
|
|
+
|
|
|
+ return newBodyPositions;
|
|
|
+ } catch (error) {
|
|
|
+ return rejectWithValue(
|
|
|
+ error.message || 'Failed to append body positions'
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const appendViewSlice = createSlice({
|
|
|
+ name: 'appendView',
|
|
|
+ initialState,
|
|
|
+ reducers: {
|
|
|
+ setModalOpen: (state, action: PayloadAction<boolean>) => {
|
|
|
+ state.isModalOpen = action.payload;
|
|
|
+ if (!action.payload) {
|
|
|
+ // 关闭模态框时清空选中
|
|
|
+ state.selectedViews = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ toggleViewSelection: (state, action: PayloadAction<View>) => {
|
|
|
+ const index = state.selectedViews.findIndex(
|
|
|
+ (v) => v.view_id === action.payload.view_id
|
|
|
+ );
|
|
|
+ if (index >= 0) {
|
|
|
+ // 已选中,取消选中
|
|
|
+ state.selectedViews.splice(index, 1);
|
|
|
+ } else {
|
|
|
+ // 未选中,添加到选中列表
|
|
|
+ state.selectedViews.push(action.payload);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ clearSelectedViews: (state) => {
|
|
|
+ state.selectedViews = [];
|
|
|
+ },
|
|
|
+ clearError: (state) => {
|
|
|
+ state.error = null;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ extraReducers: (builder) => {
|
|
|
+ builder
|
|
|
+ // fetchAvailableViewsThunk
|
|
|
+ .addCase(fetchAvailableViewsThunk.pending, (state) => {
|
|
|
+ state.loading = true;
|
|
|
+ state.error = null;
|
|
|
+ })
|
|
|
+ .addCase(fetchAvailableViewsThunk.fulfilled, (state, action) => {
|
|
|
+ state.loading = false;
|
|
|
+ state.availableViews = action.payload;
|
|
|
+ })
|
|
|
+ .addCase(fetchAvailableViewsThunk.rejected, (state, action) => {
|
|
|
+ state.loading = false;
|
|
|
+ state.error = action.payload as string;
|
|
|
+ })
|
|
|
+ // appendViewsThunk
|
|
|
+ .addCase(appendViewsThunk.pending, (state) => {
|
|
|
+ state.loading = true;
|
|
|
+ state.error = null;
|
|
|
+ })
|
|
|
+ .addCase(appendViewsThunk.fulfilled, (state) => {
|
|
|
+ state.loading = false;
|
|
|
+ state.isModalOpen = false;
|
|
|
+ state.selectedViews = [];
|
|
|
+ })
|
|
|
+ .addCase(appendViewsThunk.rejected, (state, action) => {
|
|
|
+ state.loading = false;
|
|
|
+ state.error = action.payload as string;
|
|
|
+ });
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const {
|
|
|
+ setModalOpen,
|
|
|
+ toggleViewSelection,
|
|
|
+ clearSelectedViews,
|
|
|
+ clearError,
|
|
|
+} = appendViewSlice.actions;
|
|
|
+
|
|
|
+export default appendViewSlice.reducer;
|