浏览代码

fix: resolve body position auto-selection inconsistency (close #55)

- Create bodyPositionSelection coordinator in src/domain/exam/bodyPositionSelection.ts
- Add useEffect auto-selection logic in src/pages/exam/components/BodyPositionList.tsx
- Remove worksListenerMiddleware from src/states/exam/bodyPositionListSlice.ts
- Update eslint.config.mjs to ignore src/log/logger.js
- Ensure auto-selection and manual selection use same complete logic flow
- Fix device synchronization and detail display for first body position
sw 2 周之前
父节点
当前提交
f44a14e12f

+ 1 - 1
eslint.config.mjs

@@ -16,7 +16,7 @@ export default tseslint.config(
 
     prettierConfig,
     {
-        ignores: ['babel.config.js', 'node_modules/', 'postcss.config.js', 'tailwind.config.js','**/app.config.ts', '.build/'],
+        ignores: ['babel.config.js', 'node_modules/', 'postcss.config.js', 'tailwind.config.js','**/app.config.ts', '.build/', 'src/log/logger.js'],
     },
     {
         files: ['**/*.ts', '**/*.tsx'],

+ 109 - 0
src/domain/exam/bodyPositionSelection.ts

@@ -0,0 +1,109 @@
+/**
+ * 体位选择协调器 - 统一处理体位选中的完整逻辑
+ * 确保自动选中和手动选中使用相同的业务流程
+ */
+
+import { message } from 'antd';
+import { AppDispatch } from '@/states/store';
+import {
+  setSelectedBodyPosition,
+  ExtendedBodyPosition,
+} from '@/states/exam/bodyPositionListSlice';
+import { setBodyPositionDetail } from '@/states/exam/bodyPositionDetailSlice';
+import { changeBodyPosition } from '@/API/exam/changeBodyPosition';
+
+export const selectBodyPositionWithFullLogic = async (
+  bodyPosition: ExtendedBodyPosition,
+  dispatch: AppDispatch,
+  currentKey: string,
+  showMessage = false
+): Promise<void> => {
+  try {
+    console.log(
+      `[bodyPositionSelection] Selecting body position: ${bodyPosition.view_name}`
+    );
+
+    // 1. 更新选中状态
+    dispatch(setSelectedBodyPosition(bodyPosition));
+
+    // 2. 设置详情显示区域的数据
+    dispatch(
+      setBodyPositionDetail({
+        view_name: bodyPosition.view_name,
+        view_description: bodyPosition.view_description,
+        view_icon_name: bodyPosition.view_icon_name,
+        patient_name: bodyPosition.patient_name,
+        patient_id: bodyPosition.patient_id,
+        registration_number: bodyPosition.registration_number,
+        study_description: bodyPosition.study_description,
+        body_position_image: bodyPosition.view_big_icon_name,
+        collimator_length: bodyPosition.collimator_length,
+        collimator_width: bodyPosition.collimator_width,
+        sid: bodyPosition.sid,
+      })
+    );
+
+    // 3. 如果在exam模式,同步设备到对应体位
+    if (currentKey === 'exam') {
+      await changeBodyPosition(bodyPosition.sop_instance_uid);
+
+      const successMsg = `Body position changed successfully: ${bodyPosition.view_name}`;
+      console.log(`[bodyPositionSelection] ${successMsg}`);
+
+      if (showMessage) {
+        message.success(successMsg);
+      }
+    } else {
+      console.log(
+        `[bodyPositionSelection] Current key is ${currentKey}, not executing changeBodyPosition.`
+      );
+    }
+  } catch (error) {
+    const errorMsg = 'Failed to change body position';
+    console.error(`[bodyPositionSelection] ${errorMsg}:`, error);
+
+    if (showMessage) {
+      message.error(errorMsg);
+    }
+
+    throw error; // 向上传播错误,让调用者决定如何处理
+  }
+};
+
+/**
+ * 自动选中第一个体位(通常在进入exam时调用)
+ */
+export const autoSelectFirstBodyPosition = async (
+  bodyPositions: ExtendedBodyPosition[],
+  dispatch: AppDispatch,
+  currentKey: string
+) => {
+  if (bodyPositions.length > 0) {
+    console.log('[bodyPositionSelection] Auto-selecting first body position');
+    await selectBodyPositionWithFullLogic(
+      bodyPositions[0],
+      dispatch,
+      currentKey,
+      false // 自动选中时不显示用户消息
+    );
+  }
+};
+
+/**
+ * 手动选中体位(用户点击时调用)
+ */
+export const manualSelectBodyPosition = async (
+  bodyPosition: ExtendedBodyPosition,
+  dispatch: AppDispatch,
+  currentKey: string
+) => {
+  console.log(
+    `[bodyPositionSelection] Manual selection: ${bodyPosition.view_name}`
+  );
+  await selectBodyPositionWithFullLogic(
+    bodyPosition,
+    dispatch,
+    currentKey,
+    true // 手动选中时显示用户消息
+  );
+};

+ 6 - 6
src/log/logger.js

@@ -38,12 +38,12 @@ function proxyLog(level) {
     if (ipcRenderer) {                       // ② Electron 环境
       ipcRenderer.invoke('write-log', level, msg).catch(() => {});
     } else if (typeof fetch !== 'undefined') {
-      // ③ 纯 Web 或 pkg-static:发一条异步 POST /log,不阻塞、不报错
-      fetch('/log', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: safeStringify({ level, msg })
-      }).catch(() => {}); // 404 也不抛错
+      // // ③ 纯 Web 或 pkg-static:发一条异步 POST /log,不阻塞、不报错
+      // fetch('/log', {
+      //   method: 'POST',
+      //   headers: { 'Content-Type': 'application/json' },
+      //   body: safeStringify({ level, msg })
+      // }).catch(() => {}); // 404 也不抛错
     }
   };
 }

+ 23 - 43
src/pages/exam/components/BodyPositionList.tsx

@@ -1,16 +1,15 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 import { useSelector, useDispatch } from 'react-redux';
-import {
-  setSelectedBodyPosition,
-  ExtendedBodyPosition,
-} from '../../../states/exam/bodyPositionListSlice';
-import { setBodyPositionDetail } from '../../../states/exam/bodyPositionDetailSlice';
-import { RootState } from '../../../states/store';
-import { Button, message, Image } from 'antd';
+import { ExtendedBodyPosition } from '../../../states/exam/bodyPositionListSlice';
+import { RootState, AppDispatch } from '../../../states/store';
+import { Button, Image } from 'antd';
 import AppendViewIcon from '@/assets/imgs/append-view.svg';
 import ImageViewer from './ImageViewer';
 import { getExposedImageUrl, getViewIconUrl } from '../../../API/bodyPosition';
-import { changeBodyPosition } from '@/API/exam/changeBodyPosition';
+import {
+  manualSelectBodyPosition,
+  autoSelectFirstBodyPosition,
+} from '@/domain/exam/bodyPositionSelection';
 
 interface BodyPositionListProps {
   layout: 'horizontal' | 'vertical';
@@ -21,43 +20,14 @@ const BodyPositionList: React.FC<BodyPositionListProps> = ({
   layout,
   showAddButton = true,
 }) => {
-  const dispatch = useDispatch();
-  const handleImageClick = (bodyPosition: ExtendedBodyPosition) => {
-    message.info(`Clicked on ${bodyPosition.view_name}`);
-    dispatch(setSelectedBodyPosition(bodyPosition));
-    dispatch(
-      setBodyPositionDetail({
-        view_name: bodyPosition.view_name,
-        view_description: bodyPosition.view_description,
-        view_icon_name: bodyPosition.view_icon_name,
-        patient_name: bodyPosition.patient_name,
-        patient_id: bodyPosition.patient_id,
-        registration_number: bodyPosition.registration_number,
-        study_description: bodyPosition.study_description,
-        body_position_image: bodyPosition.view_big_icon_name,
-        collimator_length: bodyPosition.collimator_length,
-        collimator_width: bodyPosition.collimator_width,
-        sid: bodyPosition.sid,
-      })
-    );
-    if (currentKey === 'exam') {
-      changeBodyPosition(bodyPosition.sop_instance_uid)
-        .then(() => {
-          message.success(
-            `'Body position changed successfully' ${bodyPosition.sop_instance_uid}`
-          );
-        })
-        .catch((error) => {
-          console.error('Error changing body position:', error);
-          message.error('Failed to change body position');
-        });
-    } else {
-      // message.info(`Current key is ${currentKey}, not executing changeBodyPosition.`);
-    }
-  };
+  const dispatch = useDispatch<AppDispatch>();
   const currentKey = useSelector(
     (state: RootState) => state.BusinessFlow.currentKey
   );
+
+  const handleImageClick = async (bodyPosition: ExtendedBodyPosition) => {
+    await manualSelectBodyPosition(bodyPosition, dispatch, currentKey);
+  };
   const bodyPositions = useSelector(
     (state: RootState) => state.bodyPositionList.bodyPositions
   );
@@ -65,6 +35,16 @@ const BodyPositionList: React.FC<BodyPositionListProps> = ({
     (state: RootState) => state.bodyPositionList.selectedBodyPosition
   );
 
+  // 在组件装载完成后,自动选中第一个体位
+  useEffect(() => {
+    if (bodyPositions.length > 0 && !selectedBodyPosition) {
+      console.log(
+        '[BodyPositionList] Auto-selecting first body position on component mount'
+      );
+      autoSelectFirstBodyPosition(bodyPositions, dispatch, currentKey);
+    }
+  }, [bodyPositions, selectedBodyPosition, dispatch, currentKey]);
+
   const addBodyPositionClick = () => {
     // dispatch(addBodyPosition({
     //   view_name: 'New View',

+ 10 - 0
src/states/businessFlowMiddlewareLogic.ts

@@ -6,6 +6,10 @@ import { suspendOrCompleteStudy } from '@/API/patient/workActions';
 import { RootState } from './store';
 import { getQuota } from '@/API/security/quotaActions';
 import { showQuotaAlert } from './security/quotaModalSlice';
+import {
+  setBodyPositions,
+  transformWorksToBodyPositions,
+} from './exam/bodyPositionListSlice';
 
 let continueBusinessFlow = '';
 
@@ -42,6 +46,12 @@ const businessFlowMiddlewareLogic: Middleware =
         store.dispatch(showQuotaAlert());
         return;
       }
+      //进入检查之前准备数据
+      const state = store.getState();
+      const works = state.examWorksCache.works;
+      transformWorksToBodyPositions(works).then((bodyPositions) => {
+        store.dispatch(setBodyPositions(bodyPositions));
+      });
       //判断逻辑和注册逻辑已经在注册时做过了
       // // 进入检查前,如果是从register来的,则判断数据合法性,执行注册等逻辑
       // if (currentKey === 'register') {

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

@@ -1,6 +1,6 @@
-import { createSlice, PayloadAction, Middleware } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import { View as BodyPosition } from '../../API/patient/viewActions';
-import { Work, addWork } from './examWorksCacheSlice';
+import { Work } from './examWorksCacheSlice';
 import { dview } from '@/domain/dview';
 import emitter from '@/utils/eventEmitter';
 import store from '@/states/store';
@@ -55,7 +55,7 @@ emitter.on('TASK_SUCCESS', (message: MqttMessage) => {
   }
 });
 
-const transformWorksToBodyPositions = async (
+export const transformWorksToBodyPositions = async (
   works: Work[]
 ): Promise<ExtendedBodyPosition[]> => {
   return await Promise.all(
@@ -90,21 +90,6 @@ const transformWorksToBodyPositions = async (
   );
 };
 
-const worksListenerMiddleware: Middleware = (store) => (next) => (action) => {
-  const result = next(action);
-
-  if (addWork.match(action)) {
-    const state = store.getState();
-    const works = state.examWorksCache.works;
-    transformWorksToBodyPositions(works).then((bodyPositions) => {
-      store.dispatch(setBodyPositions(bodyPositions));
-      store.dispatch(setSelectedBodyPosition(bodyPositions[0] || null));
-    });
-  }
-
-  return result;
-};
-
 const bodyPositionListSlice = createSlice({
   name: 'bodyPositionList',
   initialState,
@@ -254,4 +239,3 @@ export const {
   updateBodyPositionDview,
 } = bodyPositionListSlice.actions;
 export default bodyPositionListSlice.reducer;
-export { worksListenerMiddleware };

+ 1 - 4
src/states/store.ts

@@ -9,9 +9,7 @@ import productReducer from './productSlice';
 import BusinessFlowReducer from './BusinessFlowSlice';
 import systemModeReducer from './systemModeSlice';
 import examWorksCacheReducer from './exam/examWorksCacheSlice';
-import bodyPositionListReducer, {
-  worksListenerMiddleware,
-} from './exam/bodyPositionListSlice';
+import bodyPositionListReducer from './exam/bodyPositionListSlice';
 import bodyPositionDetailReducer from './exam/bodyPositionDetailSlice';
 import aprReducer from './exam/aprSlice';
 import bodyPositionListenerMiddleware from './exam/bodyPositionListener';
@@ -109,7 +107,6 @@ const store = configureStore({
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(
-      worksListenerMiddleware,
       bodyPositionListenerMiddleware,
       aprMiddleware,
       businessFlowMiddleware,