DcmCell.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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. // 监听Redux中当前格子的图像ID状态
  51. const currentCellImageId = useAppSelector(state => {
  52. const film = state.print.films.find(f => f.id === currentFilm.id);
  53. return film?.images[indexOfCell] || null;
  54. });
  55. // 统一的图像加载函数,供两种场景使用
  56. const loadImage = async (targetImageId: string, source: 'props' | 'drag' = 'props'): Promise<boolean> => {
  57. if (!stackViewport || !targetImageId) return false;
  58. try {
  59. const fullImageUrl = targetImageId.startsWith('dicomweb:')
  60. ? targetImageId
  61. : getDcmImageUrl(targetImageId);
  62. await stackViewport.setStack([fullImageUrl], 0);
  63. stackViewport.render();
  64. setHasImage(true);
  65. console.log(`[DcmCell] 图像加载成功 (${source}): ${targetImageId}`);
  66. return true;
  67. } catch (error) {
  68. console.error(`[DcmCell] 图像加载失败 (${source}):`, error);
  69. setHasImage(false);
  70. return false;
  71. }
  72. };
  73. // 初始化 Cornerstone 渲染引擎
  74. useEffect(() => {
  75. // initImageLoader();
  76. // 创建胶片特定的渲染引擎实例,确保状态隔离
  77. const engineId = `renderingEngine-${currentFilm.id}`;
  78. let engine: RenderingEngine | null = cornerstone.getRenderingEngines()?.find(en => en.id === engineId) || null;
  79. // 不存在则创建
  80. if (!engine) {
  81. engine = new RenderingEngine(engineId);
  82. }
  83. setRenderingEngine(engine);
  84. // 配置 Viewport
  85. if (viewportRef.current) {
  86. const viewportInput: cornerstone.Types.PublicViewportInput = {
  87. viewportId: viewportId,
  88. type: Enums.ViewportType.STACK, // 序列视图类型
  89. element: viewportRef.current,
  90. defaultOptions: {
  91. background: [0.1, 0.1, 0.1], // 深灰背景
  92. },
  93. };
  94. // 绑定 Viewport 到引擎
  95. engine.enableElement(viewportInput);
  96. // 获取 Viewport 实例
  97. const viewport = engine.getViewport(viewportId);
  98. setStackViewport(viewport);
  99. // ========== 添加工具支持 ==========
  100. const toolGroupId = `PRINT_TOOL_GROUP_${viewportId}`;
  101. // 创建工具组
  102. const toolGroup = cornerstoneTools.ToolGroupManager.createToolGroup(toolGroupId);
  103. if (toolGroup) {
  104. // 添加基础工具
  105. toolGroup.addTool(cornerstoneTools.StackScrollTool.toolName);
  106. toolGroup.addTool(cornerstoneTools.ZoomTool.toolName);
  107. toolGroup.addTool(cornerstoneTools.PanTool.toolName);
  108. toolGroup.addTool(cornerstoneTools.WindowLevelTool.toolName);
  109. // 激活滚轮滚动工具(解决滚轮错误)
  110. toolGroup.setToolActive(cornerstoneTools.StackScrollTool.toolName, {
  111. bindings: [{
  112. mouseButton: cornerstoneTools.Enums.MouseBindings.Wheel
  113. }]
  114. });
  115. // 激活缩放工具(右键)
  116. toolGroup.setToolActive(cornerstoneTools.ZoomTool.toolName, {
  117. bindings: [{
  118. mouseButton: cornerstoneTools.Enums.MouseBindings.Secondary
  119. }]
  120. });
  121. // 激活平移工具(中键)
  122. toolGroup.setToolActive(cornerstoneTools.PanTool.toolName, {
  123. bindings: [{
  124. mouseButton: cornerstoneTools.Enums.MouseBindings.Auxiliary
  125. }]
  126. });
  127. // 其他工具设置为被动状态
  128. toolGroup.setToolPassive(cornerstoneTools.WindowLevelTool.toolName);
  129. // 将 viewport 添加到工具组
  130. toolGroup.addViewport(viewportId, engineId);
  131. console.log(`[DcmCell] 工具组已创建并配置: ${toolGroupId}`);
  132. }
  133. }
  134. // 组件卸载时清理
  135. return () => {
  136. // 销毁工具组
  137. const toolGroupId = `PRINT_TOOL_GROUP_${viewportId}`;
  138. const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
  139. if (toolGroup) {
  140. try {
  141. // 先从工具组中移除 viewport
  142. toolGroup.removeViewports(`renderingEngine-${currentFilm.id}`, viewportId);
  143. // 然后销毁工具组
  144. cornerstoneTools.ToolGroupManager.destroyToolGroup(toolGroupId);
  145. console.log(`[DcmCell] 工具组已销毁: ${toolGroupId}`);
  146. } catch (error) {
  147. console.warn(`[DcmCell] 清理工具组时出错: ${error}`);
  148. }
  149. }
  150. // 清理 viewport
  151. if (engine) {
  152. try {
  153. engine.disableElement(viewportId);
  154. console.log(`[DcmCell] viewport 已禁用: ${viewportId}`);
  155. } catch (error) {
  156. console.warn(`[DcmCell] 禁用 viewport 时出错: ${error}`);
  157. }
  158. }
  159. };
  160. }, []);
  161. // 监听 imageId 变化,自动加载图像
  162. useEffect(() => {
  163. const loadImageFromProps = async () => {
  164. if (!stackViewport) return;
  165. // 避免重复加载相同图像
  166. if (imageId === currentImageId) return;
  167. if (imageId && imageId.trim() !== '') {
  168. console.log(`[DcmCell] 开始从 props 加载图像: ${imageId}`);
  169. const success = await loadImage(imageId, 'props');
  170. if (success) {
  171. setCurrentImageId(imageId);
  172. }
  173. } else {
  174. // imageId 为空时清空显示
  175. if (stackViewport) {
  176. try {
  177. // 清空 viewport 中的图像堆栈
  178. // await stackViewport.setStack([]);
  179. // stackViewport.render();
  180. renderingEngine.disableElement(viewportId);
  181. } catch (error) {
  182. console.warn(`[DcmCell] 清空 viewport 时出错: ${error}`);
  183. }
  184. }
  185. setHasImage(false);
  186. setCurrentImageId(null);
  187. console.log(`[DcmCell] imageId 为空,清空显示`);
  188. }
  189. };
  190. loadImageFromProps();
  191. }, [imageId, stackViewport]);
  192. // 监听Redux状态变化,当图像被删除时清理viewport
  193. useEffect(() => {
  194. if (!stackViewport) return;
  195. // 如果Redux中的图像ID为null,但viewport还有图像,则清理
  196. if (!currentCellImageId && hasImage) {
  197. try {
  198. // 清空viewport的图像栈
  199. // stackViewport.setStack([]);
  200. // stackViewport.render();
  201. renderingEngine.disableElement(viewportId);
  202. setHasImage(false);
  203. setCurrentImageId(null);
  204. console.log(`[DcmCell] 清理viewport: ${cellId} (图像已被删除)`);
  205. } catch (error) {
  206. console.warn(`[DcmCell] 清理viewport时出错:`, error);
  207. }
  208. }
  209. }, [currentCellImageId, stackViewport, hasImage, cellId]);
  210. // 处理拖放事件(核心逻辑)
  211. const handleDrop = async (e) => {
  212. e.preventDefault();
  213. e.stopPropagation();
  214. // 1. 获取拖拽传递的序列标识
  215. const seriesInstanceUID = e.dataTransfer.getData('seriesInstanceUID');
  216. if (!seriesInstanceUID) return;
  217. try {
  218. // 2. 获取该序列的所有图像 ID(需替换为实际接口)
  219. const imageIds = getDcmImageUrl(seriesInstanceUID);
  220. console.log(`拖拽后要加载图像 是 ${imageIds}`);
  221. // 3. 使用统一的加载函数
  222. const success = await loadImage(imageIds, 'drag');
  223. if (success) {
  224. setCurrentImageId(imageIds);
  225. // 4. 通知到slice
  226. dispatch(addImageToCell({ index: indexOfCell, imageId: imageIds }));
  227. }
  228. } catch (error) {
  229. console.error('[DcmCell] 拖拽加载图像失败:', error);
  230. }
  231. };
  232. // 处理格子点击事件
  233. const handleCellClick = (e: React.MouseEvent) => {
  234. e.stopPropagation();
  235. dispatch(toggleCellSelection(cellId));
  236. };
  237. // 动态样式
  238. const containerStyle = {
  239. width: '100%',
  240. height: '600px',
  241. border: isSelected ? '3px solid #0066cc' : '2px dashed #666',
  242. borderRadius: '8px',
  243. position: 'relative' as const,
  244. cursor: 'pointer',
  245. backgroundColor: isSelected ? 'rgba(0, 102, 204, 0.1)' : 'transparent',
  246. boxShadow: isSelected ? '0 0 10px rgba(0, 102, 204, 0.3)' : 'none',
  247. transition: 'all 0.2s ease-in-out'
  248. };
  249. return (
  250. <div
  251. className="viewport-container"
  252. onClick={handleCellClick}
  253. onDragOver={(e) => {
  254. e.preventDefault(); // 允许放置
  255. e.dataTransfer.dropEffect = 'copy';
  256. }}
  257. onDrop={handleDrop}
  258. style={containerStyle}
  259. >
  260. {/* Cornerstone 渲染挂载点 */}
  261. <div ref={viewportRef} style={{ width: '100%', height: '100%' }} />
  262. {/* 拖拽提示文字 - 基于Redux状态判断是否显示 */}
  263. {!currentCellImageId && !hasImage && (
  264. <div style={{
  265. position: 'absolute',
  266. top: '50%',
  267. left: '50%',
  268. transform: 'translate(-50%, -50%)',
  269. color: '#999',
  270. pointerEvents: 'none' // 防止遮挡鼠标事件
  271. }}>
  272. 拖拽序列到此处显示
  273. </div>
  274. )}
  275. </div>
  276. );
  277. };
  278. export default ViewportContainer;