|
@@ -0,0 +1,526 @@
|
|
|
+# 发送图像功能实现文档
|
|
|
+
|
|
|
+## 功能概述
|
|
|
+
|
|
|
+本文档描述了发送图像功能的完整实现方案。该功能允许用户将选中的 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: (
|
|
|
+ <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
|
|
|
+
|
|
|
+```typescript
|
|
|
+{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
|
|
|
+ - 不要混淆 `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: 初始版本,完成基本发送功能设计
|