ContentAreaLarge.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import {
  2. Row,
  3. Col,
  4. Select,
  5. InputNumber,
  6. Button,
  7. Switch,
  8. Divider,
  9. Tooltip,
  10. message,
  11. Flex,
  12. } from 'antd';
  13. import ErrorMessage from '@/components/ErrorMessage';
  14. import { patientSizes, PatientSize } from '../../states/patientSize';
  15. import { WorkstationTypeLabels } from '../../states/workstation';
  16. import { FormattedMessage } from 'react-intl';
  17. import { useSelector, useDispatch, useStore } from 'react-redux';
  18. import { deleteBodyPosition } from '../../API/patient/viewActions';
  19. import { copyPositionThunk } from '../../states/exam/examWorksCacheSlice';
  20. import {
  21. removeBodyPositionBySopInstanceUid,
  22. setByIndex,
  23. } from '../../states/exam/bodyPositionListSlice';
  24. import { RootState } from '../../states/store';
  25. import {
  26. setAprConfig,
  27. setBodysize,
  28. setWorkstation,
  29. setThickness,
  30. setIsAECEnabled,
  31. } from '../../states/exam/aprSlice';
  32. import BodyPositionList from './components/BodyPositionList';
  33. import BodyPositionDetail from './components/BodyPositionDetail';
  34. import { AppDispatch } from '@/states/store';
  35. import { useRef } from 'react';
  36. import Icon from '@/components/Icon';
  37. import ParaSettingCoordinator from '@/domain/exam/paraSettingCoordinator';
  38. import { resetDevices } from '@/states/device/deviceSlice';
  39. import { openCamera, closeCamera } from '@/states/exam/cameraSlice';
  40. import CameraModal from '@/components/CameraModal';
  41. const ContentAreaLarge = () => {
  42. const dispatch = useDispatch<AppDispatch>();
  43. const isResetting = useSelector(
  44. (state: RootState) => state.device.status === 'loading'
  45. );
  46. const store = useStore<RootState>();
  47. const aprConfig = useSelector((state: RootState) => state.apr.aprConfig);
  48. const bodysize = useSelector((state: RootState) => state.apr.bodysize);
  49. const workstation = useSelector((state: RootState) => state.apr.workstation);
  50. const thickness = useSelector((state: RootState) => state.apr.thickness);
  51. const isAECEnabled = useSelector(
  52. (state: RootState) => state.apr.isAECEnabled
  53. );
  54. const currentExposureMode = useSelector(
  55. (state: RootState) => state.apr.currentExposureMode
  56. );
  57. const productName = useSelector(
  58. (state: RootState) => state.product.productName
  59. );
  60. const handleBodysizeChange = (key: string) => {
  61. const value = patientSizes[key as PatientSize]; // 获取对应的显示文本
  62. console.log('体型 key:', key); // 例如: 'small'
  63. console.log('体型 value:', value); // 例如: 'Small'
  64. dispatch(setBodysize(value));
  65. };
  66. const handleWorkstationChange = (value: string) => {
  67. dispatch(setWorkstation(value));
  68. };
  69. const handleThicknessChange = (value: number | null) => {
  70. if (value !== null) {
  71. dispatch(setThickness(value));
  72. }
  73. };
  74. const handleAECChange = (checked: boolean) => {
  75. dispatch(setIsAECEnabled(checked));
  76. };
  77. const handleExposureModeChange = (value: string) => {
  78. ParaSettingCoordinator.setExposureMode(value);
  79. };
  80. const handleResetParameters = async () => {
  81. try {
  82. await dispatch(resetDevices());
  83. } catch (error) {
  84. console.error('Error resetting devices:', error);
  85. }
  86. };
  87. /**
  88. * 处理打开摄像头按钮点击事件
  89. */
  90. const handleOpenCamera = () => {
  91. // 获取当前选中体位的 study_id
  92. const currentStudyId = selectedBodyPosition?.study_id;
  93. if (!currentStudyId) {
  94. message.warning('请先选择一个体位');
  95. return;
  96. }
  97. console.log('[handleOpenCamera] 打开摄像头,Study ID:', currentStudyId);
  98. // dispatch openCamera action,打开摄像头
  99. dispatch(openCamera(currentStudyId));
  100. };
  101. // 1. 正常在顶层用 useSelector 订阅
  102. const selectedBodyPosition = useSelector(
  103. (state: RootState) => state.bodyPositionList.selectedBodyPosition
  104. );
  105. // 2. 用 ref 保存最新值(每次渲染都会更新)
  106. const positionRef = useRef(selectedBodyPosition);
  107. positionRef.current = selectedBodyPosition;
  108. // 3. 订阅 camera 状态
  109. const cameraIsOpen = useSelector((state: RootState) => state.camera.isOpen);
  110. const cameraStudyId = useSelector(
  111. (state: RootState) => state.camera.currentStudyId
  112. );
  113. return (
  114. <Row className="w-full p-1" style={{ height: '100%', display: 'flex' }}>
  115. <Col span={20} style={{ display: 'flex', flexDirection: 'column' }}>
  116. <Row gutter={16} style={{ flex: 1, minHeight: 0, display: 'flex' }}>
  117. <Col span={4} className="flex flex-col">
  118. <BodyPositionList layout="vertical"></BodyPositionList>
  119. </Col>
  120. <Col span={20}>
  121. <BodyPositionDetail />
  122. </Col>
  123. </Row>
  124. </Col>
  125. <Col
  126. span={4}
  127. style={{ height: '100%', overflowY: 'auto', flexShrink: 0 }}
  128. >
  129. <Row gutter={16} align="middle">
  130. <Col span={productName === 'DROS' ? 9 : 12}>
  131. <Select
  132. placeholder="选择体型"
  133. style={{ width: '100%', marginBottom: 8 }}
  134. value={bodysize}
  135. onChange={handleBodysizeChange}
  136. >
  137. {Object.entries(patientSizes).map(
  138. ([key, value]: [string, string]) => (
  139. <Select.Option key={key} value={key}>
  140. <FormattedMessage id={value} />
  141. </Select.Option>
  142. )
  143. )}
  144. </Select>
  145. </Col>
  146. <Col span={productName === 'DROS' ? 9 : 12}>
  147. <label
  148. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  149. >
  150. 厚度 (cm)
  151. </label>
  152. <InputNumber
  153. placeholder="厚度"
  154. style={{ width: '100%', marginBottom: 8 }}
  155. value={thickness || undefined}
  156. min={1}
  157. max={50}
  158. step={1}
  159. onChange={handleThicknessChange}
  160. onStep={(value, info) => {
  161. if (info.type === 'up') {
  162. ParaSettingCoordinator.increaseThickness();
  163. } else {
  164. ParaSettingCoordinator.decreaseThickness();
  165. }
  166. }}
  167. />
  168. </Col>
  169. {productName === 'DROS' && (
  170. <Col span={15}>
  171. <Select
  172. placeholder="选择工作位"
  173. style={{ width: '100%', marginBottom: 8 }}
  174. value={workstation}
  175. onChange={handleWorkstationChange}
  176. >
  177. {Object.entries(WorkstationTypeLabels).map(
  178. ([key, value]: [string, string]) => (
  179. <Select.Option key={key} value={value}>
  180. <FormattedMessage
  181. id={`workstation.${key.toLowerCase()}`}
  182. />
  183. </Select.Option>
  184. )
  185. )}
  186. </Select>
  187. </Col>
  188. )}
  189. </Row>
  190. <div>
  191. <div>
  192. <label
  193. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  194. >
  195. mA
  196. </label>
  197. <Tooltip
  198. title={
  199. currentExposureMode === 'mAs'
  200. ? '当前为 mAs 模式,mA 和 ms 不可调整'
  201. : ''
  202. }
  203. >
  204. <InputNumber
  205. disabled={currentExposureMode === 'mAs'}
  206. placeholder={currentExposureMode === 'mAs' ? '--' : 'mA'}
  207. style={{ width: '100%', marginBottom: 8 }}
  208. value={
  209. currentExposureMode === 'mAs'
  210. ? null
  211. : (aprConfig.mA ?? undefined)
  212. }
  213. onStep={(value, info) => {
  214. if (info.type === 'up') {
  215. ParaSettingCoordinator.increaseMA();
  216. } else {
  217. ParaSettingCoordinator.decreaseMA();
  218. }
  219. return false;
  220. }}
  221. />
  222. </Tooltip>
  223. </div>
  224. <div>
  225. <label
  226. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  227. >
  228. ms
  229. </label>
  230. <Tooltip
  231. title={
  232. currentExposureMode === 'mAs'
  233. ? '当前为 mAs 模式,mA 和 ms 不可调整'
  234. : ''
  235. }
  236. >
  237. <InputNumber
  238. disabled={currentExposureMode === 'mAs'}
  239. placeholder={currentExposureMode === 'mAs' ? '--' : 'ms'}
  240. style={{ width: '100%', marginBottom: 8 }}
  241. value={
  242. currentExposureMode === 'mAs'
  243. ? null
  244. : (aprConfig.ms ?? undefined)
  245. }
  246. onStep={(value, info) => {
  247. if (info.type === 'up') {
  248. ParaSettingCoordinator.increaseMS();
  249. } else {
  250. ParaSettingCoordinator.decreaseMS();
  251. }
  252. }}
  253. />
  254. </Tooltip>
  255. </div>
  256. <div>
  257. <label
  258. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  259. >
  260. mAs
  261. </label>
  262. <Tooltip
  263. title={
  264. currentExposureMode === 'time'
  265. ? '当前为 time 模式,mAs 不可调整'
  266. : ''
  267. }
  268. >
  269. <InputNumber
  270. disabled={currentExposureMode === 'time'}
  271. placeholder={currentExposureMode === 'time' ? '--' : 'mAs'}
  272. style={{ width: '100%', marginBottom: 8 }}
  273. value={
  274. currentExposureMode === 'time'
  275. ? null
  276. : (aprConfig.mAs ?? undefined)
  277. }
  278. onStep={(value, info) => {
  279. if (info.type === 'up') {
  280. ParaSettingCoordinator.increaseMAS();
  281. } else {
  282. ParaSettingCoordinator.decreaseMAS();
  283. }
  284. }}
  285. />
  286. </Tooltip>
  287. </div>
  288. <div>
  289. <label
  290. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  291. >
  292. KV
  293. </label>
  294. <InputNumber
  295. placeholder="KV"
  296. style={{ width: '100%', marginBottom: 8 }}
  297. value={aprConfig.kV ?? undefined}
  298. // onChange={(value) =>
  299. // dispatch(setAprConfig({ ...aprConfig, kV: value ?? 0 }))
  300. // }
  301. onStep={(value, info) => {
  302. if (info.type === 'up') {
  303. ParaSettingCoordinator.increaseKV();
  304. } else {
  305. ParaSettingCoordinator.decreaseKV();
  306. }
  307. }}
  308. />
  309. </div>
  310. {/* <div>
  311. <label
  312. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  313. >
  314. density
  315. </label>
  316. <InputNumber
  317. placeholder="density"
  318. style={{ width: '100%', marginBottom: 8 }}
  319. value={aprConfig.AECDensity ?? undefined}
  320. onChange={(value) =>
  321. dispatch(setAprConfig({ ...aprConfig, AECDensity: value ?? 0 }))
  322. }
  323. onStep={(value, info) => {
  324. if (info.type === 'up') {
  325. ParaSettingCoordinator.increaseDensity();
  326. } else {
  327. ParaSettingCoordinator.decreaseDensity();
  328. }
  329. }}
  330. />
  331. </div> */}
  332. </div>
  333. <div>
  334. <label
  335. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  336. >
  337. 曝光模式
  338. </label>
  339. <Select
  340. placeholder="选择曝光模式"
  341. value={currentExposureMode}
  342. onChange={handleExposureModeChange}
  343. style={{ width: '100%', marginBottom: 8 }}
  344. >
  345. <Select.Option value="mAs">mAs</Select.Option>
  346. <Select.Option value="time">time</Select.Option>
  347. </Select>
  348. </div>
  349. <div>
  350. <label
  351. style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
  352. >
  353. AEC
  354. </label>
  355. <Switch
  356. checkedChildren="开启AEC"
  357. unCheckedChildren="关闭AEC"
  358. checked={isAECEnabled}
  359. onChange={handleAECChange}
  360. style={{ marginBottom: 8 }}
  361. />
  362. </div>
  363. <Flex align="center" justify="start" gap="middle" wrap>
  364. <Button
  365. data-testid="reset-generator-btn"
  366. style={{ width: '1.5rem', height: '1.5rem' }}
  367. icon={
  368. <Icon
  369. module="module-exam"
  370. name="btn_ResetGenerator"
  371. userId="base"
  372. theme="default"
  373. size="2x"
  374. state="normal"
  375. />
  376. }
  377. title="重置参数"
  378. onClick={handleResetParameters}
  379. disabled={isResetting}
  380. />
  381. </Flex>
  382. <Divider />
  383. <Flex wrap gap="small">
  384. <Tooltip title="删除选择的体位">
  385. <Button
  386. style={{ width: '1.5rem', height: '1.5rem' }}
  387. icon={
  388. <Icon
  389. module="module-exam"
  390. name="btn_DeleteView"
  391. userId="base"
  392. theme="default"
  393. size="2x"
  394. state="normal"
  395. />
  396. }
  397. onClick={async () => {
  398. const state = store.getState().bodyPositionList;
  399. const selectedBodyPosition = state.selectedBodyPosition;
  400. const bodyPositions = state.bodyPositions;
  401. // 检查体位数量,至少保留一个
  402. if (bodyPositions.length <= 1) {
  403. message.warning('至少需要保留一个体位,无法删除');
  404. return;
  405. }
  406. console.log(
  407. `选中的体位:${JSON.stringify(selectedBodyPosition)}`
  408. );
  409. if (
  410. selectedBodyPosition &&
  411. selectedBodyPosition.sop_instance_uid
  412. ) {
  413. try {
  414. await deleteBodyPosition(
  415. selectedBodyPosition.sop_instance_uid
  416. );
  417. dispatch(
  418. removeBodyPositionBySopInstanceUid(
  419. selectedBodyPosition.sop_instance_uid
  420. )
  421. );
  422. dispatch(setByIndex(0));
  423. } catch (error) {
  424. console.error('Error deleting body position:', error);
  425. message.error('Failed to delete body position');
  426. }
  427. }
  428. }}
  429. />
  430. </Tooltip>
  431. <Tooltip title="复制选择的体位">
  432. <Button
  433. style={{ width: '1.5rem', height: '1.5rem' }}
  434. icon={
  435. <Icon
  436. module="module-exam"
  437. name="btn_Copy"
  438. userId="base"
  439. theme="default"
  440. size="2x"
  441. state="normal"
  442. />
  443. }
  444. onClick={() => {
  445. const instanceUid =
  446. store.getState().bodyPositionList.selectedBodyPosition
  447. ?.study_instance_uid ?? '';
  448. console.log('Copying position for instance UID:', instanceUid);
  449. console.log(
  450. `${store.getState().bodyPositionList.selectedBodyPosition}`
  451. );
  452. dispatch(copyPositionThunk({ instanceUid }));
  453. }}
  454. />
  455. </Tooltip>
  456. <Tooltip title="保存参数">
  457. <Button
  458. style={{ width: '1.5rem', height: '1.5rem' }}
  459. icon={
  460. <Icon
  461. module="module-exam"
  462. name="btn_Save"
  463. userId="base"
  464. theme="default"
  465. size="2x"
  466. state="normal"
  467. />
  468. }
  469. />
  470. </Tooltip>
  471. <Tooltip title="打开/关闭摄像头">
  472. <Button
  473. style={{ width: '1.5rem', height: '1.5rem' }}
  474. icon={
  475. <Icon
  476. module="module-exam"
  477. name="btn_OpenCamera"
  478. userId="base"
  479. theme="default"
  480. size="2x"
  481. state="normal"
  482. />
  483. }
  484. onClick={handleOpenCamera}
  485. />
  486. </Tooltip>
  487. <Tooltip title="拒绝">
  488. <Button
  489. style={{ width: '1.5rem', height: '1.5rem' }}
  490. icon={
  491. <Icon
  492. module="module-exam"
  493. name="btn_RejectImage"
  494. userId="base"
  495. theme="default"
  496. size="2x"
  497. state="normal"
  498. />
  499. }
  500. />
  501. </Tooltip>
  502. </Flex>
  503. <ErrorMessage />
  504. </Col>
  505. {/* 摄像头 Modal */}
  506. {cameraIsOpen && cameraStudyId && (
  507. <CameraModal
  508. visible={cameraIsOpen}
  509. studyId={cameraStudyId}
  510. onClose={() => dispatch(closeCamera())}
  511. />
  512. )}
  513. </Row>
  514. );
  515. };
  516. export default ContentAreaLarge;