|
@@ -1,5 +1,5 @@
|
|
-import React, { useEffect } from 'react';
|
|
|
|
-import { Layout, Button, Checkbox, Typography, List, Spin, Alert, message } from 'antd';
|
|
|
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
|
+import { Layout, Button, Checkbox, Typography, List, Spin, Alert, message, Modal, Progress } from 'antd';
|
|
import { ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
import { ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { switchToOperationPanel } from '@/states/patient/worklist/slices/historyPanelSwitchSlice';
|
|
import { switchToOperationPanel } from '@/states/patient/worklist/slices/historyPanelSwitchSlice';
|
|
@@ -11,6 +11,8 @@ import {
|
|
} from '@/states/output/pacsNode/pacsNodeSlice';
|
|
} from '@/states/output/pacsNode/pacsNodeSlice';
|
|
import { RootState, AppDispatch } from '@/states/store';
|
|
import { RootState, AppDispatch } from '@/states/store';
|
|
import Icon from '@/components/Icon';
|
|
import Icon from '@/components/Icon';
|
|
|
|
+import { sendStudyToPacs } from '@/API/imageActions';
|
|
|
|
+import store from '@/states/store';
|
|
|
|
|
|
const { Header, Content, Footer } = Layout;
|
|
const { Header, Content, Footer } = Layout;
|
|
const { Title } = Typography;
|
|
const { Title } = Typography;
|
|
@@ -23,6 +25,14 @@ const SendImagePage = () => {
|
|
(state: RootState) => state.pacsNode
|
|
(state: RootState) => state.pacsNode
|
|
);
|
|
);
|
|
|
|
|
|
|
|
+ // 发送进度状态
|
|
|
|
+ const [sendingProgress, setSendingProgress] = useState({
|
|
|
|
+ isSending: false,
|
|
|
|
+ total: 0,
|
|
|
|
+ completed: 0,
|
|
|
|
+ failed: 0,
|
|
|
|
+ });
|
|
|
|
+
|
|
// 组件挂载时获取PACS节点列表
|
|
// 组件挂载时获取PACS节点列表
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
dispatch(fetchPacsNodesThunk());
|
|
dispatch(fetchPacsNodesThunk());
|
|
@@ -52,16 +62,107 @@ const SendImagePage = () => {
|
|
dispatch(fetchPacsNodesThunk());
|
|
dispatch(fetchPacsNodesThunk());
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ // 执行发送逻辑
|
|
|
|
+ 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,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
// 发送图像
|
|
// 发送图像
|
|
const handleSend = () => {
|
|
const handleSend = () => {
|
|
|
|
+ // 验证节点选择
|
|
if (selectedNodeIds.length === 0) {
|
|
if (selectedNodeIds.length === 0) {
|
|
message.warning('请至少选择一个PACS节点');
|
|
message.warning('请至少选择一个PACS节点');
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- // TODO: 实现实际的发送图像功能
|
|
|
|
- message.info(`准备发送到 ${selectedNodeIds.length} 个节点`);
|
|
|
|
- console.log('Selected node IDs:', selectedNodeIds);
|
|
|
|
|
|
+ // 从 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);
|
|
|
|
+ },
|
|
|
|
+ });
|
|
};
|
|
};
|
|
|
|
|
|
// 是否全选
|
|
// 是否全选
|
|
@@ -106,6 +207,37 @@ const SendImagePage = () => {
|
|
<Content
|
|
<Content
|
|
style={{ padding: '16px', maxHeight: '100%', overflowY: 'auto' }}
|
|
style={{ padding: '16px', maxHeight: '100%', overflowY: 'auto' }}
|
|
>
|
|
>
|
|
|
|
+ {/* 发送进度 */}
|
|
|
|
+ {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>
|
|
|
|
+ )}
|
|
|
|
+
|
|
{/* 错误提示 */}
|
|
{/* 错误提示 */}
|
|
{error && (
|
|
{error && (
|
|
<Alert
|
|
<Alert
|
|
@@ -168,7 +300,7 @@ const SendImagePage = () => {
|
|
{node.name}
|
|
{node.name}
|
|
{node.is_default && (
|
|
{node.is_default && (
|
|
<span style={{ marginLeft: 8, color: '#1890ff', fontSize: '12px' }}>
|
|
<span style={{ marginLeft: 8, color: '#1890ff', fontSize: '12px' }}>
|
|
- [默认]
|
|
|
|
|
|
+ (默认节点)
|
|
</span>
|
|
</span>
|
|
)}
|
|
)}
|
|
{!node.is_enabled && (
|
|
{!node.is_enabled && (
|
|
@@ -213,9 +345,13 @@ const SendImagePage = () => {
|
|
}
|
|
}
|
|
style={{ width: '100%', maxWidth: '400px' }}
|
|
style={{ width: '100%', maxWidth: '400px' }}
|
|
onClick={handleSend}
|
|
onClick={handleSend}
|
|
- disabled={selectedNodeIds.length === 0 || loading}
|
|
|
|
|
|
+ disabled={selectedNodeIds.length === 0 || loading || sendingProgress.isSending}
|
|
|
|
+ loading={sendingProgress.isSending}
|
|
>
|
|
>
|
|
- 发送图像 ({selectedNodeIds.length})
|
|
|
|
|
|
+ {sendingProgress.isSending
|
|
|
|
+ ? `发送中... (${sendingProgress.completed + sendingProgress.failed}/${sendingProgress.total})`
|
|
|
|
+ : `发送图像 (${selectedNodeIds.length})`
|
|
|
|
+ }
|
|
</Button>
|
|
</Button>
|
|
</Footer>
|
|
</Footer>
|
|
</Layout>
|
|
</Layout>
|