Browse Source

feat: worklist双击逻辑优化 - 全曝光study直接进入处理界面

- 新增实现文档 docs/实现/体位全曝光-worklist-双击-进入检查.md
- 修改 src/domain/patient/worklistToExam.ts 添加曝光状态判断
- 当study所有体位都已曝光时,双击直接进入process界面
- 当存在未曝光体位时,保持原有逻辑进入exam界面

close #74
sw 1 week ago
parent
commit
2c0e039f0a

+ 295 - 0
docs/实现/体位全曝光-worklist-双击-进入检查.md

@@ -0,0 +1,295 @@
+# 体位全曝光 - Worklist 双击进入检查或处理
+
+## 需求说明
+
+当 study 的所有体位都已经曝光后,双击 worklist 表格项目时,应该直接进入处理界面(process),而不是进入检查界面(exam)。
+
+## 涉及的 Redux 参与者
+
+### 1. Slices(状态切片)
+
+#### BusinessFlowSlice
+
+- **文件路径**: `src/states/BusinessFlowSlice.ts`
+- **作用**: 管理整个应用的业务流程状态
+- **状态结构**:
+  ```typescript
+  interface BusinessFlowState {
+    currentKey: string; // 当前页面: 'register', 'exam', 'process' 等
+    lastKey?: string; // 上一个页面
+  }
+  ```
+- **关键 Reducer**:
+  - `setBusinessFlow(key: string)` - 切换业务流程到指定页面
+
+#### examWorksCacheSlice
+
+- **文件路径**: `src/states/exam/examWorksCacheSlice.ts`
+- **作用**: 缓存进入检查/处理界面的工作数据
+- **关键 Reducer**:
+  - `addWork(task: Task)` - 添加工作到缓存
+  - `clearWorks()` - 清空工作缓存
+
+#### bodyPositionListSlice
+
+- **文件路径**: `src/states/exam/bodyPositionListSlice.ts`
+- **作用**: 管理体位列表数据
+- **关键 Reducer**:
+  - `setBodyPositions(positions)` - 设置体位数据
+- **关键函数**:
+  - `transformWorksToBodyPositions(works)` - 将 works 转换为体位列表
+
+### 2. Actions(动作)
+
+| Action             | 作用             | 参数                                |
+| ------------------ | ---------------- | ----------------------------------- |
+| `setBusinessFlow`  | 切换业务流程页面 | `key: string` ('exam' 或 'process') |
+| `addWork`          | 添加工作到缓存   | `task: Task`                        |
+| `clearWorks`       | 清空工作缓存     | 无                                  |
+| `setBodyPositions` | 设置体位列表     | `positions: BodyPosition[]`         |
+
+### 3. Domain 逻辑层
+
+#### worklistToExam
+
+- **文件路径**: `src/domain/patient/worklistToExam.ts`
+- **当前逻辑**:
+  1. 获取 task 详情(包含所有体位信息)
+  2. 清空缓存
+  3. 保存 task 到缓存
+  4. **始终跳转到 'exam' 检查界面**
+- **需要修改**: 增加判断所有体位是否已曝光的逻辑
+
+#### worklistToProcess
+
+- **文件路径**: `src/domain/patient/worklistToExam.ts`(同文件)
+- **现有逻辑**: 批量处理选中工单,直接跳转到 'process'
+- **参考价值**: 展示了如何直接进入处理界面的流程
+
+### 4. 组件层
+
+#### WorklistTable
+
+- **文件路径**: `src/pages/patient/components/WorklistTable.tsx`
+- **作用**: 渲染 worklist 表格,处理行点击和双击事件
+- **关键事件**: `onDoubleClick` - 触发 `handleRowDoubleClick` 回调
+
+#### worklist.tsx
+
+- **文件路径**: `src/pages/patient/worklist.tsx`
+- **作用**: Worklist 主页面
+- **关键函数**:
+  - `handleRowDoubleClick(record: Task)` - 调用 `worklistToExam(record)`
+
+## 数据流关系
+
+```
+用户双击 worklist 表格行
+  ↓
+WorklistTable.onDoubleClick
+  ↓
+worklist.handleRowDoubleClick(record)
+  ↓
+worklistToExam(task)
+  ↓
+prepareWorksForExam(task) - 获取详细数据
+  ↓
+检查所有 Views 的 expose_status
+  ├─ 所有体位 === 'Exposed'
+  │   ↓
+  │   dispatch(clearWorks())
+  │   dispatch(addWork(updatedTask))
+  │   transformWorksToBodyPositions([updatedTask])
+  │   dispatch(setBodyPositions(bodyPositions))
+  │   dispatch(setBusinessFlow('process'))  ← 直接进入处理界面
+  │
+  └─ 有未曝光体位
+      ↓
+      dispatch(clearWorks())
+      dispatch(addWork(updatedTask))
+      dispatch(setBusinessFlow('exam'))  ← 进入检查界面
+```
+
+## 判断逻辑
+
+### 曝光状态判断
+
+在 `Task` 接口中,每个 task 包含 `Views: dview[]` 数组,每个 `dview` 有 `expose_status` 字段:
+
+```typescript
+interface dview {
+  view_id: string;
+  expose_status: string; // 'Exposed' 或 'Unexposed'
+  // ... 其他字段
+}
+```
+
+**判断条件**:
+
+```typescript
+const allExposed = task.Views.every((view) => view.expose_status === 'Exposed');
+```
+
+### 相关代码参考
+
+在 `src/states/exam/bodyPositionListSlice.ts` 中已有类似的判断逻辑:
+
+```typescript
+const allExposed = bodyPositions.every(
+  (bp) => bp.dview.expose_status === 'Exposed'
+);
+
+const allUnExposed = bodyPositions.every(
+  (bp) => bp.dview.expose_status === 'Unexposed'
+);
+```
+
+## 实现方案
+
+### 修改 worklistToExam 函数
+
+**文件**: `src/domain/patient/worklistToExam.ts`
+
+```typescript
+const worklistToExam = async (task: Task) => {
+  const dispatch = store.dispatch;
+
+  try {
+    // 1. 使用公共函数准备数据(获取详细信息)
+    const [updatedTask] = await prepareWorksForExam(task);
+
+    // 2. 判断所有体位是否已曝光
+    const allExposed = updatedTask.Views.every(
+      (view) => view.expose_status === 'Exposed'
+    );
+
+    // 3. 清空现有缓存
+    dispatch(clearWorks());
+
+    // 4. 保存更新后的 task 到缓存
+    dispatch(addWork(updatedTask));
+
+    // 5. 根据曝光状态决定跳转目标
+    if (allExposed) {
+      // 所有体位已曝光 - 进入处理界面
+      // 需要先转换为体位列表
+      const bodyPositions = await transformWorksToBodyPositions([updatedTask]);
+      dispatch(setBodyPositions(bodyPositions));
+      dispatch(setBusinessFlow('process'));
+    } else {
+      // 有未曝光体位 - 进入检查界面
+      dispatch(setBusinessFlow('exam'));
+    }
+  } catch (error) {
+    console.error('Error in worklistToExam:', error);
+    throw error;
+  }
+};
+```
+
+### 关键改动点
+
+1. **添加曝光状态判断**
+
+   ```typescript
+   const allExposed = updatedTask.Views.every(
+     (view) => view.expose_status === 'Exposed'
+   );
+   ```
+
+2. **条件分支处理**
+
+   - 全部曝光: 转换体位列表 → 设置体位数据 → 跳转到 'process'
+   - 有未曝光: 直接跳转到 'exam'
+
+3. **复用现有逻辑**
+   - 使用 `transformWorksToBodyPositions` 转换数据(参考 `worklistToProcess`)
+   - 使用 `setBodyPositions` 设置体位数据
+
+## 优势
+
+1. **用户体验优化**: 已完成曝光的 study 无需再进入检查界面,直接进入处理环节
+2. **逻辑清晰**: 基于体位曝光状态自动判断,无需用户手动选择
+3. **代码复用**: 充分利用现有的 `worklistToProcess` 逻辑
+4. **易于维护**: 修改集中在一个函数中,不影响其他功能
+
+## 测试场景
+
+### 场景 1: 所有体位已曝光
+
+- **操作**: 双击一个所有体位都是 'Exposed' 的 study
+- **预期**: 直接进入处理界面(process),可以看到所有已曝光的图像
+
+### 场景 2: 存在未曝光体位
+
+- **操作**: 双击一个至少有一个体位是 'Unexposed' 的 study
+- **预期**: 进入检查界面(exam),可以进行曝光操作
+
+### 场景 3: 空体位列表
+
+- **操作**: 双击一个 Views 为空数组的 study
+- **预期**: 进入检查界面(因为 `every()` 对空数组返回 true,需要特殊处理)
+
+### 场景 4: 混合状态
+
+- **操作**: 双击一个部分体位已曝光、部分未曝光的 study
+- **预期**: 进入检查界面
+
+## 边界情况处理
+
+### 1. 空 Views 数组
+
+```typescript
+// 需要额外检查 Views 是否为空
+const allExposed =
+  updatedTask.Views.length > 0 &&
+  updatedTask.Views.every((view) => view.expose_status === 'Exposed');
+```
+
+### 2. expose_status 字段可能的值
+
+根据代码搜索结果,`expose_status` 的可能值:
+
+- `'Exposed'` - 已曝光
+- `'Unexposed'` - 未曝光
+
+需要确保判断逻辑只认为值为 `'Exposed'` 的才算已曝光。
+
+### 3. API 数据异常
+
+如果 `fetchTaskDetails` 失败或返回异常数据,原有的错误处理机制会捕获并抛出错误。
+
+## 相关文件总结
+
+| 文件路径                                         | 作用               | 修改内容                       |
+| ------------------------------------------------ | ------------------ | ------------------------------ |
+| `src/domain/patient/worklistToExam.ts`           | Worklist 跳转逻辑  | **需要修改**: 添加曝光状态判断 |
+| `src/states/BusinessFlowSlice.ts`                | 业务流程管理       | 无需修改                       |
+| `src/states/exam/examWorksCacheSlice.ts`         | 工作缓存           | 无需修改                       |
+| `src/states/exam/bodyPositionListSlice.ts`       | 体位列表管理       | 无需修改,可参考判断逻辑       |
+| `src/pages/patient/worklist.tsx`                 | Worklist 页面      | 无需修改                       |
+| `src/pages/patient/components/WorklistTable.tsx` | 表格组件           | 无需修改                       |
+| `src/domain/work.ts`                             | Task 数据类型定义  | 无需修改                       |
+| `src/domain/dview.ts`                            | dview 数据类型定义 | 无需修改                       |
+
+## 实现步骤
+
+1. ✅ 分析现有代码结构,识别涉及的 Redux 参与者
+2. ✅ 梳理数据流关系,确定修改点
+3. ✅ 编写实现方案文档
+4. ⏳ 修改 `worklistToExam` 函数,添加曝光状态判断
+5. ⏳ 测试各种场景,确保功能正常
+6. ⏳ 处理边界情况
+
+## 参考代码位置
+
+- **曝光状态判断参考**: `src/states/exam/bodyPositionListSlice.ts:147-154`
+- **直接进入处理界面参考**: `src/domain/patient/worklistToExam.ts:69-113` (worklistToProcess)
+- **体位转换函数**: `src/states/exam/bodyPositionListSlice.ts` 中的 `transformWorksToBodyPositions`
+
+## 注意事项
+
+1. **数据完整性**: 确保 `prepareWorksForExam` 返回的数据包含完整的 Views 信息
+2. **状态一致性**: 进入处理界面前必须设置 bodyPositions,否则处理界面可能无法正常显示
+3. **错误处理**: 保持现有的错误处理机制,确保任何步骤失败都能正确抛出错误
+4. **性能考虑**: `transformWorksToBodyPositions` 是异步操作,需要等待完成后再跳转

+ 16 - 2
src/domain/patient/worklistToExam.ts

@@ -62,14 +62,28 @@ const worklistToExam = async (task: Task) => {
     // 使用公共函数准备数据
     const [updatedTask] = await prepareWorksForExam(task);
 
+    // 判断所有体位是否已曝光(需要检查 Views 不为空)
+    const allExposed =
+      updatedTask.Views.length > 0 &&
+      updatedTask.Views.every((view) => view.expose_status === 'Exposed');
+
     // Clear existing works in the cache
     dispatch(clearWorks());
 
     // Save the updated task to the cache
     dispatch(addWork(updatedTask));
 
-    // Proceed to Examination
-    dispatch(setBusinessFlow('exam'));
+    // 根据曝光状态决定跳转目标
+    if (allExposed) {
+      // 所有体位已曝光 - 直接进入处理界面
+      // 需要先转换为体位列表
+      const bodyPositions = await transformWorksToBodyPositions([updatedTask]);
+      dispatch(setBodyPositions(bodyPositions));
+      dispatch(setBusinessFlow('process'));
+    } else {
+      // 有未曝光体位 - 进入检查界面
+      dispatch(setBusinessFlow('exam'));
+    }
   } catch (error) {
     console.error('Error in worklistToExam:', error);
     throw error;