# 退出检查的逻辑 ## 1. 概述 本文档详细说明了从检查页面(exam)退出的完整逻辑,包括曝光状态检查、用户反馈、后端通知等流程。 退出检查是一个关键的业务流程,需要根据当前的曝光状态做出不同的处理,以确保检查数据的完整性和用户操作的安全性。 --- ## 2. 核心逻辑流程 ### 2.1 触发退出的入口 当用户尝试从检查页面离开时(即当前业务流程是 `exam`,目标是其他流程),会触发退出逻辑。 **关键判断函数:** ```typescript function isExitingExam(currentAction: string, currentKey: string): boolean { return currentAction !== 'exam' && currentKey === 'exam'; } ``` **位置:** `src/states/businessFlowMiddlewareLogic.ts` ### 2.2 退出前的曝光状态检查 系统会检查当前的曝光状态(`exposureStatus`),根据不同状态采取不同的处理策略。 --- ## 3. 曝光状态检查 ### 3.1 三种曝光状态 | 状态 | 英文 | 说明 | 处理方式 | | ------ | ------------- | -------------------------- | ------------------ | | 未曝光 | Not Exposed | 所有体位都未曝光 | 直接允许退出 | | 半曝光 | Half Exposed | 部分体位已曝光,部分未曝光 | 显示退出反馈弹窗 | | 全曝光 | Fully Exposed | 所有体位都已曝光 | 通知后端,允许退出 | ### 3.2 半曝光状态的处理(核心) **当曝光状态为 "Half Exposed" 时:** 1. **显示退出反馈弹窗** `ExamExitFeedback` 2. **阻止立即退出** - 不执行业务流程切换 3. **提供3个选项供用户选择:** #### 选项1:继续检查 - **按钮文本**: "继续检查" - **用户意图**: 留在检查页面,继续完成未曝光的体位 - **处理逻辑**: ```typescript dispatch(setFeedbackOpen(false)); // 关闭弹窗 dispatch(setBusinessFlow('exitExamSuspended')); // 挂起检查 ``` #### 选项2:保存并完成 - **按钮文本**: "保存并完成" - **按钮样式**: `type="primary"` - **用户意图**: 保存已曝光的体位,标记检查为完成状态 - **处理逻辑**: ```typescript dispatch(setFeedbackOpen(false)); // 关闭弹窗 dispatch(setBusinessFlow('exitExamCompleted')); // 完成检查 ``` #### 选项3:直接中止 - **按钮文本**: "直接中止" - **按钮样式**: `type="primary" danger` - **用户意图**: 不保存,直接中止检查 - **处理逻辑**: ```typescript dispatch(setFeedbackOpen(false)); // 仅关闭弹窗 // 注意:这里只关闭弹窗,不改变业务流程 ``` --- ## 4. 退出处理的两种模式 ### 4.1 完成检查(exitExamCompleted) **业务流程标识**: `'exitExamCompleted'` **处理逻辑**: ```typescript if (action.payload === 'exitExamCompleted') { // 1. 获取当前检查的 StudyID const studyId = state.bodyPositionList.selectedBodyPosition.work.StudyID; // 2. 调用API通知后端 - 标记为已完成 await suspendOrCompleteStudy(studyId, 'Completed'); // 3. 跳转到目标业务流程 return next({ ...action, payload: continueBusinessFlow }); } ``` **API参数**: - `study_id`: 当前检查的ID - `study_status`: `'Completed'` - 标记为已完成 ### 4.2 挂起检查(exitExamSuspended) **业务流程标识**: `'exitExamSuspended'` **处理逻辑**: ```typescript if (action.payload === 'exitExamSuspended') { // 1. 获取当前检查的 StudyID const studyId = state.bodyPositionList.selectedBodyPosition.work.StudyID; // 2. 调用API通知后端 - 标记为进行中 await suspendOrCompleteStudy(studyId, 'InProgress'); // 3. 跳转到目标业务流程 return next({ ...action, payload: continueBusinessFlow }); } ``` **API参数**: - `study_id`: 当前检查的ID - `study_status`: `'InProgress'` - 标记为进行中 ### 4.3 目标流程的保存 **全局变量**: `continueBusinessFlow` **作用**: 保存用户最初想要跳转的目标业务流程 **示例场景**: 1. 用户在检查页面,点击"工作列表"按钮 → `continueBusinessFlow = 'worklist'` 2. 系统检测到半曝光状态,显示弹窗 → 阻止跳转 3. 用户选择"保存并完成" → 调用API后,跳转到 `worklist` --- ## 5. 特殊情况处理 ### 5.1 从检查进入图像处理(process) **特殊性**: 曝光操作可能导致自动进入图像处理页面 **判断函数**: ```typescript function isFromExamToView(currentAction: string, currentKey: string): boolean { return currentAction === 'process' && currentKey === 'exam'; } ``` **处理逻辑**: ```typescript if (isFromExamToView(action.payload, currentKey)) { // 检查发生器状态 if (store.getState().generatorMonitor.acquisitionState === 1) { // 发生器正在采集 - 阻止退出 console.log('发生器正在采集,不能退出'); return next(action); } else { // 非曝光导致的退出 - 执行清理 console.log('从检查退出到处理'); unprepare(); // 清理检查相关资源 } } ``` **关键状态**: `generatorMonitor.acquisitionState` - `1`: 正在采集 - 阻止退出 - 其他值: 空闲状态 - 允许退出 --- ## 6. 核心代码位置 ### 6.1 主要文件 | 文件路径 | 作用 | 关键内容 | | ------------------------------------------------ | ---------------------- | --------------------------------- | | `src/states/businessFlowMiddlewareLogic.ts` | 业务流程中间件(核心) | 所有退出判断和处理逻辑 | | `src/pages/exam/components/ExamExitFeedback.tsx` | 退出反馈UI组件 | "检查未完成"弹窗 | | `src/pages/exam/LargeScreen.tsx` | 大屏幕组件 | 集成退出反馈弹窗 | | `src/states/exam/largeScreenSlice.ts` | 大屏幕状态管理 | 管理反馈弹窗显示状态 | | `src/states/BusinessFlowSlice.ts` | 业务流程状态管理 | 管理业务流程切换 | | `src/API/patient/workActions.ts` | API接口 | `suspendOrCompleteStudy()` 函数 | | `src/domain/exam/prepare.ts` | 检查准备逻辑 | `prepare()` 和 `unprepare()` 函数 | ### 6.2 代码片段位置 #### 退出判断逻辑 **文件**: `src/states/businessFlowMiddlewareLogic.ts` **行数**: 约 200-250 行 ```typescript if ( isExitingExam(action.payload, currentKey) && action.payload !== 'exitExamCompleted' && action.payload !== 'exitExamSuspended' && action.payload !== 'process' ) { const exposureStatus = store.getState().bodyPositionList.exposureStatus; if (exposureStatus === 'Half Exposed') { store.dispatch(setFeedbackOpen(true)); continueBusinessFlow = action.payload; return; // 阻止退出 } } ``` #### 退出反馈弹窗 **文件**: `src/pages/exam/LargeScreen.tsx` **行数**: 约 20-30 行 ```typescript { dispatch(setFeedbackOpen(false)); dispatch(setBusinessFlow('exitExamSuspended')); }} onSave={() => { dispatch(setFeedbackOpen(false)); dispatch(setBusinessFlow('exitExamCompleted')); }} onAbort={() => dispatch(setFeedbackOpen(false))} /> ``` --- ## 7. 退出流程图 ### 7.1 完整流程图 ``` 用户点击离开exam ↓ 检查是否从exam退出? ↓ (是) 判断目标页面 ├─ process (图像处理) │ ↓ │ 检查发生器状态 │ ├─ acquisitionState === 1 → 阻止退出 │ └─ 其他 → unprepare() → 允许退出 │ └─ 其他页面 (worklist, register, etc.) ↓ 检查曝光状态 ├─ Not Exposed (未曝光) │ ↓ │ 直接允许退出 │ ├─ Half Exposed (半曝光) │ ↓ │ 显示退出反馈弹窗 │ ↓ │ 用户选择 │ ├─ 继续检查 → 关闭弹窗,停留在exam │ ├─ 保存并完成 → API调用(Completed) → 跳转到目标页面 │ └─ 直接中止 → API调用(InProgress) → 跳转到目标页面 │ └─ Fully Exposed (全曝光) ↓ 通知后端 → 允许退出 ``` ### 7.2 状态转换图 ``` [Exam Page] | | 用户触发退出 ↓ [检查曝光状态] | ├─→ [Not Exposed] ──────────→ [直接退出] | ├─→ [Half Exposed] ──→ [显示弹窗] | | | ├─→ [继续检查] → [停留在Exam] | ├─→ [保存并完成] → [exitExamCompleted] | └─→ [直接中止] → [exitExamSuspended] | └─→ [Fully Exposed] ─────────→ [通知后端] → [退出] ``` --- ## 8. 关键状态变量 ### 8.1 Redux State | State路径 | 类型 | 说明 | | ---------------------------------------------------- | ------- | ---------------------------------------------------------- | | `largeScreen.isFeedbackOpen` | boolean | 退出反馈弹窗是否显示 | | `bodyPositionList.exposureStatus` | string | 曝光状态:'Not Exposed' / 'Half Exposed' / 'Fully Exposed' | | `bodyPositionList.selectedBodyPosition.work.StudyID` | string | 当前检查的Study ID | | `generatorMonitor.acquisitionState` | number | 发生器采集状态:1=采集中 | | `BusinessFlow.currentKey` | string | 当前业务流程 | ### 8.2 模块级变量 | 变量名 | 类型 | 作用 | 位置 | | ---------------------- | ------ | -------------------------------- | -------------------------------- | | `continueBusinessFlow` | string | 保存退出检查后要去的目标业务流程 | `businessFlowMiddlewareLogic.ts` | --- ## 9. API接口说明 ### 9.1 suspendOrCompleteStudy **函数签名**: ```typescript const suspendOrCompleteStudy = async ( studyId: string, studyStatus: 'InProgress' | 'Completed' ): Promise<{ code: string; description: string; solution: string; data: {} }> ``` **API端点**: ```http POST /auth/task/inspection/leave ``` **请求参数**: ```json { "study_id": "string", "study_status": "InProgress" | "Completed" } ``` **响应示例**: ```json { "code": "0x000000", "description": "Success", "solution": "", "data": {} } ``` **调用场景**: 1. **挂起检查**: `studyStatus = 'InProgress'` - 用户选择"继续检查"后 - 用户选择"直接中止"后 2. **完成检查**: `studyStatus = 'Completed'` - 用户选择"保存并完成"后 --- ## 10. 测试场景 ### 10.1 场景1:未曝光状态直接退出 **前置条件**: - 用户已登录系统 - 从工作列表进入检查页面 - 未进行任何曝光操作 **测试步骤**: 1. 验证当前在检查页面(`data-testid="exam-page"`存在) 2. 检查曝光状态为 "Not Exposed" 3. 点击导航按钮切换到工作列表 4. 验证成功退出到工作列表页面 5. 验证未显示退出反馈弹窗 **预期结果**: - ✅ 直接退出检查页面 - ✅ 未显示退出反馈弹窗 - ✅ 成功进入工作列表页面 --- ### 10.2 场景2:半曝光状态 - 继续检查 **前置条件**: - 用户已登录系统 - 从工作列表进入检查页面 - 已完成部分体位的曝光(至少1个体位已曝光,至少1个体位未曝光) **测试步骤**: 1. 验证当前在检查页面 2. 检查曝光状态为 "Half Exposed" 3. 点击导航按钮切换到工作列表 4. 验证显示退出反馈弹窗 5. 验证弹窗标题为"检查未完成" 6. 验证弹窗内容包含提示信息 7. 点击"继续检查"按钮 8. 验证弹窗关闭 9. 验证仍停留在检查页面 **预期结果**: - ✅ 显示退出反馈弹窗 - ✅ 点击"继续检查"后弹窗关闭 - ✅ 停留在检查页面,不跳转 - ✅ Redux store中 `isFeedbackOpen = false` - ✅ 业务流程仍为 `exam` --- ### 10.3 场景3:半曝光状态 - 保存并完成 **前置条件**: - 用户已登录系统 - 从工作列表进入检查页面 - 已完成部分体位的曝光 **测试步骤**: 1. 验证当前在检查页面 2. 记录当前检查的 StudyID 3. 检查曝光状态为 "Half Exposed" 4. Mock API `/auth/task/inspection/leave` 5. 点击导航按钮切换到工作列表 6. 验证显示退出反馈弹窗 7. 点击"保存并完成"按钮 8. 验证弹窗关闭 9. 验证调用了API,参数为: - `study_id`: 当前StudyID - `study_status`: "Completed" 10. 验证成功跳转到工作列表页面 **预期结果**: - ✅ 显示退出反馈弹窗 - ✅ 点击"保存并完成"后调用API - ✅ API参数正确(study_status='Completed') - ✅ 成功跳转到目标页面 - ✅ 弹窗关闭 --- ### 10.4 场景4:半曝光状态 - 直接中止 **前置条件**: - 用户已登录系统 - 从工作列表进入检查页面 - 已完成部分体位的曝光 **测试步骤**: 1. 验证当前在检查页面 2. 记录当前检查的 StudyID 3. 检查曝光状态为 "Half Exposed" 4. Mock API `/auth/task/inspection/leave` 5. 点击导航按钮切换到工作列表 6. 验证显示退出反馈弹窗 7. 点击"直接中止"按钮(危险按钮) 8. 验证弹窗关闭 9. 验证调用了API,参数为: - `study_id`: 当前StudyID - `study_status`: "InProgress" 10. 验证成功跳转到工作列表页面 **预期结果**: - ✅ 显示退出反馈弹窗 - ✅ "直接中止"按钮为危险样式(红色) - ✅ 点击后调用API - ✅ API参数正确(study_status='InProgress') - ✅ 成功跳转到目标页面 --- ### 10.5 场景5:全曝光状态退出 **前置条件**: - 用户已登录系统 - 从工作列表进入检查页面 - 已完成所有体位的曝光 **测试步骤**: 1. 验证当前在检查页面 2. 检查曝光状态为 "Fully Exposed" 3. 点击导航按钮切换到工作列表 4. 验证成功退出到工作列表页面 5. 验证未显示退出反馈弹窗 6. 验证后端收到通知(可选) **预期结果**: - ✅ 直接退出检查页面 - ✅ 未显示退出反馈弹窗 - ✅ 成功进入工作列表页面 - ✅ 后端收到完成通知(如果有) --- ### 10.6 场景6:发生器采集中阻止退出到process **前置条件**: - 用户已登录系统 - 在检查页面 - 模拟发生器正在采集(`acquisitionState = 1`) **测试步骤**: 1. 验证当前在检查页面 2. 设置发生器状态为采集中 3. 尝试跳转到图像处理页面(process) 4. 验证跳转被阻止 5. 验证仍停留在检查页面 **预期结果**: - ✅ 发生器采集中时,阻止退出到process - ✅ 停留在检查页面 - ✅ 控制台输出相应日志 --- ### 10.7 场景7:从检查正常退出到process(非采集) **前置条件**: - 用户已登录系统 - 在检查页面 - 发生器空闲(`acquisitionState != 1`) - 有已曝光的图像 **测试步骤**: 1. 验证当前在检查页面 2. 确认发生器不在采集状态 3. 点击进入图像处理页面 4. 验证成功跳转到process页面 5. 验证调用了 `unprepare()` 清理函数 **预期结果**: - ✅ 成功退出到process页面 - ✅ 执行了清理操作(unprepare) - ✅ 控制台输出相应日志 --- ### 10.8 边界场景:快速连续点击退出 **前置条件**: - 用户已登录系统 - 在检查页面(半曝光状态) **测试步骤**: 1. 快速连续点击退出按钮(3次) 2. 验证只显示一个退出反馈弹窗 3. 验证弹窗功能正常 **预期结果**: - ✅ 只显示一个弹窗 - ✅ 不会出现多个弹窗叠加 - ✅ 弹窗功能正常 --- ### 10.9 边界场景:弹窗显示时切换曝光状态 **前置条件**: - 用户已登录系统 - 在检查页面(半曝光状态) - 退出反馈弹窗已显示 **测试步骤**: 1. 显示退出反馈弹窗 2. 模拟后台完成剩余曝光(状态变为全曝光) 3. 验证弹窗行为 **预期结果**: - 弹窗仍然显示(或根据业务需求自动关闭) - 状态保持一致 --- ## 11. 相关Redux Actions和Reducers ### 11.1 Actions | Action | 类型 | Payload | 说明 | | ----------------- | ------ | ------- | -------------------- | | `setBusinessFlow` | Action | string | 切换业务流程 | | `setFeedbackOpen` | Action | boolean | 控制退出反馈弹窗显示 | ### 11.2 Selectors 推荐创建以下Selectors方便测试和使用: ```typescript // 选择曝光状态 const selectExposureStatus = (state: RootState) => state.bodyPositionList.exposureStatus; // 选择当前StudyID const selectCurrentStudyId = (state: RootState) => state.bodyPositionList.selectedBodyPosition?.work?.StudyID; // 选择反馈弹窗状态 const selectIsFeedbackOpen = (state: RootState) => state.largeScreen.isFeedbackOpen; // 选择发生器状态 const selectAcquisitionState = (state: RootState) => state.generatorMonitor.acquisitionState; ``` --- ## 12. 注意事项和最佳实践 ### 12.1 错误处理 1. **API调用失败**: - 应该有重试机制 - 向用户显示错误信息 - 记录错误日志 2. **StudyID为空**: - 在调用API前验证StudyID存在 - 如果为空,记录错误并阻止退出 ### 12.2 用户体验 1. **按钮禁用状态**: - API调用期间,按钮应该禁用 - 显示Loading状态 2. **清晰的提示信息**: - 弹窗文本应该清楚说明当前状态 - 按钮文本应该明确表达操作结果 3. **操作确认**: - "直接中止"是危险操作,使用危险样式提醒用户 ### 12.3 性能优化 1. **防抖处理**: - 退出按钮点击应该有防抖,避免重复触发 2. **状态检查优化**: - 曝光状态应该实时更新 - 避免过时的状态导致错误判断 --- ## 13. 未来改进建议 ### 13.1 功能增强 1. **自动保存**: - 定期自动保存检查进度 - 减少数据丢失风险 2. **退出历史**: - 记录用户的退出操作 - 用于审计和分析 3. **智能提示**: - 根据历史操作,智能建议用户操作 ### 13.2 代码优化 1. **状态机模式**: - 使用状态机管理退出流程 - 使逻辑更清晰 2. **抽象公共逻辑**: - 将退出逻辑抽象为独立函数 - 提高可测试性和可维护性 --- ## 14. 相关文档 - [急诊流程.md](./急诊流程.md) - 急诊模式下的特殊处理 - [体位全曝光-worklist-双击-进入检查.md](./体位全曝光-worklist-双击-进入检查.md) - 全曝光状态的判断逻辑 - [注册时体位过滤-需求-实现.md](./注册时体位过滤-需求-实现.md) - 体位选择相关逻辑 --- ## 更新记录 | 日期 | 修改人 | 修改内容 | | --------- | ------ | -------------------------------- | | 2025/10/7 | - | 创建文档,详细说明退出检查的逻辑 |