import { ReloadOutlined } from '@ant-design/icons'; import { Button, Result } from 'antd'; import { type ReactNode, useCallback, useEffect, useState } from 'react'; import { clientFetch } from '../api/client'; import { INTERFACE_VERSION } from '../api/types'; type GateState = | { status: 'loading' } | { status: 'ready' } | { status: 'unavailable'; message: string } | { status: 'incompatible'; serverVersion: string }; export default function InterfaceGate(props: { children: ReactNode }) { const [attempt, setAttempt] = useState(0); const [state, setState] = useState({ status: 'loading' }); const retry = useCallback(() => { setState({ status: 'loading' }); setAttempt(value => value + 1); }, []); useEffect(() => { let cancelled = false; void clientFetch('/api/health') .then(async response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); const handshake = await response.json() as { interfaceVersion?: string }; if (cancelled) return; if (handshake.interfaceVersion !== INTERFACE_VERSION) { setState({ status: 'incompatible', serverVersion: handshake.interfaceVersion || 'missing', }); return; } setState({ status: 'ready' }); }) .catch(error => { if (!cancelled) { setState({ status: 'unavailable', message: error instanceof Error ? error.message : String(error) }); } }); return () => { cancelled = true; }; }, [attempt]); if (state.status === 'ready') return <>{props.children}; if (state.status === 'loading') return
; const incompatible = state.status === 'incompatible'; const detail = incompatible ? `客户端接口 ${INTERFACE_VERSION},服务端接口 ${state.serverVersion}` : `健康检查失败:${state.message}`; return (
} onClick={retry}>重试} />
); }