65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
|
|
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<GateState>({ 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 <main className="workspace-boot"><span /></main>;
|
||
|
|
|
||
|
|
const incompatible = state.status === 'incompatible';
|
||
|
|
const detail = incompatible
|
||
|
|
? `客户端接口 ${INTERFACE_VERSION},服务端接口 ${state.serverVersion}`
|
||
|
|
: `健康检查失败:${state.message}`;
|
||
|
|
return (
|
||
|
|
<main className="workspace-boot">
|
||
|
|
<Result
|
||
|
|
status="error"
|
||
|
|
title={incompatible ? '客户端与服务端版本不兼容' : '服务暂不可用'}
|
||
|
|
subTitle={detail}
|
||
|
|
extra={<Button icon={<ReloadOutlined />} onClick={retry}>重试</Button>}
|
||
|
|
/>
|
||
|
|
</main>
|
||
|
|
);
|
||
|
|
}
|