feat(accounts): consolidate detail resources and move exit switching to edit

This commit is contained in:
2026-10-06 10:30:53 +08:00
parent 94df412579
commit 67bbd199b2
4 changed files with 406 additions and 73 deletions
@@ -1,22 +1,20 @@
// 账号环境出口重绑:账号即环境下唯一的换绑入口(环境跟随账号,不再有独立环境页)。
// 仅列举检测通过的出口;重绑走 POST /browsers/:alias/rebind(alias=账号 ID,成功后由父级刷新)。
// 编辑页网络出口切换:仅列举检测通过的出口,以运行环境 alias 请求并返回最新环境状态。
import { useEffect, useMemo, useState } from 'react';
import { Button, Flex, Select, Typography } from 'antd';
import { getList } from '@/services/api';
import { jsonOptions, request } from '@/requestErrorConfig';
interface RebindEnvironmentProps {
accountID: string;
binding: any;
disabled?: boolean;
onRebound: () => void | Promise<void>;
onRebound: (environment: any) => void | Promise<void>;
onError: (error: unknown) => void;
}
export default function RebindEnvironment({ accountID, binding, disabled, onRebound, onError }: RebindEnvironmentProps) {
export default function RebindEnvironment({ binding, disabled, onRebound, onError }: RebindEnvironmentProps) {
const [exits, setExits] = useState<any[]>([]);
const [exitsError, setExitsError] = useState(false);
const [exitID, setExitID] = useState('');
const [exitID, setExitID] = useState(binding.network_exit_id ?? '');
const [busy, setBusy] = useState(false);
useEffect(() => {
@@ -25,32 +23,39 @@ export default function RebindEnvironment({ accountID, binding, disabled, onRebo
.then((result) => {
if (!cancelled) setExits((result.data ?? []).filter((item: any) => item.health_status === 'healthy'));
})
.catch(() => {
if (!cancelled) setExitsError(true);
.catch((reason) => {
if (!cancelled) {
console.error('[network-exit] Failed to load exits', { environmentAlias: binding.alias, error: reason });
setExitsError(true);
onError(reason);
}
});
return () => {
cancelled = true;
};
}, []);
// 直连(当前状态)或任一健康出口;已在用的出口不重复出现在选项里。
// 当前选择回显已绑定出口;直连与检测通过的出口均可选。
const options = useMemo(() => {
const choices = [{ value: '', label: '当前机器直连' }];
for (const exit of exits) {
if (exit.id === binding.network_exit_id) continue;
choices.push({ value: exit.id, label: `${exit.id}${exit.observed_public_ip ? ` · ${exit.observed_public_ip}` : ''}` });
}
return choices;
}, [exits, binding.network_exit_id]);
}, [exits]);
const unchanged = exitID === (binding.network_exit_id ?? '');
async function rebind() {
setBusy(true);
try {
await request(`/browsers/${encodeURIComponent(accountID)}/rebind`, jsonOptions('POST', { network_exit_id: exitID }));
await onRebound();
const result = await request(`/browsers/${encodeURIComponent(binding.alias)}/rebind`, jsonOptions('POST', { network_exit_id: exitID }));
if (result?.environment?.alias !== binding.alias) {
throw new Error('网络出口切换结果中的运行环境不匹配,请刷新页面确认实际状态。');
}
await onRebound(result.environment);
} catch (reason) {
console.error('[network-exit] Failed to switch exit', { environmentAlias: binding.alias, networkExitID: exitID, error: reason });
onError(reason);
} finally {
setBusy(false);
@@ -60,21 +65,22 @@ export default function RebindEnvironment({ accountID, binding, disabled, onRebo
return (
<Flex vertical gap={8}>
<Flex align="center" gap={8} wrap="wrap">
<Typography.Text type="secondary">切换出口</Typography.Text>
<Typography.Text type="secondary">目标出口</Typography.Text>
<Select
style={{ minWidth: 220 }}
value={exitID}
onChange={setExitID}
options={options}
placeholder={exitsError ? '出口列表不可用' : '选择目标出口'}
disabled={disabled || exitsError}
disabled={disabled || exitsError || busy}
aria-label="目标网络出口"
size="small"
/>
<Button size="small" type="primary" disabled={disabled || unchanged || busy} loading={busy} onClick={rebind}>
重绑
<Button size="small" type="primary" disabled={disabled || exitsError || unchanged || busy} loading={busy} onClick={rebind}>
保存网络出口
</Button>
</Flex>
{exitsError ? <Typography.Text type="secondary" style={{ fontSize: 12 }}>出口状态未知,请稍后重试。</Typography.Text> : null}
{exitsError ? <Typography.Text type="secondary" style={{ fontSize: 12 }}>出口状态未知,请刷新页面后重试。</Typography.Text> : null}
</Flex>
);
}