ContentAreaLarge.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. import {
  2. Row,
  3. Col,
  4. Select,
  5. InputNumber,
  6. Button,
  7. Switch,
  8. Divider,
  9. Tooltip,
  10. message,
  11. Flex,
  12. Modal,
  13. } from 'antd';
  14. import ErrorMessage from '@/components/ErrorMessage';
  15. import { patientSizes, PatientSize } from '../../states/patientSize';
  16. import { WorkstationTypeLabels, WorkstationType } from '../../states/workstation';
  17. import { FormattedMessage, useIntl } from 'react-intl';
  18. import { useSelector, useDispatch, useStore } from 'react-redux';
  19. import { deleteBodyPosition } from '../../API/patient/viewActions';
  20. import { copyPositionThunk } from '../../states/exam/examWorksCacheSlice';
  21. import {
  22. removeBodyPositionBySopInstanceUid,
  23. setByIndex,
  24. judgeImageThunk,
  25. } from '../../states/exam/bodyPositionListSlice';
  26. import { RootState } from '../../states/store';
  27. import {
  28. setAprConfig,
  29. setBodysize,
  30. setWorkstation,
  31. setThickness,
  32. setIsAECEnabled,
  33. updateAprParamsThunk,
  34. } from '../../states/exam/aprSlice';
  35. import { workstationIdFromWorkstation } from '../../states/workstation';
  36. import { AprParamsUpdateRequest } from '../../API/exam/APRActions';
  37. import BodyPositionList from './components/BodyPositionList';
  38. import BodyPositionDetail from './components/BodyPositionDetail';
  39. import { AppDispatch } from '@/states/store';
  40. import { useRef } from 'react';
  41. import Icon from '@/components/Icon';
  42. import ParaSettingCoordinator from '@/domain/exam/paraSettingCoordinator';
  43. import { resetDevices } from '@/states/device/deviceSlice';
  44. import { openCamera, closeCamera } from '@/states/exam/cameraSlice';
  45. import CameraModal from '@/components/CameraModal';
  46. const ContentAreaLarge = () => {
  47. const intl = useIntl();
  48. const dispatch = useDispatch<AppDispatch>();
  49. const isResetting = useSelector(
  50. (state: RootState) => state.device.status === 'loading'
  51. );
  52. const store = useStore<RootState>();
  53. const aprConfig = useSelector((state: RootState) => state.apr.aprConfig);
  54. const bodysize = useSelector((state: RootState) => state.apr.bodysize);
  55. const workstation = useSelector((state: RootState) => state.apr.workstation);
  56. const thickness = useSelector((state: RootState) => state.apr.thickness);
  57. const isAECEnabled = useSelector(
  58. (state: RootState) => state.apr.isAECEnabled
  59. );
  60. const currentExposureMode = useSelector(
  61. (state: RootState) => state.apr.currentExposureMode
  62. );
  63. const productName = useSelector(
  64. (state: RootState) => state.product.productName
  65. );
  66. const handleBodysizeChange = (key: string) => {
  67. const value = patientSizes[key as PatientSize]; // 获取对应的显示文本
  68. console.log('体型 key:', key); // 例如: 'small'
  69. console.log('体型 value:', value); // 例如: 'Small'
  70. dispatch(setBodysize(value));
  71. };
  72. const handleWorkstationChange = (value: string) => {
  73. dispatch(setWorkstation(value));
  74. };
  75. const handleThicknessChange = (value: number | null) => {
  76. if (value !== null) {
  77. dispatch(setThickness(value));
  78. }
  79. };
  80. const handleAECChange = (checked: boolean) => {
  81. dispatch(setIsAECEnabled(checked));
  82. };
  83. const handleExposureModeChange = (value: string) => {
  84. ParaSettingCoordinator.setExposureMode(value);
  85. };
  86. const handleResetParameters = async () => {
  87. try {
  88. await dispatch(resetDevices());
  89. message.success(`重置高压发生器成功`);
  90. } catch (error) {
  91. console.error('Error resetting devices:', error);
  92. message.error(`重置高压发生器失败 ${error}`);
  93. }
  94. };
  95. /**
  96. * 处理打开摄像头按钮点击事件
  97. */
  98. const handleOpenCamera = () => {
  99. // 获取当前选中体位的 study_id
  100. const currentStudyId = selectedBodyPosition?.study_id;
  101. if (!currentStudyId) {
  102. message.warning('请先选择一个体位');
  103. return;
  104. }
  105. console.log('[handleOpenCamera] 打开摄像头,Study ID:', currentStudyId);
  106. // dispatch openCamera action,打开摄像头
  107. dispatch(openCamera(currentStudyId));
  108. };
  109. /**
  110. * 处理拒绝图像按钮点击事件
  111. */
  112. const handleReject = async () => {
  113. if (!selectedBodyPosition) {
  114. message.warning('请先选择一个体位');
  115. return;
  116. }
  117. Modal.confirm({
  118. title: '确认拒绝',
  119. content: `确定要拒绝选中的图像吗?`,
  120. okText: '确认拒绝',
  121. cancelText: '取消',
  122. okButtonProps: {
  123. danger: true,
  124. 'data-testid': 'modal-confirm-delete'
  125. },
  126. cancelButtonProps: {
  127. 'data-testid': 'modal-cancel-delete'
  128. },
  129. centered: true,
  130. onOk: async () => {
  131. try {
  132. await dispatch(
  133. judgeImageThunk({
  134. sopInstanceUid: selectedBodyPosition.sop_instance_uid,
  135. accept: false,
  136. })
  137. ).unwrap();
  138. message.success('图像已拒绝');
  139. } catch (err) {
  140. console.error('拒绝图像失败:', err);
  141. message.error('拒绝图像失败');
  142. }
  143. },
  144. });
  145. };
  146. /**
  147. * 处理恢复图像按钮点击事件
  148. */
  149. const handleRestore = async () => {
  150. if (!selectedBodyPosition) {
  151. message.warning('请先选择一个体位');
  152. return;
  153. }
  154. try {
  155. await dispatch(
  156. judgeImageThunk({
  157. sopInstanceUid: selectedBodyPosition.sop_instance_uid,
  158. accept: true,
  159. })
  160. ).unwrap();
  161. message.success('图像已恢复');
  162. } catch (err) {
  163. console.error('恢复图像失败:', err);
  164. message.error('恢复图像失败');
  165. }
  166. };
  167. /**
  168. * 处理保存参数按钮点击事件
  169. */
  170. const handleSaveParams = async () => {
  171. if (!selectedBodyPosition) {
  172. message.warning('请先选择一个体位');
  173. return;
  174. }
  175. try {
  176. // 获取工作站ID - 将字符串转换为WorkstationType枚举
  177. const workstationType = workstation as WorkstationType;
  178. const workStationId = workstationIdFromWorkstation(
  179. workstationType,
  180. productName
  181. );
  182. // 构建请求参数
  183. const request: AprParamsUpdateRequest = {
  184. work_station_id: workStationId,
  185. patient_size: bodysize,
  186. kV: aprConfig.kV,
  187. mA: aprConfig.mA,
  188. ms: aprConfig.ms,
  189. mAs: aprConfig.mAs,
  190. };
  191. // 调用 updateAprParamsThunk
  192. await dispatch(
  193. updateAprParamsThunk({
  194. id: selectedBodyPosition.view_id,
  195. request,
  196. })
  197. ).unwrap();
  198. message.success('参数保存成功');
  199. } catch (err) {
  200. console.error('保存参数失败:', err);
  201. message.error('保存参数失败');
  202. }
  203. };
  204. // 1. 正常在顶层用 useSelector 订阅
  205. const selectedBodyPosition = useSelector(
  206. (state: RootState) => state.bodyPositionList.selectedBodyPosition
  207. );
  208. // 2. 用 ref 保存最新值(每次渲染都会更新)
  209. const positionRef = useRef(selectedBodyPosition);
  210. positionRef.current = selectedBodyPosition;
  211. // 3. 订阅 camera 状态
  212. const cameraIsOpen = useSelector((state: RootState) => state.camera.isOpen);
  213. const cameraStudyId = useSelector(
  214. (state: RootState) => state.camera.currentStudyId
  215. );
  216. // 4. 计算按钮可见性
  217. const loading = useSelector(
  218. (state: RootState) => state.bodyPositionList.loading
  219. );
  220. const exposeStatus = selectedBodyPosition?.dview.expose_status;
  221. const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
  222. const isExposed = exposeStatus === 'Exposed';
  223. const isUnexposed = exposeStatus === 'Unexposed';
  224. // 按钮可见性逻辑
  225. const showRejectButton = isExposed && judgedStatus !== 'Reject';
  226. const showRestoreButton = isExposed && judgedStatus === 'Reject';
  227. const showSaveParamsButton = isUnexposed;
  228. const showDeleteButton = isUnexposed;
  229. return (
  230. <Row className="w-full p-1" style={{ height: '100%', display: 'flex' }}>
  231. <Col span={20} style={{ display: 'flex', flexDirection: 'column' }} className='h-full'>
  232. <Row gutter={16} style={{ flex: 1, minHeight: 0, display: 'flex' }} className='h-full'>
  233. <Col span={4} className="flex flex-col h-full" >
  234. <BodyPositionList layout="vertical"></BodyPositionList>
  235. </Col>
  236. <Col span={20}>
  237. <BodyPositionDetail />
  238. </Col>
  239. </Row>
  240. </Col>
  241. <Col
  242. span={4}
  243. style={{ height: '100%', overflowY: 'auto', flexShrink: 0 }}
  244. >
  245. <Row gutter={16} align="middle">
  246. <Col span={productName === 'DROS' ? 9 : 12}>
  247. <Select
  248. placeholder={<FormattedMessage id="exam.bodysize.placeholder" />}
  249. style={{ width: '100%', marginBottom: 8 }}
  250. value={!!bodysize ? intl.formatMessage({ id: bodysize }) : undefined}
  251. onChange={handleBodysizeChange}
  252. >
  253. {Object.entries(patientSizes).map(
  254. ([key, value]: [string, string]) => (
  255. <Select.Option key={key} value={key}>
  256. <FormattedMessage id={value} />
  257. </Select.Option>
  258. )
  259. )}
  260. </Select>
  261. </Col>
  262. <Col span={productName === 'DROS' ? 9 : 12}>
  263. <label
  264. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  265. >
  266. <FormattedMessage id="exam.thickness.label" />
  267. </label>
  268. <InputNumber
  269. placeholder={intl.formatMessage({ id: 'exam.thickness.placeholder' })}
  270. style={{ width: '100%', marginBottom: 8 }}
  271. value={thickness || undefined}
  272. min={1}
  273. max={50}
  274. step={1}
  275. onChange={handleThicknessChange}
  276. onStep={(value, info) => {
  277. if (info.type === 'up') {
  278. ParaSettingCoordinator.increaseThickness();
  279. } else {
  280. ParaSettingCoordinator.decreaseThickness();
  281. }
  282. }}
  283. />
  284. </Col>
  285. {productName === 'DROS' && (
  286. <Col span={15}>
  287. <Select
  288. placeholder="选择工作位"
  289. style={{ width: '100%', marginBottom: 8 }}
  290. value={workstation}
  291. onChange={handleWorkstationChange}
  292. >
  293. {Object.entries(WorkstationTypeLabels).map(
  294. ([key, value]: [string, string]) => (
  295. <Select.Option key={key} value={value}>
  296. <FormattedMessage
  297. id={`workstation.${key.toLowerCase()}`}
  298. />
  299. </Select.Option>
  300. )
  301. )}
  302. </Select>
  303. </Col>
  304. )}
  305. </Row>
  306. <div>
  307. <div>
  308. <label
  309. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  310. >
  311. mA
  312. </label>
  313. <Tooltip
  314. title={
  315. currentExposureMode === 'mAs'
  316. ? '当前为 mAs 模式,mA 和 ms 不可调整'
  317. : ''
  318. }
  319. >
  320. <InputNumber
  321. disabled={currentExposureMode === 'mAs'}
  322. placeholder={currentExposureMode === 'mAs' ? '--' : 'mA'}
  323. style={{ width: '100%', marginBottom: 8 }}
  324. value={
  325. currentExposureMode === 'mAs'
  326. ? null
  327. : (aprConfig.mA ?? undefined)
  328. }
  329. onStep={(value, info) => {
  330. if (info.type === 'up') {
  331. ParaSettingCoordinator.increaseMA();
  332. } else {
  333. ParaSettingCoordinator.decreaseMA();
  334. }
  335. return false;
  336. }}
  337. />
  338. </Tooltip>
  339. </div>
  340. <div>
  341. <label
  342. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  343. >
  344. ms
  345. </label>
  346. <Tooltip
  347. title={
  348. currentExposureMode === 'mAs'
  349. ? '当前为 mAs 模式,mA 和 ms 不可调整'
  350. : ''
  351. }
  352. >
  353. <InputNumber
  354. disabled={currentExposureMode === 'mAs'}
  355. placeholder={currentExposureMode === 'mAs' ? '--' : 'ms'}
  356. style={{ width: '100%', marginBottom: 8 }}
  357. value={
  358. currentExposureMode === 'mAs'
  359. ? null
  360. : (aprConfig.ms ?? undefined)
  361. }
  362. onStep={(value, info) => {
  363. if (info.type === 'up') {
  364. ParaSettingCoordinator.increaseMS();
  365. } else {
  366. ParaSettingCoordinator.decreaseMS();
  367. }
  368. }}
  369. />
  370. </Tooltip>
  371. </div>
  372. <div>
  373. <label
  374. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  375. >
  376. mAs
  377. </label>
  378. <Tooltip
  379. title={
  380. currentExposureMode === 'time'
  381. ? '当前为 time 模式,mAs 不可调整'
  382. : ''
  383. }
  384. >
  385. <InputNumber
  386. disabled={currentExposureMode === 'time'}
  387. placeholder={currentExposureMode === 'time' ? '--' : 'mAs'}
  388. style={{ width: '100%', marginBottom: 8 }}
  389. value={
  390. currentExposureMode === 'time'
  391. ? null
  392. : (aprConfig.mAs ?? undefined)
  393. }
  394. onStep={(value, info) => {
  395. if (info.type === 'up') {
  396. ParaSettingCoordinator.increaseMAS();
  397. } else {
  398. ParaSettingCoordinator.decreaseMAS();
  399. }
  400. }}
  401. />
  402. </Tooltip>
  403. </div>
  404. <div>
  405. <label
  406. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  407. >
  408. KV
  409. </label>
  410. <InputNumber
  411. placeholder="KV"
  412. style={{ width: '100%', marginBottom: 8 }}
  413. value={aprConfig.kV ?? undefined}
  414. // onChange={(value) =>
  415. // dispatch(setAprConfig({ ...aprConfig, kV: value ?? 0 }))
  416. // }
  417. onStep={(value, info) => {
  418. if (info.type === 'up') {
  419. ParaSettingCoordinator.increaseKV();
  420. } else {
  421. ParaSettingCoordinator.decreaseKV();
  422. }
  423. }}
  424. />
  425. </div>
  426. {/* <div>
  427. <label
  428. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  429. >
  430. density
  431. </label>
  432. <InputNumber
  433. placeholder="density"
  434. style={{ width: '100%', marginBottom: 8 }}
  435. value={aprConfig.AECDensity ?? undefined}
  436. onChange={(value) =>
  437. dispatch(setAprConfig({ ...aprConfig, AECDensity: value ?? 0 }))
  438. }
  439. onStep={(value, info) => {
  440. if (info.type === 'up') {
  441. ParaSettingCoordinator.increaseDensity();
  442. } else {
  443. ParaSettingCoordinator.decreaseDensity();
  444. }
  445. }}
  446. />
  447. </div> */}
  448. </div>
  449. <div>
  450. <label
  451. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  452. >
  453. <FormattedMessage id="exam.exposureMode.label" />
  454. </label>
  455. <Select
  456. placeholder={<FormattedMessage id="exam.exposureMode.placeholder" />}
  457. value={currentExposureMode}
  458. onChange={handleExposureModeChange}
  459. style={{ width: '100%', marginBottom: 8 }}
  460. >
  461. <Select.Option value="mAs">mAs</Select.Option>
  462. <Select.Option value="time">time</Select.Option>
  463. </Select>
  464. </div>
  465. <div>
  466. <label
  467. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  468. >
  469. AEC
  470. </label>
  471. <Switch
  472. checkedChildren={<FormattedMessage id="exam.aec.enabled" />}
  473. unCheckedChildren={<FormattedMessage id="exam.aec.disabled" />}
  474. checked={isAECEnabled}
  475. onChange={handleAECChange}
  476. style={{ marginBottom: 8 }}
  477. />
  478. </div>
  479. <Flex align="center" justify="start" gap="middle" wrap>
  480. <Button
  481. data-testid="reset-generator-btn"
  482. style={{ width: '1.5rem', height: '1.5rem' }}
  483. icon={
  484. <Icon
  485. module="module-exam"
  486. name="btn_ResetGenerator"
  487. userId="base"
  488. theme="default"
  489. size="2x"
  490. state="normal"
  491. />
  492. }
  493. title={intl.formatMessage({ id: 'exam.action.resetParams' })}
  494. onClick={handleResetParameters}
  495. disabled={isResetting}
  496. />
  497. </Flex>
  498. <Divider />
  499. <Flex wrap gap="small">
  500. {showDeleteButton && (
  501. <Tooltip title={intl.formatMessage({ id: 'exam.action.deletePosition' })}>
  502. <Button
  503. style={{ width: '1.5rem', height: '1.5rem' }}
  504. icon={
  505. <Icon
  506. module="module-exam"
  507. name="btn_DeleteView"
  508. userId="base"
  509. theme="default"
  510. size="2x"
  511. state="normal"
  512. />
  513. }
  514. onClick={async () => {
  515. const state = store.getState().bodyPositionList;
  516. const selectedBodyPosition = state.selectedBodyPosition;
  517. const bodyPositions = state.bodyPositions;
  518. // 检查体位数量,至少保留一个
  519. if (bodyPositions.length <= 1) {
  520. message.warning('至少需要保留一个体位,无法删除');
  521. return;
  522. }
  523. console.log(
  524. `选中的体位:${JSON.stringify(selectedBodyPosition)}`
  525. );
  526. if (
  527. selectedBodyPosition &&
  528. selectedBodyPosition.sop_instance_uid
  529. ) {
  530. try {
  531. await deleteBodyPosition(
  532. selectedBodyPosition.sop_instance_uid
  533. );
  534. dispatch(
  535. removeBodyPositionBySopInstanceUid(
  536. selectedBodyPosition.sop_instance_uid
  537. )
  538. );
  539. dispatch(setByIndex(0));
  540. } catch (error) {
  541. console.error('Error deleting body position:', error);
  542. message.error('Failed to delete body position');
  543. }
  544. }
  545. }}
  546. />
  547. </Tooltip>
  548. )}
  549. <Tooltip title={intl.formatMessage({ id: 'exam.action.copyPosition' })}>
  550. <Button
  551. style={{ width: '1.5rem', height: '1.5rem' }}
  552. icon={
  553. <Icon
  554. module="module-exam"
  555. name="btn_Copy"
  556. userId="base"
  557. theme="default"
  558. size="2x"
  559. state="normal"
  560. />
  561. }
  562. onClick={() => {
  563. const instanceUid =
  564. store.getState().bodyPositionList.selectedBodyPosition
  565. ?.study_instance_uid ?? '';
  566. console.log('Copying position for instance UID:', instanceUid);
  567. console.log(
  568. `${store.getState().bodyPositionList.selectedBodyPosition}`
  569. );
  570. dispatch(copyPositionThunk({ instanceUid }));
  571. }}
  572. />
  573. </Tooltip>
  574. {showSaveParamsButton && (
  575. <Tooltip title={intl.formatMessage({ id: 'exam.action.saveParams' })}>
  576. <Button
  577. style={{ width: '1.5rem', height: '1.5rem' }}
  578. icon={
  579. <Icon
  580. module="module-exam"
  581. name="btn_Save"
  582. userId="base"
  583. theme="default"
  584. size="2x"
  585. state="normal"
  586. />
  587. }
  588. onClick={handleSaveParams}
  589. />
  590. </Tooltip>
  591. )}
  592. <Tooltip title={intl.formatMessage({ id: 'exam.action.toggleCamera' })}>
  593. <Button
  594. style={{ width: '1.5rem', height: '1.5rem' }}
  595. icon={
  596. <Icon
  597. module="module-exam"
  598. name="btn_OpenCamera"
  599. userId="base"
  600. theme="default"
  601. size="2x"
  602. state="normal"
  603. />
  604. }
  605. onClick={handleOpenCamera}
  606. />
  607. </Tooltip>
  608. {showRejectButton && (
  609. <Tooltip title={intl.formatMessage({ id: 'exam.action.reject' })}>
  610. <Button
  611. style={{ width: '1.5rem', height: '1.5rem' }}
  612. loading={loading}
  613. icon={
  614. <Icon
  615. module="module-exam"
  616. name="btn_RejectImage"
  617. userId="base"
  618. theme="default"
  619. size="2x"
  620. state="normal"
  621. />
  622. }
  623. onClick={handleReject}
  624. />
  625. </Tooltip>
  626. )}
  627. {showRestoreButton && (
  628. <Tooltip title={intl.formatMessage({ id: 'exam.action.restore' })}>
  629. <Button
  630. style={{ width: '1.5rem', height: '1.5rem' }}
  631. loading={loading}
  632. icon={
  633. <Icon
  634. module="module-exam"
  635. name="btn_RestoreImage"
  636. userId="base"
  637. theme="default"
  638. size="2x"
  639. state="normal"
  640. />
  641. }
  642. onClick={handleRestore}
  643. />
  644. </Tooltip>
  645. )}
  646. </Flex>
  647. <ErrorMessage />
  648. </Col>
  649. {/* 摄像头 Modal */}
  650. {cameraIsOpen && cameraStudyId && (
  651. <CameraModal
  652. visible={cameraIsOpen}
  653. studyId={cameraStudyId}
  654. onClose={() => dispatch(closeCamera())}
  655. />
  656. )}
  657. </Row>
  658. );
  659. };
  660. export default ContentAreaLarge;