SiteInfo.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /**
  2. * 站点信息配置页面
  3. * 用于配置和管理 DICOM 站点基本信息
  4. */
  5. import React, { useEffect, useState } from 'react';
  6. import {
  7. Form,
  8. Input,
  9. InputNumber,
  10. Button,
  11. Typography,
  12. Space,
  13. message,
  14. Spin,
  15. Modal,
  16. } from 'antd';
  17. import { CopyOutlined } from '@ant-design/icons';
  18. import { SPACING } from '../../constants';
  19. import { RootState, useAppSelector } from '@/states/store';
  20. const { Title } = Typography;
  21. const { TextArea } = Input;
  22. // 站点信息数据类型
  23. interface SiteInfoData {
  24. siteName: string;
  25. aeTitle: string;
  26. port: number;
  27. institutionName?: string;
  28. siteCode?: string;
  29. description?: string;
  30. }
  31. /**
  32. * 站点信息组件
  33. *
  34. * 布局结构:
  35. * - Layout Type: Vertical Stack Layout
  36. * - Form Layout: Horizontal
  37. * - Label Width: 120px
  38. */
  39. const SiteInfo: React.FC = () => {
  40. const [form] = Form.useForm<SiteInfoData>();
  41. const [loading, setLoading] = useState(false);
  42. const [saving, setSaving] = useState(false);
  43. const [initialValues, setInitialValues] = useState<SiteInfoData | null>(null);
  44. const sn = useAppSelector((state: RootState) => state.product.sn);
  45. const { Search } = Input;
  46. // 模拟加载数据
  47. useEffect(() => {
  48. loadSiteInfo();
  49. }, []);
  50. // 加载站点信息
  51. const loadSiteInfo = async () => {
  52. setLoading(true);
  53. try {
  54. // TODO: 替换为实际的 API 调用
  55. // const data = await getSiteInfo();
  56. // 模拟数据
  57. const mockData: SiteInfoData = {
  58. siteName: 'Medical Imaging Center',
  59. aeTitle: 'PACS_SERVER',
  60. port: 104,
  61. institutionName: 'General Hospital',
  62. siteCode: 'SITE001',
  63. description: 'Main PACS server for medical imaging',
  64. };
  65. setInitialValues(mockData);
  66. form.setFieldsValue(mockData);
  67. } catch (error) {
  68. message.error('加载站点信息失败');
  69. console.error('Failed to load site info:', error);
  70. } finally {
  71. setLoading(false);
  72. }
  73. };
  74. // 保存站点信息
  75. const handleSave = async () => {
  76. try {
  77. // 触发表单验证
  78. const values = await form.validateFields();
  79. setSaving(true);
  80. // TODO: 替换为实际的 API 调用
  81. // await updateSiteInfo(values);
  82. // 模拟保存延迟
  83. await new Promise((resolve) => setTimeout(resolve, 1000));
  84. message.success('保存成功');
  85. setInitialValues(values);
  86. // 提示用户可能需要重启服务
  87. Modal.info({
  88. title: '提示',
  89. content: '站点配置已保存,部分更改可能需要重启服务后生效。',
  90. });
  91. } catch (error) {
  92. if (error instanceof Error) {
  93. message.error(`保存失败: ${error.message}`);
  94. } else {
  95. message.error('表单验证失败,请检查输入');
  96. }
  97. console.error('Failed to save site info:', error);
  98. } finally {
  99. setSaving(false);
  100. }
  101. };
  102. // 重置表单
  103. const handleReset = (): void => {
  104. if (initialValues) {
  105. // 检查是否有未保存的更改
  106. const currentValues = form.getFieldsValue();
  107. const hasChanges =
  108. JSON.stringify(currentValues) !== JSON.stringify(initialValues);
  109. if (hasChanges) {
  110. Modal.confirm({
  111. title: '确认重置',
  112. content: '您有未保存的更改,确定要重置表单吗?',
  113. onOk: () => {
  114. form.setFieldsValue(initialValues);
  115. message.info('表单已重置');
  116. },
  117. });
  118. } else {
  119. form.setFieldsValue(initialValues);
  120. message.info('表单已重置');
  121. }
  122. }
  123. };
  124. // AE Title 输入转大写
  125. const handleAETitleChange = (
  126. e: React.ChangeEvent<HTMLInputElement>
  127. ): void => {
  128. const value = e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, '');
  129. form.setFieldsValue({ aeTitle: value });
  130. };
  131. // 复制SN号码
  132. const handleCopySN = async (): Promise<void> => {
  133. if (sn) {
  134. try {
  135. await navigator.clipboard.writeText(sn);
  136. message.success('SN码已复制到剪贴板');
  137. } catch (error) {
  138. message.error('复制失败,请手动复制');
  139. console.error('Failed to copy SN:', error);
  140. }
  141. } else {
  142. message.warning('SN码为空');
  143. }
  144. };
  145. if (loading) {
  146. return (
  147. <div
  148. style={{
  149. padding: SPACING.LG,
  150. display: 'flex',
  151. justifyContent: 'center',
  152. alignItems: 'center',
  153. minHeight: 400,
  154. }}
  155. >
  156. <Spin size="large" tip="加载中..." />
  157. </div>
  158. );
  159. }
  160. return (
  161. <div style={{ padding: SPACING.LG }}>
  162. <Title level={3}>站点信息</Title>
  163. <Form
  164. form={form}
  165. layout="horizontal"
  166. labelCol={{ span: 6 }}
  167. wrapperCol={{ span: 14 }}
  168. size="large"
  169. style={{ marginTop: SPACING.LG }}
  170. >
  171. {/* SN码 */}
  172. <Form.Item label="SN码">
  173. <Space.Compact style={{ width: '100%' }}>
  174. <Input style={{ width: '90%' }} disabled value={sn || ''} />
  175. <Button
  176. style={{ width: '10%', height: 'auto' }}
  177. icon={<CopyOutlined />}
  178. onClick={handleCopySN}
  179. title="复制SN码"
  180. />
  181. </Space.Compact>
  182. </Form.Item>
  183. {/* 站点名称 */}
  184. <Form.Item
  185. label="站点名称"
  186. name="siteName"
  187. rules={[
  188. { required: true, message: '请输入站点名称' },
  189. { max: 50, message: '站点名称最多50个字符' },
  190. ]}
  191. tooltip="用于标识此 DICOM 站点的名称"
  192. >
  193. <Input placeholder="请输入站点名称" allowClear />
  194. </Form.Item>
  195. {/* AE Title */}
  196. <Form.Item
  197. label="AE Title"
  198. name="aeTitle"
  199. rules={[
  200. { required: true, message: '请输入 AE Title' },
  201. { max: 16, message: 'AE Title 最多16个字符' },
  202. {
  203. pattern: /^[A-Z0-9_]+$/,
  204. message: 'AE Title 只能包含大写字母、数字和下划线',
  205. },
  206. ]}
  207. tooltip="DICOM 应用实体标题,必须唯一"
  208. >
  209. <Input
  210. placeholder="请输入 AE Title"
  211. onChange={handleAETitleChange}
  212. maxLength={16}
  213. allowClear
  214. />
  215. </Form.Item>
  216. {/* 端口 */}
  217. <Form.Item
  218. label="端口"
  219. name="port"
  220. rules={[
  221. { required: true, message: '请输入端口号' },
  222. {
  223. type: 'number',
  224. min: 1,
  225. max: 65535,
  226. message: '端口号范围:1-65535',
  227. },
  228. ]}
  229. tooltip="DICOM 通信端口,默认为 104"
  230. >
  231. <InputNumber
  232. placeholder="请输入端口号"
  233. style={{ width: '100%' }}
  234. min={1}
  235. max={65535}
  236. />
  237. </Form.Item>
  238. {/* 机构名称 */}
  239. <Form.Item
  240. label="机构名称"
  241. name="institutionName"
  242. rules={[{ max: 100, message: '机构名称最多100个字符' }]}
  243. >
  244. <Input placeholder="请输入机构名称(可选)" allowClear />
  245. </Form.Item>
  246. {/* 站点编码 */}
  247. <Form.Item
  248. label="站点编码"
  249. name="siteCode"
  250. rules={[{ max: 20, message: '站点编码最多20个字符' }]}
  251. tooltip="用于系统内部识别的唯一编码"
  252. >
  253. <Input placeholder="请输入站点编码(可选)" allowClear />
  254. </Form.Item>
  255. {/* 描述 */}
  256. <Form.Item
  257. label="描述"
  258. name="description"
  259. rules={[{ max: 500, message: '描述最多500个字符' }]}
  260. >
  261. <TextArea
  262. placeholder="请输入站点描述(可选)"
  263. rows={4}
  264. showCount
  265. maxLength={500}
  266. allowClear
  267. />
  268. </Form.Item>
  269. {/* 操作按钮 */}
  270. <Form.Item wrapperCol={{ offset: 6, span: 14 }}>
  271. <Space>
  272. <Button
  273. type="primary"
  274. onClick={handleSave}
  275. loading={saving}
  276. disabled={loading}
  277. >
  278. 保存
  279. </Button>
  280. <Button onClick={handleReset} disabled={loading || saving}>
  281. 重置
  282. </Button>
  283. </Space>
  284. </Form.Item>
  285. </Form>
  286. </div>
  287. );
  288. };
  289. export default SiteInfo;