注册时体位过滤-需求-实现.md 12 KB

注册时体位过滤 - 需求与实现

1. 需求描述

业务场景

在患者注册流程中,用户需要选择患者类型、身体部位,然后选择体位(View)或协议(Protocol)。已选择的体位会被添加到"已选择体位列表"中。

具体需求

当用户切换患者类型时,需要自动清空已选择的体位列表(selectedViews)

  • 切换患者类型 → 清空已选择体位列表
  • 切换协议/体位模式 → 不清空已选择体位列表
  • 切换身体部位 → 不清空已选择体位列表

业务逻辑

不同患者类型对应不同的体位选项,当患者类型改变时,之前选择的体位可能不再适用于新的患者类型,因此需要清空已选择的体位列表,让用户重新选择。


2. Redux 架构分析

2.1 涉及的 Slices

patientTypeSlice.ts - 患者类型切片

位置: src/states/patientTypeSlice.ts

State 结构:

interface PatientTypeState {
  items: PatientType[]; // 所有患者类型列表
  loading: boolean;
  error: string | null;
  current: PatientType | null; // 当前选中的患者类型
}

关键 Action:

  • setCurrentPatientType: createAction<PatientType | null> - 设置当前患者类型
    • 这是一个独立创建的 action,供其他 slice 监听

bodyPartSlice.ts - 身体部位切片

位置: src/states/bodyPartSlice.ts

State 结构:

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 结构:

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 结构:

interface SelectionState {
  selected: 'protocol' | 'view'; // 当前选择模式:协议或体位
}

关键 Action:

  • setSelected - 切换选择模式

2.2 Actions 说明

独立创建的 Actions(供跨 slice 监听)

// 在 patientTypeSlice.ts 中
export const setCurrentPatientType = createAction<PatientType | null>(
  'patientType/setCurrentPatientType'
);

这个 action 被多个 slice 监听:

  • bodyPartSlice - 过滤身体部位
  • viewSelectionSlice - 清空协议和待选体位

Reducer Actions

// 在各自的 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 中:

.addCase(setCurrentPatientType, (state, action) => {
  const currentPatientType = action.payload;
  if (currentPatientType) {
    state.currentPatientType = currentPatientType;
  } else {
    // 患者类型被清空,清空相关数据
    state.currentPatientType = null;
    state.protocols = [];
    state.availableViews = [];
    console.log('患者类型已清空,协议和体位列表已重置');
  }
  // ⚠️ 问题:这里没有清空 selectedViews
})

问题: 当患者类型改变时,只清空了 protocolsavailableViews,但没有清空 selectedViews(已选择体位列表)。


4. 实现方案

4.1 修改位置

文件: src/states/patient/viewSelection/index.ts

修改的监听器: setCurrentPatientType 的 case 处理

4.2 具体代码改动

修改前:

.addCase(setCurrentPatientType, (state, action) => {
  const currentPatientType = action.payload;
  if (currentPatientType) {
    state.currentPatientType = currentPatientType;
  } else {
    // 患者类型被清空,清空相关数据
    state.currentPatientType = null;
    state.protocols = [];
    state.availableViews = [];
    console.log('患者类型已清空,协议和体位列表已重置');
  }
})

修改后:

.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 代码块之后,添加一行:

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

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 - 创建文档,定义需求和实现方案