|
|
@@ -16,7 +16,9 @@ import TTAMeasurementTool from '@/components/measures/TTAMeasurementTool';
|
|
|
import CBLOMeasurementTool from '@/components/measures/CBLOMeasurementTool';
|
|
|
import HipCoverageMeasurementTool from '@/components/measures/HipCoverageMeasurementTool';
|
|
|
import HipDorsalCoverageTool from '@/components/measures/HipDorsalCoverageTool';
|
|
|
+import ImageViewerErrorBoundary from './ImageViewerErrorBoundary';
|
|
|
import { boolean } from 'zod';
|
|
|
+import { EVENTS } from '@cornerstonejs/core';
|
|
|
|
|
|
const {
|
|
|
MagnifyTool,
|
|
|
@@ -34,6 +36,64 @@ const {
|
|
|
const { MouseBindings } = csToolsEnums;
|
|
|
// let toolGroup: cornerstoneTools.Types.IToolGroup;
|
|
|
// let currentViewportId: string;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 增强错误信息,添加更多上下文
|
|
|
+ */
|
|
|
+function enhanceError(
|
|
|
+ error: unknown,
|
|
|
+ context: {
|
|
|
+ imageUrls: string[];
|
|
|
+ imageIndex: number;
|
|
|
+ viewportId: string;
|
|
|
+ }
|
|
|
+): Error {
|
|
|
+ const baseError = error instanceof Error ? error : new Error(String(error));
|
|
|
+ const { imageUrls, imageIndex, viewportId } = context;
|
|
|
+ const contextInfo = {
|
|
|
+ imageUrls,
|
|
|
+ imageIndex,
|
|
|
+ viewportId,
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ userAgent: navigator.userAgent
|
|
|
+ };
|
|
|
+
|
|
|
+ // 创建增强的错误消息
|
|
|
+ const enhancedMessage = `[${context.viewportId}] 图像加载失败: ${baseError.message}
|
|
|
+ 上下文信息: ${JSON.stringify(contextInfo, null, 2)}`;
|
|
|
+
|
|
|
+ const enhancedError = new Error(enhancedMessage);
|
|
|
+ enhancedError.name = `ImageLoadError`;
|
|
|
+ enhancedError.stack = `${enhancedError.name}: ${enhancedMessage}\n原错误堆栈: ${baseError.stack}`;
|
|
|
+
|
|
|
+ return enhancedError;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 判断是否为严重错误,需要抛出给 Error Boundary
|
|
|
+ */
|
|
|
+function isCriticalError(error: Error): boolean {
|
|
|
+ const message = error.message.toLowerCase();
|
|
|
+
|
|
|
+ // 以下类型的错误被认为是严重的,需要用户干预
|
|
|
+ const criticalErrors = [
|
|
|
+ '图像url列表为空',
|
|
|
+ '图像索引无效',
|
|
|
+ '401', // 认证失败
|
|
|
+ '403', // 权限不足
|
|
|
+ '404', // 文件不存在
|
|
|
+ '500', // 服务器错误
|
|
|
+ '认证失败',
|
|
|
+ '权限不足',
|
|
|
+ 'unauthorized',
|
|
|
+ 'forbidden',
|
|
|
+ 'not found',
|
|
|
+ 'internal server error'
|
|
|
+ ];
|
|
|
+
|
|
|
+ return criticalErrors.some(keyword => message.includes(keyword));
|
|
|
+}
|
|
|
+
|
|
|
function getToolgroupByViewportId(currentViewportId: string) {
|
|
|
const toolGroup = ToolGroupManager.getToolGroup(
|
|
|
`STACK_TOOL_GROUP_ID_${currentViewportId}`
|
|
|
@@ -1079,6 +1139,67 @@ export function clearHipDorsalCoverageMeasurements(viewportId: string): boolean
|
|
|
console.log(`[clearHipDorsalCoverageMeasurements] Clearing HipDorsalCoverage measurements for viewport: ${viewportId}`);
|
|
|
return MeasurementToolManager.clearHipDorsalCoverageMeasurements(viewportId);
|
|
|
}
|
|
|
+export class ImageLoadError extends Error {
|
|
|
+ constructor(message: string, failedImageIds: string[]) {
|
|
|
+ super(message);
|
|
|
+ this.name = 'ImageLoadError';
|
|
|
+ this.failedImageIds = failedImageIds; // 附加失败 ID 列表
|
|
|
+ }
|
|
|
+ failedImageIds: string[];
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 安全的图像栈设置函数,带超时保护和错误捕获
|
|
|
+ * @param viewport - Cornerstone viewport 实例
|
|
|
+ * @param imageIds - 图像 ID 数组
|
|
|
+ * @param imageIndex - 当前图像索引
|
|
|
+ * @param timeout - 超时时间(毫秒),默认30秒
|
|
|
+ */
|
|
|
+export async function safeSetStack(
|
|
|
+ viewport: any,
|
|
|
+ imageIds: string[],
|
|
|
+ imageIndex: number,
|
|
|
+ timeout: number = 30000
|
|
|
+): Promise<void> {
|
|
|
+ const errors: string[] = [];
|
|
|
+
|
|
|
+ // 错误事件处理器
|
|
|
+ const handler = (evt: any) => {
|
|
|
+ console.error(`捕获到图像加载错误: ${evt.detail.imageId}`);
|
|
|
+ errors.push(evt.detail.imageId);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加错误监听器
|
|
|
+ eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, handler);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建超时 Promise
|
|
|
+ const timeoutPromise = new Promise<never>((_, reject) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ reject(new Error(`图像加载超时 (${timeout}ms)`));
|
|
|
+ }, timeout);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 等待 setStack 完成或超时
|
|
|
+ await Promise.race([
|
|
|
+ viewport.setStack(imageIds, imageIndex),
|
|
|
+ timeoutPromise
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // setStack 完成后,检查是否有错误
|
|
|
+ if (errors.length > 0) {
|
|
|
+ throw new ImageLoadError(
|
|
|
+ `图像加载失败:共 ${errors.length} 张(${errors.join(', ')})`,
|
|
|
+ errors
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`✅ 图像栈设置成功: ${imageIds.length} 张图像`);
|
|
|
+ } finally {
|
|
|
+ // 无论成功失败,都移除监听器(清理资源)
|
|
|
+ eventTarget.removeEventListener(EVENTS.IMAGE_LOAD_ERROR, handler);
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
const StackViewer = ({
|
|
|
imageIndex = 0,
|
|
|
@@ -1094,6 +1215,8 @@ const StackViewer = ({
|
|
|
selected?: boolean;
|
|
|
}) => {
|
|
|
const elementRef = useRef<HTMLDivElement>(null);
|
|
|
+ // 用于捕获异步错误并在渲染时抛出,让 Error Boundary 能够捕获
|
|
|
+ const [renderError, setRenderError] = React.useState<Error | null>(null);
|
|
|
// const action = useSelector((state: RootState) => state.functionArea.action);
|
|
|
// const dispatch = useDispatch();
|
|
|
|
|
|
@@ -1117,6 +1240,9 @@ const StackViewer = ({
|
|
|
|
|
|
useEffect(() => {
|
|
|
const setup = async () => {
|
|
|
+ // 清除之前的错误状态(如果有)
|
|
|
+ setRenderError(null);
|
|
|
+
|
|
|
// // 初始化 Cornerstone
|
|
|
// cornerstone.init();
|
|
|
// cornerstoneTools.init();
|
|
|
@@ -1190,26 +1316,60 @@ const StackViewer = ({
|
|
|
viewportId
|
|
|
) as cornerstone.Types.IStackViewport;
|
|
|
|
|
|
- // 给定一个dcm文件路径,加载并显示出来
|
|
|
+ // 增强的图像加载逻辑,包含完善的错误处理
|
|
|
try {
|
|
|
console.log(`重新加载图像----开始`);
|
|
|
- await viewport.setStack(imageUrls, imageIndex);
|
|
|
+ console.log(`图像URLs:`, imageUrls);
|
|
|
+ console.log(`图像索引:`, imageIndex);
|
|
|
+
|
|
|
+ // 1. 验证图像URLs
|
|
|
+ if (!imageUrls || imageUrls.length === 0) {
|
|
|
+ throw new Error('图像URL列表为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 验证图像索引
|
|
|
+ if (imageIndex < 0 || imageIndex >= imageUrls.length) {
|
|
|
+ throw new Error(`图像索引无效: ${imageIndex}, 有效范围: 0-${imageUrls.length - 1}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 预检查图像可访问性(可选)
|
|
|
+ // await validateImageUrls(imageUrls);
|
|
|
+
|
|
|
+ // 4. 设置图像栈,包含超时保护
|
|
|
+ await safeSetStack(viewport, imageUrls, imageIndex);
|
|
|
+
|
|
|
console.log(`重新加载图像----结束`);
|
|
|
+
|
|
|
} catch (error) {
|
|
|
- if (error instanceof Error) {
|
|
|
- console.error(
|
|
|
- '[stack.image.viewer] Error setting image stack:',
|
|
|
- error
|
|
|
- );
|
|
|
+
|
|
|
+ // 不直接抛出错误,而是保存到状态中
|
|
|
+ // 在下次渲染时抛出,这样 Error Boundary 才能捕获
|
|
|
+ const enhancedError = enhanceError(error, { imageUrls, imageIndex, viewportId });
|
|
|
+ setRenderError(enhancedError);
|
|
|
+ return; // 不再继续执行后续代码
|
|
|
+ // 根据错误严重程度决定处理策略
|
|
|
+ if (isCriticalError(enhancedError)) {
|
|
|
+ // 严重错误:保存到状态,让 Error Boundary 处理
|
|
|
+ console.error('🚨 严重图像加载错误,将在渲染时抛出给 Error Boundary:', enhancedError);
|
|
|
+ setRenderError(enhancedError);
|
|
|
+ return; // 不再继续执行后续代码
|
|
|
} else {
|
|
|
- console.error('[stack.image.viewer] Unknown error:', error);
|
|
|
+ // 非严重错误:仅记录日志,继续运行
|
|
|
+ console.warn('⚠️ 图像加载警告:', enhancedError);
|
|
|
+ // 可以在这里尝试降级策略,比如显示占位符
|
|
|
}
|
|
|
}
|
|
|
viewport.render();
|
|
|
};
|
|
|
|
|
|
setup();
|
|
|
- }, [elementRef, imageIndex, viewportId, renderingEngineId,imageUrls[0]]);
|
|
|
+ }, [elementRef, imageIndex, viewportId, renderingEngineId, imageUrls[0]]);
|
|
|
+
|
|
|
+ // 在渲染期间检查错误状态,如果有错误则抛出
|
|
|
+ // 这样 Error Boundary 就能捕获到异步错误了
|
|
|
+ if (renderError) {
|
|
|
+ throw renderError;
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
@@ -1226,4 +1386,41 @@ const StackViewer = ({
|
|
|
);
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * 带错误边界的 StackViewer 包装组件
|
|
|
+ * 自动处理图像加载错误,提供优雅的错误恢复机制
|
|
|
+ */
|
|
|
+export const StackViewerWithErrorBoundary = ({
|
|
|
+ imageIndex = 0,
|
|
|
+ imageUrls = [],
|
|
|
+ viewportId,
|
|
|
+ renderingEngineId,
|
|
|
+ selected,
|
|
|
+ maxRetries = 3,
|
|
|
+ onError
|
|
|
+}: {
|
|
|
+ imageIndex?: number;
|
|
|
+ imageUrls?: string[];
|
|
|
+ viewportId: string;
|
|
|
+ renderingEngineId: string;
|
|
|
+ selected?: boolean;
|
|
|
+ maxRetries?: number;
|
|
|
+ onError?: (error: Error, errorInfo: any) => void;
|
|
|
+}) => {
|
|
|
+ return (
|
|
|
+ <ImageViewerErrorBoundary
|
|
|
+ maxRetries={maxRetries}
|
|
|
+ onError={onError}
|
|
|
+ >
|
|
|
+ <StackViewer
|
|
|
+ imageIndex={imageIndex}
|
|
|
+ imageUrls={imageUrls}
|
|
|
+ viewportId={viewportId}
|
|
|
+ renderingEngineId={renderingEngineId}
|
|
|
+ selected={selected}
|
|
|
+ />
|
|
|
+ </ImageViewerErrorBoundary>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
export default StackViewer;
|