SendPanel.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import React, { useEffect } from 'react';
  2. import { Layout, Button, Checkbox, Typography, List, Spin, Alert, message } from 'antd';
  3. import { ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
  4. import { useDispatch, useSelector } from 'react-redux';
  5. import { switchToOperationPanel } from '@/states/patient/worklist/slices/historyPanelSwitchSlice';
  6. import {
  7. fetchPacsNodesThunk,
  8. toggleNodeSelection,
  9. selectAllNodes,
  10. deselectAllNodes,
  11. } from '@/states/output/pacsNode/pacsNodeSlice';
  12. import { RootState, AppDispatch } from '@/states/store';
  13. import Icon from '@/components/Icon';
  14. const { Header, Content, Footer } = Layout;
  15. const { Title } = Typography;
  16. const SendImagePage = () => {
  17. const dispatch = useDispatch<AppDispatch>();
  18. // 从Redux获取PACS节点状态
  19. const { nodes, loading, error, selectedNodeIds } = useSelector(
  20. (state: RootState) => state.pacsNode
  21. );
  22. // 组件挂载时获取PACS节点列表
  23. useEffect(() => {
  24. dispatch(fetchPacsNodesThunk());
  25. }, [dispatch]);
  26. // 返回操作面板
  27. const handleReturn = () => {
  28. dispatch(switchToOperationPanel());
  29. };
  30. // 切换节点选中状态
  31. const handleNodeToggle = (nodeId: number) => {
  32. dispatch(toggleNodeSelection(nodeId));
  33. };
  34. // 全选/取消全选
  35. const handleSelectAll = (checked: boolean) => {
  36. if (checked) {
  37. dispatch(selectAllNodes());
  38. } else {
  39. dispatch(deselectAllNodes());
  40. }
  41. };
  42. // 重新加载节点列表
  43. const handleReload = () => {
  44. dispatch(fetchPacsNodesThunk());
  45. };
  46. // 发送图像
  47. const handleSend = () => {
  48. if (selectedNodeIds.length === 0) {
  49. message.warning('请至少选择一个PACS节点');
  50. return;
  51. }
  52. // TODO: 实现实际的发送图像功能
  53. message.info(`准备发送到 ${selectedNodeIds.length} 个节点`);
  54. console.log('Selected node IDs:', selectedNodeIds);
  55. };
  56. // 是否全选
  57. const isAllSelected = nodes.length > 0 &&
  58. nodes.filter(node => node.is_enabled).every(node =>
  59. selectedNodeIds.includes(node.id)
  60. );
  61. // 是否部分选中
  62. const isIndeterminate = selectedNodeIds.length > 0 && !isAllSelected;
  63. return (
  64. <Layout className="h-full">
  65. {/* 顶部导航栏 */}
  66. <Header
  67. style={{
  68. display: 'flex',
  69. alignItems: 'center',
  70. padding: '0 16px',
  71. justifyContent: 'space-between',
  72. }}
  73. >
  74. <div style={{ display: 'flex', alignItems: 'center' }}>
  75. <Button
  76. type="text"
  77. icon={<ArrowLeftOutlined />}
  78. onClick={handleReturn}
  79. />
  80. <Title level={5} style={{ margin: 0, lineHeight: '48px' }}>
  81. 发送图像
  82. </Title>
  83. </div>
  84. <Button
  85. type="text"
  86. icon={<ReloadOutlined />}
  87. onClick={handleReload}
  88. loading={loading}
  89. />
  90. </Header>
  91. {/* 主体内容 */}
  92. <Content
  93. style={{ padding: '16px', maxHeight: '100%', overflowY: 'auto' }}
  94. >
  95. {/* 错误提示 */}
  96. {error && (
  97. <Alert
  98. message="加载失败"
  99. description={error}
  100. type="error"
  101. showIcon
  102. closable
  103. style={{ marginBottom: 16 }}
  104. action={
  105. <Button size="small" onClick={handleReload}>
  106. 重试
  107. </Button>
  108. }
  109. />
  110. )}
  111. {/* 加载状态 */}
  112. {loading && (
  113. <div style={{ textAlign: 'center', padding: '40px 0' }}>
  114. <Spin size="large" tip="正在加载PACS节点..." />
  115. </div>
  116. )}
  117. {/* 节点列表 */}
  118. {!loading && !error && (
  119. <>
  120. {nodes.length === 0 ? (
  121. <Alert
  122. message="暂无PACS节点"
  123. description="请先在系统管理中配置PACS节点"
  124. type="info"
  125. showIcon
  126. />
  127. ) : (
  128. <>
  129. {/* 全选复选框 */}
  130. <div style={{ marginBottom: 16, borderBottom: '1px solid #f0f0f0', paddingBottom: 8 }}>
  131. <Checkbox
  132. indeterminate={isIndeterminate}
  133. checked={isAllSelected}
  134. onChange={(e) => handleSelectAll(e.target.checked)}
  135. >
  136. 全选 ({selectedNodeIds.length}/{nodes.filter(n => n.is_enabled).length})
  137. </Checkbox>
  138. </div>
  139. {/* PACS节点列表 */}
  140. <List
  141. dataSource={nodes}
  142. renderItem={(node) => (
  143. <List.Item>
  144. <Checkbox
  145. checked={selectedNodeIds.includes(node.id)}
  146. onChange={() => handleNodeToggle(node.id)}
  147. disabled={!node.is_enabled}
  148. >
  149. <div>
  150. <div style={{ fontWeight: 500 }}>
  151. {node.name}
  152. {node.is_default && (
  153. <span style={{ marginLeft: 8, color: '#1890ff', fontSize: '12px' }}>
  154. [默认]
  155. </span>
  156. )}
  157. {!node.is_enabled && (
  158. <span style={{ marginLeft: 8, color: '#999', fontSize: '12px' }}>
  159. [已禁用]
  160. </span>
  161. )}
  162. </div>
  163. <div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
  164. {node.address}:{node.port} | AET: {node.aet}
  165. </div>
  166. </div>
  167. </Checkbox>
  168. </List.Item>
  169. )}
  170. />
  171. </>
  172. )}
  173. </>
  174. )}
  175. </Content>
  176. {/* 底部按钮 */}
  177. <Footer
  178. style={{
  179. padding: '16px',
  180. display: 'flex',
  181. justifyContent: 'center',
  182. }}
  183. >
  184. <Button
  185. type="primary"
  186. icon={
  187. <Icon
  188. module="module-output"
  189. name="Send"
  190. userId="base"
  191. theme="default"
  192. size="2x"
  193. state="normal"
  194. />
  195. }
  196. style={{ width: '100%', maxWidth: '400px' }}
  197. onClick={handleSend}
  198. disabled={selectedNodeIds.length === 0 || loading}
  199. >
  200. 发送图像 ({selectedNodeIds.length})
  201. </Button>
  202. </Footer>
  203. </Layout>
  204. );
  205. };
  206. export default SendImagePage;