Bladeren bron

feat: 实现传输队列删除发送任务功能

- 在 OutputActionPanel 中新增 handleDelete 方法实现批量删除
- 导入 Modal 组件和 deleteSendJobThunk
- 添加删除确认对话框防止误操作
- 为删除按钮绑定 onClick 事件处理器
- 实现完整的错误处理和用户提示机制
- 创建详细的实现文档,包含架构分析、交互流程、数据流等

改动文件:
- src/pages/patient/components/OutputActionPanel.tsx
- docs/实现/删除发送任务功能.md
sw 3 dagen geleden
bovenliggende
commit
cfa36d5d19
2 gewijzigde bestanden met toevoegingen van 519 en 2 verwijderingen
  1. 489 0
      docs/实现/删除发送任务功能.md
  2. 30 2
      src/pages/patient/components/OutputActionPanel.tsx

+ 489 - 0
docs/实现/删除发送任务功能.md

@@ -0,0 +1,489 @@
+# 删除发送任务功能实现文档
+
+## 概述
+
+本文档详细记录了**删除发送任务**功能的实现过程,包括架构设计、数据流、交互流程和具体实现细节。
+
+## 功能需求
+
+实现传输队列中的删除功能:
+- 用户可以在表格中选择一个或多个发送任务
+- 点击删除按钮后,弹出确认对话框
+- 确认后调用后端接口批量删除选中的任务
+- 删除成功后更新 UI 并清空选中状态
+
+## 1. 架构分析
+
+### 1.1 涉及的参与者(从粗到细)
+
+#### UI 层
+- **OutputOperationPanel** (容器组件)
+  - 位置:`src/pages/patient/components/OutputOperationPanel.tsx`
+  - 职责:组织和布局操作面板
+
+- **OutputActionPanel** (操作面板组件)
+  - 位置:`src/pages/patient/components/OutputActionPanel.tsx`
+  - 职责:渲染操作按钮(重试、删除)并处理用户交互
+  - 修改内容:
+    - 新增 `handleDelete` 方法
+    - 导入 `Modal` 组件和 `deleteSendJobThunk`
+    - 绑定删除按钮的 onClick 事件
+
+#### 状态管理层
+- **sendJobSlice** (Redux Slice)
+  - 位置:`src/states/output/sendJob/slices/sendJobSlice.ts`
+  - 参与者:
+    - `deleteSendJobThunk` - 删除任务的异步 thunk(已存在)
+    - `sendJobEntitiesSlice` - 管理发送任务数据
+    - `sendJobSelectionSlice` - 管理选中的任务 ID
+
+#### API 层
+- **sendJobActions**
+  - 位置:`src/API/output/sendJobActions.ts`
+  - 方法:`deleteSendTask(taskIds: string[])` - 调用删除接口(已存在)
+
+#### 数据模型层
+- **SendJob** (发送任务实体)
+  - 位置:`src/domain/output/sendJob.ts`
+  - 核心属性:
+    - `task_id`: 任务唯一标识
+    - `patient_name`: 患者姓名
+    - `status`: 任务状态
+
+## 2. 交互流程
+
+### 泳道图
+
+```mermaid
+sequenceDiagram
+    participant User as 用户
+    participant UI as OutputActionPanel
+    participant Modal as 确认对话框
+    participant Redux as Redux Store
+    participant Thunk as deleteSendJobThunk
+    participant API as deleteSendTask
+    participant Backend as 后端服务
+
+    User->>UI: 1. 勾选表格中的任务
+    UI->>Redux: 2. 更新 selectedIds
+    
+    User->>UI: 3. 点击删除按钮
+    UI->>UI: 4. 校验 selectedIds.length > 0
+    
+    alt 无选中任务
+        UI->>User: 显示警告:请先选择要删除的任务
+    else 有选中任务
+        UI->>Modal: 5. 显示确认对话框
+        Modal->>User: 展示:确定要删除 N 个任务吗?
+        
+        alt 用户取消
+            User->>Modal: 点击取消
+            Modal->>UI: 关闭对话框
+        else 用户确认
+            User->>Modal: 点击确定
+            Modal->>Redux: 6. dispatch(deleteSendJobThunk(selectedIds))
+            Redux->>Thunk: 7. 执行删除 thunk
+            Thunk->>API: 8. 调用 deleteSendTask(taskIds)
+            API->>Backend: 9. DELETE /auth/scp/task
+            Backend-->>API: 10. 返回结果 {code: '0x000000'}
+            API-->>Thunk: 11. 返回响应
+            
+            alt 删除成功
+                Thunk->>Redux: 12. fulfilled - 更新 state
+                Redux->>Redux: 13. 过滤掉已删除的任务
+                Redux->>Redux: 14. 清空 selectedIds
+                Redux-->>UI: 15. 触发重新渲染
+                UI->>User: 16. 显示成功消息
+            else 删除失败
+                Thunk->>Redux: rejected
+                Redux-->>UI: 错误信息
+                UI->>User: 显示错误消息
+            end
+        end
+    end
+```
+
+## 3. 数据流
+
+```
+┌─────────────┐
+│  用户操作    │
+│  勾选任务    │
+└──────┬──────┘
+       │
+       ▼
+┌─────────────────────────────┐
+│ Redux State 更新              │
+│ selectedIds = ['id1', 'id2'] │
+└──────┬──────────────────────┘
+       │
+       │ 用户点击删除按钮
+       ▼
+┌─────────────────────┐
+│ handleDelete 校验    │
+│ selectedIds.length   │
+└──────┬──────────────┘
+       │
+       ▼
+┌─────────────────────┐
+│ Modal.confirm        │
+│ 显示确认对话框        │
+└──────┬──────────────┘
+       │
+       │ 用户确认
+       ▼
+┌─────────────────────────────────────┐
+│ dispatch(deleteSendJobThunk(ids))   │
+└──────┬──────────────────────────────┘
+       │
+       ▼
+┌─────────────────────────┐
+│ API 层                  │
+│ deleteSendTask(taskIds) │
+└──────┬──────────────────┘
+       │
+       ▼
+┌─────────────────────────────────┐
+│ HTTP DELETE 请求                 │
+│ /auth/scp/task                  │
+│ body: ['id1', 'id2']            │
+└──────┬──────────────────────────┘
+       │
+       ▼
+┌─────────────────────┐
+│ 后端处理并返回       │
+│ {code: '0x000000'}  │
+└──────┬──────────────┘
+       │
+       ▼
+┌─────────────────────────────────────┐
+│ Redux Store 更新                     │
+│ - entities.data (过滤已删除项)        │
+│ - selection.selectedIds = []         │
+└──────┬──────────────────────────────┘
+       │
+       ▼
+┌─────────────────────┐
+│ UI 重新渲染          │
+│ 显示成功消息          │
+└─────────────────────┘
+```
+
+## 4. 核心数据结构
+
+### 4.1 SendJob (发送任务实体)
+```typescript
+interface SendJob {
+  task_id: string;           // 任务唯一标识
+  patient_name: string;      // 患者姓名
+  patient_id: string;        // 患者 ID
+  priority: SendJobPriority; // 优先级: 'High' | 'Medium' | 'Low'
+  status: SendJobStatus;     // 状态: 'ARRIVED' | 'SENDING' | 'FAILED' | 'SUCCESS'
+  retry_count?: number;      // 重试次数
+  destination: string;       // 目标 PACS 节点
+}
+```
+
+### 4.2 Redux State 结构
+```typescript
+// 选中状态
+interface SelectionState {
+  selectedIds: string[];  // 选中的任务 ID 列表
+}
+
+// 实体状态
+interface EntitiesState<SendJob> {
+  data: SendJob[];    // 任务列表
+  total: number;      // 总数
+}
+
+// UI 状态
+interface UIState {
+  loading: boolean;
+  error: string | null;
+}
+```
+
+### 4.3 API 接口
+
+#### 请求
+```typescript
+// DELETE /auth/scp/task
+// Content-Type: application/json
+// Body: string[] (任务 ID 数组)
+
+例如:
+["0199cd46-82f0-76c5-b1d3-9399668a1a05", "0199cd46-460d-770e-ac8e-549939a4a7d4"]
+```
+
+#### 响应
+```typescript
+interface BaseResponse {
+  code: string;           // '0x000000' 表示成功
+  description: string;    // 描述信息
+  solution: string;       // 解决方案
+  data: Record<string, any>;
+}
+```
+
+## 5. 具体实现
+
+### 5.1 组件修改:OutputActionPanel.tsx
+
+#### 导入依赖
+```typescript
+// 新增 Modal 组件和 deleteSendJobThunk
+import { Tooltip, Button, Space, message, Modal } from 'antd';
+import { retrySendJobThunk, deleteSendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
+```
+
+#### 实现 handleDelete 方法
+```typescript
+const handleDelete = async () => {
+  // 第 1 步:校验是否有选中的任务
+  if (selectedIds.length === 0) {
+    message.warning('请先选择要删除的任务');
+    return;
+  }
+
+  // 第 2 步:显示确认对话框
+  Modal.confirm({
+    title: '确认删除',
+    content: `确定要删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`,
+    okText: '确定',
+    cancelText: '取消',
+    onOk: async () => {
+      try {
+        // 第 3 步:调用删除 thunk
+        await dispatch(deleteSendJobThunk(selectedIds)).unwrap();
+        
+        // 第 4 步:显示成功消息
+        message.success(`成功删除 ${selectedIds.length} 个任务`);
+      } catch (error) {
+        // 第 5 步:错误处理
+        console.error('删除任务失败:', error);
+        message.error('删除任务失败,请稍后再试');
+      }
+    },
+  });
+};
+```
+
+#### 绑定事件处理器
+```typescript
+<Button
+  type="default"
+  icon={<Icon name="Delete" />}
+  onClick={handleDelete}  // 绑定删除处理函数
+/>
+```
+
+### 5.2 Redux 层(已存在,无需修改)
+
+#### deleteSendJobThunk 实现
+```typescript
+// src/states/output/sendJob/slices/sendJobSlice.ts
+export const deleteSendJobThunk = createDeleteThunk(
+  'sendJob',
+  async (ids: string[]) => {
+    await deleteSendTask(ids);
+    store.dispatch(sendJobSelectionSlice.actions.clearSelection());
+  }
+);
+```
+
+#### Redux Slice 配置
+```typescript
+// 在 createEntityListSlices 中自动处理
+builder.addCase(
+  deleteThunk.fulfilled,
+  (state, action: PayloadAction<string[]>) => {
+    // 从数据列表中过滤掉已删除的任务
+    state.data = state.data.filter(
+      (item) => !action.payload.includes(item.task_id)
+    );
+  }
+);
+```
+
+### 5.3 API 层(已存在,无需修改)
+
+```typescript
+// src/API/output/sendJobActions.ts
+export const deleteSendTask = async (
+  taskIds: string[]
+): Promise<BaseResponse> => {
+  try {
+    const response = await axiosInstance.delete<BaseResponse>(
+      '/auth/scp/task',
+      {
+        data: taskIds,  // 传递任务 ID 数组
+      }
+    );
+
+    if (response.data.code !== '0x000000') {
+      throw new Error(`删除发送任务失败: ${response.data.description}`);
+    }
+
+    return response.data;
+  } catch (error) {
+    console.error('Error deleting send task:', error);
+    throw error;
+  }
+};
+```
+
+## 6. 功能执行流程
+
+### 起点:用户操作
+
+```
+1. 用户在传输队列表格中勾选任务
+   ├─ 触发 checkbox onChange
+   └─ 更新 Redux: selectedIds = ['id1', 'id2', ...]
+
+2. 用户点击删除按钮
+   └─ 调用 handleDelete()
+
+3. 前置校验
+   ├─ 检查 selectedIds.length === 0
+   │  └─ 是:显示警告消息 "请先选择要删除的任务"
+   │  └─ 否:继续下一步
+   
+4. 显示确认对话框
+   ├─ 标题:确认删除
+   ├─ 内容:确定要删除选中的 N 个任务吗?此操作不可恢复。
+   └─ 按钮:[取消] [确定]
+   
+5. 用户选择
+   ├─ 点击取消:关闭对话框,不执行删除
+   └─ 点击确定:执行删除操作
+
+6. 执行删除
+   ├─ dispatch(deleteSendJobThunk(selectedIds))
+   ├─ 调用 API: deleteSendTask(taskIds)
+   ├─ 发送 HTTP DELETE 请求
+   └─ 等待响应
+
+7. 后端处理
+   ├─ 验证请求合法性
+   ├─ 从数据库删除任务记录
+   └─ 返回响应 {code: '0x000000'}
+
+8. Redux Store 更新
+   ├─ deleteSendJobThunk.fulfilled 触发
+   ├─ 从 entities.data 过滤掉已删除的任务
+   │  state.data = state.data.filter(item => !deletedIds.includes(item.task_id))
+   └─ 清空 selectedIds
+      store.dispatch(sendJobSelectionSlice.actions.clearSelection())
+
+9. UI 反馈
+   ├─ 表格重新渲染(已删除的行消失)
+   ├─ 选中状态清除
+   └─ 显示成功消息:"成功删除 N 个任务"
+```
+
+## 7. 设计决策
+
+### 7.1 为什么使用确认对话框?
+- **防止误操作**:删除是不可逆操作,需要二次确认
+- **用户体验**:明确告知将删除多少个任务
+- **行业最佳实践**:关键操作都应有确认机制
+
+### 7.2 为什么批量删除而不是逐个删除?
+- **性能考虑**:减少 HTTP 请求次数
+- **原子性**:批量操作更容易保证数据一致性
+- **后端设计**:API 已支持批量删除(接收数组参数)
+
+### 7.3 为什么在删除后清空 selectedIds?
+- **逻辑合理性**:已删除的任务不应继续保持选中状态
+- **防止二次操作**:避免用户再次点击删除时操作到不存在的任务
+- **参考重试功能**:deleteSendJobThunk 内部已实现此逻辑
+
+### 7.4 错误处理策略
+- **前置校验**:在操作前检查是否有选中任务
+- **API 错误**:catch 捕获并显示友好的错误消息
+- **日志记录**:console.error 记录详细错误信息便于调试
+
+## 8. 与重试功能的对比
+
+| 特性 | 重试功能 (handleRetry) | 删除功能 (handleDelete) |
+|------|----------------------|------------------------|
+| 操作方式 | 逐个任务重试 | 批量删除 |
+| API 调用 | `retrySendTask(taskId)` 循环调用 | `deleteSendTask(taskIds)` 一次性调用 |
+| 状态更新 | 更新任务状态为 SENDING | 从列表中移除任务 |
+| 确认对话框 | 不需要 | 需要(不可逆操作) |
+| 选中状态 | 不清空 | 清空 selectedIds |
+| 错误提示 | "重试任务失败,请稍后再试" | "删除任务失败,请稍后再试" |
+| 成功提示 | "成功提交 N 个重试任务" | "成功删除 N 个任务" |
+
+## 9. 测试要点
+
+### 9.1 功能测试
+- [ ] 未选中任务时点击删除,显示警告消息
+- [ ] 选中单个任务后点击删除,显示确认对话框
+- [ ] 选中多个任务后点击删除,显示正确的任务数量
+- [ ] 确认对话框中点击"取消",不执行删除
+- [ ] 确认对话框中点击"确定",成功删除任务
+- [ ] 删除成功后,表格中的任务消失
+- [ ] 删除成功后,selectedIds 被清空
+- [ ] API 失败时,显示错误消息
+
+### 9.2 边界测试
+- [ ] 删除全部任务后,列表为空
+- [ ] 网络异常时的错误处理
+- [ ] 并发删除(快速多次点击删除按钮)
+
+### 9.3 用户体验测试
+- [ ] 确认对话框的文案清晰易懂
+- [ ] 成功/失败消息显示时机正确
+- [ ] 删除按钮在没有选中任务时的反馈
+
+## 10. 后续优化建议
+
+### 10.1 功能增强
+1. **软删除**:实现撤销功能,允许用户恢复误删的任务
+2. **批量操作进度**:显示删除进度条(任务数量多时)
+3. **权限控制**:根据用户角色控制删除按钮的可见性
+4. **审计日志**:记录删除操作的用户和时间戳
+
+### 10.2 性能优化
+1. **防抖处理**:防止用户快速多次点击删除按钮
+2. **乐观更新**:先更新 UI,再调用 API(提升响应速度)
+3. **分页处理**:删除后自动刷新当前页数据
+
+### 10.3 用户体验优化
+1. **快捷键支持**:支持 Delete 键快速删除
+2. **批量操作提示**:删除数量超过阈值时额外提醒
+3. **加载状态**:删除过程中显示 loading 状态
+
+## 11. 总结
+
+### 实现要点
+1. **复用现有架构**:充分利用已有的 Redux 模板和 thunk 工厂
+2. **参考重试功能**:删除功能与重试功能结构相似,但有关键差异
+3. **用户体验优先**:添加确认对话框防止误操作
+4. **完善错误处理**:各个环节都有相应的错误捕获和用户提示
+
+### 修改文件
+- ✅ `src/pages/patient/components/OutputActionPanel.tsx`
+  - 导入 `Modal` 组件和 `deleteSendJobThunk`
+  - 新增 `handleDelete` 方法
+  - 绑定删除按钮的 `onClick` 事件
+
+### 无需修改(已存在)
+- `src/states/output/sendJob/slices/sendJobSlice.ts` - deleteSendJobThunk
+- `src/API/output/sendJobActions.ts` - deleteSendTask API
+- `src/domain/output/sendJob.ts` - SendJob 数据模型
+
+### 关键代码行数
+- 新增代码:约 30 行
+- 修改代码:1 行(添加 onClick)
+- 总变更:轻量级,高度可维护
+
+---
+
+**文档版本**:v1.0  
+**创建时间**:2025-10-11  
+**最后更新**:2025-10-11  
+**作者**:Cline

+ 30 - 2
src/pages/patient/components/OutputActionPanel.tsx

@@ -1,8 +1,8 @@
-import { Tooltip, Button, Space, message } from 'antd';
+import { Tooltip, Button, Space, message, Modal } from 'antd';
 import { FormattedMessage } from 'react-intl';
 import Icon from '@/components/Icon';
 import { useAppDispatch, useAppSelector } from '@/states/store';
-import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
+import { retrySendJobThunk, deleteSendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
 
 const OutputActionPanel: React.FC = () => {
   const dispatch = useAppDispatch();
@@ -30,6 +30,33 @@ const OutputActionPanel: React.FC = () => {
     }
   };
 
+  // 处理删除按钮点击
+  const handleDelete = async () => {
+    // 校验是否有选中的任务
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要删除的任务');
+      return;
+    }
+
+    // 显示确认对话框
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          // 调用删除 thunk
+          await dispatch(deleteSendJobThunk(selectedIds)).unwrap();
+          message.success(`成功删除 ${selectedIds.length} 个任务`);
+        } catch (error) {
+          console.error('删除任务失败:', error);
+          message.error('删除任务失败,请稍后再试');
+        }
+      },
+    });
+  };
+
   return (
   <Space>
     <Tooltip
@@ -79,6 +106,7 @@ const OutputActionPanel: React.FC = () => {
         }
         aria-label="删除"
         style={{ width: '1.5rem', height: '1.5rem' }}
+        onClick={handleDelete}
       />
     </Tooltip>
   </Space>