ViewerContainer.tsx 30 KB

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