const RECOVERY_STORAGE_KEY = 'cc-admin-route-asset-recovery:v1' const RECOVERY_WINDOW_MS = 30_000 const staleAssetErrorPatterns = [ /failed to fetch dynamically imported module/i, /error loading dynamically imported module/i, /importing a module script failed/i, /chunkloaderror/i, /loading (css )?chunk [\w-]+ failed/i, ] export interface NavigationRecoveryDependencies { storage: Pick now: () => number hasUnsavedWork: () => boolean confirmDiscard: () => Promise discardUnsavedWork: () => void navigate: (target: string) => void showError: (message: string) => void } interface RecoveryAttempt { target: string attemptedAt: number } function errorMessage(error: unknown) { if (error instanceof Error) return error.message return typeof error === 'string' ? error : '' } export function isStaleRouteAssetError(error: unknown) { const message = errorMessage(error) return staleAssetErrorPatterns.some((pattern) => pattern.test(message)) } function recentAttempt(dependencies: NavigationRecoveryDependencies, target: string) { try { const parsed = JSON.parse(dependencies.storage.getItem(RECOVERY_STORAGE_KEY) || 'null') as RecoveryAttempt | null return Boolean(parsed && parsed.target === target && dependencies.now() - parsed.attemptedAt < RECOVERY_WINDOW_MS) } catch { return false } } export async function recoverNavigationError( error: unknown, target: string, dependencies: NavigationRecoveryDependencies, ) { if (!isStaleRouteAssetError(error)) { dependencies.showError('页面切换失败,请重试') return false } if (recentAttempt(dependencies, target)) { dependencies.showError('页面资源加载失败,请稍后重试或手动刷新') return false } if (dependencies.hasUnsavedWork() && !await dependencies.confirmDiscard()) return true dependencies.storage.setItem(RECOVERY_STORAGE_KEY, JSON.stringify({ target, attemptedAt: dependencies.now(), } satisfies RecoveryAttempt)) dependencies.discardUnsavedWork() dependencies.navigate(target) return true } export function clearNavigationRecoveryAttempt(storage: Pick) { storage.removeItem(RECOVERY_STORAGE_KEY) }