ViewerContainer.tsx 24 KB

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