本文档描述了传输队列(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)的分层架构:
定义位置:src/domain/output/sendJob.ts
// 从领域层导入
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 层和状态层都应该从领域层导入此类型,确保类型定义的单一数据源。
interface SendJobFilter {
start_time?: string; // 开始时间 (RFC3339Nano 格式)
end_time?: string; // 结束时间 (RFC3339Nano 格式)
page: number; // 当前页码
page_size: number; // 每页数量
}
状态由 5 个 slice 组成:
{
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; // 错误信息
}
}
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',
}
}));
import { deleteSendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
// 删除单个或多个任务
dispatch(deleteSendJobThunk(['task_id_1', 'task_id_2']));
import { retrySendJobThunk } from '@/states/output/sendJob/slices/sendJobSlice';
// 重试失败的任务
dispatch(retrySendJobThunk({
taskId: 'task_id',
instanceUid: 'sop_instance_uid' // 需要图像实例 UID
}));
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 (
<div>
{/* 渲染列表 */}
{loading && <div>加载中...</div>}
{error && <div>错误: {error}</div>}
<ul>
{sendJobs.map(job => (
<li key={job.task_id}>
{job.patient_name} - {job.status}
{job.status === 'FAILED' && (
<button onClick={() => handleRetry(job.task_id, 'instance_uid')}>
重试
</button>
)}
</li>
))}
</ul>
{/* 分页 */}
<div>
<button onClick={() => handlePageChange(page - 1)} disabled={page === 1}>
上一页
</button>
<span>第 {page} 页,共 {Math.ceil(total / pageSize)} 页</span>
<button
onClick={() => handlePageChange(page + 1)}
disabled={page >= Math.ceil(total / pageSize)}
>
下一页
</button>
</div>
{/* 操作按钮 */}
<button onClick={handleRefresh}>刷新</button>
<button onClick={handleDelete} disabled={selectedIds.length === 0}>
删除选中
</button>
</div>
);
};
import { sendJobSelectionSlice } from '@/states/output/sendJob/slices/sendJobSlice';
// 选中任务
dispatch(sendJobSelectionSlice.actions.setSelectedIds(['task_id_1', 'task_id_2']));
// 清空选择
dispatch(sendJobSelectionSlice.actions.clearSelection());
import { sendJobPaginationSlice } from '@/states/output/sendJob/slices/sendJobSlice';
// 设置页码
dispatch(sendJobPaginationSlice.actions.setPage(2));
// 设置每页数量
dispatch(sendJobPaginationSlice.actions.setPageSize(20));
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());
useEffect(() => {
// 当任务数据变化时执行操作
console.log('当前任务列表:', sendJobs);
console.log('总数:', total);
}, [sendJobs, total]);
useEffect(() => {
// 当加载状态变化时
if (loading) {
console.log('正在加载...');
}
}, [loading]);
useEffect(() => {
// 当发生错误时
if (error) {
console.error('发生错误:', error);
// 可以显示错误提示
}
}, [error]);
// 在图像发送成功的处理中
const handleImageSendSuccess = () => {
// 刷新传输队列
dispatch(fetchSendQueueThunk({ page, pageSize, filters }));
};
如果需要实时更新任务状态,可以监听 MQTT 消息:
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]);
当前实现在前端进行分页:
// 在 fetchSendQueueThunk 中
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedData = response.data.slice(startIndex, endIndex);
如果后端支持分页,应该修改为:
const response = await getSendQueue({
start_time: filters.start_time,
end_time: filters.end_time,
page, // 传递页码
page_size: pageSize, // 传递每页数量
});
重试任务时需要提供图像实例 UID,这个信息可能需要从:
时间筛选使用 RFC3339Nano 格式:
// 正确格式
'2025-10-10T00:00:00.000+08:00'
'2025-10-10T23:59:59.999+08:00'
使用 Redux DevTools:
// 在浏览器中查看
window.__REDUX_DEVTOOLS_EXTENSION__
Thunks 中已包含日志:
console.log('重试发送任务,task_id:', taskId);
console.log('重试发送任务 fulfilled,result:', result);
检查浏览器 Network 标签页,查看 API 请求和响应。
WorklistTable 组件适配
OutputOperationPanel 组件集成
实时状态更新
错误处理优化