# 传输队列状态管理实现文档 ## 概述 本文档描述了传输队列(Send Job Queue)功能的状态管理实现,包括 Redux 状态设计、API 集成和组件使用方法。 ## 📁 文件结构 ``` src/ ├── domain/output/ │ └── sendJob.ts # 领域模型:发送任务数据类型 ├── API/output/ │ ├── sendJobActions.ts # API 层:传输队列接口(导入领域模型) │ └── pacsNodeActions.ts # API 层:PACS 节点接口 ├── states/output/sendJob/ │ ├── types/ │ │ └── sendJobFilter.ts # 筛选条件类型(状态层特有) │ └── slices/ │ └── sendJobSlice.ts # Redux 状态切片(导入领域模型) ├── states/store.ts # Redux Store 配置 └── pages/patient/ └── OutputList.tsx # 传输列表页面 ``` ### 架构说明 本项目采用**领域驱动设计(DDD)**的分层架构: 1. **领域层(Domain)**:定义核心业务实体,独立于技术实现 2. **API 层**:负责与后端通信,使用领域模型 3. **状态层(State)**:负责状态管理,使用领域模型 4. **UI 层**:负责展示和交互 ## 🎯 核心功能 ### 1. 数据类型定义 #### SendJob(发送任务)- 领域模型 **定义位置**:`src/domain/output/sendJob.ts` ```typescript // 从领域层导入 import { SendJob } from '@/domain/output/sendJob'; interface SendJob { task_id: string; // 任务唯一标识 patient_name: string; // 患者姓名 patient_id: string; // 患者 ID priority: 'High' | 'Medium' | 'Low'; // 优先级 status: 'ARRIVED' | 'SENDING' | 'FAILED' | 'SUCCESS'; // 状态 retry_count?: number; // 重试次数 destination: string; // 目标 PACS 节点名称 } ``` > ⚠️ **重要**:SendJob 是领域模型,定义在 `src/domain/output/sendJob.ts`。API 层和状态层都应该从领域层导入此类型,确保类型定义的单一数据源。 #### SendJobFilter(筛选条件) ```typescript interface SendJobFilter { start_time?: string; // 开始时间 (RFC3339Nano 格式) end_time?: string; // 结束时间 (RFC3339Nano 格式) page: number; // 当前页码 page_size: number; // 每页数量 } ``` ### 2. Redux 状态结构 状态由 5 个 slice 组成: ```typescript { sendJobEntities: { data: SendJob[]; // 任务列表数据 total: number; // 总数量 }, sendJobFilters: { start_time?: string; end_time?: string; page: number; page_size: number; }, sendJobPagination: { page: number; pageSize: number; }, sendJobSelection: { selectedIds: string[]; // 选中的任务 ID 列表 }, sendJobUI: { loading: boolean; // 加载状态 error?: string | null; // 错误信息 } } ``` ### 3. 异步操作(Thunks) #### fetchSendQueueThunk - 获取发送队列 ```typescript import { fetchSendQueueThunk } from '@/states/output/sendJob/slices/sendJobSlice'; // 在组件中使用 dispatch(fetchSendQueueThunk({ page: 1, pageSize: 10, filters: { start_time: '2025-10-10T00:00:00.000+08:00', end_time: '2025-10-10T23:59:59.999+08:00', } })); ``` #### deleteSendJobThunk - 删除任务 ```typescript import { deleteSendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice'; // 删除单个或多个任务 dispatch(deleteSendJobThunk(['task_id_1', 'task_id_2'])); ``` #### retrySendJobThunk - 重试任务 ```typescript import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice'; // 重试失败的任务 dispatch(retrySendJobThunk({ taskId: 'task_id', instanceUid: 'sop_instance_uid' // 需要图像实例 UID })); ``` ## 💡 组件使用示例 ### 基础用法 ```typescript import React, { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '@/states/store'; import { fetchSendQueueThunk, deleteSendJobThunk, retrySendJobThunk, sendJobActions, } from '@/states/output/sendJob/slices/sendJobSlice'; const SendJobList: React.FC = () => { const dispatch = useAppDispatch(); // 获取状态 const { data: sendJobs, total } = useAppSelector(state => state.sendJobEntities); const { loading, error } = useAppSelector(state => state.sendJobUI); const filters = useAppSelector(state => state.sendJobFilters); const { page, pageSize } = useAppSelector(state => state.sendJobPagination); const { selectedIds } = useAppSelector(state => state.sendJobSelection); // 初始加载数据 useEffect(() => { dispatch(fetchSendQueueThunk({ page, pageSize, filters })); }, [dispatch, page, pageSize, filters]); // 刷新数据 const handleRefresh = () => { dispatch(fetchSendQueueThunk({ page, pageSize, filters })); }; // 删除选中的任务 const handleDelete = () => { if (selectedIds.length > 0) { dispatch(deleteSendJobThunk(selectedIds)); } }; // 重试任务 const handleRetry = (taskId: string, instanceUid: string) => { dispatch(retrySendJobThunk({ taskId, instanceUid })); }; // 设置时间范围筛选 const handleDateRangeChange = (startTime: string, endTime: string) => { dispatch(sendJobActions.setStartTime(startTime)); dispatch(sendJobActions.setEndTime(endTime)); }; // 切换页码 const handlePageChange = (newPage: number) => { dispatch(sendJobActions.setPage(newPage)); }; return (
{/* 渲染列表 */} {loading &&
加载中...
} {error &&
错误: {error}
} {/* 分页 */}
第 {page} 页,共 {Math.ceil(total / pageSize)} 页
{/* 操作按钮 */}
); }; ``` ## 🔧 高级用法 ### 1. 选择管理 ```typescript import { sendJobSelectionSlice } from '@/states/output/sendJob/slices/sendJobSlice'; // 选中任务 dispatch(sendJobSelectionSlice.actions.setSelectedIds(['task_id_1', 'task_id_2'])); // 清空选择 dispatch(sendJobSelectionSlice.actions.clearSelection()); ``` ### 2. 分页控制 ```typescript import { sendJobPaginationSlice } from '@/states/output/sendJob/slices/sendJobSlice'; // 设置页码 dispatch(sendJobPaginationSlice.actions.setPage(2)); // 设置每页数量 dispatch(sendJobPaginationSlice.actions.setPageSize(20)); ``` ### 3. 筛选条件 ```typescript import { sendJobFiltersSlice } from '@/states/output/sendJob/slices/sendJobSlice'; // 设置完整筛选条件 dispatch(sendJobFiltersSlice.actions.setFilters({ start_time: '2025-10-10T00:00:00.000+08:00', end_time: '2025-10-10T23:59:59.999+08:00', page: 1, page_size: 10, })); // 重置筛选条件 dispatch(sendJobFiltersSlice.actions.resetFilters()); ``` ## 📊 状态监听 ### 监听任务状态变化 ```typescript useEffect(() => { // 当任务数据变化时执行操作 console.log('当前任务列表:', sendJobs); console.log('总数:', total); }, [sendJobs, total]); useEffect(() => { // 当加载状态变化时 if (loading) { console.log('正在加载...'); } }, [loading]); useEffect(() => { // 当发生错误时 if (error) { console.error('发生错误:', error); // 可以显示错误提示 } }, [error]); ``` ## 🔄 与其他功能集成 ### 1. 图像发送成功后自动刷新 ```typescript // 在图像发送成功的处理中 const handleImageSendSuccess = () => { // 刷新传输队列 dispatch(fetchSendQueueThunk({ page, pageSize, filters })); }; ``` ### 2. MQTT 消息监听 如果需要实时更新任务状态,可以监听 MQTT 消息: ```typescript useEffect(() => { // 订阅 MQTT 消息 const handleMqttMessage = (message: any) => { if (message.topic === 'send_job_status_update') { // 刷新列表 dispatch(fetchSendQueueThunk({ page, pageSize, filters })); } }; // 注册监听器 mqttService.subscribe('send_job_status_update', handleMqttMessage); return () => { // 清理监听器 mqttService.unsubscribe('send_job_status_update', handleMqttMessage); }; }, [dispatch, page, pageSize, filters]); ``` ## ⚠️ 注意事项 ### 1. 前端分页 vs 后端分页 当前实现在**前端进行分页**: ```typescript // 在 fetchSendQueueThunk 中 const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedData = response.data.slice(startIndex, endIndex); ``` 如果后端支持分页,应该修改为: ```typescript const response = await getSendQueue({ start_time: filters.start_time, end_time: filters.end_time, page, // 传递页码 page_size: pageSize, // 传递每页数量 }); ``` ### 2. 重试操作需要 instance_uid 重试任务时需要提供图像实例 UID,这个信息可能需要从: - 任务详情中获取 - 与 Study/Series/Image 数据关联 - 后端 API 返回任务时包含 ### 3. 时间格式 时间筛选使用 RFC3339Nano 格式: ```typescript // 正确格式 '2025-10-10T00:00:00.000+08:00' '2025-10-10T23:59:59.999+08:00' ``` ## 🐛 调试技巧 ### 1. 查看 Redux 状态 使用 Redux DevTools: ```typescript // 在浏览器中查看 window.__REDUX_DEVTOOLS_EXTENSION__ ``` ### 2. 控制台日志 Thunks 中已包含日志: ```typescript console.log('重试发送任务,task_id:', taskId); console.log('重试发送任务 fulfilled,result:', result); ``` ### 3. 网络请求 检查浏览器 Network 标签页,查看 API 请求和响应。 ## 📝 待完成事项 1. **WorklistTable 组件适配** - 当前 WorklistTable 组件需要调整以支持传输队列数据 - 需要创建专门的 SendJobTable 组件,或修改 WorklistTable 支持多种数据类型 2. **OutputOperationPanel 组件集成** - 添加重试按钮 - 添加删除按钮 - 添加时间范围筛选器 3. **实时状态更新** - 集成 MQTT 监听传输任务状态变化 - 自动刷新列表 4. **错误处理优化** - 显示友好的错误提示 - 添加重试机制 ## 📚 相关文档 - [DR.md 第 15 节 - 传输队列接口定义](../DR.md#15-传输队列) - [API 实现文档](../../src/API/output/sendJobActions.ts) - [Redux Toolkit 官方文档](https://redux-toolkit.js.org/) ## 🎓 学习资源 - Redux 状态管理模式 - TypeScript 类型系统 - React Hooks 最佳实践 - 分页和筛选实现模式