|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useState, useEffect, ReactElement } from 'react';
|
|
|
+import React, { useEffect } from 'react';
|
|
|
import StackViewer, {
|
|
|
activateMagnifier,
|
|
|
addLMark,
|
|
@@ -25,6 +25,14 @@ import StackViewer, {
|
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
|
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,
|
|
@@ -79,255 +87,224 @@ interface ViewerContainerProps {
|
|
|
|
|
|
const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
console.log(`[ViewerContainer] 新渲染 imageUrls:`, imageUrls);
|
|
|
- const [selectedViewers, setSelectedViewers] = useState<number[]>([]);
|
|
|
- const [gridLayout, setGridLayout] = useState<string>('1x1');
|
|
|
+
|
|
|
+ // 从 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
|
|
|
);
|
|
|
- const bodyPositions = useSelector(
|
|
|
- (state: RootState) => state.bodyPositionList.bodyPositions
|
|
|
- );
|
|
|
- // eslint-disable-next-line
|
|
|
- const [viewersState, setViewersState] = useState<ReactElement[]>([]);
|
|
|
+
|
|
|
+
|
|
|
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(start + index, event)}
|
|
|
- style={{
|
|
|
- border: selectedViewers.includes(start + index)
|
|
|
- ? '2px solid blue'
|
|
|
- : '1px solid gray',
|
|
|
- cursor: 'pointer',
|
|
|
- }}
|
|
|
+ onClick={(event) => handleSelectViewer(url, event)}
|
|
|
>
|
|
|
- {/* <input
|
|
|
- type="checkbox"
|
|
|
- checked={selectedViewers.includes(index)}
|
|
|
- onChange={(event) => handleCheckboxChange(index, event)}
|
|
|
- style={{ marginRight: '8px' }}
|
|
|
- /> */}
|
|
|
<StackViewer
|
|
|
key={start + index}
|
|
|
imageIndex={0}
|
|
|
imageUrls={[url]}
|
|
|
- viewportId={`viewport-${start + index}`}
|
|
|
+ viewportId={getViewportIdByUrl(url) as string}
|
|
|
renderingEngineId={renderingEngineId}
|
|
|
+ selected={selectedViewerUrls.includes(url)}
|
|
|
/>
|
|
|
</div>
|
|
|
));
|
|
|
};
|
|
|
|
|
|
- // useEffect(() => {
|
|
|
-
|
|
|
- // }, [imageUrls]);
|
|
|
-
|
|
|
useEffect(() => {
|
|
|
renderGrid();
|
|
|
- }, [viewersState, selectedBodyPosition, gridLayout]);
|
|
|
+ }, [selectedBodyPosition, gridLayout]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (action) {
|
|
|
console.log(
|
|
|
`[ViewerContainer] 处理功能操作: ${action}, selectedViewers:`,
|
|
|
- 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':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- // viewersState[index].props.addLMark();
|
|
|
- addLMark(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ addLMark(viewportId);
|
|
|
});
|
|
|
-
|
|
|
break;
|
|
|
case 'Add R Mark':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- addRLabel(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ addRLabel(viewportId);
|
|
|
});
|
|
|
console.log('Adding R Mark');
|
|
|
break;
|
|
|
case 'Delete Selected Mark': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- deleteSelectedMark(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ deleteSelectedMark(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
case 'Horizontal Flip': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- HorizontalFlip(`viewport-${index}`);
|
|
|
+ console.log(`开始竖直翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ HorizontalFlip(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
case 'Vertical Flip': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- VerticalFlip(`viewport-${index}`);
|
|
|
+ console.log(`开始水平翻转 in viewer container : ${JSON.stringify(selectedViewportIds)}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ VerticalFlip(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
case 'Rotate Counterclockwise 90': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- RotateCounterclockwise90(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ RotateCounterclockwise90(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
case 'Rotate Clockwise 90':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- RotateClockwise90(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ RotateClockwise90(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Rotate Any Angle':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- rotateAnyAngle(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ rotateAnyAngle(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'AddMask':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- addMask(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ addMask(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Delete Digital Mask':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- remoteMask(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ remoteMask(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Adjust Brightness and Contrast':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- adjustBrightnessAndContrast(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ adjustBrightnessAndContrast(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Crop Selected Area':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // cropSelectedArea(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Cropping Selected Area');
|
|
|
break;
|
|
|
case 'Delete Mask':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // deleteMask(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Deleting Mask');
|
|
|
break;
|
|
|
case 'Image Comparison':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // imageComparison(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Comparing Images');
|
|
|
break;
|
|
|
case 'Invert Contrast':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- invertContrast(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ invertContrast(viewportId);
|
|
|
});
|
|
|
console.log('Inverting Contrast');
|
|
|
break;
|
|
|
case '1x1 Layout':
|
|
|
- setGridLayout('1x1');
|
|
|
- // renderGrid('1x1');
|
|
|
+ dispatch(setGridLayoutAction('1x1'));
|
|
|
console.log(`1x1 Layout`);
|
|
|
break;
|
|
|
case '1x2 Layout':
|
|
|
- setGridLayout('1x2');
|
|
|
+ dispatch(setGridLayoutAction('1x2'));
|
|
|
break;
|
|
|
case '2x1 Layout':
|
|
|
- setGridLayout('2x1');
|
|
|
+ dispatch(setGridLayoutAction('2x1'));
|
|
|
break;
|
|
|
case '2x2 Layout':
|
|
|
- setGridLayout('2x2');
|
|
|
+ dispatch(setGridLayoutAction('2x2'));
|
|
|
break;
|
|
|
case 'Magnifier': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- activateMagnifier(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ activateMagnifier(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
case 'Fit Size': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- fitImageSize(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ fitImageSize(viewportId);
|
|
|
});
|
|
|
console.log('Fitting Image Size');
|
|
|
break;
|
|
|
}
|
|
|
case 'Original Size': {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- setOriginalSize(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ setOriginalSize(viewportId);
|
|
|
});
|
|
|
console.log('Setting Image to Original Size');
|
|
|
break;
|
|
|
}
|
|
|
case 'Zoom Image':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // zoomImage(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Zooming Image');
|
|
|
break;
|
|
|
case 'Reset Cursor':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // resetCursor(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Resetting Cursor');
|
|
|
break;
|
|
|
case 'Pan':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // panImage(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Panning Image');
|
|
|
break;
|
|
|
case 'Invert Image':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- InvertImage(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ InvertImage(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Reset Image':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- ResetImage(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ ResetImage(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
case 'Snapshot':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // takeSnapshot(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Taking Snapshot');
|
|
|
break;
|
|
|
case 'Advanced Processing':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // advancedProcessing(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Performing Advanced Processing');
|
|
|
break;
|
|
|
case 'Musician':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // activateMusician(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Activating Musician');
|
|
|
break;
|
|
|
case 'Image Measurement':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // imageMeasurement(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Measuring Image');
|
|
|
break;
|
|
|
case 'More':
|
|
|
- // selectedViewers.forEach((index) => {
|
|
|
- // moreOptions(`viewport-${index}`);
|
|
|
- // });
|
|
|
console.log('Showing More Options');
|
|
|
break;
|
|
|
case 'Apply Colormap':
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- ApplyColormap(`viewport-${index}`);
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ ApplyColormap(viewportId);
|
|
|
});
|
|
|
break;
|
|
|
// ==================== 线段测量相关操作 ====================
|
|
|
case '线段测量':
|
|
|
- if (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- activateLengthMeasurement(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ activateLengthMeasurement(viewportId);
|
|
|
});
|
|
|
} else {
|
|
|
// 如果没有选中的 viewport,为所有可见的 viewport 激活
|
|
@@ -339,9 +316,9 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
console.log('Activating Length Measurement');
|
|
|
break;
|
|
|
case '清除测量':
|
|
|
- if (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- clearLengthMeasurements(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ clearLengthMeasurements(viewportId);
|
|
|
});
|
|
|
} else {
|
|
|
// 如果没有选中的 viewport,清除所有可见 viewport 的测量
|
|
@@ -353,9 +330,9 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
console.log('Clearing Length Measurements');
|
|
|
break;
|
|
|
case '停用线段测量':
|
|
|
- if (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- deactivateLengthMeasurement(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ deactivateLengthMeasurement(viewportId);
|
|
|
});
|
|
|
} else {
|
|
|
// 如果没有选中的 viewport,停用所有可见 viewport 的测量工具
|
|
@@ -371,24 +348,29 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
}
|
|
|
dispatch(clearAction()); //清理后可连续同一个action触发响应
|
|
|
}
|
|
|
- }, [action, selectedViewers, dispatch]);
|
|
|
+ }, [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 (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- const success = activateLengthMeasurement(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ const success = activateLengthMeasurement(viewportId);
|
|
|
if (success) {
|
|
|
dispatch(
|
|
|
setToolActive({
|
|
|
toolName: 'LengthTool',
|
|
|
- viewportId: `viewport-${index}`,
|
|
|
+ viewportId: viewportId,
|
|
|
})
|
|
|
);
|
|
|
}
|
|
@@ -412,9 +394,9 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
break;
|
|
|
|
|
|
case '清除测量':
|
|
|
- if (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- clearLengthMeasurements(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ clearLengthMeasurements(viewportId);
|
|
|
});
|
|
|
} else {
|
|
|
// 如果没有选中的 viewport,清除所有可见 viewport 的测量
|
|
@@ -427,14 +409,14 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
break;
|
|
|
|
|
|
case '停用线段测量':
|
|
|
- if (selectedViewers.length > 0) {
|
|
|
- selectedViewers.forEach((index) => {
|
|
|
- const success = deactivateLengthMeasurement(`viewport-${index}`);
|
|
|
+ if (selectedViewportIds.length > 0) {
|
|
|
+ selectedViewportIds.forEach((viewportId) => {
|
|
|
+ const success = deactivateLengthMeasurement(viewportId);
|
|
|
if (success) {
|
|
|
dispatch(
|
|
|
setToolInactive({
|
|
|
toolName: 'LengthTool',
|
|
|
- viewportId: `viewport-${index}`,
|
|
|
+ viewportId: viewportId,
|
|
|
})
|
|
|
);
|
|
|
}
|
|
@@ -473,7 +455,7 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
// 清理测量 action
|
|
|
dispatch(clearMeasurementAction());
|
|
|
}
|
|
|
- }, [measurementAction, selectedViewers, gridLayout, dispatch]);
|
|
|
+ }, [measurementAction, selectedViewerUrls, gridLayout, dispatch]);
|
|
|
|
|
|
/**
|
|
|
* 获取当前可见的 viewport 数量
|
|
@@ -492,57 +474,24 @@ const ViewerContainer: React.FC<ViewerContainerProps> = ({ imageUrls }) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleSelectViewer = (index: number, event) => {
|
|
|
- setSelectedViewers((prev) => {
|
|
|
- const newSelected = prev.includes(index)
|
|
|
- ? prev.filter((i) => i !== index)
|
|
|
- : [...prev, index];
|
|
|
-
|
|
|
- console.log(`handleSelectViewer : ${index}`);
|
|
|
- console.log(`selectedViewers 旧值:`, prev);
|
|
|
- console.log(`selectedViewers 新值:`, newSelected);
|
|
|
-
|
|
|
- // 使用计算后的新值来更新边框,避免闭包陷阱
|
|
|
- const viewerElement = event.currentTarget;
|
|
|
- if (viewerElement) {
|
|
|
- viewerElement.style.border = newSelected.includes(index)
|
|
|
- ? '2px solid blue'
|
|
|
- : '1px solid gray';
|
|
|
- }
|
|
|
-
|
|
|
- return newSelected;
|
|
|
- });
|
|
|
+ const handleSelectViewer = (imageUrl: string, event: React.MouseEvent) => {
|
|
|
+ console.log(`handleSelectViewer : ${imageUrl}`);
|
|
|
+ console.log(`selectedViewers 旧值:`, selectedViewerUrls);
|
|
|
+ dispatch(toggleViewerSelection(imageUrl));
|
|
|
};
|
|
|
|
|
|
- // const handleCheckboxChange = (index: number, event) => {
|
|
|
- // setSelectedViewers((prev) =>
|
|
|
- // event.target.checked ? [...prev, index] : prev.filter((i) => i !== index)
|
|
|
- // );
|
|
|
-
|
|
|
- // event.target.checked = !event.target.checked;
|
|
|
- // };
|
|
|
-
|
|
|
const renderGrid = () => {
|
|
|
console.log('Rendering layout', gridLayout);
|
|
|
switch (gridLayout) {
|
|
|
case '1x1': {
|
|
|
- const index = bodyPositions
|
|
|
- .filter((bp) => bp.dview.expose_status === 'Exposed')
|
|
|
- .findIndex(
|
|
|
- (bp) =>
|
|
|
- bp.sop_instance_uid === selectedBodyPosition?.sop_instance_uid
|
|
|
- );
|
|
|
- console.log(`查找到索引:${index}`);
|
|
|
- if (index !== -1) {
|
|
|
return (
|
|
|
<div
|
|
|
className="h-full w-full"
|
|
|
style={{ display: 'grid', gridTemplateColumns: '1fr' }}
|
|
|
>
|
|
|
- {renderViewers(index, index + 1)}
|
|
|
+ {renderViewers(0, 1)}
|
|
|
</div>
|
|
|
);
|
|
|
- }
|
|
|
break;
|
|
|
}
|
|
|
case '1x2':
|