import { IconAlertTriangle } from '@douyinfe/semi-icons';
import { Card, Tag, Typography } from '@douyinfe/semi-ui';
import { Component, type ElementType, type ErrorInfo, type ReactNode, Suspense, useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { PageLoading } from '../shared/AsyncState';
import { RecoveryActions } from '../shared/RecoveryActions';
const { Title, Text } = Typography;
const CHUNK_RELOAD_KEY = 'vehicle-platform:route-chunk-reload';
const CHUNK_RELOAD_COOLDOWN_MS = 2 * 60_000;
type RecoveryTone = 'timeout' | 'update' | 'runtime' | 'shell';
function RouteRecoveryView({
tone,
eyebrow,
title,
description,
error,
children,
root = false
}: {
tone: RecoveryTone;
eyebrow: string;
title: string;
description: string;
error: Error;
children: ReactNode;
root?: boolean;
}) {
const tagColor = tone === 'update' ? 'blue' : tone === 'timeout' ? 'orange' : 'red';
return
{title}
{description}
查看技术信息
{error.message || error.name}
{children}
;
}
export function isRouteChunkError(error: unknown) {
const message = error instanceof Error ? `${error.name} ${error.message}` : String(error);
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) {
try {
const marker = JSON.parse(window.sessionStorage.getItem(CHUNK_RELOAD_KEY) ?? 'null') as { routeKey?: string; at?: number } | null;
return !marker || marker.routeKey !== routeKey || Date.now() - (marker.at ?? 0) > CHUNK_RELOAD_COOLDOWN_MS;
} catch {
return true;
}
}
function rememberAutoReload(routeKey: string) {
try {
window.sessionStorage.setItem(CHUNK_RELOAD_KEY, JSON.stringify({ routeKey, at: Date.now() }));
} catch {
// Recovery must still work when storage is unavailable.
}
}
class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey: string; retryKey: number; onRetry: () => void }, { error?: Error }> {
state: { error?: Error } = {};
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, _info: ErrorInfo) {
if (isRouteChunkError(error) && canAutoReload(this.props.routeKey)) {
rememberAutoReload(this.props.routeKey);
window.location.reload();
}
}
componentDidUpdate(previous: Readonly<{ children: ReactNode; routeKey: string; retryKey: number; onRetry: () => void }>) {
if (this.state.error && (previous.routeKey !== this.props.routeKey || previous.retryKey !== this.props.retryKey)) {
this.setState({ error: undefined });
}
}
render() {
const { error } = this.state;
if (!error) return this.props.children;
const chunkError = isRouteChunkError(error);
const chunkTimeout = isRouteChunkTimeout(error);
const tone: RecoveryTone = chunkTimeout ? 'timeout' : chunkError ? 'update' : 'runtime';
return
window.location.reload()}
/>
;
}
}
export class PlatformErrorBoundary extends Component<{ children: ReactNode }, { error?: Error }> {
state: { error?: Error } = {};
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
const { error } = this.state;
if (!error) return this.props.children;
return
window.location.reload()} />
;
}
}
export function RoutePage({ page, recreatePage, label }: { page: ElementType; recreatePage?: () => ElementType; label: string }) {
const location = useLocation();
const routeKey = `${location.pathname}${location.search}`;
const [retryKey, setRetryKey] = useState(0);
const Page = useMemo(() => retryKey && recreatePage ? recreatePage() : page, [page, recreatePage, retryKey]);
const retry = useCallback(() => setRetryKey((value) => value + 1), []);
return
}>
;
}