diff --git a/hub/admin-web/src/routes/admin/provider/+page.svelte b/hub/admin-web/src/routes/admin/provider/+page.svelte index b098f82..089388e 100644 --- a/hub/admin-web/src/routes/admin/provider/+page.svelte +++ b/hub/admin-web/src/routes/admin/provider/+page.svelte @@ -11,6 +11,11 @@ import ErrorBanner from '$lib/components/ErrorBanner.svelte'; import { toastError, toastInfo, toastSuccess } from '$lib/toast'; + type FormState = + | { kind: 'new'; error?: string } + | { kind: 'rotate'; providerId: string; error?: string } + | { kind: 'saving'; intent: 'new' | 'rotate'; providerId?: string }; + const org = $derived(resolveOrg($session.me, page.url.search)); const slug = $derived(org?.slug ?? ''); @@ -18,12 +23,23 @@ let loading = $state(true); let error = $state(null); + let formState = $state({ kind: 'new' }); let providerId = $state(''); - let rotatingId = $state(null); let baseUrl = $state(''); let authToken = $state(''); let anthropicApiKey = $state(''); - let saving = $state(false); + + const rotationId = $derived( + formState.kind === 'rotate' + ? formState.providerId + : formState.kind === 'saving' && formState.intent === 'rotate' + ? (formState.providerId ?? null) + : null + ); + const saving = $derived(formState.kind === 'saving'); + const formError = $derived( + formState.kind === 'new' || formState.kind === 'rotate' ? (formState.error ?? null) : null + ); async function load() { loading = true; @@ -39,8 +55,8 @@ } async function startRotate(row: ProviderConnectionRow) { + formState = { kind: 'rotate', providerId: row.providerId }; providerId = row.providerId; - rotatingId = row.providerId; baseUrl = ''; authToken = ''; anthropicApiKey = ''; @@ -48,45 +64,67 @@ const el = document.getElementById('base-url'); el?.scrollIntoView({ behavior: 'smooth', block: 'center' }); el?.focus(); - toastInfo(`已填入供应方 ${row.providerId},请在下方填写新接口地址与访问令牌`); + toastInfo(`已开始轮换 ${row.providerId},请填写新接口地址与访问令牌`); } function resetForm() { - rotatingId = null; + formState = { kind: 'new' }; providerId = ''; baseUrl = ''; authToken = ''; anthropicApiKey = ''; } + function cancelOrClear() { + const wasRotating = rotationId !== null; + resetForm(); + if (wasRotating) toastInfo('已取消轮换'); + } + + function showFormError(message: string, targetProviderId: string | null) { + formState = + targetProviderId === null + ? { kind: 'new', error: message } + : { kind: 'rotate', providerId: targetProviderId, error: message }; + toastError(message); + } + async function save() { + const targetProviderId = formState.kind === 'rotate' ? formState.providerId : null; + const intent = targetProviderId === null ? 'new' : 'rotate'; const id = providerId.trim(); if (id === '') { - toastError('请填写供应方 ID'); + showFormError('请填写供应方 ID', targetProviderId); return; } const url = baseUrl.trim(); const token = authToken.trim(); if (url === '' || token === '') { - toastError('接口地址与访问令牌均为必填'); + showFormError('接口地址与访问令牌均为必填', targetProviderId); return; } - saving = true; const body: { baseUrl: string; authToken: string; anthropicApiKey?: string } = { baseUrl: url, authToken: token, }; const key = anthropicApiKey.trim(); if (key !== '') body.anthropicApiKey = key; + formState = + intent === 'rotate' + ? { kind: 'saving', intent, providerId: id } + : { kind: 'saving', intent }; try { const saved = await api.rotateProviderConnection(slug, id, body); - toastSuccess(saved.activeVersion === 1 ? '已创建 BYOK 连接' : '凭据已轮换'); resetForm(); + toastSuccess(saved.activeVersion === 1 ? '已创建 BYOK 连接' : '凭据已轮换'); await load(); } catch (err) { - toastError(err instanceof Error ? err.message : String(err)); - } finally { - saving = false; + const message = err instanceof Error ? err.message : String(err); + formState = + intent === 'rotate' + ? { kind: 'rotate', providerId: id, error: message } + : { kind: 'new', error: message }; + toastError(message); } } @@ -134,7 +172,14 @@ {fmtDate(row.updatedAt)} {#if row.mode === 'BYOK'} - + {:else} 平台管理 {/if} @@ -148,37 +193,79 @@
-

{rotatingId ? `轮换凭据 · ${rotatingId}` : '新建 / 轮换 BYOK 凭据'}

-

- 密钥仅写入新版本,旧版本归档;保存时需重新填写接口地址与访问令牌。平台托管连接不在此处管理。 +

+ {rotationId ? `轮换凭据 · ${rotationId}` : '新建 BYOK 凭据'} +

+

+ {#if saving} + 正在验证新凭据;验证通过后才会切换版本,请勿重复提交。 + {:else if rotationId} + 已选择供应方 {rotationId}。点击“开始轮换”只打开此表单;填写新接口地址和访问令牌后,点击“验证并保存”才会生效。 + {:else} + 填写供应方 ID、接口地址和访问令牌,保存前会先验证凭据。平台托管连接不在此处管理。 + {/if}

供应方 ID - - {#if rotatingId} -

轮换中:ID 已锁定。如需新建其他供应方,点"清空"后填写新 ID。

+ + {#if rotationId} +

+ 轮换目标已锁定为 {rotationId}。如需新建其他供应方,请先取消轮换。 +

{/if}
接口地址 - +
访问令牌 - +
Anthropic API Key(可选) - +
+ {#if formError} + + {/if}
- +
-{/if} \ No newline at end of file +{/if}