diff --git a/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.test.tsx b/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.test.tsx index 452a2c81..87f5794b 100644 --- a/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.test.tsx @@ -15,6 +15,7 @@ describe('route recovery boundary', () => { it('recognizes deployment-related lazy chunk failures', () => { expect(isRouteChunkError(new TypeError('Failed to fetch dynamically imported module: /assets/Monitor-old.js'))).toBe(true); expect(isRouteChunkError(new Error('Loading chunk 18 failed'))).toBe(true); + expect(isRouteChunkError(Object.assign(new Error('Route chunk load timed out after 12000ms: history'), { name: 'RouteChunkLoadTimeoutError' }))).toBe(true); expect(isRouteChunkError(new Error('Cannot read properties of undefined'))).toBe(false); }); @@ -36,6 +37,20 @@ describe('route recovery boundary', () => { expect(screen.getByText(/旧标签页仍引用上一版本资源/)).toBeInTheDocument(); }); + it('explains a bounded route timeout instead of leaving the loading skeleton forever', () => { + vi.spyOn(console, 'error').mockImplementation(() => undefined); + window.sessionStorage.setItem('vehicle-platform:route-chunk-reload', JSON.stringify({ routeKey: '/history', at: Date.now() })); + const TimeoutPage = () => { + throw Object.assign(new Error('Route chunk load timed out after 10000ms: history'), { name: 'RouteChunkLoadTimeoutError' }); + }; + + render(); + + expect(screen.getByRole('alert')).toHaveTextContent('页面资源加载超时'); + expect(screen.getByText(/网络暂时不稳定/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /刷新当前页面/ })).toBeInTheDocument(); + }); + it('keeps a meaningful content skeleton visible while a route chunk is pending', () => { const PendingPage = lazy(() => new Promise(() => undefined)); const { container } = render(); diff --git a/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.tsx b/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.tsx index d4be24fc..ddaddbdf 100644 --- a/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.tsx +++ b/vehicle-data-platform/apps/web/src/v2/routing/RouteBoundary.tsx @@ -8,7 +8,11 @@ const CHUNK_RELOAD_COOLDOWN_MS = 2 * 60_000; export function isRouteChunkError(error: unknown) { const message = error instanceof Error ? `${error.name} ${error.message}` : String(error); - return /ChunkLoadError|Loading chunk .+ failed|Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/i.test(message); + return /ChunkLoadError|RouteChunkLoadTimeoutError|Route chunk load timed out|Loading chunk .+ failed|Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/i.test(message); +} + +function isRouteChunkTimeout(error: unknown) { + return error instanceof Error && (error.name === 'RouteChunkLoadTimeoutError' || /Route chunk load timed out/i.test(error.message)); } function canAutoReload(routeKey: string) { @@ -52,12 +56,13 @@ class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey const { error } = this.state; if (!error) return this.props.children; const chunkError = isRouteChunkError(error); + const chunkTimeout = isRouteChunkTimeout(error); return
- {chunkError ? '检测到页面版本更新' : '页面运行异常'} -

{chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}

-

{chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。更换筛选条件会自动重试,也可刷新当前页面。'}

+ {chunkTimeout ? '页面资源加载超时' : chunkError ? '检测到页面版本更新' : '页面运行异常'} +

{chunkTimeout ? '当前模块没有在预期时间内加载完成' : chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}

+

{chunkTimeout ? '系统已等待并尝试恢复,可能是网络暂时不稳定。刷新后会重新加载当前模块,不会丢失服务端数据。' : chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。更换筛选条件会自动重试,也可刷新当前页面。'}

{error.message || error.name}