DcmCell.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // ViewportContainer.jsx
  2. import React, { useRef, useEffect, useState, useMemo } from 'react';
  3. import * as cornerstone from '@cornerstonejs/core';
  4. import * as cornerstoneTools from '@cornerstonejs/tools';
  5. import {
  6. RenderingEngine,
  7. Enums
  8. } from '@cornerstonejs/core';
  9. import { v4 as uuidv4 } from 'uuid';
  10. import { getDcmImageUrl } from '@/API/bodyPosition';
  11. import { useDispatch } from 'react-redux';
  12. import { addImageToCell, Film, toggleCellSelection } from '@/states/print/printSlice';
  13. import { useAppSelector } from '@/states/store';
  14. // import { dicomWebImageLoader } from '@cornerstonejs/dicom-web-image-loader';
  15. // // 初始化图像加载器(关键:必须提前注册)
  16. // const initImageLoader = () => {
  17. // // 注册 DICOM Web 加载器(根据实际场景选择加载器)
  18. // cornerstone.registerImageLoader('dicomweb', dicomWebImageLoader);
  19. // // 配置加载器(Worker 路径需指向实际文件)
  20. // dicomWebImageLoader.configure({
  21. // webWorkerPath: '/cornerstone-workers/',
  22. // maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
  23. // });
  24. // };
  25. type ViewportContainerProps = {
  26. imageId: string;
  27. currentFilm: Film; // 传入当前胶片对象
  28. indexOfCell: number; // 可选,格子索引
  29. className?: string; // 可选
  30. };
  31. const ViewportContainer = ({ imageId, className, currentFilm, indexOfCell }: ViewportContainerProps) => {
  32. console.log(`ViewportContainer 得到的属性 :${imageId}`)
  33. const viewportRef = useRef(null);
  34. const [renderingEngine, setRenderingEngine] = useState<any>(null);
  35. const [stackViewport, setStackViewport] = useState<any>(null);
  36. const [hasImage, setHasImage] = useState<boolean>(false); // 跟踪是否已加载图像
  37. const [currentImageId, setCurrentImageId] = useState<string | null>(null); // 跟踪当前已加载的图像ID
  38. // ✅ 用 useMemo 保证 id 只生成一次,包含胶片ID和格子索引确保唯一性
  39. const viewportId = useMemo(() =>
  40. `stackViewport-${currentFilm.id}-${indexOfCell}-${uuidv4()}`,
  41. [currentFilm.id, indexOfCell]
  42. );
  43. const dispatch = useDispatch();
  44. // 生成格子的唯一标识符
  45. const cellId = `${currentFilm.id}-${indexOfCell}`;
  46. // 检查当前格子是否被选中
  47. const isSelected = useAppSelector(state =>
  48. state.print.selectedCells.includes(cellId)
  49. );
  50. // 统一的图像加载函数,供两种场景使用
  51. const loadImage = async (targetImageId: string, source: 'props' | 'drag' = 'props'): Promise<boolean> => {
  52. if (!stackViewport || !targetImageId) return false;
  53. try {
  54. const fullImageUrl = targetImageId.startsWith('dicomweb:')
  55. ? targetImageId
  56. : getDcmImageUrl(targetImageId);
  57. await stackViewport.setStack([fullImageUrl], 0);
  58. stackViewport.render();
  59. setHasImage(true);
  60. console.log(`[DcmCell] 图像加载成功 (${source}): ${targetImageId}`);
  61. return true;
  62. } catch (error) {
  63. console.error(`[DcmCell] 图像加载失败 (${source}):`, error);
  64. setHasImage(false);
  65. return false;
  66. }
  67. };
  68. // 初始化 Cornerstone 渲染引擎
  69. useEffect(() => {
  70. // initImageLoader();
  71. // 创建胶片特定的渲染引擎实例,确保状态隔离
  72. const engineId = `renderingEngine-${currentFilm.id}`;
  73. let engine: RenderingEngine | null = cornerstone.getRenderingEngines()?.find(en => en.id === engineId) || null;
  74. // 不存在则创建
  75. if (!engine) {
  76. engine = new RenderingEngine(engineId);
  77. }
  78. setRenderingEngine(engine);
  79. // 配置 Viewport
  80. if (viewportRef.current) {
  81. const viewportInput: cornerstone.Types.PublicViewportInput = {
  82. viewportId: viewportId,
  83. type: Enums.ViewportType.STACK, // 序列视图类型
  84. element: viewportRef.current,
  85. defaultOptions: {
  86. background: [0.1, 0.1, 0.1], // 深灰背景
  87. },
  88. };
  89. // 绑定 Viewport 到引擎
  90. engine.enableElement(viewportInput);
  91. // 获取 Viewport 实例
  92. const viewport = engine.getViewport(viewportId);
  93. setStackViewport(viewport);
  94. // ========== 添加工具支持 ==========
  95. const toolGroupId = `PRINT_TOOL_GROUP_${viewportId}`;
  96. // 创建工具组
  97. const toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId);
  98. if (toolGroup) {
  99. // 添加基础工具
  100. toolGroup.addTool(cornerstoneTools.StackScrollTool.toolName);
  101. toolGroup.addTool(cornerstoneTools.ZoomTool.toolName);
  102. toolGroup.addTool(cornerstoneTools.PanTool.toolName);
  103. toolGroup.addTool(cornerstoneTools.WindowLevelTool.toolName);
  104. // 激活滚轮滚动工具(解决滚轮错误)
  105. toolGroup.setToolActive(cornerstoneTools.StackScrollTool.toolName, {
  106. bindings: [{
  107. mouseButton: cornerstoneTools.Enums.MouseBindings.Wheel
  108. }]
  109. });
  110. // 激活缩放工具(右键)
  111. toolGroup.setToolActive(cornerstoneTools.ZoomTool.toolName, {
  112. bindings: [{
  113. mouseButton: cornerstoneTools.Enums.MouseBindings.Secondary
  114. }]
  115. });
  116. // 激活平移工具(中键)
  117. toolGroup.setToolActive(cornerstoneTools.PanTool.toolName, {
  118. bindings: [{
  119. mouseButton: cornerstoneTools.Enums.MouseBindings.Auxiliary
  120. }]
  121. });
  122. // 其他工具设置为被动状态
  123. toolGroup.setToolPassive(cornerstoneTools.WindowLevelTool.toolName);
  124. // 将 viewport 添加到工具组
  125. toolGroup.addViewport(viewportId, engineId);
  126. console.log(`[DcmCell] 工具组已创建并配置: ${toolGroupId}`);
  127. }
  128. }
  129. // 组件卸载时清理
  130. return () => {
  131. // 销毁工具组
  132. const toolGroupId = `PRINT_TOOL_GROUP_${viewportId}`;
  133. const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
  134. if (toolGroup) {
  135. try {
  136. // 先从工具组中移除 viewport
  137. toolGroup.removeViewports(`renderingEngine-${currentFilm.id}`, viewportId);
  138. // 然后销毁工具组
  139. cornerstoneTools.ToolGroupManager.destroyToolGroup(toolGroupId);
  140. console.log(`[DcmCell] 工具组已销毁: ${toolGroupId}`);
  141. } catch (error) {
  142. console.warn(`[DcmCell] 清理工具组时出错: ${error}`);
  143. }
  144. }
  145. // 清理 viewport
  146. if (engine) {
  147. try {
  148. engine.disableElement(viewportId);
  149. console.log(`[DcmCell] viewport 已禁用: ${viewportId}`);
  150. } catch (error) {
  151. console.warn(`[DcmCell] 禁用 viewport 时出错: ${error}`);
  152. }
  153. }
  154. };
  155. }, []);
  156. // 监听 imageId 变化,自动加载图像
  157. useEffect(() => {
  158. const loadImageFromProps = async () => {
  159. if (!stackViewport) return;
  160. // 避免重复加载相同图像
  161. if (imageId === currentImageId) return;
  162. if (imageId && imageId.trim() !== '') {
  163. console.log(`[DcmCell] 开始从 props 加载图像: ${imageId}`);
  164. const success = await loadImage(imageId, 'props');
  165. if (success) {
  166. setCurrentImageId(imageId);
  167. }
  168. } else {
  169. // imageId 为空时清空显示
  170. if (stackViewport) {
  171. try {
  172. // 清空 viewport 中的图像堆栈
  173. await stackViewport.setStack([], 0);
  174. stackViewport.render();
  175. } catch (error) {
  176. console.warn(`[DcmCell] 清空 viewport 时出错: ${error}`);
  177. }
  178. }
  179. setHasImage(false);
  180. setCurrentImageId(null);
  181. console.log(`[DcmCell] imageId 为空,清空显示`);
  182. }
  183. };
  184. loadImageFromProps();
  185. }, [imageId, stackViewport]);
  186. // 处理拖放事件(核心逻辑)
  187. const handleDrop = async (e) => {
  188. e.preventDefault();
  189. e.stopPropagation();
  190. // 1. 获取拖拽传递的序列标识
  191. const seriesInstanceUID = e.dataTransfer.getData('seriesInstanceUID');
  192. if (!seriesInstanceUID) return;
  193. try {
  194. // 2. 获取该序列的所有图像 ID(需替换为实际接口)
  195. const imageIds = getDcmImageUrl(seriesInstanceUID);
  196. console.log(`拖拽后要加载图像 是 ${imageIds}`);
  197. // 3. 使用统一的加载函数
  198. const success = await loadImage(imageIds, 'drag');
  199. if (success) {
  200. setCurrentImageId(imageIds);
  201. // 4. 通知到slice
  202. dispatch(addImageToCell({ index: indexOfCell, imageId: imageIds }));
  203. }
  204. } catch (error) {
  205. console.error('[DcmCell] 拖拽加载图像失败:', error);
  206. }
  207. };
  208. // 处理格子点击事件
  209. const handleCellClick = (e: React.MouseEvent) => {
  210. e.stopPropagation();
  211. dispatch(toggleCellSelection(cellId));
  212. };
  213. // 动态样式
  214. const containerStyle = {
  215. width: '100%',
  216. height: '600px',
  217. border: isSelected ? '3px solid #0066cc' : '2px dashed #666',
  218. borderRadius: '8px',
  219. position: 'relative' as const,
  220. cursor: 'pointer',
  221. backgroundColor: isSelected ? 'rgba(0, 102, 204, 0.1)' : 'transparent',
  222. boxShadow: isSelected ? '0 0 10px rgba(0, 102, 204, 0.3)' : 'none',
  223. transition: 'all 0.2s ease-in-out'
  224. };
  225. return (
  226. <div
  227. className="viewport-container"
  228. onClick={handleCellClick}
  229. onDragOver={(e) => {
  230. e.preventDefault(); // 允许放置
  231. e.dataTransfer.dropEffect = 'copy';
  232. }}
  233. onDrop={handleDrop}
  234. style={containerStyle}
  235. >
  236. {/* Cornerstone 渲染挂载点 */}
  237. <div ref={viewportRef} style={{ width: '100%', height: '100%' }} />
  238. {/* 拖拽提示文字 - 只在没有图像时显示 */}
  239. {!hasImage && (
  240. <div style={{
  241. position: 'absolute',
  242. top: '50%',
  243. left: '50%',
  244. transform: 'translate(-50%, -50%)',
  245. color: '#999',
  246. pointerEvents: 'none' // 防止遮挡鼠标事件
  247. }}>
  248. 拖拽序列到此处显示
  249. </div>
  250. )}
  251. </div>
  252. );
  253. };
  254. export default ViewportContainer;