# 发送图像功能实现文档 ## 功能概述 本文档描述了发送图像功能的完整实现方案。该功能允许用户将选中的 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} 个发送任务