ReRegister功能设计.md 18 KB

ReRegister功能设计文档

功能概述

ReRegister功能允许用户在historylist表格中选中一个study后,点击"ReRegister"按钮,将当前患者的基本信息获取并填充到注册页面表单中,实现快速重新注册。

参与者分析

组件层

  • ActionPanel - 包含ReRegister按钮的组件
  • RegisterPage - 注册页面主组件
  • BasicInfoForm - 注册表单组件
  • HistoryList - 历史记录列表页面

状态管理层

  • BusinessFlowSlice - 管理业务流程状态
  • formSlice - 管理注册表单数据
  • historySelectionSlice - 管理历史记录选中状态
  • historyEntitiesSlice - 管理历史记录实体数据
  • reregisterSlice - 管理ReRegister功能的状态

数据层

  • Task - 任务数据结构
  • RegisterInfo - 注册信息数据结构
  • workActions - API操作函数

工具层

  • registerLogic - 注册逻辑处理函数
  • useRegisterState - 注册状态管理Hook
  • mapTaskToRegisterInfo - 数据映射函数

数据结构分析

Task (历史记录数据结构)

interface Task {
  StudyInstanceUID: string;
  StudyID: string;
  AccessionNumber: string;
  PatientID: string;
  PatientName: string;
  PatientSize: string;
  PatientAge: string;
  PatientSex: string;
  patient_dob?: string;
  ref_physician?: string;
  weight?: number;
  thickness?: number;
  length?: number;
  comment?: string;
  // 宠物专用字段
  owner_name?: string;
  chip_number?: string;
  variety: string;
  sex_neutered: string;
  // 人医专用字段
  pregnancy_status?: string;
}

RegisterInfo (注册信息数据结构)

interface RegisterInfo {
  accession_number: string;
  patient_id: string;
  patient_name: string;
  patient_size: string;
  patient_age: string;
  patient_dob: string;
  patient_sex: string;
  patient_type: string;
  ref_physician: string;
  operator_id: string;
  modality: string;
  weight: number;
  thickness: number;
  length: number;
  study_type: 'Normal' | 'Emergency';
  comment: string;
  views: View[];
  // 宠物专用字段
  owner_name: string;
  sex_neutered: string;
  chip_number: string;
  variety: string;
  // 人医专用字段
  pregnancy_status: string;
  is_anaesthesia: boolean;
  is_sedation: boolean;
}

实现流程

🔄 执行流程

sequenceDiagram
    participant User as 用户
    participant ActionPanel as ActionPanel组件
    participant Redux as Redux Store
    participant RegisterPage as RegisterPage
    participant BasicInfoForm as BasicInfoForm

    User->>ActionPanel: 点击ReRegister按钮
    ActionPanel->>Redux: 获取选中的study数据
    Redux-->>ActionPanel: 返回Task数据
    ActionPanel->>Redux: 触发ReRegister action
    Redux->>RegisterPage: 切换到注册页面
    RegisterPage->>BasicInfoForm: 填充表单数据
    BasicInfoForm->>User: 显示预填充的表单

📊 数据流图

graph TD
    A[HistoryList选中] --> B[ActionPanel]
    B --> C[获取Task数据]
    C --> D[数据映射 Task→RegisterInfo]
    D --> E[更新Redux formSlice]
    E --> F[切换到RegisterPage]
    F --> G[BasicInfoForm显示预填充数据]
    
    H[BusinessFlow] --> I[设置currentKey为'register']
    I --> F

🏗️ 类图

classDiagram
    class ActionPanel {
        +handleReRegister()
        +getSelectedWorks()
    }
    
    class RegisterPage {
        +handleRegister()
        +useEffect cleanup
    }
    
    class BasicInfoForm {
        +form: FormInstance
        +onValuesChange()
    }
    
    class formSlice {
        +setFormData()
        +clearFormData()
    }
    
    class BusinessFlowSlice {
        +setBusinessFlow()
    }
    
    class reregisterSlice {
        +setSourceTask()
        +setRegisterInfo()
        +clearReRegister()
    }
    
    class Task {
        +StudyID
        +PatientName
        +PatientID
        +PatientSize
        +PatientAge
        +PatientSex
        +patient_dob
        +ref_physician
        +weight
        +thickness
        +length
        +comment
        +owner_name
        +chip_number
        +variety
        +sex_neutered
        +pregnancy_status
    }
    
    class RegisterInfo {
        +accession_number
        +patient_id
        +patient_name
        +patient_size
        +patient_age
        +patient_dob
        +patient_sex
        +patient_type
        +ref_physician
        +operator_id
        +modality
        +weight
        +thickness
        +length
        +study_type
        +comment
        +views
        +owner_name
        +sex_neutered
        +chip_number
        +variety
        +pregnancy_status
        +is_anaesthesia
        +is_sedation
    }
    
    ActionPanel --> Task : 获取数据
    Task --> RegisterInfo : 数据映射
    RegisterInfo --> formSlice : 存储数据
    formSlice --> BasicInfoForm : 填充表单
    ActionPanel --> BusinessFlowSlice : 切换页面
    ActionPanel --> reregisterSlice : 存储ReRegister数据

详细实现步骤

1. 创建新的Redux Slice

需要创建一个新的slice来管理ReRegister功能的状态:

// src/states/patient/reregister/reregisterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Task } from '@/domain/work';
import { RegisterInfo } from '@/API/patient/workActions';

interface ReRegisterState {
  sourceTask: Task | null;
  registerInfo: RegisterInfo | null;
  loading: boolean;
  error: string | null;
}

const initialState: ReRegisterState = {
  sourceTask: null,
  registerInfo: null,
  loading: false,
  error: null,
};

export const reregisterSlice = createSlice({
  name: 'reregister',
  initialState,
  reducers: {
    setSourceTask: (state, action: PayloadAction<Task>) => {
      state.sourceTask = action.payload;
    },
    setRegisterInfo: (state, action: PayloadAction<RegisterInfo>) => {
      state.registerInfo = action.payload;
    },
    clearReRegister: (state) => {
      state.sourceTask = null;
      state.registerInfo = null;
      state.error = null;
    },
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },
    setError: (state, action: PayloadAction<string>) => {
      state.error = action.payload;
    },
  },
});

export const { 
  setSourceTask, 
  setRegisterInfo, 
  clearReRegister, 
  setLoading, 
  setError 
} = reregisterSlice.actions;

// Selectors
export const selectSourceTask = (state: { reregister: ReRegisterState }) => state.reregister.sourceTask;
export const selectRegisterInfo = (state: { reregister: ReRegisterState }) => state.reregister.registerInfo;
export const selectReRegisterLoading = (state: { reregister: ReRegisterState }) => state.reregister.loading;
export const selectReRegisterError = (state: { reregister: ReRegisterState }) => state.reregister.error;

export type { ReRegisterState };
export default reregisterSlice.reducer;

2. 数据映射函数

创建从Task到RegisterInfo的映射函数:

// src/domain/patient/taskToRegister.ts
import { Task } from '@/domain/work';
import { RegisterInfo } from '@/API/patient/workActions';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);

export const mapTaskToRegisterInfo = (task: Task): RegisterInfo => {
  // 处理年龄格式转换
  let patientAge = task.PatientAge || '000Y';
  
  // 确保年龄格式为3位数字+单位
  const ageNumber = parseInt(patientAge.substring(0, 3)) || 0;
  const ageUnit = patientAge.length > 3 ? patientAge.substring(3) : 'Y';
  
  // 处理日期格式
  let patientDob = '';
  if (task.patient_dob) {
    try {
      patientDob = dayjs.utc(task.patient_dob).format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
    } catch (error) {
      console.error('Error parsing patient_dob:', error);
      patientDob = '';
    }
  }
  
  // 处理体重、厚度、长度等数值字段
  const weight = task.weight ? Number(task.weight) : 0;
  const thickness = task.Thickness ? Number(task.Thickness) : 0;
  const length = task.length ? Number(task.length) : 0;
  
  return {
    accession_number: task.AccessionNumber || '',
    patient_id: task.PatientID || '',
    patient_name: task.PatientName || '',
    patient_size: task.PatientSize || 'Medium',
    patient_age: `${ageNumber.toString().padStart(3, '0')}${ageUnit}`,
    patient_dob: patientDob,
    patient_sex: task.PatientSex || '',
    patient_type: task.PatientType || '',
    ref_physician: task.ref_physician || '',
    operator_id: task.OperatorID || '',
    modality: task.Modality || 'DX',
    weight: weight,
    thickness: thickness,
    length: length,
    study_type: 'Normal',
    comment: task.comment || '',
    views: [], // ReRegister时不保留视图信息
    // 宠物专用字段
    owner_name: task.owner_name || '',
    sex_neutered: task.sex_neutered || 'UNALTERED',
    chip_number: task.chip_number || '',
    variety: task.variety || '',
    // 人医专用字段
    pregnancy_status: task.pregnancy_status || 'NOT_PREGNANT',
    // 添加缺失的字段
    is_anaesthesia: task.is_anaesthesia || false,
    is_sedation: task.is_sedation || false,
  };
};

3. 更新ActionPanel组件

修改ActionPanel组件,添加ReRegister功能:

// src/pages/patient/components/ActionPanel.tsx
import { setSourceTask, setRegisterInfo } from '@/states/patient/reregister/reregisterSlice';
import { mapTaskToRegisterInfo } from '@/domain/patient/taskToRegister';
import { setBusinessFlow } from '@/states/BusinessFlowSlice';

// 在handleReRegister函数中
const handleReRegister = () => {
  const selectedIds = getSelectedWorkIds();
  
  // 验证选择
  if (selectedIds.length === 0) {
    message.warning('请先选择要重新注册的项目');
    return;
  }
  
  if (selectedIds.length > 1) {
    message.warning('只能重新注册一个项目');
    return;
  }
  
  const works = getWorksFromWorklistOrHistory();
  const task = works.find((item) => item.StudyID === selectedIds[0]);
  
  // 验证任务数据
  if (!task) {
    message.error('找不到选中的任务数据');
    return;
  }
  
  // 验证必要字段
  if (!task.PatientName || !task.PatientID) {
    message.error('任务数据不完整,缺少必要信息');
    return;
  }
  
  try {
    // 设置源任务数据
    dispatch(setSourceTask(task));
    
    // 将Task数据映射为RegisterInfo
    const registerInfo = mapTaskToRegisterInfo(task);
    
    // 验证映射后的数据
    if (!registerInfo.patient_name || !registerInfo.patient_id) {
      throw new Error('数据映射失败,缺少必要信息');
    }
    
    // 设置注册信息
    dispatch(setRegisterInfo(registerInfo));
    
    // 切换到注册页面
    dispatch(setBusinessFlow('register'));
    
    message.success('已切换到注册页面,表单已预填充');
  } catch (error) {
    console.error('ReRegister error:', error);
    const errorMessage = error instanceof Error ? error.message : '重新注册失败,请重试';
    message.error(`重新注册失败: ${errorMessage}`);
  }
};

4. 更新RegisterPage组件

修改RegisterPage组件,处理ReRegister数据:

// src/pages/patient/register.tsx
import { selectRegisterInfo } from '@/states/patient/reregister/reregisterSlice';
import { clearReRegister } from '@/states/patient/reregister/reregisterSlice';

const RegisterPage: React.FC = () => {
  const screens = useBreakpoint();
  const [form] = Form.useForm();
  const dispatch = useDispatch();
  const { selectedViews, currentPatientType } = useRegisterState();
  const productName = useSelector(
    (state: RootState) => state.product.productName
  );
  const reregisterInfo = useSelector(selectRegisterInfo);
  
  // 当有ReRegister数据时,填充表单
  useEffect(() => {
    if (reregisterInfo) {
      try {
        console.log('ReRegister数据已获取,填充表单:', reregisterInfo);
        
        // 验证数据完整性
        if (!reregisterInfo.patient_name || !reregisterInfo.patient_id) {
          console.error('ReRegister数据不完整:', reregisterInfo);
          message.warning('预填充数据不完整,请手动填写必要信息');
          return;
        }
        
        // 设置表单值
        form.setFieldsValue(reregisterInfo);
        
        // 更新Redux表单数据
        dispatch(setFormData(reregisterInfo));
        
        // 显示成功消息
        message.success('已从历史记录预填充表单数据');
      } catch (error) {
        console.error('填充表单时出错:', error);
        message.error('预填充表单数据时出错,请手动填写');
      }
    }
  }, [reregisterInfo, form, dispatch]);
  
  // 清理ReRegister数据
  useEffect(() => {
    return () => {
      dispatch(clearReRegister());
    };
  }, [dispatch]);
  
  // ... 其他现有代码
};

🧪 测试方案

测试场景

  1. 正常流程测试

    • 在historylist中选择一个study
    • 点击ReRegister按钮
    • 验证是否切换到注册页面
    • 验证表单是否被正确填充
  2. 多选测试

    • 选择多个study
    • 点击ReRegister按钮
    • 验证是否显示警告信息
  3. 空选择测试

    • 不选择任何study
    • 点击ReRegister按钮
    • 验证是否显示警告信息
  4. 数据映射测试

    • 验证Task数据是否正确映射到RegisterInfo
    • 验证特殊字段(如年龄、日期)是否正确转换
  5. 错误处理测试

    • 模拟网络错误
    • 验证错误信息是否正确显示

测试用例

// 测试数据
const mockTask: Task = {
  StudyID: 'STUDY001',
  PatientName: '测试患者',
  PatientID: 'P001',
  PatientSize: 'Medium',
  PatientAge: '030Y',
  PatientSex: 'male',
  patient_dob: '1990-01-01',
  ref_physician: '测试医生',
  weight: 70,
  thickness: 20,
  length: 180,
  comment: '测试注释',
  owner_name: '测试主人',
  chip_number: '123456789',
  variety: '测试品种',
  sex_neutered: 'UNALTERED',
  pregnancy_status: 'NOT_PREGNANT',
};

// 测试映射函数
test('mapTaskToRegisterInfo should correctly map Task to RegisterInfo', () => {
  const result = mapTaskToRegisterInfo(mockTask);
  expect(result.patient_name).toBe('测试患者');
  expect(result.patient_age).toBe('030Y');
  expect(result.patient_dob).toBeDefined();
});

🐛 潜在问题分析

边界情况

  1. 数据缺失处理

    • 当Task中某些字段为空时,如何处理默认值
    • 解决方案:在映射函数中设置合理的默认值
  2. 数据类型转换

    • 年龄格式的转换(如"30Y" vs "030Y")
    • 解决方案:统一使用标准格式
  3. 页面切换时机

    • 在数据加载完成前切换页面可能导致用户体验问题
    • 解决方案:添加loading状态

异常处理

  1. 网络错误

    • 在获取Task数据时可能发生网络错误
    • 解决方案:添加try-catch和错误提示
  2. 数据验证失败

    • 映射后的数据可能不符合注册表单验证规则
    • 解决方案:在映射后进行数据验证
  3. 并发操作

    • 用户可能在数据加载过程中进行其他操作
    • 解决方案:使用loading状态防止重复操作

实现优先级

  1. 高优先级

    • 基本ReRegister功能实现
    • 数据映射功能
    • 页面切换逻辑
  2. 中优先级

    • 错误处理
    • 加载状态
    • 数据验证
  3. 低优先级

    • 性能优化
    • 高级错误处理
    • 日志记录

实现结果

已完成的功能

  1. Redux状态管理

    • 创建了 reregisterSlice 来管理ReRegister功能的状态
    • 实现了设置源任务、注册信息、加载状态和错误处理的功能
    • 添加了相应的selector函数
  2. 数据映射

    • 实现了 mapTaskToRegisterInfo 函数,将Task数据转换为RegisterInfo
    • 处理了特殊字段转换,如年龄格式、日期格式等
    • 添加了错误处理和默认值设置
  3. 用户界面

    • 在ActionPanel中实现了handleReRegister函数
    • 添加了选择验证和错误提示
    • 实现了从historylist到register页面的导航
  4. 表单预填充

    • 在RegisterPage中添加了ReRegister数据处理逻辑
    • 实现了表单自动预填充功能
    • 添加了数据验证和错误处理
  5. 测试用例

    • 创建了Redux slice的测试用例
    • 实现了数据映射函数的测试用例
    • 覆盖了正常流程和边界情况

代码文件

  1. 状态管理

    • src/states/patient/reregister/reregisterSlice.ts - Redux slice实现
    • src/states/patient/reregister/reregisterSlice.test.ts - 测试用例
  2. 数据映射

    • src/domain/patient/taskToRegister.ts - 数据映射函数
    • src/domain/patient/taskToRegister.test.ts - 测试用例
  3. 组件更新

    • src/pages/patient/components/ActionPanel.tsx - 添加ReRegister功能
    • src/pages/patient/register.tsx - 添加表单预填充逻辑
  4. Store配置

    • src/states/store.ts - 添加新的reducer

使用流程

  1. 用户在historylist中选择一个study
  2. 点击ReRegister按钮
  3. 系统验证选择并获取Task数据
  4. 将Task数据映射为RegisterInfo
  5. 更新Redux状态并切换到注册页面
  6. 注册页面自动预填充表单数据

错误处理

实现了完善的错误处理机制:

  • 选择验证(空选择、多选)
  • 数据完整性验证
  • 数据映射错误处理
  • 表单填充错误处理
  • 用户友好的错误提示

测试覆盖

测试用例覆盖了以下场景:

  • Redux slice的状态管理
  • 数据映射的正确性
  • 边界情况处理(缺失字段、无效格式)
  • 错误情况的处理

总结

ReRegister功能已成功实现,提供了完整的从历史记录快速重新注册的流程。通过Redux状态管理、数据映射和表单预填充,用户可以轻松地基于历史记录创建新的注册,大大提高了工作效率。