ViewerContainer.tsx 24 KB

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