Browse Source

feat(business flow): implement exit exam logic

dengdx 2 days ago
parent
commit
fe25612984

+ 22 - 0
src/pages/exam/LargeScreen.tsx

@@ -1,8 +1,18 @@
 import { Layout, Row } from 'antd';
+import { useDispatch, useSelector } from 'react-redux';
+import { setFeedbackOpen } from '../../states/exam/largeScreenSlice';
+import { setBusinessFlow } from '../../states/BusinessFlowSlice';
+import { RootState } from '../../states/store';
 import DeviceArea from './DeviceArea';
 import ContentAreaLarge from './ContentAreaLarge';
+import ExamExitFeedback from './components/ExamExitFeedback';
 
 const LargeScreen = () => {
+  const dispatch = useDispatch();
+  const isFeedbackOpen = useSelector(
+    (state: RootState) => state.largeScreen.isFeedbackOpen
+  );
+
   return (
     <Layout className="border-4 border-solid border-red-500">
       <Row className="border-4 border-solid border-red-500">
@@ -11,6 +21,18 @@ const LargeScreen = () => {
       <Row>
         <ContentAreaLarge />
       </Row>
+      <ExamExitFeedback
+        open={isFeedbackOpen}
+        onContinue={() => {
+          dispatch(setFeedbackOpen(false));
+          dispatch(setBusinessFlow('exitExamSuspend'));
+        }}
+        onSave={() => {
+          dispatch(setFeedbackOpen(false));
+          dispatch(setBusinessFlow('exitExamCompleted'));
+        }}
+        onAbort={() => dispatch(setFeedbackOpen(false))}
+      />
     </Layout>
   );
 };

+ 57 - 0
src/pages/exam/components/ExamExitFeedback.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import { Modal, Button, Space } from 'antd';
+import { ExclamationCircleFilled } from '@ant-design/icons';
+
+export interface ExamExitFeedbackProps {
+  /** 是否显示组件 */
+  open: boolean;
+  /** 点击"挂起"按钮的处理函数 */
+  onContinue: () => void;
+  /** 点击"保存并完成"按钮的处理函数 */
+  onSave: () => void;
+  /** 点击"直接中止"按钮的处理函数 */
+  onAbort: () => void;
+  /** 自定义标题 */
+  title?: string;
+  /** 自定义提示内容 */
+  message?: string;
+}
+
+const ExamExitFeedback: React.FC<ExamExitFeedbackProps> = ({
+  open,
+  onContinue,
+  onSave,
+  onAbort,
+  title = '检查未完成',
+  message = '当前检查的曝光步骤尚未完成。中止后,本次检查的进度将不会保存。',
+}) => {
+  return (
+    <Modal
+      title={
+        <Space>
+          <ExclamationCircleFilled style={{ color: '#faad14' }} />
+          <span>{title}</span>
+        </Space>
+      }
+      open={open}
+      onCancel={onContinue}
+      footer={
+        <Space>
+          <Button onClick={onContinue}>继续检查</Button>
+          <Button type="primary" onClick={onSave}>
+            保存并完成
+          </Button>
+          <Button type="primary" danger onClick={onAbort}>
+            直接中止
+          </Button>
+        </Space>
+      }
+      centered
+      closable={false}
+    >
+      <p>{message}</p>
+    </Modal>
+  );
+};
+
+export default ExamExitFeedback;

+ 68 - 18
src/states/businessFlowMiddlewareLogic.ts

@@ -1,11 +1,16 @@
-import { Action, Middleware } from '@reduxjs/toolkit';
+import { Middleware, PayloadAction } from '@reduxjs/toolkit';
 import prepare, { unprepare } from '../domain/exam/prepare';
 import { BusinessFlowState, setBusinessFlow } from './BusinessFlowSlice';
+import { setFeedbackOpen } from './exam/largeScreenSlice';
+
+let continueBusinessFlow = '';
 
 const businessFlowMiddlewareLogic: Middleware =
-  (store) => (next) => (action: Action) => {
+  (store) => (next) => (action: PayloadAction<string>) => {
     //const result = next(action);
-    console.log(`[businessFlowMiddleware] Action dispatched: ${action.type}`);
+    console.log(
+      `[businessFlowMiddleware] Action dispatched: ${action.type} ${action.payload}`
+    );
     if (action.type !== setBusinessFlow.type) {
       return next(action); // Only handle setBusinessFlow actions
     }
@@ -15,28 +20,73 @@ const businessFlowMiddlewareLogic: Middleware =
     console.log(
       `[businessFlowMiddleware] Current business flow is now: ${state.currentKey} ; Last key was: ${state.lastKey}`
     );
+    //进入检查
     if (currentKey === 'exam') {
       prepare();
     }
-    if (currentKey !== 'exam' && state.lastKey === 'exam') {
-      if (currentKey === 'view') {
-        //从检查进入图像处理,有可能是曝光导致的,怎么知道呢?看发生器状态
-        if (store.getState().generatorMonitor.acquisitionState === 1) {
-          //发生器正在采集
-          console.log(
-            `[businessFlowMiddleware] Exiting exam flow, but generator is still acquiring.`
-          );
-          return next(action); // 发生器正在采集,不能退出
-        }
+    if (isFromExamToView(action.payload, currentKey)) {
+      //从检查进入图像处理,有可能是曝光导致的,怎么知道呢?看发生器状态
+      if (store.getState().generatorMonitor.acquisitionState === 1) {
+        //发生器正在采集
+        console.log(
+          `[businessFlowMiddleware] Exiting exam flow, but generator is still acquiring.`
+        );
+        return next(action); // 发生器正在采集,不能退出
+      } else {
+        //非曝光导致的从检查进入处理      // 说明从检查退出 , 执行清理
+        console.log(
+          `[businessFlowMiddleware] Exiting exam flow, last key was: ${state.lastKey}`
+        );
+        unprepare();
+      }
+    }
+    if (isExitingExam(action.payload, currentKey)) {
+      const exposureStatus = store.getState().bodyPositionList.exposureStatus;
+      if (exposureStatus === 'Half Exposed') {
+        store.dispatch(setFeedbackOpen(true));
+        // store.dispatch({ type: 'SET_CONTINUE_BUSINESS_FLOW', payload: currentKey });
+        continueBusinessFlow = action.payload; // 保存退出检查时,要去哪个业务流程
+      } else if (exposureStatus === 'Fully Exposed') {
+        // Notify backend
+        console.log(
+          '[businessFlowMiddleware] Fully Exposed, notifying backend'
+        );
+        // Proceed with the action
+        return next(action);
+      } else if (exposureStatus === 'Not Exposed') {
+        // Proceed with the action
+        return next(action);
       }
-      // 说明从检查退出 , 执行清理
+    }
+    //退出检查的中间过程
+    if (action.payload === 'exitExamCompleted') {
+      // Notify backend with different interfaces for completed and suspended
       console.log(
-        `[businessFlowMiddleware] Exiting exam flow, last key was: ${state.lastKey}`
+        `[businessFlowMiddleware] Notifying backend for ${action.payload}`
       );
-      unprepare();
+      // todo 把所有未曝光的体位从study删除掉--服务端和slice。
+      return next({ ...action, type: continueBusinessFlow });
     }
-
-    return next(action);
+    if (action.payload === 'exitExamSuspended') {
+      // Notify backend with different interfaces for completed and suspended 暂时不通知后端
+      console.log(
+        `[businessFlowMiddleware] Notifying backend for ${action.payload}`
+      );
+      return next({ ...action, type: continueBusinessFlow });
+    }
+    next(action);
   };
+/**
+ * 从检查到图像处理
+ */
+function isFromExamToView(currentAction: string, currentKey: string): boolean {
+  return currentAction === 'view' && currentKey === 'exam';
+}
+function isExitingExam(currentAction: string, currentKey: string): boolean {
+  console.log(
+    `[businessFlowMiddleware] Checking if exiting exam: ${currentAction} vs ${currentKey}`
+  );
+  return currentAction !== 'exam' && currentKey === 'exam';
+}
 
 export default businessFlowMiddlewareLogic;

+ 48 - 3
src/states/exam/bodyPositionListSlice.ts

@@ -30,11 +30,13 @@ export interface ExtendedBodyPosition extends BodyPosition {
 interface BodyPositionListState {
   bodyPositions: ExtendedBodyPosition[];
   selectedBodyPosition: ExtendedBodyPosition | null;
+  exposureStatus: 'Half Exposed' | 'Fully Exposed' | 'Not Exposed' | null;
 }
 
 const initialState: BodyPositionListState = {
   bodyPositions: [],
   selectedBodyPosition: null,
+  exposureStatus: null,
 };
 
 emitter.on('TASK_SUCCESS', (message: MqttMessage) => {
@@ -101,6 +103,14 @@ 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);
     },
@@ -125,7 +135,6 @@ const bodyPositionListSlice = createSlice({
         state.selectedBodyPosition = state.bodyPositions[action.payload];
       }
     },
-
     addViewsToBodyPosition: (
       state,
       action: PayloadAction<{ instanceUid: string; dviews: dview[] }>
@@ -143,7 +152,6 @@ const bodyPositionListSlice = createSlice({
         state.bodyPositions.push(newBodyPosition);
       }
     },
-
     removeBodyPositionBySopInstanceUid: (
       state,
       action: PayloadAction<string>
@@ -152,7 +160,6 @@ const bodyPositionListSlice = createSlice({
         (bodyPosition) => bodyPosition.sop_instance_uid !== action.payload
       );
     },
-
     updateBodyPositionDview: (
       state,
       action: PayloadAction<{ sopInstanceUid: string; msg: MqttMessage }>
@@ -192,8 +199,45 @@ const bodyPositionListSlice = createSlice({
       }
     },
   },
+  extraReducers: (builder) => {
+    builder.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,
@@ -201,6 +245,7 @@ export const {
   setByIndex,
   addViewsToBodyPosition,
   removeBodyPositionBySopInstanceUid,
+  updateBodyPositionDview,
 } = bodyPositionListSlice.actions;
 export default bodyPositionListSlice.reducer;
 export { worksListenerMiddleware };

+ 22 - 0
src/states/exam/largeScreenSlice.ts

@@ -0,0 +1,22 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface LargeScreenState {
+  isFeedbackOpen: boolean;
+}
+
+const initialState: LargeScreenState = {
+  isFeedbackOpen: false,
+};
+
+const largeScreenSlice = createSlice({
+  name: 'largeScreen',
+  initialState,
+  reducers: {
+    setFeedbackOpen: (state, action: PayloadAction<boolean>) => {
+      state.isFeedbackOpen = action.payload;
+    },
+  },
+});
+
+export const { setFeedbackOpen } = largeScreenSlice.actions;
+export default largeScreenSlice.reducer;

+ 2 - 0
src/states/store.ts

@@ -26,6 +26,7 @@ import {
   workUISlice,
 } from './patient/worklist/slices/workSlice';
 import generatorMonitorReducer from './exam/generatorMonitorSlice';
+import largeScreenReducer from './exam/largeScreenSlice';
 
 const store = configureStore({
   reducer: {
@@ -49,6 +50,7 @@ const store = configureStore({
     workUI: workUISlice.reducer,
     search: searchReducer,
     generatorMonitor: generatorMonitorReducer,
+    largeScreen: largeScreenReducer,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(