BusinessZone.tsx.md 11 KB

BusinessZone.tsx 文档

文件职责

BusinessZone 是业务功能导航菜单组件,负责:

  1. 业务功能导航:展示系统主要业务功能入口(患者管理、检查、图像处理、打印等)
  2. 权限控制:根据当前位置和数据状态动态计算按钮可用性
  3. 层级菜单:支持一级菜单和子菜单(如患者管理包含注册、任务列表等)
  4. 国际化支持:所有菜单项标签支持多语言
  5. 视觉反馈:高亮当前选中的菜单项

实现方式

1. 技术栈

  • React:组件化开发
  • Redux:全局状态管理(业务流程、权限、数据选择状态)
  • react-intl:国际化(FormattedMessage)
  • Ant Design:Flex 布局
  • 权限系统permissionMap 模块的权限计算

2. 核心数据流

Redux State (currentKey, dataState)
          ↓
   getBtnAvailability()
          ↓
   btnAvailability 对象
          ↓
     useItems() Hook
          ↓
    items 菜单配置
          ↓
      渲染菜单按钮

3. 菜单结构

BusinessZone
├── 患者管理 (patient_management) - 可展开
│   ├── 注册 (register)
│   ├── 任务列表 (worklist)
│   ├── 历史列表 (historylist)
│   ├── 归档列表 (archivelist)
│   ├── 回收站 (bin)
│   └── 输出列表 (outputlist)
├── 检查 (exam)
├── ———— (分隔线)
├── 图像处理 (process)
└── 打印 (print)

实现思路

1. 动态权限计算

根据当前业务流程位置和数据状态计算每个按钮的可用性:

// 1. 获取当前位置
const currentKey = useSelector(state => state.BusinessFlow.currentKey);

// 2. 根据位置获取数据状态
const dataState: DataState = useSelector((state: RootState) => {
  if (currentKey === 'worklist') {
    return {
      hasSelection: state.workSelection.selectedIds.length > 0,
      hasExposedImage: /* ... */
    };
  }
  // ... 其他位置
});

// 3. 计算按钮可用性
const btnAvailability = getBtnAvailability(currentKey, dataState);

权限计算逻辑

  • 不同的业务流程位置(LocationKey)有不同的权限规则
  • 某些操作需要数据状态支持(如选择了项目、有曝光图像等)
  • getBtnAvailability 函数封装了复杂的权限逻辑

2. 菜单配置生成

使用自定义 Hook useItems 生成菜单配置:

function useItems(btnAvailability: Record<string, boolean>) {
  return [
    {
      key: 'patient_management',
      label: <FormattedMessage id="patient" />,
      icon: 'Registration',
      disabled: !btnAvailability['patient_management'],
      children: [/* 子菜单 */]
    },
    // ... 其他菜单项
  ];
}

设计优势

  • 集中管理菜单配置
  • 权限状态自动绑定到 disabled 属性
  • 支持国际化标签
  • 支持分隔线和层级结构

3. 层级菜单展开/收起

使用局部状态控制子菜单的显示:

const [floatingMenuVisible, setFloatingMenuVisible] = useState(false);

const handlePatientManagementClick = () => {
  setFloatingMenuVisible(!floatingMenuVisible);
};

交互流程

  1. 点击"患者管理" → 切换 floatingMenuVisible 状态
  2. 渲染条件:floatingMenuVisible && <子菜单>
  3. 子菜单项点击 → 调用 onMenuClick 回调

4. 当前菜单高亮

通过动态生成 className 实现当前菜单项的视觉高亮:

const buttonClassNameGenerator = (
  itemkey: string,
  currentkey: string
): string => {
  const originalName = 'w-full max-w-full whitespace-nowrap ...';
  if (itemkey === currentkey) {
    return originalName + ' border border-red-500 ';
  }
  return originalName;
};

5. 文本溢出处理

菜单按钮使用多层溢出控制确保文本不会撑破布局:

// IconButton 样式
style={{
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  whiteSpace: 'nowrap'
}}

// 内部 span 样式
<span style={{
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  whiteSpace: 'nowrap',
  flex: 1,
  minWidth: 0
}}>

技巧

  • 外层容器 overflow: hidden 限制边界
  • textOverflow: ellipsis 显示省略号
  • whiteSpace: nowrap 禁止换行
  • flex: 1 + minWidth: 0 允许弹性收缩

边界

输入

  • Props
    • onMenuClick?: (key: string) => void:菜单项点击回调
  • Redux State
    • currentKey:当前业务流程标识(BusinessFlowSlice)
    • workSelection.selectedIds:任务列表选择状态
    • historySelection.selectedIds:历史列表选择状态
    • bodyPositionList.exposureStatus:曝光状态
    • permission.triggerPermissionCalc:权限重新计算触发器

输出

  • 渲染输出:业务功能菜单,包括:

    • 一级菜单按钮
    • 子菜单按钮(患者管理展开时)
    • 分隔线
    • 当前菜单高亮
    • 禁用状态显示
  • 事件派发

    • onMenuClick(key):通知父组件切换业务流程

职责边界

负责

  • ✅ 业务功能菜单的渲染
  • ✅ 根据权限计算按钮可用性
  • ✅ 子菜单的展开/收起交互
  • ✅ 当前菜单项的视觉反馈
  • ✅ 国际化标签显示
  • ✅ 文本溢出处理

不负责

  • ❌ 权限规则的定义(由 permissionMap 模块负责)
  • ❌ 业务流程的实际切换(由父组件处理 onMenuClick)
  • ❌ 数据状态的维护(由各自的 Redux slice 负责)
  • ❌ 图标的加载和渲染(由 Icon 组件负责)
  • ❌ 国际化文本的翻译(由 react-intl 和语言包负责)

涉及概念

1. 权限控制(Permission Control)

根据用户角色、当前状态、数据条件等因素动态控制功能可用性。

实现方式

const btnAvailability = getBtnAvailability(
  currentKey as LocationKey,
  dataState
);

disabled: !btnAvailability['exam'];

关键点

  • 位置相关:不同位置有不同的权限规则
  • 数据相关:某些操作需要数据支持(如选择、曝光)
  • 集中计算:权限逻辑集中在 permissionMap 模块

2. 国际化(Internationalization, i18n)

支持多语言界面,根据用户语言设置显示对应文本。

使用方式

<FormattedMessage
  id="patient"
  defaultMessage={'语言包中没有定义patient的翻译文本'}
/>

组成部分

  • id:翻译键,在语言包中查找
  • defaultMessage:找不到翻译时的默认文本

3. 自定义 Hook

将组件逻辑提取为可复用的 Hook 函数。

示例

function useItems(btnAvailability: Record<string, boolean>) {
  return [
    /* 菜单配置 */
  ];
}

优势

  • 分离关注点:UI 渲染和数据逻辑分离
  • 可测试性:可以单独测试 Hook
  • 可读性:主组件代码更简洁

4. 条件渲染(Conditional Rendering)

根据状态或条件决定是否渲染元素。

方式

// 三元运算符
{item.type === 'divider' ? <hr /> : <IconButton />}

// 逻辑与运算符
{item.key === 'patient_management' && floatingMenuVisible && <SubMenu />}

5. 动态 ClassName

根据条件动态生成 CSS 类名。

实现

const buttonClassNameGenerator = (itemkey: string, currentkey: string) => {
  const base = 'w-full ...';
  return itemkey === currentkey ? base + ' border border-red-500' : base;
};

应用

  • 当前菜单高亮
  • 不同状态的样式
  • 响应式样式

6. 状态提升(Lifting State Up)

子菜单的展开状态在本组件维护,而菜单选择状态由父组件通过 Redux 维护。

分工

  • 本地状态:UI 交互状态(展开/收起)
  • 全局状态:业务状态(当前选择的菜单)

7. Flexbox 布局

使用 Flex 容器实现垂直菜单布局。

<Flex vertical className="min-h-0 overflow-y-auto flex-grow">

关键属性

  • vertical:垂直方向排列
  • flex-grow:占据剩余空间
  • overflow-y-auto:内容溢出时显示滚动条
  • min-h-0:允许收缩到最小高度

8. 数据驱动的禁用状态

按钮的可用性完全由数据驱动,而非硬编码。

流程

数据状态变化 → 权限重新计算 → disabled 属性更新 → UI 自动更新

9. 选择器优化(Selector Optimization)

使用 useSelector 订阅 Redux 状态,只在相关状态变化时重新渲染。

示例

// 订阅特定状态切片
const currentKey = useSelector(
  (state: RootState) => state.BusinessFlow.currentKey
);

// 触发器模式:订阅触发权限重新计算
useSelector((state: RootState) => state.permission.triggerPermissionCalc);

注意事项

1. 调试代码未清理

代码中包含多个 console.log 语句:

disabled: console.log(`我要看看exam对应的值:${!btnAvailability['exam']}`),
  !btnAvailability['exam'];

问题

  • 影响代码可读性
  • 生产环境不应有调试日志
  • 使用逗号表达式比较晦涩

改进建议:移除或使用专门的日志系统

2. 注释代码未清理

文件中有大量被注释掉的代码:

// const [visibleItems, setVisibleItems] = useState(items);
// const businessZoneRef = useRef<HTMLDivElement>(null);
// useEffect(() => { ... }

建议:使用版本控制系统管理代码历史,移除无用注释

3. 默认消息不规范

国际化的 defaultMessage 使用了非标准文本:

defaultMessage={'语言包中没有定义patient的翻译文本'}

问题

  • 不是有效的回退文本
  • 用户看到会困惑

改进建议:使用英文或有意义的默认文本

4. 按钮禁用逻辑问题

子菜单按钮的禁用状态使用的是父菜单的 item.disabled

<IconButton
  disabled={item.disabled}  // 应该是 child.disabled
>

潜在问题:子菜单项无法独立控制禁用状态

5. 数据状态计算可优化

dataState 的计算逻辑有重复代码,可以提取为独立函数。

6. 触发器订阅用途不明确

useSelector((state: RootState) => state.permission.triggerPermissionCalc);

这个订阅没有使用返回值,仅用于触发重新渲染。更好的方式是明确说明这个订阅的用途。

7. 图标名称重复使用

许多菜单项都使用 'Registration' 图标,可能是占位符,需要更新为正确的图标。

相关文件

  • src/domain/permissionMap.ts:权限规则定义和计算逻辑
  • src/states/BusinessFlowSlice.ts:业务流程状态管理
  • src/states/workSelection.ts:任务列表选择状态
  • src/states/historySelection.ts:历史列表选择状态
  • src/states/bodyPositionList.ts:体位列表和曝光状态
  • src/states/permission.ts:权限状态管理
  • src/components/IconButton.tsx:图标按钮组件
  • src/components/Icon/index.tsx:图标组件
  • src/assets/i18n/:国际化语言包