Bläddra i källkod

feat: 实现RIS数据双击保存功能

- 新增双击worklist数据行触发RIS保存的功能
- 通过entry_id字段区分RIS数据和本地study数据
- RIS数据双击时自动保存到本地并刷新列表
- 本地数据双击时保持原有进入检查的逻辑

新增文件:
- docs/实现RIS数据双击保存功能.md: 功能实现文档
- src/domain/patient/risSaveLogic.ts: RIS保存业务逻辑封装

修改文件:
- src/pages/patient/worklist.tsx: 添加数据类型判断和保存逻辑调用

技术实现:
- 采用分层架构,将业务逻辑从UI层分离到domain层
- 复用现有的saveRisBatch API和消息处理逻辑
- 保持与ActionPanel中保存功能的一致性
- 自动刷新列表显示最新数据状态

测试要点:
- 双击有entry_id的RIS数据行触发保存
- 双击无entry_id的本地数据行进入检查
- 保存成功/失败的用户反馈
- 列表自动刷新功能
dengdx 4 veckor sedan
förälder
incheckning
cebb057583

+ 247 - 0
docs/实现RIS数据双击保存功能.md

@@ -0,0 +1,247 @@
+# RIS数据双击保存功能实现方案
+
+## 需求描述
+
+在worklist页面中,实现通过双击列表中的数据行来触发"RIS数据保存到本地"功能的需求。具体要求:
+
+- 双击RIS数据行(具备entry_id字段)时,自动触发保存到本地的功能
+- 双击本地study数据行(无entry_id字段)时,保持原有进入检查的逻辑
+- 保存成功后显示成功提示并自动刷新列表
+- 保存失败时显示错误提示
+
+## 当前状态分析
+
+### 现有实现
+1. **双击逻辑**:`src/pages/patient/worklist.tsx` 中的 `handleRowDoubleClick` 函数目前对所有数据行都调用 `worklistToExam(record)` 进入检查
+2. **保存逻辑**:`src/pages/patient/components/ActionPanel.tsx` 中的 `handleRisSave` 函数实现了"RIS数据保存到本地"的完整功能
+3. **数据区分**:RIS数据具有 `entry_id` 字段,本地study数据没有此字段
+
+### 存在问题
+- 双击事件没有区分数据类型,所有双击都进入检查
+- 保存逻辑集中在UI组件中,缺乏业务逻辑分离
+
+## 实现思路
+
+### 架构设计
+采用分层架构,将业务逻辑从UI层分离:
+
+1. **Domain层**:`src/domain/patient/risSaveLogic.ts`
+   - 封装RIS保存的核心业务逻辑
+   - 负责API调用、消息处理、列表刷新等
+   - 与 `worklistToExam.ts` 保持一致的架构风格
+
+2. **UI层**:`src/pages/patient/worklist.tsx`
+   - 负责数据类型判断
+   - 调用相应的domain函数处理业务逻辑
+
+### 技术方案
+- 在双击事件中通过 `record.entry_id` 字段判断数据类型
+- RIS数据:调用domain层的保存函数
+- 本地数据:调用原有的 `worklistToExam` 函数
+- 保持与ActionPanel中保存逻辑的一致性
+
+## 具体实现步骤
+
+### 步骤1:创建Domain层逻辑
+创建 `src/domain/patient/risSaveLogic.ts` 文件:
+
+```typescript
+import { saveRisBatch } from '@/API/patient/risActions';
+import { Modal, message } from 'antd';
+import store from '@/states/store';
+import { setPage, setPageSize } from '@/states/patient/worklist/slices/searchSlice';
+import { fetchWorkThunk } from '@/states/patient/worklist/slices/workSlice';
+import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
+
+/**
+ * RIS数据保存成功弹框
+ */
+const showSaveSuccessModal = (count: number) => {
+  Modal.success({
+    title: '🎉 保存成功',
+    content: (
+      <div style={{ padding: '16px 0' }}>
+        <p style={{ fontSize: '16px', margin: '8px 0' }}>
+          成功保存 <strong style={{ color: '#52c41a' }}>{count}</strong> 条RIS数据到本地
+        </p>
+        <p style={{ color: '#666', margin: '8px 0' }}>
+          系统将自动刷新列表显示最新数据
+        </p>
+      </div>
+    ),
+    okText: '确定',
+    centered: true,
+    afterClose: () => {
+      // 弹框自动消失后执行列表刷新
+      triggerSearch();
+    }
+  });
+};
+
+/**
+ * 触发搜索刷新列表(复用SearchPanel的逻辑)
+ */
+const triggerSearch = () => {
+  const dispatch = store.dispatch;
+  const searchState = store.getState().search;
+
+  // 重置分页
+  dispatch(setPage(1));
+  dispatch(setPageSize(10));
+
+  // 构建过滤条件
+  const filters: WorkFilter = {
+    patient_id: searchState.id,
+    patient_name: searchState.name,
+    access_number: searchState.acc_no,
+    start_time: searchState.start_time,
+    end_time: searchState.end_time,
+    status: 'Arrived,InProgress',
+  };
+
+  // 调用搜索
+  dispatch(fetchWorkThunk({ page: 1, pageSize: 10, filters }));
+};
+
+/**
+ * 保存单个RIS数据到本地
+ * @param entryId RIS条目ID
+ */
+export const saveRisData = async (entryId: string) => {
+  try {
+    console.log('RIS双击保存本地参数:', entryId);
+
+    // 调用RIS批量保存API(传递单个entryId的数组)
+    await saveRisBatch([entryId]);
+
+    console.log('RIS双击保存本地成功');
+
+    // 显示成功弹框(暂逝后自动刷新)
+    showSaveSuccessModal(1);
+
+  } catch (error) {
+    console.error('RIS双击保存本地失败:', error);
+    const errorMessage = error instanceof Error ? error.message : '保存过程中发生未知错误';
+    Modal.error({
+      title: '❌ 保存失败',
+      content: (
+        <div style={{ padding: '16px 0' }}>
+          <p style={{ margin: '8px 0' }}>{errorMessage}</p>
+          <p style={{ color: '#666', fontSize: '14px' }}>
+            请检查网络连接或联系技术支持
+          </p>
+        </div>
+      ),
+      okText: '确定',
+      centered: true
+    });
+  }
+};
+```
+
+### 步骤2:修改UI层逻辑
+修改 `src/pages/patient/worklist.tsx` 文件:
+
+```typescript
+// 添加导入
+import { saveRisData } from '@/domain/patient/risSaveLogic';
+
+// 修改handleRowDoubleClick函数
+const handleRowDoubleClick = (record: Task) => {
+  console.log(
+    '[WorklistTable] Row double-clicked:',
+    JSON.stringify(record, null, 2)
+  );
+
+  // 判断是否为RIS数据
+  if (record.entry_id) {
+    // RIS数据:触发保存到本地
+    saveRisData(record.entry_id);
+  } else {
+    // 本地study数据:进入检查
+    worklistToExam(record);
+  }
+};
+```
+
+## 测试方案
+
+### 功能测试
+1. **RIS数据双击保存测试**:
+   - 前置条件:worklist中存在有entry_id的RIS数据
+   - 操作步骤:
+     1. 在worklist页面找到一条有entry_id的RIS数据行
+     2. 双击该数据行
+   - 预期结果:
+     1. 触发RIS保存API调用
+     2. 显示保存成功弹框
+     3. 弹框自动关闭后列表自动刷新
+     4. 控制台显示相应的日志信息
+
+2. **本地数据双击进入检查测试**:
+   - 前置条件:worklist中存在无entry_id的本地study数据
+   - 操作步骤:
+     1. 在worklist页面找到一条无entry_id的本地数据行
+     2. 双击该数据行
+   - 预期结果:
+     1. 正常进入检查流程
+     2. 页面跳转到exam或process页面
+
+### 异常测试
+1. **保存失败测试**:
+   - 前置条件:模拟网络异常或API错误
+   - 操作步骤:双击RIS数据行
+   - 预期结果:显示保存失败的错误弹框
+
+2. **边界情况测试**:
+   - 测试entry_id为空字符串的情况
+   - 测试entry_id为undefined的情况
+   - 测试网络超时的情况
+
+### 回归测试
+1. **原有功能验证**:
+   - 验证单选/多选功能正常
+   - 验证其他按钮功能正常
+   - 验证分页、搜索功能正常
+
+2. **性能测试**:
+   - 测试大量数据下的双击响应速度
+   - 测试连续双击的处理能力
+
+## 风险评估
+
+### 技术风险
+1. **API兼容性**:确保 `saveRisBatch` API能正确处理单个entryId的数组
+2. **状态管理**:确保列表刷新后Redux状态正确更新
+3. **错误处理**:确保异常情况下的用户体验友好
+
+### 业务风险
+1. **数据一致性**:确保保存后的数据状态正确显示
+2. **用户体验**:确保操作反馈及时且明确
+
+## 实施计划
+
+1. **开发阶段**:
+   - 创建 `risSaveLogic.ts` 文件
+   - 修改 `worklist.tsx` 文件
+   - 本地测试验证
+
+2. **测试阶段**:
+   - 功能测试
+   - 异常测试
+   - 回归测试
+
+3. **部署阶段**:
+   - 代码审查
+   - 生产环境部署
+   - 监控验证
+
+## 验收标准
+
+- [ ] 双击RIS数据行能正确触发保存功能
+- [ ] 双击本地数据行能正常进入检查
+- [ ] 保存成功/失败有适当的用户反馈
+- [ ] 列表在保存成功后自动刷新
+- [ ] 所有原有功能正常工作
+- [ ] 代码通过ESLint检查
+- [ ] 单元测试覆盖核心逻辑

+ 75 - 0
src/domain/patient/risSaveLogic.ts

@@ -0,0 +1,75 @@
+import { saveRisBatch } from '@/API/patient/risActions';
+import { Modal } from 'antd';
+import store from '@/states/store';
+import { setPage, setPageSize } from '@/states/patient/worklist/slices/searchSlice';
+import { fetchWorkThunk } from '@/states/patient/worklist/slices/workSlice';
+import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
+
+/**
+ * RIS数据保存成功弹框
+ */
+const showSaveSuccessModal = (count: number) => {
+  Modal.success({
+    title: '🎉 保存成功',
+    content: `成功保存 ${count} 条RIS数据到本地\n\n系统将自动刷新列表显示最新数据`,
+    okText: '确定',
+    centered: true,
+    afterClose: () => {
+      // 弹框自动消失后执行列表刷新
+      triggerSearch();
+    }
+  });
+};
+
+/**
+ * 触发搜索刷新列表(复用SearchPanel的逻辑)
+ */
+const triggerSearch = () => {
+  const dispatch = store.dispatch;
+  const searchState = store.getState().search;
+
+  // 重置分页
+  dispatch(setPage(1));
+  dispatch(setPageSize(10));
+
+  // 构建过滤条件
+  const filters: WorkFilter = {
+    patient_id: searchState.id,
+    patient_name: searchState.name,
+    access_number: searchState.acc_no,
+    start_time: searchState.start_time,
+    end_time: searchState.end_time,
+    status: 'Arrived,InProgress',
+  };
+
+  // 调用搜索
+  dispatch(fetchWorkThunk({ page: 1, pageSize: 10, filters }));
+};
+
+/**
+ * 保存单个RIS数据到本地
+ * @param entryId RIS条目ID
+ */
+export const saveRisData = async (entryId: string) => {
+  try {
+    console.log('RIS双击保存本地参数:', entryId);
+
+    // 调用RIS批量保存API(传递单个entryId的数组)
+    await saveRisBatch([entryId]);
+
+    console.log('RIS双击保存本地成功');
+
+    // 显示成功弹框(暂逝后自动刷新)
+    showSaveSuccessModal(1);
+
+  } catch (error) {
+    console.error('RIS双击保存本地失败:', error);
+    const errorMessage = error instanceof Error ? error.message : '保存过程中发生未知错误';
+    Modal.error({
+      title: '❌ 保存失败',
+      content: `${errorMessage}\n\n请检查网络连接或联系技术支持`,
+      okText: '确定',
+      centered: true
+    });
+  }
+};

+ 10 - 1
src/pages/patient/worklist.tsx

@@ -19,6 +19,7 @@ import PatientPortraitFloat from './components/PatientPortraitFloat';
 import { RootState, AppDispatch } from '../../states/store';
 import { Task } from '@/domain/work';
 import worklistToExam from '../../domain/patient/worklistToExam';
+import { saveRisData } from '../../domain/patient/risSaveLogic';
 import { columnConfigService } from '@/config/tableColumns';
 import { ColumnConfig } from '@/config/tableColumns/types/columnConfig';
 import { useMultiSelection } from '@/hooks/useMultiSelection';
@@ -126,7 +127,15 @@ const WorklistPage: React.FC = () => {
       '[WorklistTable] Row double-clicked:',
       JSON.stringify(record, null, 2)
     );
-    worklistToExam(record);
+
+    // 判断是否为RIS数据
+    if (record.entry_id) {
+      // RIS数据:触发保存到本地
+      saveRisData(record.entry_id);
+    } else {
+      // 本地study数据:进入检查
+      worklistToExam(record);
+    }
   };
 
   return (