# 注册时体位过滤 - 需求与实现 ## 1. 需求描述 ### 业务场景 在患者注册流程中,用户需要选择患者类型、身体部位,然后选择体位(View)或协议(Protocol)。已选择的体位会被添加到"已选择体位列表"中。 ### 具体需求 **当用户切换患者类型时,需要自动清空已选择的体位列表(selectedViews)** - ✅ **切换患者类型** → 清空已选择体位列表 - ❌ **切换协议/体位模式** → 不清空已选择体位列表 - ❌ **切换身体部位** → 不清空已选择体位列表 ### 业务逻辑 不同患者类型对应不同的体位选项,当患者类型改变时,之前选择的体位可能不再适用于新的患者类型,因此需要清空已选择的体位列表,让用户重新选择。 --- ## 2. Redux 架构分析 ### 2.1 涉及的 Slices #### `patientTypeSlice.ts` - 患者类型切片 **位置**: `src/states/patientTypeSlice.ts` **State 结构**: ```typescript interface PatientTypeState { items: PatientType[]; // 所有患者类型列表 loading: boolean; error: string | null; current: PatientType | null; // 当前选中的患者类型 } ``` **关键 Action**: - `setCurrentPatientType: createAction` - 设置当前患者类型 - 这是一个独立创建的 action,供其他 slice 监听 #### `bodyPartSlice.ts` - 身体部位切片 **位置**: `src/states/bodyPartSlice.ts` **State 结构**: ```typescript interface BodyPartState { items: BodyPart[]; // 所有身体部位列表 loading: boolean; error: string | null; byPatientType: BodyPart[]; // 根据患者类型过滤后的身体部位 current: BodyPart | null; // 当前选中的身体部位 } ``` **关键 Reducer**: - `setCurrentBodyPart` - 设置当前身体部位 **监听关系**: - 监听 `setCurrentPatientType`,根据患者类型过滤身体部位列表 #### `viewSelection/index.ts` - 体位选择切片(核心) **位置**: `src/states/patient/viewSelection/index.ts` **State 结构**: ```typescript interface ViewSelectionState { selectedViews: ExtendedView[]; // ⭐ 已选择体位列表(本次需求要清空的) availableViews: View[]; // 待选择体位列表 protocols: Procedure[]; // 协议列表 currentBodyPart: BodyPart | null; currentPatientType: PatientType | null; currentSelectionType: { selected: 'protocol' | 'view' }; } ``` **关键 Reducers**: - `addSelectedView` - 添加单个体位到已选列表 - `clearSelectedViews` - 清空已选体位列表 - `removeSelectedView` - 移除单个已选体位 **监听关系**: - 监听 `setCurrentPatientType` - 更新当前患者类型,清空协议和待选体位 - 监听 `setCurrentBodyPart` - 更新当前身体部位,清空协议和待选体位 - 监听 `setSelected` - 更新选择模式(协议/体位) #### `SelectionTypeSlice.ts` - 选择类型切片 **位置**: `src/states/patient/register/SelectionTypeSlice.ts` **State 结构**: ```typescript interface SelectionState { selected: 'protocol' | 'view'; // 当前选择模式:协议或体位 } ``` **关键 Action**: - `setSelected` - 切换选择模式 --- ### 2.2 Actions 说明 #### 独立创建的 Actions(供跨 slice 监听) ```typescript // 在 patientTypeSlice.ts 中 export const setCurrentPatientType = createAction( 'patientType/setCurrentPatientType' ); ``` 这个 action 被多个 slice 监听: - `bodyPartSlice` - 过滤身体部位 - `viewSelectionSlice` - 清空协议和待选体位 #### Reducer Actions ```typescript // 在各自的 slice 中定义 setCurrentBodyPart; // bodyPartSlice setSelected; // SelectionTypeSlice addSelectedView; // viewSelectionSlice clearSelectedViews; // viewSelectionSlice ``` --- ## 3. 数据流动关系 ### 3.1 用户操作流程 ``` 用户选择患者类型 ↓ dispatch(setCurrentPatientType(patientType)) ↓ ├─→ patientTypeSlice │ └─→ 更新 state.current │ ├─→ bodyPartSlice (监听) │ └─→ 过滤身体部位: state.byPatientType = filter(items, patientTypeId) │ └─→ 清空当前选择: state.current = null │ └─→ viewSelectionSlice (监听) └─→ 更新 state.currentPatientType └─→ 清空 state.protocols = [] └─→ 清空 state.availableViews = [] └─→ ⭐ 需要添加:清空 state.selectedViews = [] ``` ### 3.2 现有监听逻辑(问题所在) **在 `viewSelectionSlice` 的 extraReducers 中**: ```typescript .addCase(setCurrentPatientType, (state, action) => { const currentPatientType = action.payload; if (currentPatientType) { state.currentPatientType = currentPatientType; } else { // 患者类型被清空,清空相关数据 state.currentPatientType = null; state.protocols = []; state.availableViews = []; console.log('患者类型已清空,协议和体位列表已重置'); } // ⚠️ 问题:这里没有清空 selectedViews }) ``` **问题**: 当患者类型改变时,只清空了 `protocols` 和 `availableViews`,但没有清空 `selectedViews`(已选择体位列表)。 --- ## 4. 实现方案 ### 4.1 修改位置 **文件**: `src/states/patient/viewSelection/index.ts` **修改的监听器**: `setCurrentPatientType` 的 case 处理 ### 4.2 具体代码改动 **修改前**: ```typescript .addCase(setCurrentPatientType, (state, action) => { const currentPatientType = action.payload; if (currentPatientType) { state.currentPatientType = currentPatientType; } else { // 患者类型被清空,清空相关数据 state.currentPatientType = null; state.protocols = []; state.availableViews = []; console.log('患者类型已清空,协议和体位列表已重置'); } }) ``` **修改后**: ```typescript .addCase(setCurrentPatientType, (state, action) => { const currentPatientType = action.payload; if (currentPatientType) { state.currentPatientType = currentPatientType; } else { // 患者类型被清空,清空相关数据 state.currentPatientType = null; state.protocols = []; state.availableViews = []; console.log('患者类型已清空,协议和体位列表已重置'); } // ⭐ 新增:无论是选择新患者类型还是清空,都清空已选择体位列表 state.selectedViews = []; }) ``` ### 4.3 修改说明 在 `setCurrentPatientType` 监听器的 `if-else` 代码块**之后**,添加一行: ```typescript state.selectedViews = []; ``` **为什么放在 if-else 之后?** - 无论是选择新的患者类型(`if` 分支) - 还是清空患者类型(`else` 分支) - 都需要清空已选择的体位列表 这样可以避免代码重复。 ### 4.4 不需要修改的地方 1. **`setCurrentBodyPart` 监听器** - 保持不变 - 理由:切换身体部位时,不需要清空已选择体位 2. **`setSelected` 监听器** - 保持不变 - 理由:切换协议/体位模式时,不需要清空已选择体位 3. **其他任何文件** - 都不需要修改 --- ## 5. 测试验证 ### 5.1 E2E 测试场景(Given-When-Then) #### 测试场景 1:切换患者类型时清空已选体位列表 **Given**(前置条件): - 用户已登录系统 - 用户在患者注册页面 - 系统已加载患者类型列表 - 系统已加载身体部位列表 **When**(操作步骤): 1. 选择患者类型 "Human" 2. 选择身体部位 "颅骨" 3. 切换到"体位"选择模式 4. 添加体位 "颅骨前后位" 到已选列表 5. 验证已选列表包含 1 个体位 6. 切换患者类型到 "SpecialType" **Then**(预期结果): - 已选择体位列表被清空(长度为 0) - Redux store 中 `selectedViews` 为空数组 - 控制台输出:"患者类型已变更,已选择体位列表已清空" --- #### 测试场景 2:切换协议/体位模式时保持已选体位 **Given**(前置条件): - 用户已登录系统 - 用户在患者注册页面 - 已选择患者类型 "Human" - 已选择身体部位 "颅骨" - 当前在"协议"选择模式 **When**(操作步骤): 1. 在"协议"模式下添加协议 2. 验证已选列表包含协议对应的体位 3. 切换到"体位"选择模式 **Then**(预期结果): - 已选择体位列表保持不变 - Redux store 中 `selectedViews` 保持原有内容 - 体位数量不变 --- #### 测试场景 3:切换身体部位时保持已选体位 **Given**(前置条件): - 用户已登录系统 - 用户在患者注册页面 - 已选择患者类型 "Human" - 已选择身体部位 "颅骨" **When**(操作步骤): 1. 添加体位 "颅骨前后位" 到已选列表 2. 验证已选列表包含 1 个体位 3. 切换身体部位到 "颈部" **Then**(预期结果): - 已选择体位列表保持不变 - Redux store 中 `selectedViews` 保持原有内容 - 体位数量仍为 1 --- ### 5.2 单元测试(Redux Slice) #### 测试 viewSelectionSlice reducer ```typescript describe('viewSelectionSlice - setCurrentPatientType', () => { it('should clear selectedViews when patient type changes', () => { const initialState = { selectedViews: [ { view_id: 'View_1', guid: 'guid-1' }, { view_id: 'View_2', guid: 'guid-2' }, ], availableViews: [], protocols: [], currentBodyPart: null, currentPatientType: { patient_type_id: 'Human' }, currentSelectionType: { selected: 'protocol' }, }; const newPatientType = { patient_type_id: 'SpecialType', patient_type_name: 'SpecialType', }; const action = setCurrentPatientType(newPatientType); const newState = viewSelectionReducer(initialState, action); expect(newState.selectedViews).toEqual([]); expect(newState.currentPatientType).toEqual(newPatientType); }); }); ``` ### 5.3 验证清单 **功能验证**: - [ ] 切换患者类型时,`selectedViews` 被清空 - [ ] 切换协议/体位模式时,`selectedViews` 保持不变 - [ ] 切换身体部位时,`selectedViews` 保持不变 - [ ] 控制台日志正确输出状态变化 **UI 验证**: - [ ] 已选体位列表 UI 在患者类型切换后清空 - [ ] 体位数量显示正确更新 - [ ] 无 UI 错误或闪烁 **边界情况**: - [ ] 已选列表为空时切换患者类型,不报错 - [ ] 已选列表有多个体位时切换患者类型,全部清空 - [ ] 快速连续切换患者类型,状态保持一致 --- ## 6. 相关文件清单 ### 需要修改的文件 - `src/states/patient/viewSelection/index.ts` ### 相关参考文件 - `src/states/patientTypeSlice.ts` - `src/states/bodyPartSlice.ts` - `src/states/patient/register/SelectionTypeSlice.ts` - `src/domain/patient/registerLogic.ts` (使用 selectedViews 的地方) --- ## 7. 注意事项 1. **Redux Toolkit 的 Immer 集成**: - 可以直接使用 `state.selectedViews = []` 来修改状态 - Redux Toolkit 会自动处理不可变性 2. **Action 监听顺序**: - `setCurrentPatientType` 是一个独立创建的 action - 多个 slice 可以同时监听同一个 action - 监听器之间没有执行顺序保证 3. **日志输出**: - 建议在清空 `selectedViews` 后添加日志,方便调试 - 例如:`console.log('患者类型已变更,已选择体位列表已清空')` --- ## 8. 扩展思考 ### 是否需要提示用户? 当切换患者类型导致已选体位被清空时,是否需要: - 弹出确认对话框? - 显示提示消息? - 还是静默清空? **建议**: 根据业务需要决定。如果用户可能误操作,建议添加确认对话框。 ### 其他可能的改进 1. 添加"撤销"功能,允许用户恢复上一次的选择 2. 在切换患者类型前,保存当前选择到临时状态 3. 如果用户频繁切换,考虑添加节流或防抖 --- ## 更新记录 | 日期 | 修改人 | 修改内容 | | --------- | ------ | ---------------------------- | | 2025/10/7 | - | 创建文档,定义需求和实现方案 |