Browse Source

添加复制体位的实现,还需要和后端调试

dengdx 1 week ago
parent
commit
b5407afb18

+ 119 - 1
src/API/patient/workActions.ts

@@ -1,6 +1,8 @@
 import { Series } from '@/domain/series';
 import { Series } from '@/domain/series';
 import axiosInstance from '../interceptor';
 import axiosInstance from '../interceptor';
 import { Task } from '@/domain/work';
 import { Task } from '@/domain/work';
+import { dview } from '@/domain/dview';
+import { XImage } from '@/domain/xImage';
 
 
 interface View {
 interface View {
   view_id: string;
   view_id: string;
@@ -85,6 +87,98 @@ export interface RegisterWorkResponse {
   solution: string;
   solution: string;
   data: RegisterWorkResponseData;
   data: RegisterWorkResponseData;
 }
 }
+
+export interface AcquisitionContext {
+  WorkStationID: number;
+  PatientSize: string;
+  KVP: number;
+  MA: number;
+  MAS: number;
+  MS: number;
+  TechMode: number;
+  AECDensity: number;
+  AECField: number;
+  AECFilm: number;
+  DAP: number;
+  DOSE: number;
+  FocalSpot: string;
+  SID: number;
+  TOD: number;
+  TubeLoad: string;
+  GridType: string;
+  FilterType: string;
+  EXI: number;
+  DI: number;
+  PositionNumber: string;
+  CollimatorFilter: string;
+  CollimatorLength: string;
+  CollimatorWidth: string;
+  CollimatorCenter: string;
+  FpdAcquisitionMode: string;
+  FrameRate: number;
+  EntranceDoseInmGy: number;
+}
+
+export interface ImageFunctionParameter {
+  Name: string;
+  Min: number;
+  Max: number;
+  Step: number;
+  Value: number;
+  ValueType: string;
+  IsEnabled: boolean;
+}
+
+export interface ImageFunction {
+  FunctionName: string;
+  ImageStyle: string;
+  Parameters: ImageFunctionParameter[];
+  CurveType: ImageFunctionParameter;
+  WindowLevelGradeParameters: ImageFunctionParameter[];
+}
+
+export interface ImageProcessingContext {
+  ImageFunction: ImageFunction;
+}
+
+export interface CopyPositionResponseData {
+  series_instance_uid: string;
+  study_instance_uid: string;
+  study_id: string;
+  procedure_id: string;
+  body_part: string;
+  performed_datetime: string | null;
+  performed_protocol_code_meaning: string;
+  performed_protocol_code_value: string;
+  sort: number;
+  product: string;
+  is_pre_install: boolean;
+  images: {
+    sop_instance_id: string;
+    series_instance_uid: string;
+    study_instance_uid: string;
+    secondary_sop_uid: string;
+    study_id: string;
+    view_id: string;
+    view_description: string;
+    image_status: string;
+    image_file_path: string;
+    acquisition_mode: string;
+    acquisition_context: AcquisitionContext | null;
+    img_proc_context: ImageProcessingContext | null;
+    sort: number;
+    product: string;
+    is_pre_install: boolean;
+  }[];
+}
+
+export interface CopyPositionResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: CopyPositionResponseData;
+}
+
 // 充当列表框架的filter
 // 充当列表框架的filter
 export interface TaskListQuery {
 export interface TaskListQuery {
   patient_id?: string;
   patient_id?: string;
@@ -192,4 +286,28 @@ const fetchTaskDetails = async (
   }
   }
 };
 };
 
 
-export { fetchTaskDetails };
+const copyImage = async (instanceUid: string): Promise<dview[]> => {
+  try {
+    const response = await axiosInstance.post('/auth/image/copy', {
+      instance_uid: instanceUid,
+    });
+    const dviews: dview[] = response.data.series.flatMap((series: Series) =>
+      series.images.map((image: XImage) => ({
+        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, // Assuming view_type is the same as view_id
+      }))
+    );
+
+    return dviews;
+  } catch (error) {
+    console.error('Error copying image:', error);
+    throw error;
+  }
+};
+
+export { copyImage, fetchTaskDetails };

+ 28 - 2
src/pages/exam/ContentAreaLarge.tsx

@@ -19,8 +19,9 @@ import {
 import { patientSizes } from '../../states/patientSize';
 import { patientSizes } from '../../states/patientSize';
 import { WorkstationTypeLabels } from '../../states/workstation';
 import { WorkstationTypeLabels } from '../../states/workstation';
 import { FormattedMessage } from 'react-intl';
 import { FormattedMessage } from 'react-intl';
-import { useSelector, useDispatch } from 'react-redux';
+import { useSelector, useDispatch, useStore } from 'react-redux';
 import { deleteBodyPosition } from '../../API/patient/viewActions';
 import { deleteBodyPosition } from '../../API/patient/viewActions';
+import { copyPositionThunk } from '../../states/exam/examWorksCacheSlice';
 import { setByIndex } from '../../states/exam/bodyPositionListSlice';
 import { setByIndex } from '../../states/exam/bodyPositionListSlice';
 import { RootState } from '../../states/store';
 import { RootState } from '../../states/store';
 import {
 import {
@@ -32,9 +33,12 @@ import {
 } from '../../states/exam/aprSlice';
 } from '../../states/exam/aprSlice';
 import BodyPositionList from './components/BodyPositionList';
 import BodyPositionList from './components/BodyPositionList';
 import BodyPositionDetail from './components/BodyPositionDetail';
 import BodyPositionDetail from './components/BodyPositionDetail';
+import { AppDispatch } from '@/states/store';
+import { useRef } from 'react';
 
 
 const ContentAreaLarge = () => {
 const ContentAreaLarge = () => {
-  const dispatch = useDispatch();
+  const dispatch = useDispatch<AppDispatch>();
+  const store = useStore<RootState>();
   const aprConfig = useSelector((state: RootState) => state.apr.aprConfig);
   const aprConfig = useSelector((state: RootState) => state.apr.aprConfig);
   const bodysize = useSelector((state: RootState) => state.apr.bodysize);
   const bodysize = useSelector((state: RootState) => state.apr.bodysize);
   const workstation = useSelector((state: RootState) => state.apr.workstation);
   const workstation = useSelector((state: RootState) => state.apr.workstation);
@@ -61,6 +65,15 @@ const ContentAreaLarge = () => {
     dispatch(setCurrentExposureMode(value));
     dispatch(setCurrentExposureMode(value));
   };
   };
 
 
+  // 1. 正常在顶层用 useSelector 订阅
+  const selectedBodyPosition = useSelector(
+    (state: RootState) => state.bodyPositionList.selectedBodyPosition
+  );
+
+  // 2. 用 ref 保存最新值(每次渲染都会更新)
+  const positionRef = useRef(selectedBodyPosition);
+  positionRef.current = selectedBodyPosition;
+
   return (
   return (
     <Row>
     <Row>
       <Col span={16}>
       <Col span={16}>
@@ -218,6 +231,19 @@ const ContentAreaLarge = () => {
                   type="primary"
                   type="primary"
                   style={{ width: '100%' }}
                   style={{ width: '100%' }}
                   icon={<CopyOutlined />}
                   icon={<CopyOutlined />}
+                  onClick={() => {
+                    const instanceUid =
+                      store.getState().bodyPositionList.selectedBodyPosition
+                        ?.study_instance_uid ?? '';
+                    console.log(
+                      'Copying position for instance UID:',
+                      instanceUid
+                    );
+                    console.log(
+                      `${store.getState().bodyPositionList.selectedBodyPosition}`
+                    );
+                    dispatch(copyPositionThunk({ instanceUid }));
+                  }}
                 />
                 />
               </Tooltip>
               </Tooltip>
             </Col>
             </Col>

+ 2 - 1
src/pages/exam/components/BodyPositionList.tsx

@@ -75,7 +75,7 @@ const BodyPositionList: React.FC<BodyPositionListProps> = ({ layout }) => {
             const viewDetail = await fetchViewDetail(view.view_id);
             const viewDetail = await fetchViewDetail(view.view_id);
             return {
             return {
               ...viewDetail,
               ...viewDetail,
-              sop_instance_uid: view.sop_instance_uid,
+              // sop_instance_uid: view.sop_instance_uid,
               collimator_length:
               collimator_length:
                 viewDetail.config_object.DX.CollimatorSizeLength || 0,
                 viewDetail.config_object.DX.CollimatorSizeLength || 0,
               collimator_width:
               collimator_width:
@@ -87,6 +87,7 @@ const BodyPositionList: React.FC<BodyPositionListProps> = ({ layout }) => {
               study_description: work.StudyDescription,
               study_description: work.StudyDescription,
               body_position_image: viewDetail.view_icon_name,
               body_position_image: viewDetail.view_icon_name,
               work: work,
               work: work,
+              study_instance_uid: work.StudyInstanceUID,
             };
             };
           })
           })
         )
         )

+ 20 - 0
src/states/exam/bodyPositionListSlice.ts

@@ -1,6 +1,7 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import { View as BodyPosition } from '../../API/patient/viewActions';
 import { View as BodyPosition } from '../../API/patient/viewActions';
 import { Work } from './examWorksCacheSlice';
 import { Work } from './examWorksCacheSlice';
+import { dview } from '@/domain/dview';
 
 
 export interface ExtendedBodyPosition extends BodyPosition {
 export interface ExtendedBodyPosition extends BodyPosition {
   patient_name: string;
   patient_name: string;
@@ -53,6 +54,24 @@ const bodyPositionListSlice = createSlice({
         state.selectedBodyPosition = state.bodyPositions[action.payload];
         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);
+      }
+    },
   },
   },
 });
 });
 
 
@@ -61,5 +80,6 @@ export const {
   setBodyPositions,
   setBodyPositions,
   setSelectedBodyPosition,
   setSelectedBodyPosition,
   setByIndex,
   setByIndex,
+  addViewsToBodyPosition,
 } = bodyPositionListSlice.actions;
 } = bodyPositionListSlice.actions;
 export default bodyPositionListSlice.reducer;
 export default bodyPositionListSlice.reducer;

+ 50 - 3
src/states/exam/examWorksCacheSlice.ts

@@ -1,12 +1,17 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
 import { Task as Work } from '@/domain/work';
 import { Task as Work } from '@/domain/work';
+import { copyImage } from '@/API/patient/workActions';
+import { dview } from '@/domain/dview';
+import { addViewsToBodyPosition } from './bodyPositionListSlice';
 
 
 interface ExamWorksCacheState {
 interface ExamWorksCacheState {
   works: Work[];
   works: Work[];
+  loading: boolean;
 }
 }
 
 
 const initialState: ExamWorksCacheState = {
 const initialState: ExamWorksCacheState = {
   works: [],
   works: [],
+  loading: false,
 };
 };
 
 
 const examWorksCacheSlice = createSlice({
 const examWorksCacheSlice = createSlice({
@@ -18,15 +23,57 @@ const examWorksCacheSlice = createSlice({
     },
     },
     removeWork: (state, action: PayloadAction<string>) => {
     removeWork: (state, action: PayloadAction<string>) => {
       state.works = state.works.filter(
       state.works = state.works.filter(
-        (work) => work.AccessionNumber !== action.payload
+        (work) => work.StudyInstanceUID !== action.payload
       );
       );
     },
     },
     clearWorks: (state) => {
     clearWorks: (state) => {
       state.works = [];
       state.works = [];
     },
     },
+    setLoading: (state, action: PayloadAction<boolean>) => {
+      state.loading = action.payload;
+    },
+    addDviews: (
+      state,
+      action: PayloadAction<{ instanceUid: string; dviews: dview[] }>
+    ) => {
+      const work = state.works.find(
+        (work) => work.StudyInstanceUID === action.payload.instanceUid
+      );
+      if (work) {
+        work.Views.push(...action.payload.dviews);
+      }
+    },
   },
   },
 });
 });
 
 
-export const { addWork, removeWork, clearWorks } = examWorksCacheSlice.actions;
+export const { addWork, removeWork, clearWorks, setLoading, addDviews } =
+  examWorksCacheSlice.actions;
+export const copyPositionThunk = createAsyncThunk(
+  'examWorksCache/copyPosition',
+  async ({ instanceUid }: { instanceUid: string }, { dispatch }) => {
+    dispatch(setLoading(true));
+    try {
+      const dviews: dview[] = await copyImage(instanceUid);
+      // const dviews: dview[] = response.data.series.flatMap((series: any) =>
+      //   series.images.map((image: any) => ({
+      //     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 // Assuming view_type is the same as view_id
+      //   }))
+      // );
+      dispatch(addDviews({ instanceUid, dviews }));
+      dispatch(addViewsToBodyPosition({ instanceUid, dviews }));
+    } catch (error) {
+      console.error('Error in copyPositionThunk:', error);
+    } finally {
+      dispatch(setLoading(false));
+    }
+  }
+);
+
 export default examWorksCacheSlice.reducer;
 export default examWorksCacheSlice.reducer;
 export type { Work };
 export type { Work };