Преглед изворни кода

worklist 双击后能显示追加体位对话框,但是会追加失败

dengdx пре 3 недеља
родитељ
комит
a41f2a5447

+ 136 - 0
docs/实现/双击RIS条目后追加体位功能设计.md

@@ -0,0 +1,136 @@
+# 双击RIS条目后追加体位功能设计
+
+## 需求整理
+
+### 功能需求
+- 在工作列表(worklist)中双击RIS条目时,先执行保存到本地的操作
+- 保存成功后,自动打开追加体位对话框
+- 用户可以在对话框中选择要追加的体位
+- 确认后将选中的体位追加到当前检查中
+
+### 用户场景
+1. 用户在工作列表中浏览RIS数据
+2. 双击某个RIS条目
+3. 系统自动保存该RIS数据到本地
+4. 保存成功后弹出追加体位对话框
+5. 用户选择需要追加的体位
+6. 确认追加,体位添加到检查中
+
+### 业务规则
+- 只有RIS数据(entry_id存在)才触发此流程
+- 保存失败时不打开追加对话框
+- 追加体位需要基于当前选中的体位信息
+- 追加操作失败时给出相应提示
+
+## 参与者
+
+### 核心组件
+- **WorklistPage** (`src/pages/patient/worklist.tsx`): 工作列表页面,处理双击事件
+- **AppendViewModal** (`src/pages/exam/components/AppendViewModal.tsx`): 追加体位对话框组件
+- **BodyPositionList** (`src/pages/exam/components/BodyPositionList.tsx`): 体位列表组件(参考追加体位按钮实现)
+
+### 业务逻辑
+- **saveRisData** (`src/domain/patient/risSaveLogic.ts`): RIS数据保存逻辑
+- **saveRisBatch** (`src/API/patient/risActions.ts`): RIS批量保存API
+- **appendViewsThunk** (`src/states/exam/appendViewSlice.ts`): 追加体位Redux异步操作
+
+### Redux状态
+- `selectedBodyPosition`: 当前选中的体位信息
+- `selectedViews`: 用户选中的待追加视图
+- `viewSelection`: 视图选择状态管理
+
+### 辅助组件
+- **BodyPositionFilter**: 身体部位过滤器
+- **RegisterAvailableList**: 可选体位列表
+- **SelectedProtocolList**: 已选体位列表
+
+## 业务链路
+
+```
+用户双击RIS条目
+    ↓
+WorklistPage.handleRowDoubleClick(record)
+    ↓ (判断为RIS数据)
+调用 saveRisData(record.entry_id, onSuccessCallback)
+    ↓ (异步保存)
+saveRisSingle(entryId, []) → API调用
+    ↓ (保存成功)
+显示成功弹框 + 执行onSuccessCallback
+    ↓
+打开 AppendViewModal 追加体位对话框
+    ↓
+用户选择体位 → 确认追加
+    ↓
+appendViewsThunk() → 追加体位到检查
+    ↓ (追加成功)
+自动调用 worklistToExam() 进入检查页面
+```
+
+## 数据流
+
+### 流程图 (Mermaid)
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant W as WorklistPage
+    participant S as saveRisData
+    participant A as AppendViewModal
+    participant T as appendViewsThunk
+
+    U->>W: 双击RIS条目
+    W->>S: saveRisData(entryId, callback)
+    S->>S: 调用saveRisSingle(entryId, [])
+    S->>W: 保存成功回调 + study信息
+    W->>A: 打开模态框 + 传递study信息
+    A->>U: 显示追加体位界面
+    U->>A: 选择体位
+    U->>A: 确认追加
+    A->>T: appendViewsThunk(study_id, views)
+    T->>A: 追加成功
+    A->>W: 调用onSuccess回调
+    W->>W: 创建Task对象 + 调用worklistToExam()
+    W->>U: 进入检查页面
+```
+
+### 数据传递链路
+
+1. **双击事件**: `record: Task` → `handleRowDoubleClick` → `saveRisData(entryId, onSuccess)`
+2. **保存成功回调**: `onSuccess` 函数执行 → 设置 `isAppendModalOpen = true`
+3. **模态框初始化**: `selectedBodyPosition` → `setCurrentBodyPart(bodyPartId)`
+4. **用户选择**: `selectedViews` 数组收集用户选择的体位
+5. **确认追加**: `selectedViews + study_id + currentWork` → `appendViewsThunk`
+
+### 输入数据
+- `record` (Task): 双击的RIS条目数据,包含 `entry_id` 用于保存
+- `selectedBodyPosition`: 当前检查的体位信息,用于初始化追加模态框
+
+### 输出数据
+- 保存成功的RIS数据本地化
+- 新追加的体位列表更新到当前检查
+
+## 实现方案
+
+### 方案选择
+采用方案1: 修改saveRisData接受成功回调参数,在保存成功后执行回调打开模态框。
+
+### 代码修改点
+1. **risSaveLogic.ts**: 修改 `saveRisData` 函数使用 `saveRisSingle` API,返回保存的study信息
+2. **AppendViewModal.tsx**: 添加 `studyId` 和 `currentWork` 参数,支持worklist页面调用
+3. **worklist.tsx**: 添加 `AppendViewModal` 组件和相关状态管理,传递study信息
+4. **worklist.tsx**: 修改 `handleRowDoubleClick` 使用回调接收study信息并打开模态框
+
+### 状态管理
+在WorklistPage中添加:
+- `isAppendModalOpen: boolean` 状态控制模态框显示
+- `currentStudy: any` 状态存储当前保存的study信息
+- 导入 `AppendViewModal` 组件
+- 处理模态框关闭逻辑,清空study信息
+
+### 问题修复
+**问题**: 弹出的对话框中点击"追加"按钮提示"未找到当前检查信息"
+**原因**: AppendViewModal依赖Redux状态 `selectedBodyPosition`,但worklist页面没有设置此状态
+**解决方案**:
+- 修改AppendViewModal支持接收外部传入的 `studyId` 和 `currentWork` 参数
+- 在worklist页面保存RIS数据成功后,传递study信息给AppendViewModal
+- AppendViewModal优先使用传入的参数,其次使用Redux状态

+ 20 - 7
src/domain/patient/risSaveLogic.ts

@@ -1,4 +1,4 @@
-import { saveRisBatch } from '@/API/patient/risActions';
+import { saveRisSingle } from '@/API/patient/risActions';
 import { Modal } from 'antd';
 import store from '@/states/store';
 import { setPage, setPageSize } from '@/states/patient/worklist/slices/searchSlice';
@@ -8,7 +8,7 @@ import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
 /**
  * RIS数据保存成功弹框
  */
-const showSaveSuccessModal = (count: number) => {
+const showSaveSuccessModal = (count: number, onSuccess?: () => void) => {
   Modal.success({
     title: '🎉 保存成功',
     content: `成功保存 ${count} 条RIS数据到本地\n\n系统将自动刷新列表显示最新数据`,
@@ -17,6 +17,10 @@ const showSaveSuccessModal = (count: number) => {
     afterClose: () => {
       // 弹框自动消失后执行列表刷新
       triggerSearch();
+      // 执行成功回调(如果提供)
+      if (onSuccess) {
+        onSuccess();
+      }
     }
   });
 };
@@ -49,18 +53,27 @@ const triggerSearch = () => {
 /**
  * 保存单个RIS数据到本地
  * @param entryId RIS条目ID
+ * @param onSuccess 可选的成功回调函数,接收保存的study信息
  */
-export const saveRisData = async (entryId: string) => {
+export const saveRisData = async (entryId: string, onSuccess?: (study: any) => void) => {
   try {
     console.log('RIS双击保存本地参数:', entryId);
 
-    // 调用RIS批量保存API(传递单个entryId的数组)
-    await saveRisBatch([entryId]);
+    // 调用RIS单个保存API(传递entryId和空views数组)
+    const response = await saveRisSingle({
+      entry_id: entryId,
+      views: [] // 保存时不指定体位,后面可以追加
+    });
 
-    console.log('RIS双击保存本地成功');
+    console.log('RIS双击保存本地成功,Study ID:', response.data.study_id);
 
     // 显示成功弹框(暂逝后自动刷新)
-    showSaveSuccessModal(1);
+    showSaveSuccessModal(1, () => {
+      // 执行成功回调,传递study信息
+      if (onSuccess) {
+        onSuccess(response.data);
+      }
+    });
 
   } catch (error) {
     console.error('RIS双击保存本地失败:', error);

+ 43 - 9
src/pages/exam/components/AppendViewModal.tsx

@@ -15,11 +15,19 @@ import SelectedProtocolList from '@/pages/patient/components/register.selected.v
 interface AppendViewModalProps {
   open: boolean;
   onCancel: () => void;
+  work?: any; // 可选的检查信息,用于worklist页面调用
+  studyId?: string; // 可选的study ID,用于worklist页面调用
+  currentWork?: any; // 可选的当前工作信息,用于worklist页面调用
+  onSuccess?: () => void; // 可选的成功回调,用于追加成功后执行额外操作
 }
 
 const AppendViewModal: React.FC<AppendViewModalProps> = ({
   open,
   onCancel,
+  work,
+  studyId,
+  currentWork,
+  onSuccess,
 }) => {
   console.log('[AppendViewModal] Rendering with open:', open);
   const dispatch = useDispatch<AppDispatch>();
@@ -37,14 +45,23 @@ const AppendViewModal: React.FC<AppendViewModalProps> = ({
       '[AppendViewModal] useEffect triggered, open:',
       open,
       'selectedBodyPosition:',
-      selectedBodyPosition
+      selectedBodyPosition,
+      'work:',
+      work
     );
-    if (open && selectedBodyPosition) {
-      // 打开模态框时,初始化当前身体部位
-      const bodyPartId = selectedBodyPosition.body_part_id;
-      dispatch(setCurrentBodyPart(bodyPartId));
+    if (open) {
+      if (selectedBodyPosition) {
+        // 正常检查页面:使用selectedBodyPosition
+        const bodyPartId = selectedBodyPosition.body_part_id;
+        dispatch(setCurrentBodyPart(bodyPartId));
+      } else if (work) {
+        // worklist页面:使用传入的work信息
+        // 对于RIS数据,我们可能需要从其他地方获取bodyPart信息
+        // 暂时设置为空,后面可以根据需要调整
+        dispatch(setCurrentBodyPart(null));
+      }
     }
-  }, [open, selectedBodyPosition, dispatch]);
+  }, [open, selectedBodyPosition, work, dispatch]);
 
   const handleConfirm = async (): Promise<void> => {
     if (selectedViews.length === 0) {
@@ -52,7 +69,19 @@ const AppendViewModal: React.FC<AppendViewModalProps> = ({
       return;
     }
 
-    if (!selectedBodyPosition) {
+    // 确定study_id和currentWork
+    let finalStudyId: string;
+    let finalCurrentWork: any;
+
+    if (selectedBodyPosition) {
+      // 正常检查页面:使用selectedBodyPosition
+      finalStudyId = selectedBodyPosition.work.StudyID;
+      finalCurrentWork = selectedBodyPosition.work;
+    } else if (studyId && currentWork) {
+      // worklist页面:使用传入的参数
+      finalStudyId = studyId;
+      finalCurrentWork = currentWork;
+    } else {
       message.error('未找到当前检查信息');
       return;
     }
@@ -60,15 +89,20 @@ const AppendViewModal: React.FC<AppendViewModalProps> = ({
     try {
       await dispatch(
         appendViewsThunk({
-          study_id: selectedBodyPosition.work.StudyID,
+          study_id: finalStudyId,
           views: selectedViews,
-          currentWork: selectedBodyPosition.work,
+          currentWork: finalCurrentWork,
         })
       ).unwrap();
 
       message.success(`成功追加 ${selectedViews.length} 个体位`);
       dispatch(setModalOpen(false));
       onCancel();
+
+      // 调用成功回调
+      if (onSuccess) {
+        onSuccess();
+      }
     } catch (error) {
       message.error('追加体位失败');
       console.error('[AppendViewModal] Error appending views:', error);

+ 82 - 2
src/pages/patient/worklist.tsx

@@ -23,6 +23,7 @@ import { saveRisData } from '../../domain/patient/risSaveLogic';
 import { columnConfigService } from '@/config/tableColumns';
 import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
 import { useMultiSelection } from '@/hooks/useMultiSelection';
+import AppendViewModal from '../exam/components/AppendViewModal';
 
 const { useBreakpoint } = Grid;
 
@@ -31,6 +32,8 @@ const WorklistPage: React.FC = () => {
   const [drawerVisible, setDrawerVisible] = useState(false);
   const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>([]); // 新增:列配置状态
   const [selectedPatientForPortrait, setSelectedPatientForPortrait] = useState<Task | null>(null); // 照片显示用的选中患者
+  const [isAppendModalOpen, setIsAppendModalOpen] = useState(false); // 追加体位模态框状态
+  const [currentStudy, setCurrentStudy] = useState<any>(null); // 当前保存的study信息
 
   // 启用RIS自动同步(在后台静默运行)
   useRisAutoSync();
@@ -130,8 +133,12 @@ const WorklistPage: React.FC = () => {
 
     // 判断是否为RIS数据
     if (record.entry_id) {
-      // RIS数据:触发保存到本地
-      saveRisData(record.entry_id);
+      // RIS数据:触发保存到本地,保存成功后打开追加体位对话框
+      saveRisData(record.entry_id, (study) => {
+        console.log('[WorklistPage] RIS数据保存成功,Study ID:', study.study_id, '打开追加体位对话框');
+        setCurrentStudy(study);
+        setIsAppendModalOpen(true);
+      });
     } else {
       // 本地study数据:进入检查
       worklistToExam(record);
@@ -228,6 +235,79 @@ const WorklistPage: React.FC = () => {
           </Col>
         </Row>
       )}
+
+      {/* 追加体位模态框 */}
+      <AppendViewModal
+        open={isAppendModalOpen}
+        onCancel={() => {
+          setIsAppendModalOpen(false);
+          setCurrentStudy(null); // 关闭时清空study信息
+        }}
+        studyId={currentStudy?.study_id}
+        currentWork={currentStudy}
+        onSuccess={() => {
+          // 追加成功后进入检查
+          if (currentStudy) {
+            // 创建Task对象用于进入检查
+            const taskForExam: Task = {
+              StudyInstanceUID: currentStudy.study_instance_uid,
+              StudyID: currentStudy.study_id,
+              SpecificCharacterSet: currentStudy.specific_character_set,
+              AccessionNumber: currentStudy.accession_number,
+              PatientID: currentStudy.patient_id,
+              PatientName: currentStudy.patient_name,
+              DisplayPatientName: currentStudy.patient_name,
+              PatientSize: currentStudy.patient_size,
+              PatientAge: currentStudy.patient_age,
+              PatientSex: currentStudy.patient_sex,
+              AdmittingTime: currentStudy.admitting_time || '',
+              RegSource: currentStudy.reg_source,
+              StudyStatus: currentStudy.study_status,
+              RequestedProcedureID: '',
+              PerformedProtocolCodeValue: '',
+              PerformedProtocolCodeMeaning: '',
+              PerformedProcedureStepID: '',
+              StudyDescription: currentStudy.study_description,
+              StudyStartDatetime: currentStudy.study_start_datetime || '',
+              ScheduledProcedureStepStartDate: currentStudy.scheduled_procedure_step_start_date || '',
+              StudyLock: currentStudy.study_lock,
+              OperatorID: currentStudy.operator_name,
+              Modality: currentStudy.modality,
+              Views: [], // 会在worklistToExam中填充
+              Thickness: currentStudy.thickness,
+              PatientType: currentStudy.patient_type,
+              StudyType: currentStudy.study_type,
+              QRCode: '',
+              IsExported: currentStudy.is_exported,
+              IsEdited: currentStudy.is_edited,
+              WorkRef: '',
+              IsAppended: currentStudy.is_appended,
+              CreationTime: currentStudy.create_time,
+              MappedStatus: currentStudy.mapped_status,
+              IsDelete: false,
+              patient_dob: currentStudy.patient_dob,
+              ref_physician: currentStudy.ref_physician,
+              weight: currentStudy.weight,
+              length: currentStudy.length,
+              comment: currentStudy.comment,
+              // 宠物相关字段
+              owner_name: currentStudy.owner_name,
+              chip_number: currentStudy.chip_number,
+              variety: currentStudy.variety,
+              sex_neutered: currentStudy.sex_neutered,
+              is_anaesthesia: currentStudy.is_anaesthesia,
+              is_sedation: currentStudy.is_sedation,
+              pregnancy_status: currentStudy.pregnancy_status,
+              // 患者照片字段
+              portrait_status: currentStudy.portrait_status,
+              portrait_file: currentStudy.portrait_file,
+            };
+
+            console.log('[WorklistPage] 追加成功,进入检查:', taskForExam.StudyID);
+            worklistToExam(taskForExam);
+          }
+        }}
+      />
     </div>
   );
 };