# 从任务清单进入检查的流程 ## 📋 文档概述 **文档名称**: 从任务清单进入检查的流程总结 **创建日期**: 2025-12-22 **文档目的**: 全面总结从 Worklist/History 任务清单双击进入检查或处理界面的完整业务流程 **涵盖范围**: - 用户交互流程 - 核心业务逻辑 - 数据流转关系 - Redux 状态管理 - 路由导航机制 --- ## 📊 主流程泳道图 ```mermaid sequenceDiagram participant User as 👤 用户 participant UI as 🖥️ UI层
(Worklist/History) participant Domain as ⚙️ Domain层
(worklistToExam) participant API as 🌐 后端API participant Redux as 📦 Redux Store participant Router as 🔀 路由系统 User->>UI: 双击任务行 UI->>Domain: worklistToExam(task) Note over Domain: 1. 准备数据阶段 Domain->>API: fetchTaskDetails(StudyID) API-->>Domain: 返回完整Study数据
(series + images) Note over Domain: 2. 数据转换阶段 Domain->>Domain: 转换为Task格式
(包含所有Views) Note over Domain: 3. 曝光状态判断 Domain->>Domain: 检查所有Views的
expose_status alt 所有体位已曝光 Note over Domain: 分支A: 进入处理界面 Domain->>Redux: clearWorks() Domain->>Redux: addWork(updatedTask) Domain->>Domain: transformWorksToBodyPositions() Domain->>Redux: setBodyPositions(bodyPositions) Domain->>Redux: setBusinessFlow('process') Redux-->>Router: 触发路由变更 Router-->>User: 导航到处理界面 else 存在未曝光体位 Note over Domain: 分支B: 进入检查界面 Domain->>Redux: clearWorks() Domain->>Redux: addWork(updatedTask) Domain->>Redux: setBusinessFlow('exam') Redux-->>Router: 触发路由变更 Router-->>User: 导航到检查界面 end ``` --- ## 🔀 核心决策流程图 ```mermaid flowchart TD Start([用户双击任务]) --> Fetch[调用 fetchTaskDetails API] Fetch --> Transform[转换数据格式
Series + Images → Views] Transform --> Check{检查曝光状态} Check -->|Views.length > 0 &&
所有 expose_status === 'Exposed'| AllExposed[所有体位已曝光] Check -->|存在任何
expose_status !== 'Exposed'| HasUnexposed[存在未曝光体位] AllExposed --> ClearA[清空缓存 clearWorks] ClearA --> AddA[保存任务 addWork] AddA --> TransformBP[转换为体位列表
transformWorksToBodyPositions] TransformBP --> SetBP[设置体位数据
setBodyPositions] SetBP --> ToProcess[设置流程为 'process'] ToProcess --> ProcessPage([进入处理界面]) HasUnexposed --> ClearB[清空缓存 clearWorks] ClearB --> AddB[保存任务 addWork] AddB --> ToExam[设置流程为 'exam'] ToExam --> ExamPage([进入检查界面]) style AllExposed fill:#90EE90 style HasUnexposed fill:#FFB6C1 style ProcessPage fill:#4169E1,color:#fff style ExamPage fill:#FF8C00,color:#fff ``` --- ## 🏊 完整业务泳道图(包含多个入口) ```mermaid flowchart TB subgraph UserActions[用户操作层] W[Worklist双击] H[History双击] end subgraph DomainLogic[业务逻辑层] WTE[worklistToExam函数] PWE[prepareWorksForExam
准备详细数据] subgraph DataProcess[数据处理] FetchAPI[调用 fetchTaskDetails API] MapData[映射数据:
Series → Views
Images → dview] end subgraph Decision[决策逻辑] CheckStatus{检查曝光状态
allExposed?} end end subgraph StateManagement[状态管理层 Redux] ClearCache[clearWorks
清空缓存] AddCache[addWork
保存任务] SetBP[setBodyPositions
设置体位列表] SetFlow[setBusinessFlow
设置业务流程] end subgraph Routing[路由层] ToExamRoute[导航到 /exam] ToProcessRoute[导航到 /process] end subgraph Pages[页面层] ExamPage[📋 检查界面
Exam Page] ProcessPage[🖼️ 处理界面
Process Page] end W --> WTE H --> WTE WTE --> PWE PWE --> FetchAPI FetchAPI --> MapData MapData --> CheckStatus CheckStatus -->|是
所有体位已曝光| PathA[处理路径] CheckStatus -->|否
存在未曝光体位| PathB[检查路径] PathA --> ClearCache ClearCache --> AddCache AddCache --> SetBP SetBP --> SetFlowProcess[setBusinessFlow
'process'] SetFlowProcess --> ToProcessRoute ToProcessRoute --> ProcessPage PathB --> ClearCache2[clearWorks] ClearCache2 --> AddCache2[addWork] AddCache2 --> SetFlowExam[setBusinessFlow
'exam'] SetFlowExam --> ToExamRoute ToExamRoute --> ExamPage style W fill:#E6F3FF style H fill:#E6F3FF style CheckStatus fill:#FFF4E6 style ExamPage fill:#FF8C00,color:#fff style ProcessPage fill:#4169E1,color:#fff ``` --- ## 📋 详细流程说明 ### 阶段1: 用户触发操作 **入口点**: - Worklist 页面双击任务行 - History 页面双击任务行 **触发函数**: - `handleRowDoubleClick(record: Task)` → `worklistToExam(task)` --- ### 阶段2: 数据准备 (prepareWorksForExam) **输入**: `Task` 对象(可能包含不完整的 Views 数据) **处理流程**: 1. 调用 `fetchTaskDetails(StudyID)` 获取完整的 Study 数据 2. 接收后端返回的 `series` 和 `images` 数据 3. 使用 `flatMap` 将 Series 和 Images 转换为 `dview[]` 格式 4. 构建包含完整 Views 的 Task 对象 **输出**: `Task` 对象(包含完整的 Views 数据) **数据映射关系**: ```typescript TaskDetails.series[] → flatMap → Task.Views[] ├─ Series.images[] → map → dview[] └─ 每个 Image 映射为一个 dview 对象 ``` **dview 字段映射**: | 源字段 (Image/Series) | 目标字段 (dview) | 说明 | |----------------------|------------------|------| | `image.view_id` | `view_id` | 体位ID | | `series.series_instance_uid` | `series_instance_uid` | Series UID | | `taskDetails.study_instance_uid` | `study_instance_uid` | Study UID | | `image.sop_instance_uid` | `PrimarySopUID` | 图像 SOP UID | | `image.expose_status` | `expose_status` | **曝光状态(关键)** | | `image.image_file_path` | `image_file` | 图像文件路径 | | `image.thumbnail_file` | `thumbnail_file` | 缩略图路径 | --- ### 阶段3: 曝光状态判断 ⚠️ **核心决策点** **判断逻辑**: ```typescript const allExposed = updatedTask.Views.length > 0 && updatedTask.Views.every((view) => view.expose_status === 'Exposed'); ``` **决策规则**: | 条件 | 判断结果 | 目标界面 | |------|---------|---------| | `Views.length === 0` | `allExposed = false` | Exam(检查界面) | | `Views.length > 0` 且所有 `expose_status === 'Exposed'` | `allExposed = true` | **Process(处理界面)** | | `Views.length > 0` 但存在 `expose_status !== 'Exposed'` | `allExposed = false` | Exam(检查界面) | **状态值说明**: - `'Exposed'` - 已曝光(已拍摄图像) - `'Unexposed'` - 未曝光(待拍摄) --- ### 阶段4: Redux 状态更新 #### 4A. 进入处理界面 (allExposed === true) **步骤顺序**: ```typescript 1. dispatch(clearWorks()) // 清空现有工作缓存 2. dispatch(addWork(updatedTask)) // 保存新任务到缓存 3. bodyPositions = await transformWorksToBodyPositions([updatedTask]) // 转换为体位列表 4. dispatch(setBodyPositions(bodyPositions)) // 设置体位列表数据 5. dispatch(setBusinessFlow('process')) // 切换业务流程到 process ``` **Redux 状态变化**: | Slice | Action | 状态变化 | |-------|--------|---------| | `examWorksCache` | `clearWorks` | `works: []` | | `examWorksCache` | `addWork` | `works: [updatedTask]` | | `bodyPositionList` | `setBodyPositions` | `positions: ExtendedBodyPosition[]` | | `businessFlow` | `setBusinessFlow` | `currentKey: 'process'` | **关键依赖**: 必须先设置 `bodyPositions`,否则处理界面无法正常显示 --- #### 4B. 进入检查界面 (allExposed === false) **步骤顺序**: ```typescript 1. dispatch(clearWorks()) // 清空现有工作缓存 2. dispatch(addWork(updatedTask)) // 保存新任务到缓存 3. dispatch(setBusinessFlow('exam')) // 切换业务流程到 exam ``` **Redux 状态变化**: | Slice | Action | 状态变化 | |-------|--------|---------| | `examWorksCache` | `clearWorks` | `works: []` | | `examWorksCache` | `addWork` | `works: [updatedTask]` | | `businessFlow` | `setBusinessFlow` | `currentKey: 'exam'` | **说明**: 检查界面不需要 `bodyPositions`,因为会在界面内动态生成和管理 --- ### 阶段5: 路由导航 **触发机制**: `setBusinessFlow` 的 Redux 状态变化会触发路由监听 **导航目标**: - `businessFlow.currentKey === 'exam'` → 导航到 `/exam` 路由 - `businessFlow.currentKey === 'process'` → 导航到 `/process` 路由 --- ## 📊 数据流转关系表 | 阶段 | 输入 | 处理函数/逻辑 | 输出 | |------|------|--------------|------| | **1. API 调用** | `StudyID: string` | `fetchTaskDetails(StudyID)` | `TaskDetails` (包含 series, images) | | **2. 数据转换** | `TaskDetails` | `Series.flatMap(s => s.images.map(...))` | `Task` (包含 Views) | | **3. 状态判断** | `Task.Views[]` | `Views.every(v => v.expose_status === 'Exposed')` | `boolean` (allExposed) | | **4A. 体位转换** | `Task[]` | `transformWorksToBodyPositions(tasks)` | `ExtendedBodyPosition[]` | | **4B. Redux 更新** | `Task`, `allExposed` | 条件分支 dispatch | Redux State 更新 | | **5. 路由导航** | `businessFlow.currentKey` | 路由监听 | 页面切换 | --- ## 🎯 四种业务场景 ### 场景矩阵 | 入口 | 曝光状态 | 判断条件 | 目标界面 | 测试文件 | |------|---------|---------|---------|---------| | **Worklist** | 未曝光 | `allExposed = false` | **Exam** | `worklist-enter-exam-unexposed.cy.ts` | | **Worklist** | 已曝光 | `allExposed = true` | **Process** | `worklist-enter-process-exposed.cy.ts` | | **History** | 未曝光 | `allExposed = false` | **Exam** | `history-enter-exam-unexposed.cy.ts` | | **History** | 已曝光 | `allExposed = true` | **Process** | `history-enter-process-exposed.cy.ts` | --- ### 场景1: Worklist → Exam(未曝光) ```mermaid sequenceDiagram participant User participant Worklist participant worklistToExam participant API participant Redux participant ExamPage User->>Worklist: 双击未曝光任务 Worklist->>worklistToExam: worklistToExam(task) worklistToExam->>API: fetchTaskDetails(StudyID) API-->>worklistToExam: TaskDetails (有未曝光体位) worklistToExam->>worklistToExam: allExposed = false worklistToExam->>Redux: clearWorks() + addWork() worklistToExam->>Redux: setBusinessFlow('exam') Redux-->>ExamPage: 导航到检查界面 ``` **关键验证点**: - ✅ API 返回包含 `expose_status: 'Unexposed'` 的 images - ✅ `businessFlow.currentKey === 'exam'` - ✅ `examWorksCache.works.length === 1` - ✅ 检查界面主要元素可见 --- ### 场景2: Worklist → Process(已曝光) ```mermaid sequenceDiagram participant User participant Worklist participant worklistToExam participant API participant Redux participant ProcessPage User->>Worklist: 双击已曝光任务 Worklist->>worklistToExam: worklistToExam(task) worklistToExam->>API: fetchTaskDetails(StudyID) API-->>worklistToExam: TaskDetails (所有体位已曝光) worklistToExam->>worklistToExam: allExposed = true worklistToExam->>Redux: clearWorks() + addWork() worklistToExam->>worklistToExam: transformWorksToBodyPositions() worklistToExam->>Redux: setBodyPositions() + setBusinessFlow('process') Redux-->>ProcessPage: 导航到处理界面 ``` **关键验证点**: - ✅ API 返回所有 images 的 `expose_status === 'Exposed'` - ✅ `businessFlow.currentKey === 'process'` - ✅ `bodyPositionList.positions.length > 0` - ✅ 处理界面主要元素可见 --- ### 场景3: History → Exam(未曝光) **流程**: 与场景1相同,仅入口点不同(从 History 页面触发) --- ### 场景4: History → Process(已曝光) **流程**: 与场景2相同,仅入口点不同(从 History 页面触发) --- ## 🗂️ 核心文件清单 ### Domain 业务逻辑层 | 文件路径 | 职责 | 关键函数 | |---------|------|---------| | `src/domain/patient/worklistToExam.ts` | **核心业务逻辑** | `worklistToExam()`, `prepareWorksForExam()`, `worklistToProcess()` | ### UI 组件层 | 文件路径 | 职责 | 关键函数/组件 | |---------|------|--------------| | `src/pages/patient/worklist.tsx` | Worklist 页面 | `handleRowDoubleClick()` | | `src/pages/patient/HistoryList.tsx` | History 页面 | `handleRowDoubleClick()` | | `src/pages/patient/components/WorklistTable.tsx` | 表格组件 | `onDoubleClick` 事件处理 | ### Redux 状态管理层 | 文件路径 | Slice 名称 | 关键 Actions | |---------|-----------|-------------| | `src/states/exam/examWorksCacheSlice.ts` | `examWorksCache` | `clearWorks()`, `addWork()` | | `src/states/exam/bodyPositionListSlice.ts` | `bodyPositionList` | `setBodyPositions()`, `transformWorksToBodyPositions()` | | `src/states/BusinessFlowSlice.ts` | `businessFlow` | `setBusinessFlow()` | ### API 层 | 文件路径 | 职责 | 关键函数 | |---------|------|---------| | `src/API/patient/workActions.ts` | 任务相关 API | `fetchTaskDetails()` | ### 类型定义 | 文件路径 | 定义类型 | |---------|---------| | `src/domain/work.ts` | `Task` 接口 | | `src/domain/dview.ts` | `dview` 接口(体位视图) | | `src/domain/series.ts` | `Series` 接口 | | `src/domain/xImage.ts` | `XImage` 接口 | --- ## 🔍 核心代码片段 ### 1. worklistToExam 函数(完整逻辑) ```typescript // 文件: src/domain/patient/worklistToExam.ts const worklistToExam = async (task: Task) => { const dispatch = store.dispatch; try { // 步骤1: 准备详细数据 const [updatedTask] = await prepareWorksForExam(task); // 步骤2: 判断曝光状态 const allExposed = updatedTask.Views.length > 0 && updatedTask.Views.every((view) => view.expose_status === 'Exposed'); // 步骤3: 清空并保存任务 dispatch(clearWorks()); dispatch(addWork(updatedTask)); // 步骤4: 根据状态决定流程 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; } }; ``` --- ### 2. prepareWorksForExam 函数(数据准备) ```typescript // 文件: src/domain/patient/worklistToExam.ts export const prepareWorksForExam = async ( works: Task | Task[] ): Promise => { const workArray = Array.isArray(works) ? works : [works]; const preparedWorks = await Promise.all( workArray.map(async (work) => { // 调用 API 获取详细数据 const taskDetails = await fetchTaskDetails(work.StudyID); // 转换数据格式: Series → Views return { ...work, Views: taskDetails.series.flatMap((series: Series) => series.images.map( (image: XImage) => ({ view_id: image.view_id, series_instance_uid: series.series_instance_uid, study_instance_uid: taskDetails.study_instance_uid, study_id: taskDetails.study_id, procedure_id: series.procedure_id, view_description: image.view_description, view_type: '', PrimarySopUID: image.sop_instance_uid, expose_status: image.expose_status, // 关键字段 judged_status: image.judged_status, image_file_path: image.image_file_path, image_file: image.image_file_path, thumbnail_file: image.thumbnail_file || '', }) satisfies dview ) ), } as Task; }) ); return preparedWorks; }; ``` --- ### 3. 曝光状态判断(关键逻辑) ```typescript // 判断条件 const allExposed = updatedTask.Views.length > 0 && // 1. 确保有体位数据 updatedTask.Views.every((view) => view.expose_status === 'Exposed'); // 2. 所有体位已曝光 // 决策分支 if (allExposed) { // 路径A: 进入处理界面 const bodyPositions = await transformWorksToBodyPositions([updatedTask]); dispatch(setBodyPositions(bodyPositions)); dispatch(setBusinessFlow('process')); } else { // 路径B: 进入检查界面 dispatch(setBusinessFlow('exam')); } ``` --- ## 🧪 测试验证要点 ### Redux 状态验证 #### 验证 examWorksCache ```typescript cy.window() .its('store') .invoke('getState') .its('examWorksCache') .its('works') .should('have.length', 1) .its(0) .should((task) => { expect(task).to.have.property('StudyID'); expect(task.Views).to.be.an('array'); expect(task.Views.length).to.be.greaterThan(0); }); ``` #### 验证 businessFlow(检查界面) ```typescript cy.window() .its('store') .invoke('getState') .its('businessFlow') .its('currentKey') .should('eq', 'exam'); ``` #### 验证 businessFlow(处理界面) ```typescript cy.window() .its('store') .invoke('getState') .its('businessFlow') .its('currentKey') .should('eq', 'process'); ``` #### 验证 bodyPositionList(处理界面) ```typescript cy.window() .its('store') .invoke('getState') .its('bodyPositionList') .its('positions') .should('have.length.greaterThan', 0) .each((bp) => { expect(bp).to.have.property('dview'); expect(bp.dview).to.have.property('expose_status', 'Exposed'); }); ``` --- ### API Mock 验证 #### Mock 未曝光任务详情 ```typescript cy.intercept('GET', '/dr/api/v1/auth/study/*', { statusCode: 200, body: { study_id: 'TEST001', study_instance_uid: '1.2.3.4.5', series: [ { series_instance_uid: '1.2.3.4.5.1', images: [ { sop_instance_uid: '1.2.3.4.5.1.1', view_id: 'VIEW001', expose_status: 'Unexposed', // 关键: 未曝光 // ... 其他字段 } ] } ] } }).as('getStudyDetailsUnexposed'); ``` #### Mock 已曝光任务详情 ```typescript cy.intercept('GET', '/dr/api/v1/auth/study/*', { statusCode: 200, body: { study_id: 'TEST002', study_instance_uid: '1.2.3.4.6', series: [ { series_instance_uid: '1.2.3.4.6.1', images: [ { sop_instance_uid: '1.2.3.4.6.1.1', view_id: 'VIEW002', expose_status: 'Exposed', // 关键: 已曝光 image_file_path: '/path/to/image.dcm', thumbnail_file: '/path/to/thumb.jpg', // ... 其他字段 } ] } ] } }).as('getStudyDetailsExposed'); ``` --- ## 🐛 边界情况处理 ### 1. Views 为空数组 **情况**: `updatedTask.Views.length === 0` **判断结果**: `allExposed = false`(因为前置条件失败) **处理**: 进入检查界面(Exam) **原因**: 没有体位数据,需要在检查界面配置 --- ### 2. 混合曝光状态 **情况**: 部分体位 `expose_status === 'Exposed'`,部分 `'Unexposed'` **判断结果**: `allExposed = false`(`every()` 条件不满足) **处理**: 进入检查界面(Exam) **原因**: 存在未完成的曝光任务,需要继续检查 --- ### 3. expose_status 其他值 **可能值**: - `'Exposed'` - 已曝光(已拍摄) - `'Unexposed'` - 未曝光(待拍摄) - 可能存在其他状态(如 `'Deleted'`, `'Archived'` 等) **处理**: - 只有完全等于 `'Exposed'` 才算已曝光 - 其他任何值都视为未曝光,进入检查界面 --- ### 4. API 调用失败 **情况**: `fetchTaskDetails()` 抛出错误 **处理**: ```typescript try { // ... 业务逻辑 } catch (error) { console.error('Error in worklistToExam:', error); throw error; // 向上层抛出,由调用方处理 } ``` **说明**: 错误会冒泡到 UI 层,通常会显示错误提示 --- ## 📝 设计模式和架构特点 ### 1. 关注点分离 | 层次 | 职责 | 示例文件 | |------|------|---------| | **UI 层** | 用户交互、事件触发 | `worklist.tsx`, `HistoryList.tsx` | | **Domain 层** | 业务逻辑、数据转换 | `worklistToExam.ts` | | **API 层** | 后端通信 | `workActions.ts` | | **State 层** | 状态管理 | `examWorksCacheSlice.ts`, `businessFlowSlice.ts` | --- ### 2. 单一职责原则 - `prepareWorksForExam()` - 仅负责数据准备和转换 - `worklistToExam()` - 负责业务逻辑和流程控制 - `transformWorksToBodyPositions()` - 仅负责体位列表转换 --- ### 3. 异步流程处理 所有 API 调用和数据转换都使用 `async/await` 模式: ```typescript const [updatedTask] = await prepareWorksForExam(task); const bodyPositions = await transformWorksToBodyPositions([updatedTask]); ``` --- ### 4. 数据不可变性 使用 Redux Toolkit 的 Immer,确保状态更新的不可变性: ```typescript dispatch(clearWorks()); // 不直接修改 state,而是 dispatch action dispatch(addWork(updatedTask)); ``` --- ## 🚀 性能优化考虑 ### 1. 并行数据获取 `prepareWorksForExam` 使用 `Promise.all` 并行处理多个任务: ```typescript const preparedWorks = await Promise.all( workArray.map(async (work) => { const taskDetails = await fetchTaskDetails(work.StudyID); return { ...work, Views: ... }; }) ); ``` **优势**: 多个任务同时请求,减少总等待时间 --- ### 2. 条件性体位转换 只有在进入处理界面时才执行体位转换: ```typescript if (allExposed) { const bodyPositions = await transformWorksToBodyPositions([updatedTask]); dispatch(setBodyPositions(bodyPositions)); dispatch(setBusinessFlow('process')); } ``` **优势**: 避免不必要的数据转换计算 --- ### 3. 缓存清理 每次进入前清空旧缓存,避免数据冲突: ```typescript dispatch(clearWorks()); // 清理旧数据 dispatch(addWork(updatedTask)); // 添加新数据 ``` --- ## 📚 相关文档索引 ### 实现文档 - [体位全曝光-worklist-双击-进入检查.md](./实现/体位全曝光-worklist-双击-进入检查.md) - [exam体位列表-双击已曝光体位进入处理.md](./实现/exam体位列表-双击已曝光体位进入处理.md) ### 测试文档 - [进入检查功能测试方案.md](./测试/进入检查功能测试方案.md) ### 代码文件 - [worklistToExam.ts](../src/domain/patient/worklistToExam.ts) - 核心业务逻辑 - [worklist.tsx](../src/pages/patient/worklist.tsx) - Worklist 页面 - [HistoryList.tsx](../src/pages/patient/HistoryList.tsx) - History 页面 --- ## ✅ 总结 ### 核心要点 1. **双击触发**: 从 Worklist 或 History 双击任务行触发流程 2. **数据准备**: 通过 API 获取完整的 Study 数据,转换为 Views 3. **状态判断**: 根据所有体位的 `expose_status` 判断是否全部曝光 4. **流程分支**: - 全部已曝光 → 处理界面 (Process) - 存在未曝光 → 检查界面 (Exam) 5. **状态管理**: 通过 Redux 管理任务缓存、体位列表和业务流程 ### 关键判断逻辑 ```typescript const allExposed = updatedTask.Views.length > 0 && updatedTask.Views.every((view) => view.expose_status === 'Exposed'); ``` ### 测试覆盖 - ✅ Worklist → Exam (未曝光) - ✅ Worklist → Process (已曝光) - ✅ History → Exam (未曝光) - ✅ History → Process (已曝光) --- **文档版本**: v1.0 **最后更新**: 2025-12-22 **维护者**: Development Team