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
{eyebrow}
{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 }> ; }