|
@@ -0,0 +1,781 @@
|
|
|
+# 发送图像功能 - View Mode 实现文档
|
|
|
+
|
|
|
+## 1. 功能概述
|
|
|
+
|
|
|
+### 1.1 功能描述
|
|
|
+在图像查看页面(View Mode)中,用户可以选择当前查看的图像并将其发送到指定的 PACS 节点。本功能提供了一个发送面板,允许用户:
|
|
|
+- 查看当前选中的图像
|
|
|
+- 全选或取消选中所有图像
|
|
|
+- 选择目标 PACS 节点
|
|
|
+- 批量发送图像到多个 PACS 节点
|
|
|
+
|
|
|
+### 1.2 用户场景
|
|
|
+1. 用户在 ViewerContainer 中查看多张图像
|
|
|
+2. 点击"发送"按钮,打开发送面板
|
|
|
+3. 在发送面板中选择要发送的图像(全选/部分选择)
|
|
|
+4. 选择目标 PACS 节点(可多选)
|
|
|
+5. 点击"发送图像"按钮执行发送
|
|
|
+6. 查看发送进度和结果
|
|
|
+
|
|
|
+## 2. 涉及的参与者
|
|
|
+
|
|
|
+### 2.1 组件层级
|
|
|
+
|
|
|
+#### 页面组件
|
|
|
+- **TransferArea.tsx** - 发送按钮入口组件
|
|
|
+- **SendPanelForView.tsx** - 发送图像面板主组件
|
|
|
+
|
|
|
+#### Redux 状态管理
|
|
|
+- **panelSwitchSliceForView.ts** - 面板切换状态管理
|
|
|
+- **viewerContainerSlice.ts** - Viewer 选择状态管理
|
|
|
+- **pacsNodeSlice.ts** - PACS 节点状态管理
|
|
|
+
|
|
|
+#### API 服务
|
|
|
+- **imageActions.ts** - 图像发送 API
|
|
|
+
|
|
|
+#### 工具函数
|
|
|
+- **bodyPosition.ts** - 提供 `getDcmImageUrl` 函数
|
|
|
+
|
|
|
+### 2.2 核心类型/接口
|
|
|
+
|
|
|
+```typescript
|
|
|
+// Viewer Container 状态
|
|
|
+interface ViewerContainerState {
|
|
|
+ gridLayout: GridLayout;
|
|
|
+ selectedViewers: string[]; // 选中的 imageUrls
|
|
|
+ allViewers: string[]; // 所有的 imageUrls
|
|
|
+}
|
|
|
+
|
|
|
+// PACS 节点
|
|
|
+interface PacsNode {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+ address: string;
|
|
|
+ port: number;
|
|
|
+ aet: string;
|
|
|
+ is_enabled: boolean;
|
|
|
+ is_default: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+// PACS 节点状态
|
|
|
+interface PacsNodeState {
|
|
|
+ nodes: PacsNode[];
|
|
|
+ loading: boolean;
|
|
|
+ error: string | null;
|
|
|
+ selectedNodeIds: number[];
|
|
|
+}
|
|
|
+
|
|
|
+// 发送图像请求
|
|
|
+interface SendImageRequest {
|
|
|
+ sop_instance_uid: string;
|
|
|
+ pacs_name: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 发送进度状态
|
|
|
+interface SendingProgress {
|
|
|
+ isSending: boolean;
|
|
|
+ total: number;
|
|
|
+ completed: number;
|
|
|
+ failed: number;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 关键方法
|
|
|
+
|
|
|
+#### Redux Actions
|
|
|
+```typescript
|
|
|
+// viewerContainerSlice
|
|
|
+- selectAllViewers() // 选中所有 viewers
|
|
|
+- deselectAllViewers() // 取消选中所有 viewers (新增)
|
|
|
+- setSelectedViewers(urls[]) // 设置选中的 viewers
|
|
|
+
|
|
|
+// pacsNodeSlice
|
|
|
+- fetchPacsNodesThunk() // 异步获取 PACS 节点列表
|
|
|
+- toggleNodeSelection(id) // 切换节点选中状态
|
|
|
+- selectAllNodes() // 全选节点
|
|
|
+- deselectAllNodes() // 取消全选节点
|
|
|
+
|
|
|
+// panelSwitchSliceForView
|
|
|
+- switchToSendPanel() // 切换到发送面板
|
|
|
+- switchToOperationPanel() // 返回操作面板
|
|
|
+```
|
|
|
+
|
|
|
+#### 组件方法
|
|
|
+```typescript
|
|
|
+// SendPanelForView
|
|
|
+- handleSelectAllViewers() // 全选 viewers
|
|
|
+- handleDeselectAllViewers() // 取消选中 viewers
|
|
|
+- handleNodeToggle(nodeId) // 切换节点选中
|
|
|
+- handleSend() // 发送图像
|
|
|
+- performSend(sopUids, names) // 执行发送逻辑
|
|
|
+- extractSopInstanceUid(url) // 提取 SOP Instance UID
|
|
|
+```
|
|
|
+
|
|
|
+## 3. 实现计划
|
|
|
+
|
|
|
+### 3.1 需要创建的内容
|
|
|
+
|
|
|
+1. **新增 Redux Action**
|
|
|
+ - 文件:`src/states/view/viewerContainerSlice.ts`
|
|
|
+ - 内容:添加 `deselectAllViewers` reducer
|
|
|
+ ```typescript
|
|
|
+ deselectAllViewers: (state) => {
|
|
|
+ state.selectedViewers = [];
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **实现文档**
|
|
|
+ - 文件:`docs/实现/发送图像功能-ViewMode.md`(本文档)
|
|
|
+ - 内容:完整的功能实现文档
|
|
|
+
|
|
|
+### 3.2 需要修改的内容
|
|
|
+
|
|
|
+1. **SendPanelForView.tsx** - 主要实现文件
|
|
|
+ - 导入必要的依赖
|
|
|
+ - 实现状态管理
|
|
|
+ - 实现 UI 渲染
|
|
|
+ - 实现业务逻辑
|
|
|
+
|
|
|
+### 3.3 无需删除的内容
|
|
|
+
|
|
|
+所有现有代码保持不变,仅进行功能扩展。
|
|
|
+
|
|
|
+## 4. 交互流程
|
|
|
+
|
|
|
+### 4.1 泳道图
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────┐ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌─────┐
|
|
|
+│ 用户 │ │ TransferArea │ │ SendPanelForView │ │ Redux Store │ │ API │
|
|
|
+└────┬────┘ └──────┬───────┘ └────────┬─────────┘ └──────┬──────┘ └──┬──┘
|
|
|
+ │ │ │ │ │
|
|
|
+ │ 1. 点击发送 │ │ │ │
|
|
|
+ ├──────────────>│ │ │ │
|
|
|
+ │ │ 2. switchToSendPanel│ │ │
|
|
|
+ │ ├────────────────────────────────────────>│ │
|
|
|
+ │ │ │ 3. 面板切换 │ │
|
|
|
+ │ │ │<─────────────────────┤ │
|
|
|
+ │ │ │ │ │
|
|
|
+ │ │ │ 4. fetchPacsNodes │ │
|
|
|
+ │ │ ├────────────────────>│ │
|
|
|
+ │ │ │ │ 5. API 请求 │
|
|
|
+ │ │ │ ├─────────────>│
|
|
|
+ │ │ │ │ 6. 节点列表 │
|
|
|
+ │ │ │ │<─────────────┤
|
|
|
+ │ │ │ 7. 更新节点状态 │ │
|
|
|
+ │ │ │<─────────────────────┤ │
|
|
|
+ │ │ │ 8. 渲染界面 │ │
|
|
|
+ │ │ │ │ │
|
|
|
+ │ 9. 点击"全选" │ │ │ │
|
|
|
+ ├──────────────────────────────────>│ │ │
|
|
|
+ │ │ │ 10. selectAllViewers│ │
|
|
|
+ │ │ ├────────────────────>│ │
|
|
|
+ │ │ │ 11. 更新选中状态 │ │
|
|
|
+ │ │ │<─────────────────────┤ │
|
|
|
+ │ │ │ │ │
|
|
|
+ │ 12. 选择节点 │ │ │ │
|
|
|
+ ├──────────────────────────────────>│ │ │
|
|
|
+ │ │ │ 13. toggleNode │ │
|
|
|
+ │ │ ├────────────────────>│ │
|
|
|
+ │ │ │ │ │
|
|
|
+ │ 14. 发送图像 │ │ │ │
|
|
|
+ ├──────────────────────────────────>│ │ │
|
|
|
+ │ │ │ 15. 验证 │ │
|
|
|
+ │ │ │ 16. 确认对话框 │ │
|
|
|
+ │<──────────────────────────────────┤ │ │
|
|
|
+ │ │ │ │ │
|
|
|
+ │ 17. 确认 │ │ │ │
|
|
|
+ ├──────────────────────────────────>│ │ │
|
|
|
+ │ │ │ 18. performSend │ │
|
|
|
+ │ │ │ │ 19. 发送图像 │
|
|
|
+ │ │ │ ├─────────────>│
|
|
|
+ │ │ │ │ 20. 返回结果 │
|
|
|
+ │ │ │ │<─────────────┤
|
|
|
+ │ │ │ 21. 更新进度 │ │
|
|
|
+ │ │ │ 22. 完成提示 │ │
|
|
|
+ │<──────────────────────────────────┤ │ │
|
|
|
+ │ │ │ │ │
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 状态流转图
|
|
|
+
|
|
|
+```
|
|
|
+[用户点击发送]
|
|
|
+ ↓
|
|
|
+[面板切换: OperationPanel → SendPanel]
|
|
|
+ ↓
|
|
|
+[加载 PACS 节点列表]
|
|
|
+ ↓
|
|
|
+[显示发送面板]
|
|
|
+ ├── 顶部: [全选按钮] [取消选中按钮]
|
|
|
+ ├── 中间: [PACS 节点列表]
|
|
|
+ └── 底部: [发送图像按钮]
|
|
|
+ ↓
|
|
|
+[用户操作选择]
|
|
|
+ ├── 选择 Viewers (全选/部分)
|
|
|
+ └── 选择 PACS 节点 (多选)
|
|
|
+ ↓
|
|
|
+[点击发送图像]
|
|
|
+ ↓
|
|
|
+[验证选择]
|
|
|
+ ├── selectedViewers.length > 0 ✓
|
|
|
+ └── selectedNodeIds.length > 0 ✓
|
|
|
+ ↓
|
|
|
+[显示确认对话框]
|
|
|
+ ├── 发送数量: N 个 viewers × M 个节点
|
|
|
+ └── 节点列表: PACS1, PACS2, ...
|
|
|
+ ↓
|
|
|
+[用户确认] → [开始发送]
|
|
|
+ ↓
|
|
|
+[发送循环]
|
|
|
+ for each viewer:
|
|
|
+ for each pacsNode:
|
|
|
+ ├── 调用 sendImageToPacs()
|
|
|
+ ├── 更新进度
|
|
|
+ └── 处理错误
|
|
|
+ ↓
|
|
|
+[显示结果]
|
|
|
+ ├── 成功: X 个
|
|
|
+ └── 失败: Y 个
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 数据流详解
|
|
|
+
|
|
|
+### 5.1 初始化数据流
|
|
|
+
|
|
|
+```
|
|
|
+组件挂载 (SendPanelForView)
|
|
|
+ ↓
|
|
|
+useEffect: dispatch(fetchPacsNodesThunk())
|
|
|
+ ↓
|
|
|
+API 请求: GET /auth/scp
|
|
|
+ ↓
|
|
|
+返回数据: { data: { scp: PacsNode[] } }
|
|
|
+ ↓
|
|
|
+Redux 更新: pacsNodeSlice.nodes = response.data.scp
|
|
|
+ ↓
|
|
|
+自动选择默认节点: selectedNodeIds = [defaultNode.id]
|
|
|
+ ↓
|
|
|
+组件重新渲染
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 选择数据流
|
|
|
+
|
|
|
+#### Viewer 选择
|
|
|
+```
|
|
|
+用户操作: 点击"全选"
|
|
|
+ ↓
|
|
|
+dispatch(selectAllViewers())
|
|
|
+ ↓
|
|
|
+Redux 更新: selectedViewers = [...allViewers]
|
|
|
+ ↓
|
|
|
+组件重新渲染: 所有 viewers 显示为选中状态
|
|
|
+```
|
|
|
+
|
|
|
+#### PACS 节点选择
|
|
|
+```
|
|
|
+用户操作: 点击节点复选框
|
|
|
+ ↓
|
|
|
+handleNodeToggle(nodeId)
|
|
|
+ ↓
|
|
|
+dispatch(toggleNodeSelection(nodeId))
|
|
|
+ ↓
|
|
|
+Redux 更新:
|
|
|
+ if (nodeId in selectedNodeIds):
|
|
|
+ remove from array
|
|
|
+ else:
|
|
|
+ add to array
|
|
|
+ ↓
|
|
|
+组件重新渲染: 节点选中状态更新
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 发送数据流
|
|
|
+
|
|
|
+```
|
|
|
+用户点击"发送图像"
|
|
|
+ ↓
|
|
|
+handleSend()
|
|
|
+ ↓
|
|
|
+验证:
|
|
|
+ - selectedViewers.length > 0 ✓
|
|
|
+ - selectedNodeIds.length > 0 ✓
|
|
|
+ ↓
|
|
|
+提取数据:
|
|
|
+ sopInstanceUids = selectedViewers.map(extractSopInstanceUid)
|
|
|
+ pacsNames = nodes.filter(...).map(n => n.name)
|
|
|
+ ↓
|
|
|
+Modal.confirm() - 显示确认对话框
|
|
|
+ ↓
|
|
|
+用户确认 → performSend(sopInstanceUids, pacsNames)
|
|
|
+ ↓
|
|
|
+初始化进度: { isSending: true, total, completed: 0, failed: 0 }
|
|
|
+ ↓
|
|
|
+双层循环发送:
|
|
|
+ for sopUid in sopInstanceUids:
|
|
|
+ for pacsName in pacsNames:
|
|
|
+ try:
|
|
|
+ await sendImageToPacs(sopUid, pacsName)
|
|
|
+ completed++
|
|
|
+ 更新进度状态
|
|
|
+ catch error:
|
|
|
+ failed++
|
|
|
+ 更新进度状态
|
|
|
+ log error
|
|
|
+ ↓
|
|
|
+显示结果:
|
|
|
+ if failed == 0:
|
|
|
+ message.success("全部成功")
|
|
|
+ else:
|
|
|
+ message.warning("部分失败")
|
|
|
+ ↓
|
|
|
+重置进度状态: { isSending: false, ... }
|
|
|
+```
|
|
|
+
|
|
|
+## 6. 数据结构详解
|
|
|
+
|
|
|
+### 6.1 ViewerContainer 状态示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+{
|
|
|
+ gridLayout: '2x2',
|
|
|
+ selectedViewers: [
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671.dcm",
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668672.dcm"
|
|
|
+ ],
|
|
|
+ allViewers: [
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671.dcm",
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668672.dcm",
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668673.dcm",
|
|
|
+ "dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668674.dcm"
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 PACS 节点状态示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+{
|
|
|
+ nodes: [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: "PACS_Main",
|
|
|
+ address: "192.168.1.100",
|
|
|
+ port: 11112,
|
|
|
+ aet: "PACS1",
|
|
|
+ is_enabled: true,
|
|
|
+ is_default: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: "PACS_Backup",
|
|
|
+ address: "192.168.1.101",
|
|
|
+ port: 11112,
|
|
|
+ aet: "PACS2",
|
|
|
+ is_enabled: true,
|
|
|
+ is_default: false
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: "PACS_Archive",
|
|
|
+ address: "192.168.1.102",
|
|
|
+ port: 11112,
|
|
|
+ aet: "PACS3",
|
|
|
+ is_enabled: false,
|
|
|
+ is_default: false
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ selectedNodeIds: [1, 2],
|
|
|
+ loading: false,
|
|
|
+ error: null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 发送进度状态示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 发送中
|
|
|
+{
|
|
|
+ isSending: true,
|
|
|
+ total: 6, // 2 images × 3 PACS nodes
|
|
|
+ completed: 4, // 已完成 4 个
|
|
|
+ failed: 1 // 失败 1 个
|
|
|
+}
|
|
|
+
|
|
|
+// 发送完成
|
|
|
+{
|
|
|
+ isSending: false,
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ failed: 0
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 imageUrl 与 SOP Instance UID 映射
|
|
|
+
|
|
|
+```typescript
|
|
|
+// imageUrl 格式
|
|
|
+"dicomweb:http://localhost:8000/pub/dcm/1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671.dcm"
|
|
|
+
|
|
|
+// 提取 SOP Instance UID
|
|
|
+const extractSopInstanceUid = (imageUrl: string): string => {
|
|
|
+ // 匹配 .dcm 前的最后一段
|
|
|
+ const match = imageUrl.match(/\/([^\/]+)\.dcm$/);
|
|
|
+ return match ? match[1] : '';
|
|
|
+};
|
|
|
+
|
|
|
+// 结果
|
|
|
+"1.2.276.0.1000000.5.1.4.701601461.19649.1749545373.668671"
|
|
|
+```
|
|
|
+
|
|
|
+## 7. 执行流程(用户视角)
|
|
|
+
|
|
|
+### 7.1 完整用户旅程
|
|
|
+
|
|
|
+```
|
|
|
+第1步: 查看图像
|
|
|
+ 用户在 View 页面查看图像
|
|
|
+ ViewerContainer 显示多张图像
|
|
|
+ 部分图像已被选中(selectedViewers)
|
|
|
+
|
|
|
+第2步: 打开发送面板
|
|
|
+ 用户点击 TransferArea 的"发送"按钮
|
|
|
+ 面板切换到 SendPanelForView
|
|
|
+ 自动加载 PACS 节点列表
|
|
|
+
|
|
|
+第3步: 查看当前状态
|
|
|
+ 顶部显示:
|
|
|
+ - [全选] 按钮
|
|
|
+ - [取消选中] 按钮
|
|
|
+ 中间显示:
|
|
|
+ - PACS 节点列表
|
|
|
+ - 每个节点显示: 名称、地址、AET
|
|
|
+ - 已启用节点可选,已禁用节点不可选
|
|
|
+ - 默认节点自动选中
|
|
|
+ 底部显示:
|
|
|
+ - [发送图像 (N)] 按钮
|
|
|
+ - N = 当前选中的节点数量
|
|
|
+
|
|
|
+第4步: 调整图像选择(可选)
|
|
|
+ 选项A: 点击"全选"
|
|
|
+ → 所有 viewers 被选中
|
|
|
+ 选项B: 点击"取消选中"
|
|
|
+ → 所有 viewers 取消选中
|
|
|
+ 选项C: 保持当前选择
|
|
|
+ → 不做任何操作
|
|
|
+
|
|
|
+第5步: 选择 PACS 节点
|
|
|
+ 用户勾选/取消勾选节点复选框
|
|
|
+ 至少选择一个节点
|
|
|
+ 可以选择多个节点
|
|
|
+
|
|
|
+第6步: 点击"发送图像"
|
|
|
+ 系统验证:
|
|
|
+ ✓ 至少有1个 viewer 被选中
|
|
|
+ ✓ 至少有1个 PACS 节点被选中
|
|
|
+
|
|
|
+ 验证通过 → 显示确认对话框
|
|
|
+ 验证失败 → 显示警告消息
|
|
|
+
|
|
|
+第7步: 确认发送
|
|
|
+ 对话框显示:
|
|
|
+ - 即将发送 X 个图像
|
|
|
+ - 发送到 Y 个 PACS 节点
|
|
|
+ - 节点列表: PACS1, PACS2, ...
|
|
|
+ - 总任务数: X × Y
|
|
|
+
|
|
|
+ 用户选择:
|
|
|
+ → [确认发送] → 继续
|
|
|
+ → [取消] → 返回面板
|
|
|
+
|
|
|
+第8步: 发送过程
|
|
|
+ 显示进度条:
|
|
|
+ - 总任务数: X × Y
|
|
|
+ - 已完成: N
|
|
|
+ - 已失败: M
|
|
|
+ - 进度百分比
|
|
|
+
|
|
|
+ 后台执行:
|
|
|
+ for 每个图像:
|
|
|
+ for 每个节点:
|
|
|
+ 发送图像
|
|
|
+ 更新进度
|
|
|
+
|
|
|
+第9步: 查看结果
|
|
|
+ 全部成功:
|
|
|
+ → 显示成功消息: "发送完成!成功发送 N 个任务"
|
|
|
+ 部分失败:
|
|
|
+ → 显示警告消息: "发送完成!成功 X 个,失败 Y 个"
|
|
|
+
|
|
|
+ 用户可以:
|
|
|
+ → 继续发送其他图像
|
|
|
+ → 点击返回按钮回到操作面板
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 异常情况处理
|
|
|
+
|
|
|
+```
|
|
|
+场景1: 未选择 viewer
|
|
|
+ 用户点击"发送图像"
|
|
|
+ → 显示警告: "请先选择要发送的图像"
|
|
|
+ → 停留在当前面板
|
|
|
+
|
|
|
+场景2: 未选择 PACS 节点
|
|
|
+ 用户点击"发送图像"
|
|
|
+ → 显示警告: "请至少选择一个PACS节点"
|
|
|
+ → 停留在当前面板
|
|
|
+
|
|
|
+场景3: 加载 PACS 节点失败
|
|
|
+ 进入面板时加载失败
|
|
|
+ → 显示错误提示框
|
|
|
+ → 提供"重试"按钮
|
|
|
+ → 用户可以重新加载
|
|
|
+
|
|
|
+场景4: 发送部分失败
|
|
|
+ 发送过程中部分失败
|
|
|
+ → 继续发送其他任务
|
|
|
+ → 统计成功和失败数量
|
|
|
+ → 最后显示汇总结果
|
|
|
+ → 失败详情记录在控制台
|
|
|
+
|
|
|
+场景5: 发送全部失败
|
|
|
+ 所有发送任务都失败
|
|
|
+ → 显示警告消息
|
|
|
+ → 提示用户检查网络或节点配置
|
|
|
+```
|
|
|
+
|
|
|
+## 8. 核心代码实现
|
|
|
+
|
|
|
+### 8.1 SOP Instance UID 提取函数
|
|
|
+
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 从 imageUrl 中提取 SOP Instance UID
|
|
|
+ * @param imageUrl - 图像 URL,格式: "dicomweb:http://localhost:8000/pub/dcm/1.2.3.4.5.dcm"
|
|
|
+ * @returns SOP Instance UID
|
|
|
+ */
|
|
|
+const extractSopInstanceUid = (imageUrl: string): string => {
|
|
|
+ // 匹配 .dcm 扩展名前的最后一段路径
|
|
|
+ const match = imageUrl.match(/\/([^\/]+)\.dcm$/);
|
|
|
+ if (!match) {
|
|
|
+ console.error('无法从 imageUrl 提取 SOP Instance UID:', imageUrl);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return match[1];
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 全选/取消选中实现
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 全选
|
|
|
+const handleSelectAllViewers = () => {
|
|
|
+ dispatch(selectAllViewers());
|
|
|
+};
|
|
|
+
|
|
|
+// 取消选中
|
|
|
+const handleDeselectAllViewers = () => {
|
|
|
+ dispatch(deselectAllViewers());
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 发送图像核心逻辑
|
|
|
+
|
|
|
+```typescript
|
|
|
+const performSend = async (
|
|
|
+ sopInstanceUids: string[],
|
|
|
+ pacsNames: string[]
|
|
|
+) => {
|
|
|
+ const total = sopInstanceUids.length * pacsNames.length;
|
|
|
+
|
|
|
+ setSendingProgress({
|
|
|
+ isSending: true,
|
|
|
+ total,
|
|
|
+ completed: 0,
|
|
|
+ failed: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ let completed = 0;
|
|
|
+ let failed = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (const sopUid of sopInstanceUids) {
|
|
|
+ for (const pacsName of pacsNames) {
|
|
|
+ try {
|
|
|
+ await sendImageToPacs(sopUid, pacsName);
|
|
|
+ completed++;
|
|
|
+ setSendingProgress({
|
|
|
+ isSending: true,
|
|
|
+ total,
|
|
|
+ completed,
|
|
|
+ failed,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ failed++;
|
|
|
+ setSendingProgress({
|
|
|
+ isSending: true,
|
|
|
+ total,
|
|
|
+ completed,
|
|
|
+ failed,
|
|
|
+ });
|
|
|
+ console.error(
|
|
|
+ `发送失败 [SOP: ${sopUid} -> PACS: ${pacsName}]:`,
|
|
|
+ error
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示结果
|
|
|
+ if (failed === 0) {
|
|
|
+ message.success(`发送完成!成功发送 ${completed} 个任务`);
|
|
|
+ } else {
|
|
|
+ message.warning(
|
|
|
+ `发送完成!成功 ${completed} 个,失败 ${failed} 个`
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ setSendingProgress({
|
|
|
+ isSending: false,
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ failed: 0,
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 8.4 发送按钮点击处理
|
|
|
+
|
|
|
+```typescript
|
|
|
+const handleSend = () => {
|
|
|
+ // 验证 viewer 选择
|
|
|
+ if (selectedViewers.length === 0) {
|
|
|
+ message.warning('请先选择要发送的图像');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证节点选择
|
|
|
+ if (selectedNodeIds.length === 0) {
|
|
|
+ message.warning('请至少选择一个PACS节点');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取 SOP Instance UIDs
|
|
|
+ const sopInstanceUids = selectedViewers
|
|
|
+ .map(extractSopInstanceUid)
|
|
|
+ .filter(uid => uid !== ''); // 过滤无效 UID
|
|
|
+
|
|
|
+ if (sopInstanceUids.length === 0) {
|
|
|
+ message.error('无法提取图像 UID,请检查图像数据');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取节点名称
|
|
|
+ const selectedNodes = nodes.filter(node =>
|
|
|
+ selectedNodeIds.includes(node.id)
|
|
|
+ );
|
|
|
+ const pacsNames = selectedNodes.map(node => node.name);
|
|
|
+ const nodeNames = pacsNames.join(', ');
|
|
|
+
|
|
|
+ // 显示确认对话框
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认发送图像',
|
|
|
+ content: (
|
|
|
+ <div>
|
|
|
+ <p>
|
|
|
+ 即将发送 <strong>{sopInstanceUids.length}</strong> 个图像到以下 PACS 节点:
|
|
|
+ </p>
|
|
|
+ <p style={{ marginTop: 8, color: '#1890ff' }}>
|
|
|
+ {nodeNames}
|
|
|
+ </p>
|
|
|
+ <p style={{ marginTop: 8 }}>
|
|
|
+ 共 <strong>{sopInstanceUids.length * pacsNames.length}</strong> 个发送任务
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ okText: '确认发送',
|
|
|
+ cancelText: '取消',
|
|
|
+ okButtonProps: { danger: true },
|
|
|
+ onOk: async () => {
|
|
|
+ await performSend(sopInstanceUids, pacsNames);
|
|
|
+ },
|
|
|
+ });
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+## 9. UI 设计
|
|
|
+
|
|
|
+### 9.1 布局结构
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────┐
|
|
|
+│ Header: [<] 发送图像 [重载] │
|
|
|
+├─────────────────────────────────────────┤
|
|
|
+│ Content: │
|
|
|
+│ │
|
|
|
+│ ┌─────────────────────────────────┐ │
|
|
|
+│ │ [√ 全选] [□ 取消选中] │ │
|
|
|
+│ └─────────────────────────────────┘ │
|
|
|
+│ │
|
|
|
+│ ┌─────────────────────────────────┐ │
|
|
|
+│ │ □ PACS_Main │ │
|
|
|
+│ │ 192.168.1.100:11112 | AET:... │ │
|
|
|
+│ ├─────────────────────────────────┤ │
|
|
|
+│ │ □ PACS_Backup (默认节点) │ │
|
|
|
+│ │ 192.168.1.101:11112 | AET:... │ │
|
|
|
+│ ├─────────────────────────────────┤ │
|
|
|
+│ │ □ PACS_Archive [已禁用] │ │
|
|
|
+│ │ 192.168.1.102:11112 | AET:... │ │
|
|
|
+│ └─────────────────────────────────┘ │
|
|
|
+│ │
|
|
|
+├─────────────────────────────────────────┤
|
|
|
+│ Footer: │
|
|
|
+│ ┌──────────────────────────────┐ │
|
|
|
+│ │ [图标] 发送图像 (2) │ │
|
|
|
+│ └──────────────────────────────┘ │
|
|
|
+└─────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 发送进度显示
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────┐
|
|
|
+│ 正在发送: 5 / 12 │
|
|
|
+│ ████████████░░░░░░░░░░░░ 42% │
|
|
|
+│ 成功: 4 失败: 1 │
|
|
|
+└─────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 9.3 确认对话框
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────┐
|
|
|
+│ 确认发送图像 │
|
|
|
+├─────────────────────────────────────┤
|
|
|
+│ │
|
|
|
+│ 即将发送 3 个图像到以下 PACS 节点: │
|
|
|
+│ │
|
|
|
+│ PACS_Main, PACS_Backup │
|
|
|
+│ │
|
|
|
+│ 共 6 个发送任务 │
|
|
|
+│ │
|
|
|
+├─────────────────────────────────────┤
|
|
|
+│ [取消] [确认发送] │
|
|
|
+└─────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+## 10. 测试要点
|
|
|
+
|
|
|
+### 10.1 功能测试
|
|
|
+
|
|
|
+1. **面板切换**
|
|
|
+ - 点击发送按钮,面板正确切换
|
|
|
+ - 点击返回按钮,返回操作面板
|
|
|
+
|
|
|
+2. **PACS 节点加载**
|
|
|
+ - 面板打开时自动加载节点
|
|
|
+ - 加载成功显示节点列表
|
|
|
+ - 加载失败显示错误提示
|
|
|
+ - 重载按钮功能正常
|
|
|
+
|
|
|
+3. **Viewer 选
|