123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- import React, { useEffect } from 'react';
- import StackViewer, {
- activateMagnifier,
- addLMark,
- addMark,
- addRLabel,
- addCustomMark,
- adjustBrightnessAndContrast,
- deleteSelectedMark,
- fitImageSize,
- HorizontalFlip,
- invertContrast,
- InvertImage,
- remoteMask,
- ResetImage,
- rotateAnyAngle,
- RotateClockwise90,
- RotateCounterclockwise90,
- setOriginalSize,
- VerticalFlip,
- activateLengthMeasurement,
- deactivateLengthMeasurement,
- clearLengthMeasurements,
- activateAngleMeasurement,
- activateTibialPlateauAngleMeasurement,
- activateDARAMeasurement,
- activateHipDIMeasurement,
- activateHipNHAAngleMeasurement,
- activateTPLOMeasurement,
- activateTTAMeasurement,
- activateCBLOMeasurement,
- activateHipCoverageMeasurement,
- activateHipDorsalCoverageMeasurement,
- } from './viewers/stack.image.viewer';
- import { useSelector, useDispatch } from 'react-redux';
- import { getDcmImageUrl } from '@/API/bodyPosition';
- import store, { RootState } from '@/states/store';
- import { clearAction } from '@/states/view/functionAreaSlice';
- import {
- selectGridLayout,
- selectSelectedViewers,
- selectAllViewerUrls,
- setGridLayout as setGridLayoutAction,
- setAllViewers,
- toggleViewerSelection,
- } from '@/states/view/viewerContainerSlice';
- import {
- clearMeasurementAction,
- selectCurrentMeasurementAction,
- setToolActive,
- setToolInactive,
- } from '@/states/view/measurementPanelSlice';
- import * as cornerstone from '@cornerstonejs/core';
- import * as cornerstoneTools from '@cornerstonejs/tools';
- import { SystemMode } from '@/states/systemModeSlice';
- import * as cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
- import { MeasurementToolManager } from '@/utils/measurementToolManager';
- const renderingEngineId = 'myRenderingEngine';
- // 测量工具配置映射
- interface MeasurementToolConfig {
- toolName: string;
- activateFunction: (viewportId: string) => boolean;
- logPrefix: string;
- }
- const MEASUREMENT_TOOL_CONFIGS: Record<string, MeasurementToolConfig> = {
- '胫骨平台夹角': {
- toolName: 'TibialPlateauAngleTool',
- activateFunction: activateTibialPlateauAngleMeasurement,
- logPrefix: 'TibialPlateauAngle',
- },
- '髋臼水平角': {
- toolName: 'DARAMeasurementTool',
- activateFunction: activateDARAMeasurement,
- logPrefix: 'DARA',
- },
- '髋关节牵引指数': {
- toolName: 'HipDIMeasurementTool',
- activateFunction: activateHipDIMeasurement,
- logPrefix: 'HipDI',
- },
- '髋关节水平角': {
- toolName: 'HipNHAAngleMeasurementTool',
- activateFunction: activateHipNHAAngleMeasurement,
- logPrefix: 'HipNHA',
- },
- '心锥比': {
- toolName: 'VHSMeasurementTool',
- activateFunction: MeasurementToolManager.activateVHSMeasurementTool,
- logPrefix: 'VHS',
- },
- '胫骨平台骨切开术': {
- toolName: 'TPLOMeasurementTool',
- activateFunction: activateTPLOMeasurement,
- logPrefix: 'TPLO',
- },
- '胫骨结节前移术': {
- toolName: 'TTAMeasurementTool',
- activateFunction: activateTTAMeasurement,
- logPrefix: 'TTA',
- },
- '水平截骨术': {
- toolName: 'CBLOMeasurementTool',
- activateFunction: activateCBLOMeasurement,
- logPrefix: 'CBLO',
- },
- '股骨头覆盖率': {
- toolName: 'HipCoverageMeasurementTool',
- activateFunction: activateHipCoverageMeasurement,
- logPrefix: 'HipCoverage',
- },
- '髋臼背覆盖': {
- toolName: 'HipDorsalCoverageTool',
- activateFunction: activateHipDorsalCoverageMeasurement,
- logPrefix: 'HipDorsalCoverage',
- },
- };
- const setup = () => {
- // 初始化 Cornerstone
- cornerstone.init();
- cornerstoneTools.init();
- const state = store.getState();
- console.log(`当前系统模式:${state.systemMode.mode}`);
- const token =
- state.systemMode.mode === SystemMode.Emergency
- ? state.product.guest
- : state.userInfo.token;
- console.log(`token stack.image.viewer: ${token}`);
- cornerstoneDICOMImageLoader.init({
- maxWebWorkers: navigator.hardwareConcurrency || 1,
- errorInterceptor: (error) => {
- if (error.status === 401) {
- console.error('Authentication failed. Please refresh the token.');
- }
- console.error(`请求dcm文件出错:${error}`);
- },
- beforeSend: (xhr, imageId, defaultHeaders) => {
- return {
- ...defaultHeaders,
- Authorization: `Bearer ${token}`,
- Language: 'en',
- Product: 'DROS',
- Source: 'Electron',
- };
- },
- });
- // 创建渲染引擎
- new cornerstone.RenderingEngine(renderingEngineId);
- };
- setup();
- interface ViewerContainerProps {
- imageUrls: string[];
- }
- const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
- console.log(`[ViewerContainer] 新渲染 imageUrls:`, imageUrls);
- // 从 Redux 获取状态
- const gridLayout = useSelector(selectGridLayout);
- const selectedViewerUrls = useSelector(selectSelectedViewers);
- const allViewerUrls = useSelector(selectAllViewerUrls);
- const action = useSelector((state: RootState) => state.functionArea.action);
- const measurementAction = useSelector(selectCurrentMeasurementAction);
- const dispatch = useDispatch();
- const selectedBodyPosition = useSelector(
- (state: RootState) => state.bodyPositionList.selectedBodyPosition
- );
- console.log(`[ViewerContainer] rerendered]`);
- // 当 imageUrls 改变时,更新 Redux 中的 allViewers
- useEffect(() => {
- if (imageUrls.length > 0 && JSON.stringify(imageUrls) !== JSON.stringify(allViewerUrls)) {
- dispatch(setAllViewers(imageUrls));
- }
- }, [imageUrls, allViewerUrls, dispatch]);
- // 将 imageUrl 转换为 viewportId
- const getViewportIdByUrl = (url: string): string | null => {
- return `viewport-${url}`;
- };
- const renderViewers = (start: number, end: number) => {
- console.log(`Rendering viewers from ${start} to ${end}`);
- return imageUrls.slice(start, end).map((url, index) => (
- <div
- key={start + index}
- onClick={(event) => handleSelectViewer(url, event)}
- >
- <StackViewer
- key={start + index}
- imageIndex={0}
- imageUrls={[url]}
- viewportId={getViewportIdByUrl(url) as string}
- renderingEngineId={renderingEngineId}
- selected={selectedViewerUrls.includes(url)}
- />
- </div>
- ));
- };
- /**
- * 通用测量工具激活函数
- * @param config 测量工具配置
- * @param selectedViewportIds 选中的 viewport IDs
- */
- const activateMeasurementTool = (
- config: MeasurementToolConfig,
- selectedViewportIds: string[]
- ) => {
- console.log(`开始${config.logPrefix}测量`);
- const viewportIds = selectedViewportIds.length > 0
- ? selectedViewportIds
- : Array.from({ length: getVisibleViewportCount() }, (_, i) => `viewport-${i}`);
- viewportIds.forEach((viewportId) => {
- const success = config.activateFunction(viewportId);
- if (success) {
- console.log(`激活${config.logPrefix}测量工具成功`);
- dispatch(
- setToolActive({
- toolName: config.toolName,
- viewportId: viewportId,
- })
- );
- }
- });
- console.log(`Activating ${config.logPrefix} Measurement from MeasurementPanel`);
- };
- /**
- * 获取当前可见的 viewport 数量
- */
- const getVisibleViewportCount = (): number => {
- switch (gridLayout) {
- case '1x1':
- return 1;
- case '1x2':
- case '2x1':
- return Math.min(2, imageUrls.length);
- case '2x2':
- return Math.min(4, imageUrls.length);
- default:
- return 1;
- }
- };
- useEffect(() => {
- if (action) {
- console.log(
- `[ViewerContainer] 处理功能操作: ${action}, selectedViewers:`,
- selectedViewerUrls
- );
- // 将选中的 imageUrl 转换为 viewportId
- const selectedViewportIds = selectedViewerUrls
- .map(getViewportIdByUrl)
- .filter((id): id is string => id !== null);
- // Handle the action
- switch (action) {
- case 'Add L Mark':
- selectedViewportIds.forEach((viewportId) => {
- addLMark(viewportId);
- });
- break;
- case 'Add R Mark':
- selectedViewportIds.forEach((viewportId) => {
- addRLabel(viewportId);
- });
- console.log('Adding R Mark');
- break;
- case 'Delete Selected Mark': {
- selectedViewportIds.forEach((viewportId) => {
- deleteSelectedMark(viewportId);
- });
- break;
- }
- case 'Horizontal Flip': {
- console.log(`开始竖直翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
- selectedViewportIds.forEach((viewportId) => {
- HorizontalFlip(viewportId);
- });
- break;
- }
- case 'Vertical Flip': {
- console.log(`开始水平翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
- selectedViewportIds.forEach((viewportId) => {
- VerticalFlip(viewportId);
- });
- break;
- }
- case 'Rotate Counterclockwise 90': {
- selectedViewportIds.forEach((viewportId) => {
- RotateCounterclockwise90(viewportId);
- });
- break;
- }
- case 'Rotate Clockwise 90':
- selectedViewportIds.forEach((viewportId) => {
- RotateClockwise90(viewportId);
- });
- break;
- case 'Rotate Any Angle':
- selectedViewportIds.forEach((viewportId) => {
- rotateAnyAngle(viewportId);
- });
- break;
- case 'AddMark':
- selectedViewportIds.forEach((viewportId) => {
- addMark(viewportId);
- });
- break;
- case 'AddCustomMark':
- // 从markPanel状态获取选中的标记文本(用于自定义标记)
- const selectedMarkText = store.getState().markPanel.selectedMark;
- if (selectedMarkText) {
- selectedViewportIds.forEach((viewportId) => {
- addCustomMark(viewportId, selectedMarkText);
- });
- }
- break;
- case 'AddPredefinedMark':
- // 从action payload中提取预定义标记文本
- const predefinedMarkMatch = action.match(/AddPredefinedMark:(.+)/);
- if (predefinedMarkMatch) {
- const markText = predefinedMarkMatch[1];
- selectedViewportIds.forEach((viewportId) => {
- addCustomMark(viewportId, markText);
- });
- }
- break;
- case 'AddTimestamp':
- const now = new Date();
- const timestamp = now.getFullYear() + '-' +
- String(now.getMonth() + 1).padStart(2, '0') + '-' +
- String(now.getDate()).padStart(2, '0') + ' ' +
- String(now.getHours()).padStart(2, '0') + ':' +
- String(now.getMinutes()).padStart(2, '0') + ':' +
- String(now.getSeconds()).padStart(2, '0');
- selectedViewportIds.forEach((viewportId) => {
- addCustomMark(viewportId, timestamp);
- });
- break;
- case 'Delete Digital Mask':
- selectedViewportIds.forEach((viewportId) => {
- remoteMask(viewportId);
- });
- break;
- case 'Adjust Brightness and Contrast':
- selectedViewportIds.forEach((viewportId) => {
- adjustBrightnessAndContrast(viewportId);
- });
- break;
- case 'Crop Selected Area':
- console.log('Cropping Selected Area');
- break;
- case 'Delete Mask':
- console.log('Deleting Mask');
- break;
- case 'Image Comparison':
- console.log('Comparing Images');
- break;
- case 'Invert Contrast':
- selectedViewportIds.forEach((viewportId) => {
- invertContrast(viewportId);
- });
- console.log('Inverting Contrast');
- break;
- case '1x1 Layout':
- dispatch(setGridLayoutAction('1x1'));
- console.log(`1x1 Layout`);
- break;
- case '1x2 Layout':
- dispatch(setGridLayoutAction('1x2'));
- break;
- case '2x1 Layout':
- dispatch(setGridLayoutAction('2x1'));
- break;
- case '2x2 Layout':
- dispatch(setGridLayoutAction('2x2'));
- break;
- case 'Magnifier': {
- selectedViewportIds.forEach((viewportId) => {
- activateMagnifier(viewportId);
- });
- break;
- }
- case 'Fit Size': {
- selectedViewportIds.forEach((viewportId) => {
- fitImageSize(viewportId);
- });
- console.log('Fitting Image Size');
- break;
- }
- case 'Original Size': {
- selectedViewportIds.forEach((viewportId) => {
- setOriginalSize(viewportId);
- });
- console.log('Setting Image to Original Size');
- break;
- }
- case 'Zoom Image':
- console.log('Zooming Image');
- break;
- case 'Reset Cursor':
- console.log('Resetting Cursor');
- break;
- case 'Pan':
- console.log('Panning Image');
- break;
- case 'Invert Image':
- selectedViewportIds.forEach((viewportId) => {
- InvertImage(viewportId);
- });
- break;
- case 'Reset Image':
- selectedViewportIds.forEach((viewportId) => {
- ResetImage(viewportId);
- });
- break;
- case 'Snapshot':
- console.log('Taking Snapshot');
- break;
- case 'Advanced Processing':
- console.log('Performing Advanced Processing');
- break;
- case 'Musician':
- console.log('Activating Musician');
- break;
- case 'Image Measurement':
- console.log('Measuring Image');
- break;
- case 'More':
- console.log('Showing More Options');
- break;
- // ==================== 线段测量相关操作 ====================
- case '线段测量':
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- activateLengthMeasurement(viewportId);
- });
- } else {
- // 如果没有选中的 viewport,为所有可见的 viewport 激活
- const visibleViewportCount = getVisibleViewportCount();
- for (let i = 0; i < visibleViewportCount; i++) {
- activateLengthMeasurement(`viewport-${i}`);
- }
- }
- console.log('Activating Length Measurement');
- break;
- case '清除测量':
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- clearLengthMeasurements(viewportId);
- });
- } else {
- // 如果没有选中的 viewport,清除所有可见 viewport 的测量
- const visibleViewportCount = getVisibleViewportCount();
- for (let i = 0; i < visibleViewportCount; i++) {
- clearLengthMeasurements(`viewport-${i}`);
- }
- }
- console.log('Clearing Length Measurements');
- break;
- case '停用线段测量':
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- deactivateLengthMeasurement(viewportId);
- });
- } else {
- // 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
- const visibleViewportCount = getVisibleViewportCount();
- for (let i = 0; i < visibleViewportCount; i++) {
- deactivateLengthMeasurement(`viewport-${i}`);
- }
- }
- console.log('Deactivating Length Measurement');
- break;
- default:
- break;
- }
- dispatch(clearAction()); //清理后可连续同一个action触发响应
- }
- }, [action, selectedViewerUrls, dispatch]);
- // ==================== 测量面板 Action 处理 ====================
- useEffect(() => {
- if (measurementAction) {
- console.log(`[ViewerContainer] 处理测量操作: ${measurementAction}`);
- // 将选中的 imageUrl 转换为 viewportId
- const selectedViewportIds = selectedViewerUrls
- .map(getViewportIdByUrl)
- .filter((id): id is string => id !== null);
- // 处理测量相关操作
- switch (measurementAction) {
- case '线段测量':
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- const success = activateLengthMeasurement(viewportId);
- if (success) {
- dispatch(
- setToolActive({
- toolName: 'LengthTool',
- viewportId: viewportId,
- })
- );
- }
- });
- } else {
- // 如果没有选中的 viewport,为所有可见的 viewport 激活
- const visibleViewportCount = getVisibleViewportCount();
- for (let i = 0; i < visibleViewportCount; i++) {
- const success = activateLengthMeasurement(`viewport-${i}`);
- if (success) {
- dispatch(
- setToolActive({
- toolName: 'LengthTool',
- viewportId: `viewport-${i}`,
- })
- );
- }
- }
- }
- console.log('Activating Length Measurement from MeasurementPanel');
- break;
- case '清除测量':
- if (selectedViewportIds.length > 0) {
- MeasurementToolManager.clearLengthMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearAngleMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearTibialPlateauAngleMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearDARAMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearHipDIMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearHipNHAAngleMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearVHSMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearTPLOMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearTTAMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearCBLOMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearHipCoverageMeasurementsForViewports(selectedViewportIds);
- MeasurementToolManager.clearHipDorsalCoverageMeasurementsForViewports(selectedViewportIds);
- }
- console.log('Clearing All Measurements from MeasurementPanel');
- break;
- case '停用线段测量':
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- const success = deactivateLengthMeasurement(viewportId);
- if (success) {
- dispatch(
- setToolInactive({
- toolName: 'LengthTool',
- viewportId: viewportId,
- })
- );
- }
- });
- }
- console.log('Deactivating Length Measurement from MeasurementPanel');
- break;
- case '角度测量':
- console.log(`开始角度测量`);
- if (selectedViewportIds.length > 0) {
- selectedViewportIds.forEach((viewportId) => {
- const success = activateAngleMeasurement(viewportId);
- if (success) {
- console.log(`激活角度测量工具成功`);
- dispatch(
- setToolActive({
- toolName: 'AngleTool',
- viewportId: viewportId,
- })
- );
- }
- });
- }
- console.log('Activating Angle Measurement from MeasurementPanel');
- break;
- case '胫骨平台夹角':
- case '髋臼水平角':
- case '髋关节牵引指数':
- case '髋关节水平角':
- case '心锥比':
- case '胫骨平台骨切开术':
- case '胫骨结节前移术':
- case '水平截骨术':
- case '股骨头覆盖率':
- case '髋臼背覆盖': {
- const config = MEASUREMENT_TOOL_CONFIGS[measurementAction];
- if (config) {
- activateMeasurementTool(config, selectedViewportIds);
- }
- break;
- }
- case '测量校正':
- console.log('Measurement Calibration - 功能待实现');
- break;
- default:
- console.log(`未处理的测量操作: ${measurementAction}`);
- break;
- }
- // 清理测量 action
- dispatch(clearMeasurementAction());
- }
- }, [measurementAction, selectedViewerUrls, gridLayout, dispatch]);
- const handleSelectViewer = (imageUrl: string, event: React.MouseEvent) => {
- console.log(`handleSelectViewer : ${imageUrl}`);
- console.log(`selectedViewers 旧值:`, selectedViewerUrls);
- dispatch(toggleViewerSelection(imageUrl));
- };
- const renderGrid = () => {
- console.log('Rendering layout', gridLayout);
- switch (gridLayout) {
- case '1x1': {
- // 变成单分格时,选中对应 selectedBodyPosition 的 URL
- let urlToSelect = allViewerUrls[0]; // 默认选第一个作为fallback
- if (selectedBodyPosition?.sop_instance_uid) {
- const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
- const urlIndex = allViewerUrls.indexOf(correspondingUrl);
- if (urlIndex !== -1) {
- urlToSelect = allViewerUrls[urlIndex];
- }
- }
- // 检查当前选中状态,避免无限循环
- const currentSelected = selectedViewerUrls[0];
- if (currentSelected !== urlToSelect) {
- if (currentSelected) {
- dispatch(toggleViewerSelection(currentSelected)); // 取消选中当前
- }
- dispatch(toggleViewerSelection(urlToSelect)); // 选中新的
- }
- // 找到体位列表中选中体位对应的url
- let viewerIndex = 0;
- if (selectedBodyPosition?.sop_instance_uid) {
- const correspondingUrl = getDcmImageUrl(selectedBodyPosition.sop_instance_uid);
- const urlIndex = imageUrls.indexOf(correspondingUrl);
- if (urlIndex !== -1) {
- viewerIndex = urlIndex;
- }
- }
- return (
- <div
- className="h-full w-full"
- style={{ display: 'grid', gridTemplateColumns: '1fr' }}
- >
- {renderViewers(viewerIndex, viewerIndex + 1)}
- </div>
- );
- break;
- }
- case '1x2':
- return (
- <div
- className="h-full w-full"
- style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}
- >
- {renderViewers(0, 2)}
- </div>
- );
- case '2x1':
- return (
- <div className="h-full w-full grid grid-cols-1 grid-rows-2">
- {renderViewers(0, 2)}
- </div>
- );
- case '2x2':
- return (
- <div
- className="h-full w-full"
- style={{
- display: 'grid',
- gridTemplateColumns: '1fr 1fr',
- gridTemplateRows: '1fr 1fr',
- }}
- >
- {renderViewers(0, 4)}
- </div>
- );
- default:
- return null;
- }
- };
- return <>{renderGrid()}</>;
- };
- export default ViewerContainer;
|