ViewerContainer.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. import React, { useEffect } from 'react';
  2. import StackViewer, {
  3. activateMagnifier,
  4. addLMark,
  5. addMark,
  6. addRLabel,
  7. addCustomMark,
  8. adjustBrightnessAndContrast,
  9. deleteSelectedMark,
  10. fitImageSize,
  11. HorizontalFlip,
  12. invertContrast,
  13. InvertImage,
  14. remoteMask,
  15. ResetImage,
  16. rotateAnyAngle,
  17. RotateClockwise90,
  18. RotateCounterclockwise90,
  19. setOriginalSize,
  20. VerticalFlip,
  21. activateLengthMeasurement,
  22. deactivateLengthMeasurement,
  23. clearLengthMeasurements,
  24. activateAngleMeasurement,
  25. activateTibialPlateauAngleMeasurement,
  26. activateDARAMeasurement,
  27. activateHipDIMeasurement,
  28. activateHipNHAAngleMeasurement,
  29. activateTPLOMeasurement,
  30. activateTTAMeasurement,
  31. activateCBLOMeasurement,
  32. activateHipCoverageMeasurement,
  33. activateHipDorsalCoverageMeasurement,
  34. } from './viewers/stack.image.viewer';
  35. import { useSelector, useDispatch } from 'react-redux';
  36. import { getDcmImageUrl } from '@/API/bodyPosition';
  37. import store, { RootState } from '@/states/store';
  38. import { clearAction } from '@/states/view/functionAreaSlice';
  39. import {
  40. selectGridLayout,
  41. selectSelectedViewers,
  42. selectAllViewerUrls,
  43. setGridLayout as setGridLayoutAction,
  44. setAllViewers,
  45. toggleViewerSelection,
  46. } from '@/states/view/viewerContainerSlice';
  47. import {
  48. clearMeasurementAction,
  49. selectCurrentMeasurementAction,
  50. setToolActive,
  51. setToolInactive,
  52. } from '@/states/view/measurementPanelSlice';
  53. import * as cornerstone from '@cornerstonejs/core';
  54. import * as cornerstoneTools from '@cornerstonejs/tools';
  55. import { SystemMode } from '@/states/systemModeSlice';
  56. import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
  57. import { MeasurementToolManager } from '@/utils/measurementToolManager';
  58. const renderingEngineId = 'myRenderingEngine';
  59. // 测量工具配置映射
  60. interface MeasurementToolConfig {
  61. toolName: string;
  62. activateFunction: (viewportId: string) => boolean;
  63. logPrefix: string;
  64. }
  65. const MEASUREMENT_TOOL_CONFIGS: Record<string, MeasurementToolConfig> = {
  66. '胫骨平台夹角': {
  67. toolName: 'TibialPlateauAngleTool',
  68. activateFunction: activateTibialPlateauAngleMeasurement,
  69. logPrefix: 'TibialPlateauAngle',
  70. },
  71. '髋臼水平角': {
  72. toolName: 'DARAMeasurementTool',
  73. activateFunction: activateDARAMeasurement,
  74. logPrefix: 'DARA',
  75. },
  76. '髋关节牵引指数': {
  77. toolName: 'HipDIMeasurementTool',
  78. activateFunction: activateHipDIMeasurement,
  79. logPrefix: 'HipDI',
  80. },
  81. '髋关节水平角': {
  82. toolName: 'HipNHAAngleMeasurementTool',
  83. activateFunction: activateHipNHAAngleMeasurement,
  84. logPrefix: 'HipNHA',
  85. },
  86. '心锥比': {
  87. toolName: 'VHSMeasurementTool',
  88. activateFunction: MeasurementToolManager.activateVHSMeasurementTool,
  89. logPrefix: 'VHS',
  90. },
  91. '胫骨平台骨切开术': {
  92. toolName: 'TPLOMeasurementTool',
  93. activateFunction: activateTPLOMeasurement,
  94. logPrefix: 'TPLO',
  95. },
  96. '胫骨结节前移术': {
  97. toolName: 'TTAMeasurementTool',
  98. activateFunction: activateTTAMeasurement,
  99. logPrefix: 'TTA',
  100. },
  101. '水平截骨术': {
  102. toolName: 'CBLOMeasurementTool',
  103. activateFunction: activateCBLOMeasurement,
  104. logPrefix: 'CBLO',
  105. },
  106. '股骨头覆盖率': {
  107. toolName: 'HipCoverageMeasurementTool',
  108. activateFunction: activateHipCoverageMeasurement,
  109. logPrefix: 'HipCoverage',
  110. },
  111. '髋臼背覆盖': {
  112. toolName: 'HipDorsalCoverageTool',
  113. activateFunction: activateHipDorsalCoverageMeasurement,
  114. logPrefix: 'HipDorsalCoverage',
  115. },
  116. };
  117. const setup = () => {
  118. // 初始化 Cornerstone
  119. cornerstone.init();
  120. cornerstoneTools.init();
  121. const state = store.getState();
  122. console.log(`当前系统模式:${state.systemMode.mode}`);
  123. const token =
  124. state.systemMode.mode === SystemMode.Emergency
  125. ? state.product.guest
  126. : state.userInfo.token;
  127. console.log(`token stack.image.viewer: ${token}`);
  128. cornerstoneDICOMImageLoader.init({
  129. maxWebWorkers: navigator.hardwareConcurrency || 1,
  130. errorInterceptor: (error) => {
  131. if (error.status === 401) {
  132. console.error('Authentication failed. Please refresh the token.');
  133. }
  134. console.error(`请求dcm文件出错:${error}`);
  135. },
  136. beforeSend: (xhr, imageId, defaultHeaders) => {
  137. return {
  138. ...defaultHeaders,
  139. Authorization: `Bearer ${token}`,
  140. Language: 'en',
  141. Product: 'DROS',
  142. Source: 'Electron',
  143. };
  144. },
  145. });
  146. // 创建渲染引擎
  147. new cornerstone.RenderingEngine(renderingEngineId);
  148. };
  149. setup();
  150. interface ViewerContainerProps {
  151. imageUrls: string[];
  152. }
  153. const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
  154. console.log(`[ViewerContainer] 新渲染 imageUrls:`, imageUrls);
  155. // 从 Redux 获取状态
  156. const gridLayout = useSelector(selectGridLayout);
  157. const selectedViewerUrls = useSelector(selectSelectedViewers);
  158. const allViewerUrls = useSelector(selectAllViewerUrls);
  159. const action = useSelector((state: RootState) => state.functionArea.action);
  160. const measurementAction = useSelector(selectCurrentMeasurementAction);
  161. const dispatch = useDispatch();
  162. const selectedBodyPosition = useSelector(
  163. (state: RootState) => state.bodyPositionList.selectedBodyPosition
  164. );
  165. console.log(`[ViewerContainer] rerendered]`);
  166. // 当 imageUrls 改变时,更新 Redux 中的 allViewers
  167. useEffect(() => {
  168. if (imageUrls.length > 0 && JSON.stringify(imageUrls) !== JSON.stringify(allViewerUrls)) {
  169. dispatch(setAllViewers(imageUrls));
  170. }
  171. }, [imageUrls, allViewerUrls, dispatch]);
  172. // 将 imageUrl 转换为 viewportId
  173. const getViewportIdByUrl = (url: string): string | null => {
  174. return `viewport-${url}`;
  175. };
  176. const renderViewers = (start: number, end: number) => {
  177. console.log(`Rendering viewers from ${start} to ${end}`);
  178. return imageUrls.slice(start, end).map((url, index) => (
  179. <div
  180. key={start + index}
  181. onClick={(event) => handleSelectViewer(url, event)}
  182. >
  183. <StackViewer
  184. key={start + index}
  185. imageIndex={0}
  186. imageUrls={[url]}
  187. viewportId={getViewportIdByUrl(url) as string}
  188. renderingEngineId={renderingEngineId}
  189. selected={selectedViewerUrls.includes(url)}
  190. />
  191. </div>
  192. ));
  193. };
  194. /**
  195. * 通用测量工具激活函数
  196. * @param config 测量工具配置
  197. * @param selectedViewportIds 选中的 viewport IDs
  198. */
  199. const activateMeasurementTool = (
  200. config: MeasurementToolConfig,
  201. selectedViewportIds: string[]
  202. ) => {
  203. console.log(`开始${config.logPrefix}测量`);
  204. const viewportIds = selectedViewportIds.length > 0
  205. ? selectedViewportIds
  206. : Array.from({ length: getVisibleViewportCount() }, (_, i) => `viewport-${i}`);
  207. viewportIds.forEach((viewportId) => {
  208. const success = config.activateFunction(viewportId);
  209. if (success) {
  210. console.log(`激活${config.logPrefix}测量工具成功`);
  211. dispatch(
  212. setToolActive({
  213. toolName: config.toolName,
  214. viewportId: viewportId,
  215. })
  216. );
  217. }
  218. });
  219. console.log(`Activating ${config.logPrefix} Measurement from MeasurementPanel`);
  220. };
  221. /**
  222. * 获取当前可见的 viewport 数量
  223. */
  224. const getVisibleViewportCount = (): number => {
  225. switch (gridLayout) {
  226. case '1x1':
  227. return 1;
  228. case '1x2':
  229. case '2x1':
  230. return Math.min(2, imageUrls.length);
  231. case '2x2':
  232. return Math.min(4, imageUrls.length);
  233. default:
  234. return 1;
  235. }
  236. };
  237. useEffect(() => {
  238. if (action) {
  239. console.log(
  240. `[ViewerContainer] 处理功能操作: ${action}, selectedViewers:`,
  241. selectedViewerUrls
  242. );
  243. // 将选中的 imageUrl 转换为 viewportId
  244. const selectedViewportIds = selectedViewerUrls
  245. .map(getViewportIdByUrl)
  246. .filter((id): id is string => id !== null);
  247. // Handle the action
  248. switch (action) {
  249. case 'Add L Mark':
  250. selectedViewportIds.forEach((viewportId) => {
  251. addLMark(viewportId);
  252. });
  253. break;
  254. case 'Add R Mark':
  255. selectedViewportIds.forEach((viewportId) => {
  256. addRLabel(viewportId);
  257. });
  258. console.log('Adding R Mark');
  259. break;
  260. case 'Delete Selected Mark': {
  261. selectedViewportIds.forEach((viewportId) => {
  262. deleteSelectedMark(viewportId);
  263. });
  264. break;
  265. }
  266. case 'Horizontal Flip': {
  267. console.log(`开始竖直翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
  268. selectedViewportIds.forEach((viewportId) => {
  269. HorizontalFlip(viewportId);
  270. });
  271. break;
  272. }
  273. case 'Vertical Flip': {
  274. console.log(`开始水平翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
  275. selectedViewportIds.forEach((viewportId) => {
  276. VerticalFlip(viewportId);
  277. });
  278. break;
  279. }
  280. case 'Rotate Counterclockwise 90': {
  281. selectedViewportIds.forEach((viewportId) => {
  282. RotateCounterclockwise90(viewportId);
  283. });
  284. break;
  285. }
  286. case 'Rotate Clockwise 90':
  287. selectedViewportIds.forEach((viewportId) => {
  288. RotateClockwise90(viewportId);
  289. });
  290. break;
  291. case 'Rotate Any Angle':
  292. selectedViewportIds.forEach((viewportId) => {
  293. rotateAnyAngle(viewportId);
  294. });
  295. break;
  296. case 'AddMark':
  297. selectedViewportIds.forEach((viewportId) => {
  298. addMark(viewportId);
  299. });
  300. break;
  301. case 'AddCustomMark':
  302. // 从markPanel状态获取选中的标记文本(用于自定义标记)
  303. const selectedMarkText = store.getState().markPanel.selectedMark;
  304. if (selectedMarkText) {
  305. selectedViewportIds.forEach((viewportId) => {
  306. addCustomMark(viewportId, selectedMarkText);
  307. });
  308. }
  309. break;
  310. case 'AddPredefinedMark':
  311. // 从action payload中提取预定义标记文本
  312. const predefinedMarkMatch = action.match(/AddPredefinedMark:(.+)/);
  313. if (predefinedMarkMatch) {
  314. const markText = predefinedMarkMatch[1];
  315. selectedViewportIds.forEach((viewportId) => {
  316. addCustomMark(viewportId, markText);
  317. });
  318. }
  319. break;
  320. case 'AddTimestamp':
  321. const now = new Date();
  322. const timestamp = now.getFullYear() + '-' +
  323. String(now.getMonth() + 1).padStart(2, '0') + '-' +
  324. String(now.getDate()).padStart(2, '0') + ' ' +
  325. String(now.getHours()).padStart(2, '0') + ':' +
  326. String(now.getMinutes()).padStart(2, '0') + ':' +
  327. String(now.getSeconds()).padStart(2, '0');
  328. selectedViewportIds.forEach((viewportId) => {
  329. addCustomMark(viewportId, timestamp);
  330. });
  331. break;
  332. case 'Delete Digital Mask':
  333. selectedViewportIds.forEach((viewportId) => {
  334. remoteMask(viewportId);
  335. });
  336. break;
  337. case 'Adjust Brightness and Contrast':
  338. selectedViewportIds.forEach((viewportId) => {
  339. adjustBrightnessAndContrast(viewportId);
  340. });
  341. break;
  342. case 'Crop Selected Area':
  343. console.log('Cropping Selected Area');
  344. break;
  345. case 'Delete Mask':
  346. console.log('Deleting Mask');
  347. break;
  348. case 'Image Comparison':
  349. console.log('Comparing Images');
  350. break;
  351. case 'Invert Contrast':
  352. selectedViewportIds.forEach((viewportId) => {
  353. invertContrast(viewportId);
  354. });
  355. console.log('Inverting Contrast');
  356. break;
  357. case '1x1 Layout':
  358. dispatch(setGridLayoutAction('1x1'));
  359. console.log(`1x1 Layout`);
  360. break;
  361. case '1x2 Layout':
  362. dispatch(setGridLayoutAction('1x2'));
  363. break;
  364. case '2x1 Layout':
  365. dispatch(setGridLayoutAction('2x1'));
  366. break;
  367. case '2x2 Layout':
  368. dispatch(setGridLayoutAction('2x2'));
  369. break;
  370. case 'Magnifier': {
  371. selectedViewportIds.forEach((viewportId) => {
  372. activateMagnifier(viewportId);
  373. });
  374. break;
  375. }
  376. case 'Fit Size': {
  377. selectedViewportIds.forEach((viewportId) => {
  378. fitImageSize(viewportId);
  379. });
  380. console.log('Fitting Image Size');
  381. break;
  382. }
  383. case 'Original Size': {
  384. selectedViewportIds.forEach((viewportId) => {
  385. setOriginalSize(viewportId);
  386. });
  387. console.log('Setting Image to Original Size');
  388. break;
  389. }
  390. case 'Zoom Image':
  391. console.log('Zooming Image');
  392. break;
  393. case 'Reset Cursor':
  394. console.log('Resetting Cursor');
  395. break;
  396. case 'Pan':
  397. console.log('Panning Image');
  398. break;
  399. case 'Invert Image':
  400. selectedViewportIds.forEach((viewportId) => {
  401. InvertImage(viewportId);
  402. });
  403. break;
  404. case 'Reset Image':
  405. selectedViewportIds.forEach((viewportId) => {
  406. ResetImage(viewportId);
  407. });
  408. break;
  409. case 'Snapshot':
  410. console.log('Taking Snapshot');
  411. break;
  412. case 'Advanced Processing':
  413. console.log('Performing Advanced Processing');
  414. break;
  415. case 'Musician':
  416. console.log('Activating Musician');
  417. break;
  418. case 'Image Measurement':
  419. console.log('Measuring Image');
  420. break;
  421. case 'More':
  422. console.log('Showing More Options');
  423. break;
  424. // ==================== 线段测量相关操作 ====================
  425. case '线段测量':
  426. if (selectedViewportIds.length > 0) {
  427. selectedViewportIds.forEach((viewportId) => {
  428. activateLengthMeasurement(viewportId);
  429. });
  430. } else {
  431. // 如果没有选中的 viewport,为所有可见的 viewport 激活
  432. const visibleViewportCount = getVisibleViewportCount();
  433. for (let i = 0; i < visibleViewportCount; i++) {
  434. activateLengthMeasurement(`viewport-${i}`);
  435. }
  436. }
  437. console.log('Activating Length Measurement');
  438. break;
  439. case '清除测量':
  440. if (selectedViewportIds.length > 0) {
  441. selectedViewportIds.forEach((viewportId) => {
  442. clearLengthMeasurements(viewportId);
  443. });
  444. } else {
  445. // 如果没有选中的 viewport,清除所有可见 viewport 的测量
  446. const visibleViewportCount = getVisibleViewportCount();
  447. for (let i = 0; i < visibleViewportCount; i++) {
  448. clearLengthMeasurements(`viewport-${i}`);
  449. }
  450. }
  451. console.log('Clearing Length Measurements');
  452. break;
  453. case '停用线段测量':
  454. if (selectedViewportIds.length > 0) {
  455. selectedViewportIds.forEach((viewportId) => {
  456. deactivateLengthMeasurement(viewportId);
  457. });
  458. } else {
  459. // 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
  460. const visibleViewportCount = getVisibleViewportCount();
  461. for (let i = 0; i < visibleViewportCount; i++) {
  462. deactivateLengthMeasurement(`viewport-${i}`);
  463. }
  464. }
  465. console.log('Deactivating Length Measurement');
  466. break;
  467. default:
  468. break;
  469. }
  470. dispatch(clearAction()); //清理后可连续同一个action触发响应
  471. }
  472. }, [action, selectedViewerUrls, dispatch]);
  473. // ==================== 测量面板 Action 处理 ====================
  474. useEffect(() => {
  475. if (measurementAction) {
  476. console.log(`[ViewerContainer] 处理测量操作: ${measurementAction}`);
  477. // 将选中的 imageUrl 转换为 viewportId
  478. const selectedViewportIds = selectedViewerUrls
  479. .map(getViewportIdByUrl)
  480. .filter((id): id is string => id !== null);
  481. // 处理测量相关操作
  482. switch (measurementAction) {
  483. case '线段测量':
  484. if (selectedViewportIds.length > 0) {
  485. selectedViewportIds.forEach((viewportId) => {
  486. const success = activateLengthMeasurement(viewportId);
  487. if (success) {
  488. dispatch(
  489. setToolActive({
  490. toolName: 'LengthTool',
  491. viewportId: viewportId,
  492. })
  493. );
  494. }
  495. });
  496. } else {
  497. // 如果没有选中的 viewport,为所有可见的 viewport 激活
  498. const visibleViewportCount = getVisibleViewportCount();
  499. for (let i = 0; i < visibleViewportCount; i++) {
  500. const success = activateLengthMeasurement(`viewport-${i}`);
  501. if (success) {
  502. dispatch(
  503. setToolActive({
  504. toolName: 'LengthTool',
  505. viewportId: `viewport-${i}`,
  506. })
  507. );
  508. }
  509. }
  510. }
  511. console.log('Activating Length Measurement from MeasurementPanel');
  512. break;
  513. case '清除测量':
  514. if (selectedViewportIds.length > 0) {
  515. MeasurementToolManager.clearLengthMeasurementsForViewports(selectedViewportIds);
  516. MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
  517. MeasurementToolManager.clearTibialPlateauAngleMeasurementsForViewports(selectedViewportIds);
  518. MeasurementToolManager.clearDARAMeasurementsForViewports(selectedViewportIds);
  519. MeasurementToolManager.clearHipDIMeasurementsForViewports(selectedViewportIds);
  520. MeasurementToolManager.clearHipNHAAngleMeasurementsForViewports(selectedViewportIds);
  521. MeasurementToolManager.clearVHSMeasurementsForViewports(selectedViewportIds);
  522. MeasurementToolManager.clearTPLOMeasurementsForViewports(selectedViewportIds);
  523. MeasurementToolManager.clearTTAMeasurementsForViewports(selectedViewportIds);
  524. MeasurementToolManager.clearCBLOMeasurementsForViewports(selectedViewportIds);
  525. MeasurementToolManager.clearHipCoverageMeasurementsForViewports(selectedViewportIds);
  526. MeasurementToolManager.clearHipDorsalCoverageMeasurementsForViewports(selectedViewportIds);
  527. }
  528. console.log('Clearing All Measurements from MeasurementPanel');
  529. break;
  530. case '停用线段测量':
  531. if (selectedViewportIds.length > 0) {
  532. selectedViewportIds.forEach((viewportId) => {
  533. const success = deactivateLengthMeasurement(viewportId);
  534. if (success) {
  535. dispatch(
  536. setToolInactive({
  537. toolName: 'LengthTool',
  538. viewportId: viewportId,
  539. })
  540. );
  541. }
  542. });
  543. }
  544. console.log('Deactivating Length Measurement from MeasurementPanel');
  545. break;
  546. case '角度测量':
  547. console.log(`开始角度测量`);
  548. if (selectedViewportIds.length > 0) {
  549. selectedViewportIds.forEach((viewportId) => {
  550. const success = activateAngleMeasurement(viewportId);
  551. if (success) {
  552. console.log(`激活角度测量工具成功`);
  553. dispatch(
  554. setToolActive({
  555. toolName: 'AngleTool',
  556. viewportId: viewportId,
  557. })
  558. );
  559. }
  560. });
  561. }
  562. console.log('Activating Angle Measurement from MeasurementPanel');
  563. break;
  564. case '胫骨平台夹角':
  565. case '髋臼水平角':
  566. case '髋关节牵引指数':
  567. case '髋关节水平角':
  568. case '心锥比':
  569. case '胫骨平台骨切开术':
  570. case '胫骨结节前移术':
  571. case '水平截骨术':
  572. case '股骨头覆盖率':
  573. case '髋臼背覆盖': {
  574. const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
  575. if (config) {
  576. activateMeasurementTool(config, selectedViewportIds);
  577. }
  578. break;
  579. }
  580. case '测量校正':
  581. console.log('Measurement Calibration - 功能待实现');
  582. break;
  583. default:
  584. console.log(`未处理的测量操作: ${measurementAction}`);
  585. break;
  586. }
  587. // 清理测量 action
  588. dispatch(clearMeasurementAction());
  589. }
  590. }, [measurementAction, selectedViewerUrls, gridLayout, dispatch]);
  591. const handleSelectViewer = (imageUrl: string, event: React.MouseEvent) => {
  592. console.log(`handleSelectViewer : ${imageUrl}`);
  593. console.log(`selectedViewers 旧值:`, selectedViewerUrls);
  594. dispatch(toggleViewerSelection(imageUrl));
  595. };
  596. const renderGrid = () => {
  597. console.log('Rendering layout', gridLayout);
  598. switch (gridLayout) {
  599. case '1x1': {
  600. // 变成单分格时,选中对应 selectedBodyPosition 的 URL
  601. let urlToSelect = allViewerUrls[0]; // 默认选第一个作为fallback
  602. if (selectedBodyPosition?.sop_instance_uid) {
  603. const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
  604. const urlIndex = allViewerUrls.indexOf(correspondingUrl);
  605. if (urlIndex !== -1) {
  606. urlToSelect = allViewerUrls[urlIndex];
  607. }
  608. }
  609. // 检查当前选中状态,避免无限循环
  610. const currentSelected = selectedViewerUrls[0];
  611. if (currentSelected !== urlToSelect) {
  612. if (currentSelected) {
  613. dispatch(toggleViewerSelection(currentSelected)); // 取消选中当前
  614. }
  615. dispatch(toggleViewerSelection(urlToSelect)); // 选中新的
  616. }
  617. // 找到体位列表中选中体位对应的url
  618. let viewerIndex = 0;
  619. if (selectedBodyPosition?.sop_instance_uid) {
  620. const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
  621. const urlIndex = imageUrls.indexOf(correspondingUrl);
  622. if (urlIndex !== -1) {
  623. viewerIndex = urlIndex;
  624. }
  625. }
  626. return (
  627. <div
  628. className="h-full w-full"
  629. style={{ display: 'grid', gridTemplateColumns: '1fr' }}
  630. >
  631. {renderViewers(viewerIndex, viewerIndex + 1)}
  632. </div>
  633. );
  634. break;
  635. }
  636. case '1x2':
  637. return (
  638. <div
  639. className="h-full w-full"
  640. style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
  641. >
  642. {renderViewers(0, 2)}
  643. </div>
  644. );
  645. case '2x1':
  646. return (
  647. <div className="h-full w-full grid grid-cols-1 grid-rows-2">
  648. {renderViewers(0, 2)}
  649. </div>
  650. );
  651. case '2x2':
  652. return (
  653. <div
  654. className="h-full w-full"
  655. style={{
  656. display: 'grid',
  657. gridTemplateColumns: '1fr 1fr',
  658. gridTemplateRows: '1fr 1fr',
  659. }}
  660. >
  661. {renderViewers(0, 4)}
  662. </div>
  663. );
  664. default:
  665. return null;
  666. }
  667. };
  668. return <>{renderGrid()}</>;
  669. };
  670. export default ViewerContainer;