# 发送图像功能实现文档 ## 功能概述 本文档描述了发送图像功能的完整实现方案。该功能允许用户将选中的 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 ```typescript 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. 数据获取方式 ```typescript // ✅ 正确方式:在事件处理器中使用 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. 发送进度状态 ```typescript const [sendingProgress, setSendingProgress] = useState({ isSending: false, // 是否正在发送 total: 0, // 总任务数 completed: 0, // 已完成数 failed: 0, // 失败数 }); ``` ### 2. 发送逻辑实现 ```typescript 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. 发送按钮处理器 ```typescript 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: (

即将发送 {selectedStudyIds.length} 个 Study 到以下 PACS 节点:

{nodeNames}

{selectedStudyIds.length * selectedNodeIds.length} 个发送任务

), okText: '确认发送', cancelText: '取消', okButtonProps: { danger: true }, onOk: async () => { await performSend(selectedStudyIds, selectedNodeIds); }, }); }; ``` ### 4. 进度条 UI ```typescript {sendingProgress.isSending && (
正在发送: {sendingProgress.completed + sendingProgress.failed} / {sendingProgress.total}
0 ? 'exception' : 'active'} strokeColor={sendingProgress.failed > 0 ? '#ff4d4f' : '#1890ff'} />
成功: {sendingProgress.completed} 失败: {sendingProgress.failed}
)} ``` ## 关键技术决策 ### 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 - 不要混淆 `workSelection` 和 `historySelection` 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: 初始版本,完成基本发送功能设计