ContentAreaLarge.tsx 17 KB

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