import type { ReactNode } from 'react' import { Loader2 } from 'lucide-react' import { API_PROBE_INTERVAL_MS, retryApiHealth, useApiHealth, useApiUnavailable } from '@/shared/api' import { Button } from '@/shared/ui' /** * App-wide「api error」intermediate state. * * `ApiErrorGate` sits above the router: while the rolling failure rate stays * above the threshold in `@/shared/api/health` it replaces the whole tree with * the outage screen, so a dead backend reads as one clear page instead of a * grid of half-rendered panels each showing its own error. * * Unmounting is also the recovery mechanism: the health store probes the * service on its own, and as soon as one request answers, the tree remounts and * every view refetches through `useAsync` — no document reload involved. */ export function ApiErrorGate({ children }: { children: ReactNode }) { const unavailable = useApiUnavailable() if (unavailable) return return <>{children} } /** Failure reason in words: status 0 / transport error means "no answer". */ function failureLabel(status: number | null): string { if (status === null) return '无响应' if (status === 0) return '网络错误(无响应)' if (status >= 500) return `HTTP ${status} · 服务端错误` return `HTTP ${status}` } function ServiceUnavailableScreen() { const { failures, samples, errorRate, burstFailures, lastFailureStatus, lastFailureUrl, probing, lastProbeAt, } = useApiHealth() const percent = Math.round(errorRate * 100) const path = lastFailureUrl ? lastFailureUrl.replace(/^https?:\/\/[^/]+/, '') : null return (
服务暂不可用

服务暂不可用

接口调用持续失败(连续 {burstFailures} 次请求无成功响应),页面已暂停渲染。 请确认后端服务是否已启动。

{`最近一次失败:${failureLabel(lastFailureStatus)}${path ? ` · ${path}` : ''} | 窗口失败率 ${percent}%(${failures}/${samples})`}

每 {Math.round(API_PROBE_INTERVAL_MS / 1000)}s 自动探测一次 {lastProbeAt ? ` · 上次 ${new Date(lastProbeAt).toLocaleTimeString([], { hour12: false })}` : ''}

) }