fix(hub): lock provider id during BYOK rotate + scroll/focus feedback

Clicking 轮换 on a BYOK provider row prefilled the 供应方 ID below but gave
no signal — no scroll, no focus, no toast — so it looked dead ("点不进去").
The 供应方 ID input stayed editable too: changing it silently created a new
connection (version 1) instead of rotating the one clicked.

- startRotate now locks the id (readonly), scrolls the form into view,
  focuses 接口地址, and toasts "已填入供应方 X…".
- Heading switches between 轮换凭据 · <id> and 新建 / 轮换 BYOK 凭据;
  an inline hint explains how to create a different provider (清空).
- save() distinguishes create vs rotate via the returned activeVersion
  (version 1 → 已创建 BYOK 连接, else 凭据已轮换).
- Add toastInfo to $lib/toast for the info-level affordance.

Backend unchanged: list/rotate/reject-member/reject-platform-overwrite tests
already pass; this is a UI feedback + id-lock fix.
This commit is contained in:
2026-07-24 14:39:26 +08:00
parent 744c707a39
commit 4de290a73c
2 changed files with 22 additions and 7 deletions
+3
View File
@@ -32,3 +32,6 @@ export function toastSuccess(message: string): void {
export function toastError(message: string): void { export function toastError(message: string): void {
pushToast(message, 'error', 5000); pushToast(message, 'error', 5000);
} }
export function toastInfo(message: string): void {
pushToast(message, 'info');
}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { tick } from 'svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { api, type ProviderConnectionRow } from '$lib/api'; import { api, type ProviderConnectionRow } from '$lib/api';
import { session } from '$lib/session'; import { session } from '$lib/session';
@@ -8,7 +9,7 @@
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import LoadingState from '$lib/components/LoadingState.svelte'; import LoadingState from '$lib/components/LoadingState.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte'; import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import { toastError, toastSuccess } from '$lib/toast'; import { toastError, toastInfo, toastSuccess } from '$lib/toast';
const org = $derived(resolveOrg($session.me, page.url.search)); const org = $derived(resolveOrg($session.me, page.url.search));
const slug = $derived(org?.slug ?? ''); const slug = $derived(org?.slug ?? '');
@@ -18,6 +19,7 @@
let error = $state<string | null>(null); let error = $state<string | null>(null);
let providerId = $state(''); let providerId = $state('');
let rotatingId = $state<string | null>(null);
let baseUrl = $state(''); let baseUrl = $state('');
let authToken = $state(''); let authToken = $state('');
let anthropicApiKey = $state(''); let anthropicApiKey = $state('');
@@ -36,14 +38,21 @@
} }
} }
function startRotate(row: ProviderConnectionRow) { async function startRotate(row: ProviderConnectionRow) {
providerId = row.providerId; providerId = row.providerId;
rotatingId = row.providerId;
baseUrl = ''; baseUrl = '';
authToken = ''; authToken = '';
anthropicApiKey = ''; anthropicApiKey = '';
await tick();
const el = document.getElementById('base-url');
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
el?.focus();
toastInfo(`已填入供应方 ${row.providerId},请在下方填写新接口地址与访问令牌`);
} }
function resetForm() { function resetForm() {
rotatingId = null;
providerId = ''; providerId = '';
baseUrl = ''; baseUrl = '';
authToken = ''; authToken = '';
@@ -70,8 +79,8 @@
const key = anthropicApiKey.trim(); const key = anthropicApiKey.trim();
if (key !== '') body.anthropicApiKey = key; if (key !== '') body.anthropicApiKey = key;
try { try {
await api.rotateProviderConnection(slug, id, body); const saved = await api.rotateProviderConnection(slug, id, body);
toastSuccess('凭据已轮换'); toastSuccess(saved.activeVersion === 1 ? '已创建 BYOK 连接' : '凭据已轮换');
resetForm(); resetForm();
await load(); await load();
} catch (err) { } catch (err) {
@@ -139,14 +148,17 @@
</div> </div>
<div class="saas-card-pad"> <div class="saas-card-pad">
<h3 class="saas-section-title mb-1">轮换 BYOK 凭据</h3> <h3 class="saas-section-title mb-1">{rotatingId ? `轮换凭据 · ${rotatingId}` : '新建 / 轮换 BYOK 凭据'}</h3>
<p class="saas-muted mb-4"> <p class="saas-muted mb-4">
密钥仅写入新版本,旧版本归档;保存时需重新填写接口地址与访问令牌。平台托管连接不在此处管理。 密钥仅写入新版本,旧版本归档;保存时需重新填写接口地址与访问令牌。平台托管连接不在此处管理。
</p> </p>
<div class="grid gap-5"> <div class="grid gap-5">
<div> <div>
<Label.Root class="saas-label" for="provider-id">供应方 ID</Label.Root> <Label.Root class="saas-label" for="provider-id">供应方 ID</Label.Root>
<input id="provider-id" class="saas-input font-mono text-sm" bind:value={providerId} placeholder="openrouter" /> <input id="provider-id" class="saas-input font-mono text-sm" bind:value={providerId} placeholder="openrouter" readonly={rotatingId !== null} />
{#if rotatingId}
<p class="mt-1 text-xs text-surface-500">轮换中:ID 已锁定。如需新建其他供应方,点"清空"后填写新 ID。</p>
{/if}
</div> </div>
<div> <div>
<Label.Root class="saas-label" for="base-url">接口地址</Label.Root> <Label.Root class="saas-label" for="base-url">接口地址</Label.Root>