发送图像功能.md 16 KB

发送图像功能实现文档

功能概述

本文档描述了发送图像功能的完整实现方案。该功能允许用户将选中的 Study 发送到一个或多个 PACS 节点。

业务流程

用户选择Study(在history表格中)
    ↓
打开发送面板(SendPanel)
    ↓
系统自动加载并显示PACS节点列表
    ↓
系统自动选中默认节点
    ↓
用户选择目标PACS节点(可多选)
    ↓
点击"发送图像"按钮
    ↓
显示确认对话框
    ↓
用户确认
    ↓
批量发送到所有选中的PACS节点
    ↓
显示发送进度
    ↓
发送完成,显示结果统计
    ↓
停留在发送面板(不自动返回)

参与者清单

1. UI 组件层

SendPanel.tsx

  • 路径: src/pages/output/SendPanel.tsx
  • 职责:
    • 显示 PACS 节点列表
    • 处理节点选择
    • 显示发送进度
    • 执行发送操作

2. 状态管理层

pacsNodeSlice

  • 路径: src/states/output/pacsNode/pacsNodeSlice.ts
  • 职责:
    • 管理 PACS 节点列表
    • 管理节点选择状态
    • 自动选择默认节点

historySelectionSlice

  • 路径: src/states/patient/worklist/slices/history.ts
  • 职责:
    • 管理 history 表格的选中状态
    • 提供选中的 Study IDs

historyPanelSwitchSlice

  • 路径: src/states/patient/worklist/slices/historyPanelSwitchSlice.ts
  • 职责:
    • 管理面板切换状态
    • 切换到发送面板/操作面板

3. API 层

imageActions.ts

  • 路径: src/API/imageActions.ts
  • 关键方法:
    • sendStudyToPacs(studyId: string, pacsName: string): 按 Study 批量发送图像

pacsNodeActions.ts

  • 路径: src/API/output/pacsNodeActions.ts
  • 关键方法:
    • getPacsNodeList(): 获取 PACS 节点列表

4. 领域模型层

PacsNode

interface PacsNode {
  id: number;
  name: string;           // 节点名称,发送时使用
  type: string;
  address: string;
  port: number;
  aet: string;            // AE Title
  aec: string;            // AE Calling
  is_enabled: boolean;    // 是否启用
  is_default: boolean;    // 是否为默认节点
}

数据流

1. 初始化流程

SendPanel 组件挂载
    ↓
useEffect 触发
    ↓
dispatch(fetchPacsNodesThunk())
    ↓
调用 getPacsNodeList() API
    ↓
返回 PacsNode[] 数据
    ↓
pacsNodeSlice 更新 state
    ↓
自动选中 is_default=true 的节点
    ↓
UI 更新,显示节点列表

2. 发送流程

用户点击"发送图像"按钮
    ↓
handleSend() 被调用
    ↓
从 store 获取数据:
  - historySelection.selectedIds (Study IDs)
  - pacsNode.selectedNodeIds (PACS 节点 IDs)
  - pacsNode.nodes (节点详细信息)
    ↓
验证数据有效性
    ↓
显示确认对话框
    ↓
用户确认
    ↓
performSend() 被调用
    ↓
转换节点 ID 为节点名称
    ↓
计算总任务数 = studyIds.length × pacsNames.length
    ↓
初始化发送进度状态
    ↓
循环发送:
  for studyId in selectedStudyIds:
    for pacsName in selectedPacsNames:
      调用 sendStudyToPacs(studyId, pacsName)
      更新进度 (completed/failed)
    ↓
发送完成
    ↓
显示结果统计
    ↓
重置进度状态

3. 数据获取方式

// ✅ 正确方式:在事件处理器中使用 store.getState()
const handleSend = () => {
  const selectedStudyIds = store.getState().historySelection.selectedIds;
  // ...
};

// ❌ 错误方式:在事件处理器中使用 hook
const handleSend = () => {
  // const selectedStudyIds = useAppSelector(...); // 这样会报错
};

组件交互泳道图

用户          SendPanel组件        pacsNodeSlice        historySelection        API
│                │                    │                      │                   │
├─选择Study─────>│                    │                      │                   │
│                │                    │                      ├─更新selectedIds   │
│                │                    │                      │                   │
├─打开发送面板──>│                    │                      │                   │
│                │                    │                      │                   │
│                ├─挂载,dispatch─────>│                      │                   │
│                │  fetchPacsNodes   │                      │                   │
│                │                    ├─调用API─────────────────────────────────>│
│                │                    │                      │                   │
│                │                    │<─返回PacsNode[]──────────────────────────┤
│                │                    │                      │                   │
│                │                    ├─自动选中默认节点      │                   │
│                │                    │                      │                   │
│                │<─渲染节点列表───────┤                      │                   │
│                │                    │                      │                   │
├─勾选节点───────>│                    │                      │                   │
│                ├─dispatch─────────>│                      │                   │
│                │  toggleSelection  │                      │                   │
│                │                    ├─更新selectedNodeIds  │                   │
│                │                    │                      │                   │
├─点击发送───────>│                    │                      │                   │
│                │                    │                      │                   │
│                ├─获取数据:          │                      │                   │
│                │  store.getState() │                      │                   │
│                │    .historySelection                      │                   │
│                │    .pacsNode       │                      │                   │
│                │                    │                      │                   │
│                ├─显示确认对话框      │                      │                   │
│                │                    │                      │                   │
├─确认───────────>│                    │                      │                   │
│                │                    │                      │                   │
│                ├─循环调用sendStudyToPacs()─────────────────────────────────────>│
│                │                    │                      │                   │
│                │                    │                      │<─返回结果─────────┤
│                │                    │                      │                   │
│                ├─更新进度状态        │                      │                   │
│                │                    │                      │                   │
│                ├─显示完成消息        │                      │                   │
│                │                    │                      │                   │
├─查看结果───────>│                    │                      │                   │
│  (停留在面板)  │                    │                      │                   │

核心代码实现

1. 发送进度状态

const [sendingProgress, setSendingProgress] = useState({
  isSending: false,      // 是否正在发送
  total: 0,              // 总任务数
  completed: 0,          // 已完成数
  failed: 0,             // 失败数
});

2. 发送逻辑实现

const performSend = async (studyIds: string[], nodeIds: number[]) => {
  // 获取节点名称
  const selectedNodes = nodes.filter(node => nodeIds.includes(node.id));
  const pacsNames = selectedNodes.map(node => node.name);
  
  // 计算总任务数
  const total = studyIds.length * pacsNames.length;
  
  // 初始化进度
  setSendingProgress({
    isSending: true,
    total,
    completed: 0,
    failed: 0,
  });
  
  let completed = 0;
  let failed = 0;
  
  try {
    // 双层循环发送
    for (const studyId of studyIds) {
      for (const pacsName of pacsNames) {
        try {
          await sendStudyToPacs(studyId, pacsName);
          completed++;
          setSendingProgress({ isSending: true, total, completed, failed });
        } catch (error) {
          failed++;
          setSendingProgress({ isSending: true, total, completed, failed });
          console.error(`发送失败 [Study: ${studyId} -> PACS: ${pacsName}]:`, error);
        }
      }
    }
    
    // 显示结果
    if (failed === 0) {
      message.success(`发送完成!成功发送 ${completed} 个任务`);
    } else {
      message.warning(`发送完成!成功 ${completed} 个,失败 ${failed} 个`);
    }
  } finally {
    // 重置状态
    setSendingProgress({ isSending: false, total: 0, completed: 0, failed: 0 });
  }
};

3. 发送按钮处理器

const handleSend = async () => {
  // 验证节点选择
  if (selectedNodeIds.length === 0) {
    message.warning('请至少选择一个PACS节点');
    return;
  }
  
  // ✅ 从 store 获取选中的 Study IDs
  const selectedStudyIds = store.getState().historySelection.selectedIds;
  
  // 验证 Study 选择
  if (selectedStudyIds.length === 0) {
    message.warning('请先选择要发送的 Study');
    return;
  }
  
  // 获取选中节点名称
  const selectedNodes = nodes.filter(node => selectedNodeIds.includes(node.id));
  const nodeNames = selectedNodes.map(node => node.name).join(', ');
  
  // 显示确认对话框
  Modal.confirm({
    title: '确认发送图像',
    content: (
      <div>
        <p>即将发送 <strong>{selectedStudyIds.length}</strong> 个 Study 到以下 PACS 节点:</p>
        <p style={{ marginTop: 8, color: '#1890ff' }}>{nodeNames}</p>
        <p style={{ marginTop: 8 }}>共 <strong>{selectedStudyIds.length * selectedNodeIds.length}</strong> 个发送任务</p>
      </div>
    ),
    okText: '确认发送',
    cancelText: '取消',
    okButtonProps: { danger: true },
    onOk: async () => {
      await performSend(selectedStudyIds, selectedNodeIds);
    },
  });
};

4. 进度条 UI

{sendingProgress.isSending && (
  <div style={{ 
    marginBottom: 16, 
    padding: 16, 
    backgroundColor: '#f0f5ff', 
    borderRadius: 4 
  }}>
    <div style={{ marginBottom: 8, fontWeight: 500 }}>
      正在发送: {sendingProgress.completed + sendingProgress.failed} / {sendingProgress.total}
    </div>
    <Progress 
      percent={Math.round(
        (sendingProgress.completed + sendingProgress.failed) / sendingProgress.total * 100
      )}
      status={sendingProgress.failed > 0 ? 'exception' : 'active'}
      strokeColor={sendingProgress.failed > 0 ? '#ff4d4f' : '#1890ff'}
    />
    <div style={{ 
      marginTop: 8, 
      fontSize: '12px', 
      color: '#666',
      display: 'flex',
      justifyContent: 'space-between'
    }}>
      <span>成功: {sendingProgress.completed}</span>
      <span style={{ color: '#ff4d4f' }}>失败: {sendingProgress.failed}</span>
    </div>
  </div>
)}

关键技术决策

1. 状态管理策略

问题: 发送进度是否需要放在 Redux 中?

决策: 使用组件内部 useState 管理

理由:

  • 发送状态是临时的,不需要跨组件共享
  • 组件卸载时状态自动清理
  • 避免增加 Redux 复杂度

2. 数据获取方式

问题: 如何在事件处理器中获取 Redux 状态?

决策: 使用 store.getState() 而不是 useAppSelector

理由:

  • useAppSelector 是 Hook,只能在组件顶层使用
  • 事件处理器是普通函数,需要使用 store.getState()
  • 这是 Redux 官方推荐的方式

3. 错误处理策略

问题: 单个发送任务失败时如何处理?

决策: 记录失败但继续发送其他任务

理由:

  • 提高用户体验,不因单个失败阻塞全部
  • 最终展示成功/失败统计,用户可以看到全貌
  • 失败详情记录在控制台,方便调试

4. 发送策略

问题: 串行还是并行发送?

决策: 串行发送(一个接一个)

理由:

  • 避免并发请求过多导致服务器压力
  • 便于跟踪进度和错误
  • 保证发送顺序可预测
  • 未来可优化为半并发(如 3 个并发)

5. 用户体验设计

问题: 发送完成后是否自动返回?

决策: 停留在发送面板

理由:

  • 用户可以查看发送结果
  • 避免信息丢失
  • 用户自主决定何时返回

功能特性

✅ 已实现

  1. PACS 节点管理

    • 自动加载节点列表
    • 显示节点详细信息(地址、端口、AET)
    • 支持启用/禁用状态
    • 默认节点标注
  2. 节点选择

    • 单选/多选支持
    • 全选/取消全选
    • 自动选中默认节点
    • 禁用已禁用的节点
  3. 发送功能

    • 批量发送 Study
    • 发送前确认对话框
    • 实时进度显示
    • 成功/失败统计
    • 错误容错处理
  4. 用户体验

    • 加载状态提示
    • 错误信息展示
    • 刷新功能
    • 返回功能

📋 待优化

  1. 性能优化

    • 支持并发发送(控制并发数)
    • 大批量发送时的性能优化
  2. 功能增强

    • 支持发送任务队列
    • 支持暂停/恢复发送
    • 支持查看发送历史
  3. 用户体验

    • 发送详情展开/折叠
    • 失败任务重试功能
    • 导出发送报告

测试要点

1. 功能测试

  • 打开发送面板,验证节点列表加载
  • 验证默认节点自动选中
  • 选择/取消选择节点
  • 全选/取消全选功能
  • 未选择节点时点击发送,验证提示
  • 未选择 Study 时点击发送,验证提示
  • 正常发送流程,验证确认对话框
  • 确认发送,验证进度显示
  • 验证发送成功提示
  • 验证发送失败处理
  • 验证停留在发送面板
  • 点击返回按钮,验证返回到操作面板

2. 边界测试

  • 0 个 Study 发送
  • 1 个 Study 发送
  • 多个 Study 发送
  • 所有节点禁用时的表现
  • 网络异常时的表现
  • 发送过程中刷新页面

3. 性能测试

  • 大量 Study (>100) 发送性能
  • 多节点 (>10) 同时发送性能
  • 进度更新频率对性能的影响

注意事项

  1. 数据源

    • 必须从 historySelection 获取 Study IDs
    • 不要混淆 workSelectionhistorySelection
  2. Hook 使用

    • 不要在事件处理器中使用 useAppSelector
    • 在事件处理器中使用 store.getState()
  3. 错误处理

    • 单个发送失败不应阻止其他发送
    • 需要记录所有错误信息
  4. 用户提示

    • 发送前必须有确认对话框
    • 发送完成必须有结果提示
    • 进度必须实时更新

相关文件

  • src/pages/output/SendPanel.tsx - 发送面板组件
  • src/states/output/pacsNode/pacsNodeSlice.ts - PACS 节点状态管理
  • src/states/patient/worklist/slices/history.ts - History 列表状态管理
  • src/API/imageActions.ts - 图像发送 API
  • src/API/output/pacsNodeActions.ts - PACS 节点 API

更新记录

  • 2025-10-11: 初始版本,完成基本发送功能设计