mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat: improve routing policy diagnostics
This commit is contained in:
@@ -131,208 +131,208 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 新建时的 Display Name -->
|
||||
<div
|
||||
v-if="selectedType === '__new__'"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">显示名称</Label>
|
||||
<Input
|
||||
v-model="form.new_display_name"
|
||||
class="mt-1"
|
||||
placeholder="例如:My OIDC Provider"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">配置标识</Label>
|
||||
<Input
|
||||
v-model="form.new_provider_type"
|
||||
class="mt-1"
|
||||
placeholder="custom_oidc_work"
|
||||
autocomplete="off"
|
||||
@blur="normalizeNewProviderType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 凭证配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client ID</Label>
|
||||
<Input
|
||||
v-model="form.client_id"
|
||||
class="mt-1"
|
||||
placeholder="client_id"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||
<Input
|
||||
v-model="form.client_secret"
|
||||
masked
|
||||
class="mt-1"
|
||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回调地址 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||
<Input
|
||||
v-model="form.redirect_uri"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||
<Input
|
||||
v-model="form.frontend_callback_url"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:5173/auth/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom_oidc 必填端点 -->
|
||||
<div
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/authorize"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/token"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/api/user"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图标 URL -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">图标 URL</Label>
|
||||
<Input
|
||||
v-model="form.icon_url"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/icon.svg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
登录页显示的 Provider 图标,留空使用默认图标
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项(折叠) -->
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||
高级选项
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||
<div class="space-y-6">
|
||||
<!-- 新建时的 Display Name -->
|
||||
<div
|
||||
v-if="selectedType === '__new__'"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Scopes</Label>
|
||||
<Label class="block text-sm font-medium">显示名称</Label>
|
||||
<Input
|
||||
v-model="form.scopes_input"
|
||||
v-model="form.new_display_name"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||
placeholder="例如:My OIDC Provider"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
空格/逗号分隔;留空使用默认值
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- linuxdo 的可选端点覆盖 -->
|
||||
<div
|
||||
v-if="!isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||
<Textarea
|
||||
v-model="form.attribute_mapping_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
placeholder="{"id": "user_id", "username": "login"}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">
|
||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||
</Label>
|
||||
<Textarea
|
||||
v-model="form.extra_config_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
:placeholder="extraConfigPlaceholder"
|
||||
/>
|
||||
<p
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="mt-1 text-xs text-muted-foreground"
|
||||
>
|
||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">配置标识</Label>
|
||||
<Input
|
||||
v-model="form.new_provider_type"
|
||||
class="mt-1"
|
||||
placeholder="custom_oidc_work"
|
||||
autocomplete="off"
|
||||
@blur="normalizeNewProviderType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 凭证配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client ID</Label>
|
||||
<Input
|
||||
v-model="form.client_id"
|
||||
class="mt-1"
|
||||
placeholder="client_id"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Client Secret</Label>
|
||||
<Input
|
||||
v-model="form.client_secret"
|
||||
masked
|
||||
class="mt-1"
|
||||
:placeholder="hasSecret ? '已设置(留空保持不变)' : '请输入 secret'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回调地址 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Redirect URI(后端回调)</Label>
|
||||
<Input
|
||||
v-model="form.redirect_uri"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:8084/api/oauth/xxx/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">前端回调页</Label>
|
||||
<Input
|
||||
v-model="form.frontend_callback_url"
|
||||
class="mt-1"
|
||||
placeholder="http://localhost:5173/auth/callback"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom_oidc 必填端点 -->
|
||||
<div
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/authorize"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/oauth/token"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/api/user"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图标 URL -->
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">图标 URL</Label>
|
||||
<Input
|
||||
v-model="form.icon_url"
|
||||
class="mt-1"
|
||||
placeholder="https://example.com/icon.svg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
登录页显示的 Provider 图标,留空使用默认图标
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 高级选项(折叠) -->
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
|
||||
高级选项
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4 pl-4 border-l-2 border-border">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Scopes</Label>
|
||||
<Input
|
||||
v-model="form.scopes_input"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_scopes?.join(' ') || '留空使用默认值'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
空格/逗号分隔;留空使用默认值
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- linuxdo 的可选端点覆盖 -->
|
||||
<div
|
||||
v-if="!isSelectedCustomProvider"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Authorization URL</Label>
|
||||
<Input
|
||||
v-model="form.authorization_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_authorization_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Token URL</Label>
|
||||
<Input
|
||||
v-model="form.token_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_token_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Userinfo URL</Label>
|
||||
<Input
|
||||
v-model="form.userinfo_url_override"
|
||||
class="mt-1"
|
||||
:placeholder="selectedTypeMeta?.default_userinfo_url || '默认'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">Attribute Mapping</Label>
|
||||
<Textarea
|
||||
v-model="form.attribute_mapping_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
placeholder="{"id": "user_id", "username": "login"}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="block text-sm font-medium">
|
||||
{{ isSelectedCustomProvider ? 'Allowed Domains / Extra Config' : 'Extra Config' }}
|
||||
</Label>
|
||||
<Textarea
|
||||
v-model="form.extra_config_json"
|
||||
class="mt-1 font-mono text-xs"
|
||||
rows="3"
|
||||
:placeholder="extraConfigPlaceholder"
|
||||
/>
|
||||
<p
|
||||
v-if="isSelectedCustomProvider"
|
||||
class="mt-1 text-xs text-muted-foreground"
|
||||
>
|
||||
自定义 OIDC 必填;填写 Authorization / Token / Userinfo URL 所属域名。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
v-if="lastTestResult"
|
||||
|
||||
@@ -1155,12 +1155,12 @@
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] leading-none">
|
||||
<span class="text-muted-foreground font-medium shrink-0">{{ getQuotaProgressLabel(item.label) }}</span>
|
||||
<span
|
||||
v-if="getQuotaProgressResetDisplayText(item)"
|
||||
data-testid="pool-quota-reset-text"
|
||||
class="text-muted-foreground/80 tabular-nums truncate"
|
||||
:title="getQuotaProgressResetDisplayText(item)"
|
||||
>{{ getQuotaProgressResetDisplayText(item) }}</span>
|
||||
<span
|
||||
v-if="getQuotaProgressResetDisplayText(item)"
|
||||
data-testid="pool-quota-reset-text"
|
||||
class="text-muted-foreground/80 tabular-nums truncate"
|
||||
:title="getQuotaProgressResetDisplayText(item)"
|
||||
>{{ getQuotaProgressResetDisplayText(item) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="relative flex-1 h-1.5 rounded-full bg-border overflow-hidden">
|
||||
|
||||
@@ -3,25 +3,7 @@
|
||||
<PageHeader
|
||||
title="调度策略"
|
||||
description="管理调度分组、模型范围、默认调度模式和规则配置"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="loading"
|
||||
@click="refreshPage"
|
||||
>
|
||||
<RefreshCw
|
||||
class="mr-2 h-4 w-4"
|
||||
:class="{ 'animate-spin': loading || loadingGlobalModels }"
|
||||
/>
|
||||
刷新
|
||||
</Button>
|
||||
<Button @click="startCreate">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
新建策略
|
||||
</Button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
/>
|
||||
|
||||
<div class="mt-6 grid gap-5 xl:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<Card class="overflow-hidden">
|
||||
@@ -35,7 +17,17 @@
|
||||
共 {{ groups.length }} 个
|
||||
</p>
|
||||
</div>
|
||||
<SlidersHorizontal class="h-4 w-4 text-muted-foreground" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground/70 hover:text-foreground"
|
||||
:disabled="loading"
|
||||
aria-label="添加策略"
|
||||
title="添加策略"
|
||||
@click="startCreate"
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,49 +39,111 @@
|
||||
正在加载调度策略
|
||||
</div>
|
||||
<div
|
||||
v-else-if="groups.length === 0"
|
||||
class="rounded-lg border border-dashed border-border/70 px-4 py-8 text-center"
|
||||
>
|
||||
<p class="text-sm font-medium">
|
||||
暂无调度策略
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
可以先创建一个默认分组
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-for="group in groups"
|
||||
v-else
|
||||
:key="group.id"
|
||||
type="button"
|
||||
class="mb-2 w-full rounded-lg border px-4 py-3 text-left transition-colors"
|
||||
:class="group.id === selectedGroupId
|
||||
? 'border-primary/60 bg-primary/10'
|
||||
: 'border-border/60 bg-background hover:border-primary/40 hover:bg-muted/50'"
|
||||
@click="selectGroup(group)"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ group.name }}
|
||||
</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{{ group.description || '未填写描述' }}
|
||||
</p>
|
||||
<div
|
||||
v-if="groups.length === 0 && !isCreating"
|
||||
class="rounded-lg border border-dashed border-border/70 px-4 py-8 text-center"
|
||||
>
|
||||
<p class="text-sm font-medium">
|
||||
暂无调度策略
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
可以先创建一个默认分组
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isCreating && draft"
|
||||
class="rounded-lg border border-primary/60 bg-primary/10 px-4 py-2.5 text-left"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ draft.name || '新调度策略' }}
|
||||
</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{{ draft.description || '未填写描述' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<Badge :variant="draft.enabled ? 'default' : 'secondary'">
|
||||
{{ draft.enabled ? '启用' : '停用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5 flex items-end justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span v-if="draft.is_system_default">系统默认</span>
|
||||
<span>{{ draft.config_json.allowed_models.length || '全部' }} 模型范围</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
:class="draft.enabled
|
||||
? 'text-primary hover:text-primary'
|
||||
: 'text-muted-foreground/70 hover:text-foreground'"
|
||||
:aria-label="draft.enabled ? '停用策略' : '启用策略'"
|
||||
:title="draft.enabled ? '已启用,点击停用' : '已停用,点击启用'"
|
||||
@click="setDraftEnabled(!draft.enabled)"
|
||||
>
|
||||
<Power class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="group.enabled ? 'default' : 'secondary'"
|
||||
class="shrink-0"
|
||||
>
|
||||
{{ group.enabled ? '启用' : '停用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>v{{ group.version }}</span>
|
||||
<span v-if="group.is_system_default">系统默认</span>
|
||||
<span>{{ group.config_json.allowed_models.length || '全部' }} 模型范围</span>
|
||||
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="w-full rounded-lg border px-4 py-2.5 text-left transition-colors"
|
||||
:class="group.id === selectedGroupId
|
||||
? 'border-primary/60 bg-primary/10'
|
||||
: 'border-border/60 bg-background hover:border-primary/40 hover:bg-muted/50'"
|
||||
@click="selectGroup(group)"
|
||||
@keydown.enter.prevent="selectGroup(group)"
|
||||
@keydown.space.prevent="selectGroup(group)"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">
|
||||
{{ group.name }}
|
||||
</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{{ group.description || '未填写描述' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<Badge :variant="displayGroupEnabled(group) ? 'default' : 'secondary'">
|
||||
{{ displayGroupEnabled(group) ? '启用' : '停用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5 flex items-end justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span v-if="group.is_system_default">系统默认</span>
|
||||
<span>{{ group.config_json.allowed_models.length || '全部' }} 模型范围</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
:class="displayGroupEnabled(group)
|
||||
? 'text-primary hover:text-primary'
|
||||
: 'text-muted-foreground/70 hover:text-foreground'"
|
||||
:aria-label="displayGroupEnabled(group) ? '停用策略' : '启用策略'"
|
||||
:title="displayGroupEnabled(group) ? '已启用,点击停用' : '已停用,点击启用'"
|
||||
@keydown.stop
|
||||
@click.stop="updateGroupEnabled(group, !displayGroupEnabled(group))"
|
||||
>
|
||||
<Power class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -104,12 +158,6 @@
|
||||
<h2 class="text-base font-semibold">
|
||||
{{ isCreating ? '新建调度策略' : draft.name || '未命名策略' }}
|
||||
</h2>
|
||||
<Badge
|
||||
v-if="!isCreating"
|
||||
variant="outline"
|
||||
>
|
||||
v{{ draft.version }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="draft.is_system_default"
|
||||
variant="secondary"
|
||||
@@ -123,42 +171,50 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="draft.is_system_default ? 'border-primary/50 bg-primary/10 text-primary' : ''"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:class="draft.is_system_default
|
||||
? 'text-primary hover:text-primary'
|
||||
: 'text-muted-foreground/70 hover:text-foreground'"
|
||||
:aria-label="draft.is_system_default ? '系统默认' : '设为系统默认'"
|
||||
:title="draft.is_system_default ? '系统默认' : '设为系统默认'"
|
||||
@click="draft.is_system_default = !draft.is_system_default"
|
||||
>
|
||||
<Star class="mr-2 h-4 w-4" />
|
||||
{{ draft.is_system_default ? '系统默认' : '设为系统默认' }}
|
||||
<Star class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground/70 hover:text-foreground"
|
||||
:disabled="!canSaveDraft"
|
||||
aria-label="保存"
|
||||
title="保存"
|
||||
@click="saveDraft"
|
||||
>
|
||||
<Save
|
||||
class="mr-2 h-4 w-4"
|
||||
class="h-4 w-4"
|
||||
:class="{ 'animate-pulse': saving }"
|
||||
/>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!isCreating"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground/70 hover:text-destructive"
|
||||
:disabled="deleting"
|
||||
aria-label="删除"
|
||||
title="删除"
|
||||
@click="deleteDraft"
|
||||
>
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
删除
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 p-5">
|
||||
<div class="grid gap-3 lg:grid-cols-[240px_minmax(0,1fr)_160px]">
|
||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.2fr)_320px]">
|
||||
<label class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">名称</span>
|
||||
<Input
|
||||
@@ -173,45 +229,35 @@
|
||||
placeholder="例如:默认策略 / 高推理策略 / 号池优先策略"
|
||||
/>
|
||||
</label>
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border border-border/60 px-3 py-2 text-sm">
|
||||
<span>启用策略</span>
|
||||
<Switch v-model="draft.enabled" />
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">
|
||||
维度
|
||||
</span>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="sortingScope === 'unified'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="setSortingScope('unified')"
|
||||
>
|
||||
统一调度
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="sortingScope === 'per_model'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="setSortingScope('per_model')"
|
||||
>
|
||||
区分模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="space-y-3 rounded-lg border border-border/60 p-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium">
|
||||
排序作用范围
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
统一排序对策略内全部模型生效;按模型排序需先挑选模型,再逐个配置排序与调度方式。
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1 sm:max-w-[320px]">
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="sortingScope === 'unified'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="setSortingScope('unified')"
|
||||
>
|
||||
统一排序
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="sortingScope === 'per_model'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="setSortingScope('per_model')"
|
||||
>
|
||||
按模型排序
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="sortingScope === 'unified'"
|
||||
class="space-y-4"
|
||||
@@ -225,7 +271,7 @@
|
||||
统一作用于策略范围内的全部模型。
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">优先级模式</span>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
@@ -284,28 +330,20 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-else
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="grid gap-4 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<div class="flex flex-col gap-3 rounded-lg border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-sm font-medium">全局模型</span>
|
||||
<Badge variant="outline">
|
||||
{{ filteredGlobalModels.length }}
|
||||
</Badge>
|
||||
</div>
|
||||
<section v-else>
|
||||
<div class="flex max-h-[560px] flex-col gap-3 overflow-hidden rounded-lg border border-border/60 p-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<Input
|
||||
v-model="globalModelSearch"
|
||||
placeholder="搜索模型"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="grid grid-cols-3 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1 text-xs">
|
||||
<button
|
||||
v-for="filter in modelFilterOptions"
|
||||
v-for="filter in modelFilters"
|
||||
:key="filter.value"
|
||||
type="button"
|
||||
class="h-7 rounded-md px-2 text-xs font-medium transition-colors"
|
||||
class="h-9 rounded-md px-3 font-medium transition-colors"
|
||||
:class="modelFilter === filter.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@@ -314,82 +352,71 @@
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="loadingGlobalModels"
|
||||
class="rounded-md border border-dashed border-border/70 px-3 py-4 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
正在加载全局模型
|
||||
</div>
|
||||
<div
|
||||
v-else-if="globalModelsError"
|
||||
class="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{{ globalModelsError }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="globalModels.length === 0"
|
||||
class="rounded-md border border-dashed border-border/70 px-3 py-4 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
暂无可选择的全局模型
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredGlobalModels.length === 0"
|
||||
class="rounded-md border border-dashed border-border/70 px-3 py-4 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
未匹配到模型
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="max-h-[640px] overflow-y-auto"
|
||||
>
|
||||
<button
|
||||
v-for="model in filteredGlobalModels"
|
||||
:key="model.id"
|
||||
type="button"
|
||||
class="mb-1 flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors"
|
||||
:class="activePerModelPolicy?.model === model.name
|
||||
? 'bg-primary/10 text-foreground'
|
||||
: 'hover:bg-muted/60'"
|
||||
@click="selectGlobalModel(model.name)"
|
||||
>
|
||||
<span
|
||||
class="h-2 w-2 shrink-0 rounded-full"
|
||||
:class="hasModelPolicy(model.name)
|
||||
? 'bg-primary'
|
||||
: 'bg-muted-foreground/20'"
|
||||
:title="hasModelPolicy(model.name) ? '已配置' : '未配置'"
|
||||
/>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-medium">{{ model.display_name || model.name }}</span>
|
||||
<span class="block truncate text-xs text-muted-foreground">{{ model.name }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 rounded-lg border border-border/60 p-4">
|
||||
<template v-if="activePerModelPolicy">
|
||||
<div class="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div class="min-w-0 space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">当前模型</span>
|
||||
<div class="truncate text-sm font-medium">
|
||||
{{ globalModelLabel(activePerModelPolicy.model) }}
|
||||
</div>
|
||||
<div class="truncate text-xs text-muted-foreground">
|
||||
{{ activePerModelPolicy.model }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div
|
||||
v-if="loadingGlobalModels"
|
||||
class="rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
正在加载模型
|
||||
</div>
|
||||
<div
|
||||
v-else-if="globalModelsError"
|
||||
class="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{{ globalModelsError }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="modelRows.length === 0"
|
||||
class="rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
{{ globalModelSearch.trim() ? '未匹配到模型' : modelFilter === 'configured' ? '暂无已配置模型' : '暂无未配置模型' }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="min-h-0 flex-1 space-y-2 overflow-y-auto pr-1"
|
||||
>
|
||||
<div
|
||||
v-for="row in modelRows"
|
||||
:key="row.name"
|
||||
class="rounded-lg border transition-colors"
|
||||
:class="selectedPerModelName === row.name
|
||||
? 'border-primary/50 bg-primary/5'
|
||||
: 'border-border/60'"
|
||||
>
|
||||
<div class="flex w-full items-center gap-3 px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-3 text-left text-sm"
|
||||
@click="selectGlobalModel(row.name)"
|
||||
>
|
||||
<span
|
||||
v-if="row.configured"
|
||||
class="h-2 w-2 shrink-0 rounded-full bg-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Plus
|
||||
v-else
|
||||
class="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-medium">{{ row.displayName }}</span>
|
||||
<span class="block truncate text-xs text-muted-foreground">{{ row.name }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<template v-if="selectedPerModelName === row.name && activePerModelPolicy">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0 text-muted-foreground/70 hover:text-foreground"
|
||||
:disabled="copySourceCandidates.length === 0"
|
||||
title="加载其他模型配置"
|
||||
>
|
||||
<Copy class="mr-2 h-4 w-4" />
|
||||
加载
|
||||
<Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -410,108 +437,110 @@
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0 text-muted-foreground/70 hover:text-foreground"
|
||||
:disabled="!canSaveCurrentModel"
|
||||
title="保存到草稿"
|
||||
@click="saveCurrentModel"
|
||||
>
|
||||
<Save
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
保存到草稿
|
||||
<Save class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="hasModelPolicy(activePerModelPolicy.model)"
|
||||
type="button"
|
||||
:variant="canRemoveCurrentModel ? 'destructive' : 'outline'"
|
||||
size="sm"
|
||||
:class="canRemoveCurrentModel ? 'shadow-sm' : 'text-muted-foreground'"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
:class="canRemoveCurrentModel ? 'text-muted-foreground/70 hover:text-destructive' : 'text-muted-foreground/30'"
|
||||
:disabled="!canRemoveCurrentModel"
|
||||
:title="canRemoveCurrentModel ? '移除当前模型排序' : '当前有未保存改动,不能移除'"
|
||||
@click="removePerModelPolicy(activePerModelPolicy.model)"
|
||||
>
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
移除
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0"
|
||||
@click="selectGlobalModel(row.name)"
|
||||
>
|
||||
<ChevronDown
|
||||
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||
:class="selectedPerModelName === row.name ? 'rotate-180' : ''"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 space-y-3 rounded-lg border border-border/60 p-4">
|
||||
<div>
|
||||
<div
|
||||
v-if="selectedPerModelName === row.name && activePerModelPolicy"
|
||||
class="border-t border-border/60 p-4"
|
||||
>
|
||||
<div class="mb-4 space-y-3 rounded-lg border border-border/60 p-4">
|
||||
<h3 class="text-sm font-medium">
|
||||
优先级模式与调度策略
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
仅作用于当前选中的模型。
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">优先级模式</span>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelPriorityMode(activePerModelPolicy.model) === 'provider'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelPriorityMode(activePerModelPolicy.model, 'provider')"
|
||||
>
|
||||
<Layers class="h-4 w-4" />
|
||||
Provider
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelPriorityMode(activePerModelPolicy.model) === 'global_key'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelPriorityMode(activePerModelPolicy.model, 'global_key')"
|
||||
>
|
||||
<Key class="h-4 w-4" />
|
||||
Key
|
||||
</button>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">优先级模式</span>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelPriorityMode(activePerModelPolicy.model) === 'provider'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelPriorityMode(activePerModelPolicy.model, 'provider')"
|
||||
>
|
||||
<Layers class="h-4 w-4" />
|
||||
Provider
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelPriorityMode(activePerModelPolicy.model) === 'global_key'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelPriorityMode(activePerModelPolicy.model, 'global_key')"
|
||||
>
|
||||
<Key class="h-4 w-4" />
|
||||
Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">调度策略</span>
|
||||
<div class="grid grid-cols-3 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<button
|
||||
v-for="mode in schedulingModes"
|
||||
:key="mode.value"
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelSchedulingMode(activePerModelPolicy.model) === mode.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelSchedulingMode(activePerModelPolicy.model, mode.value)"
|
||||
>
|
||||
{{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
<span class="text-muted-foreground">调度策略</span>
|
||||
<div class="grid grid-cols-3 gap-1 rounded-lg bg-muted/40 p-1">
|
||||
<button
|
||||
v-for="mode in schedulingModes"
|
||||
:key="mode.value"
|
||||
type="button"
|
||||
class="h-9 rounded-md px-3 text-sm font-medium transition-colors"
|
||||
:class="modelSchedulingMode(activePerModelPolicy.model) === mode.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/60 hover:text-foreground'"
|
||||
@click="updateModelSchedulingMode(activePerModelPolicy.model, mode.value)"
|
||||
>
|
||||
{{ mode.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RoutingPriorityPolicyEditor
|
||||
:config="activeConfigForReading"
|
||||
:model="activePerModelPolicy.model"
|
||||
:priority-mode="modelPriorityMode(activePerModelPolicy.model)"
|
||||
:scheduling-mode="modelSchedulingMode(activePerModelPolicy.model)"
|
||||
:show-priority-mode="false"
|
||||
:show-scheduling-mode="false"
|
||||
:subtitle="`仅作用于 ${activePerModelPolicy.model}`"
|
||||
@update:config="updateEditingConfig"
|
||||
@update:priority-mode="mode => updateModelPriorityMode(activePerModelPolicy.model, mode)"
|
||||
@update:scheduling-mode="mode => updateModelSchedulingMode(activePerModelPolicy.model, mode)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RoutingPriorityPolicyEditor
|
||||
:config="activeConfigForReading"
|
||||
:model="activePerModelPolicy.model"
|
||||
:priority-mode="modelPriorityMode(activePerModelPolicy.model)"
|
||||
:scheduling-mode="modelSchedulingMode(activePerModelPolicy.model)"
|
||||
:show-priority-mode="false"
|
||||
:show-scheduling-mode="false"
|
||||
:subtitle="`仅作用于 ${activePerModelPolicy.model}`"
|
||||
@update:config="updateEditingConfig"
|
||||
@update:priority-mode="mode => updateModelPriorityMode(activePerModelPolicy.model, mode)"
|
||||
@update:scheduling-mode="mode => updateModelSchedulingMode(activePerModelPolicy.model, mode)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg border border-dashed border-border/70 px-4 py-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
在左侧添加模型后即可配置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -556,10 +585,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Copy, Key, Layers, Plus, RefreshCw, Save, SlidersHorizontal, Star, Trash2 } from 'lucide-vue-next'
|
||||
import { ChevronDown, Copy, Key, Layers, Plus, Power, Save, Star, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
import { PageContainer, PageHeader } from '@/components/layout'
|
||||
import { Badge, Button, Card, Input, Switch } from '@/components/ui'
|
||||
import { Badge, Button, Card, Input } from '@/components/ui'
|
||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||
import { AlertDialog } from '@/components/common'
|
||||
import {
|
||||
@@ -605,7 +634,12 @@ interface RoutingGroupDraft {
|
||||
}
|
||||
|
||||
type SortingScope = 'unified' | 'per_model'
|
||||
type ModelFilter = 'all' | 'configured' | 'unconfigured'
|
||||
type ModelFilter = 'configured' | 'unconfigured'
|
||||
|
||||
const modelFilters: Array<{ value: ModelFilter; label: string }> = [
|
||||
{ value: 'unconfigured', label: '未配置' },
|
||||
{ value: 'configured', label: '已配置' },
|
||||
]
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
@@ -615,12 +649,6 @@ const schedulingModes: Array<{ value: RoutingSchedulingMode; label: string }> =
|
||||
{ value: 'fixed_order', label: '固定顺序' },
|
||||
]
|
||||
|
||||
const modelFilterOptions: Array<{ value: ModelFilter; label: string }> = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'configured', label: '已配置' },
|
||||
{ value: 'unconfigured', label: '未配置' },
|
||||
]
|
||||
|
||||
const groups = ref<RoutingGroupRecord[]>([])
|
||||
const selectedGroupId = ref<string | null>(null)
|
||||
const draft = ref<RoutingGroupDraft | null>(null)
|
||||
@@ -629,7 +657,7 @@ const sortingScope = ref<SortingScope>('unified')
|
||||
const selectedPerModelName = ref<string | null>(null)
|
||||
const editingConfig = ref<RoutingGroupConfig | null>(null)
|
||||
const globalModelSearch = ref('')
|
||||
const modelFilter = ref<ModelFilter>('all')
|
||||
const modelFilter = ref<ModelFilter>('unconfigured')
|
||||
const globalModels = ref<GlobalModelResponse[]>([])
|
||||
const loadingGlobalModels = ref(false)
|
||||
const globalModelsError = ref<string | null>(null)
|
||||
@@ -668,22 +696,53 @@ const firstStepSchedulingMode = computed<RoutingSchedulingMode>(() => {
|
||||
return draft.value?.config_json.default_policy.scheduling_mode ?? 'cache_affinity'
|
||||
})
|
||||
|
||||
const filteredGlobalModels = computed(() => {
|
||||
interface ModelRow {
|
||||
name: string
|
||||
displayName: string
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
const modelRows = computed<ModelRow[]>(() => {
|
||||
const query = globalModelSearch.value.trim().toLowerCase()
|
||||
const filter = modelFilter.value
|
||||
const models = [...globalModels.value].sort((left, right) =>
|
||||
left.name.localeCompare(right.name)
|
||||
)
|
||||
return models.filter(model => {
|
||||
if (query
|
||||
&& !model.name.toLowerCase().includes(query)
|
||||
&& !model.display_name?.toLowerCase().includes(query)) {
|
||||
return false
|
||||
}
|
||||
if (filter === 'configured' && !hasModelPolicy(model.name)) return false
|
||||
if (filter === 'unconfigured' && hasModelPolicy(model.name)) return false
|
||||
return true
|
||||
})
|
||||
const seen = new Set<string>()
|
||||
const rows: ModelRow[] = []
|
||||
|
||||
for (const policy of perModelPolicies.value) {
|
||||
const name = policy.model
|
||||
const found = globalModels.value.find(item => item.name === name)
|
||||
rows.push({
|
||||
name,
|
||||
displayName: found?.display_name || name,
|
||||
configured: true,
|
||||
})
|
||||
seen.add(name)
|
||||
}
|
||||
|
||||
for (const model of globalModels.value) {
|
||||
if (seen.has(model.name)) continue
|
||||
rows.push({
|
||||
name: model.name,
|
||||
displayName: model.display_name || model.name,
|
||||
configured: false,
|
||||
})
|
||||
}
|
||||
|
||||
return rows
|
||||
.filter(row => {
|
||||
if (modelFilter.value === 'configured' && !row.configured) return false
|
||||
if (modelFilter.value === 'unconfigured' && row.configured) return false
|
||||
if (!query) return true
|
||||
return (
|
||||
row.name.toLowerCase().includes(query)
|
||||
|| row.displayName.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
.sort((left, right) => {
|
||||
if (left.configured !== right.configured) {
|
||||
return left.configured ? -1 : 1
|
||||
}
|
||||
return left.name.localeCompare(right.name)
|
||||
})
|
||||
})
|
||||
|
||||
function normalizeRecord(group: RoutingGroupRecord): RoutingGroupRecord {
|
||||
@@ -730,13 +789,32 @@ function selectGroup(group: RoutingGroupRecord): void {
|
||||
resetEditingConfig()
|
||||
}
|
||||
|
||||
function displayGroupEnabled(group: RoutingGroupRecord): boolean {
|
||||
if (!isCreating.value && group.id === selectedGroupId.value && draft.value) {
|
||||
return draft.value.enabled
|
||||
}
|
||||
return group.enabled
|
||||
}
|
||||
|
||||
function setDraftEnabled(value: boolean): void {
|
||||
if (!draft.value) return
|
||||
draft.value.enabled = value
|
||||
}
|
||||
|
||||
function updateGroupEnabled(group: RoutingGroupRecord, value: boolean): void {
|
||||
if (isCreating.value || group.id !== selectedGroupId.value || !draft.value) {
|
||||
selectGroup(group)
|
||||
}
|
||||
setDraftEnabled(value)
|
||||
}
|
||||
|
||||
function startCreate(): void {
|
||||
isCreating.value = true
|
||||
selectedGroupId.value = null
|
||||
draft.value = {
|
||||
name: '新调度策略',
|
||||
description: '',
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
is_system_default: groups.value.length === 0,
|
||||
config_json: createEmptyRoutingGroupConfig(),
|
||||
version: 1,
|
||||
@@ -803,7 +881,6 @@ const canRemoveCurrentModel = computed(() => {
|
||||
&& currentModelPersisted.value
|
||||
&& !saving.value
|
||||
&& !editingDirty.value
|
||||
&& !draftDirty.value
|
||||
})
|
||||
|
||||
function syncEditorStateFromConfig(config: RoutingGroupConfig): void {
|
||||
@@ -863,7 +940,7 @@ function updateFirstStepSchedulingMode(mode: RoutingSchedulingMode): void {
|
||||
|
||||
function removePerModelPolicy(model: string): void {
|
||||
if (!draft.value) return
|
||||
if (perModelEditingActive.value && (editingDirty.value || draftDirty.value)) {
|
||||
if (perModelEditingActive.value && editingDirty.value) {
|
||||
showError('请先保存当前改动后再移除模型')
|
||||
return
|
||||
}
|
||||
@@ -873,18 +950,27 @@ function removePerModelPolicy(model: string): void {
|
||||
if (selectedPerModelName.value === model) {
|
||||
selectedPerModelName.value = null
|
||||
}
|
||||
modelFilter.value = 'unconfigured'
|
||||
updateDraftConfig(next)
|
||||
resetEditingConfig()
|
||||
}
|
||||
|
||||
function selectGlobalModel(model: string): void {
|
||||
if (!model) return
|
||||
if (model === selectedPerModelName.value) return
|
||||
if (model === selectedPerModelName.value) {
|
||||
resetEditingConfig()
|
||||
selectedPerModelName.value = null
|
||||
return
|
||||
}
|
||||
const shouldAddModel = !hasModelPolicy(model)
|
||||
if (perModelEditingActive.value && editingDirty.value) {
|
||||
switchModelTarget.value = model
|
||||
switchModelDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
if (shouldAddModel) {
|
||||
resetEditingConfig()
|
||||
}
|
||||
selectedPerModelName.value = model
|
||||
}
|
||||
|
||||
@@ -1013,11 +1099,6 @@ function replaceGroup(group: RoutingGroupRecord): void {
|
||||
selectGroup(normalized)
|
||||
}
|
||||
|
||||
function refreshPage(): void {
|
||||
void fetchGroups()
|
||||
void loadGlobalModels()
|
||||
}
|
||||
|
||||
async function fetchGroups(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -1106,6 +1187,7 @@ function saveCurrentModel(): void {
|
||||
next = { ...next, allowed_models: [...next.allowed_models, model] }
|
||||
}
|
||||
updateDraftConfig(next)
|
||||
modelFilter.value = 'configured'
|
||||
resetEditingConfig()
|
||||
success('当前模型配置已保存到草稿,点击外层保存后生效')
|
||||
}
|
||||
|
||||
@@ -33,375 +33,375 @@
|
||||
</template>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="flex items-center justify-between rounded-lg border border-border p-4">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
||||
<CloudUpload class="w-4.5 h-4.5" />
|
||||
<div class="flex items-center justify-between rounded-lg border border-border p-4">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-9 h-9 rounded-lg bg-primary/10 text-primary flex items-center justify-center shrink-0">
|
||||
<CloudUpload class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="text-sm font-medium">
|
||||
自动备份
|
||||
</h4>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
{{ backup.config.value.enabled ? '已启用' : '未启用' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="text-sm font-medium">
|
||||
自动备份
|
||||
</h4>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
{{ backup.config.value.enabled ? '已启用' : '未启用' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="backup.config.value.enabled"
|
||||
@update:model-value="backup.config.value.enabled = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label
|
||||
for="backup-scope"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
备份范围
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="backup.config.value.scope"
|
||||
@update:model-value="setScope"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-scope"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="config">
|
||||
配置数据
|
||||
</SelectItem>
|
||||
<SelectItem value="users">
|
||||
用户数据
|
||||
</SelectItem>
|
||||
<SelectItem value="data">
|
||||
完整备份
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-bucket"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Bucket
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-bucket"
|
||||
:model-value="backup.config.value.bucket"
|
||||
class="mt-1"
|
||||
placeholder="aether-backups"
|
||||
@update:model-value="backup.config.value.bucket = String($event)"
|
||||
<Switch
|
||||
:model-value="backup.config.value.enabled"
|
||||
@update:model-value="backup.config.value.enabled = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-endpoint"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Endpoint
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-endpoint"
|
||||
:model-value="backup.config.value.endpoint"
|
||||
class="mt-1"
|
||||
placeholder="https://s3.example.com"
|
||||
@update:model-value="backup.config.value.endpoint = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-access-key"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Access Key ID
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-access-key"
|
||||
:model-value="backup.config.value.accessKeyId"
|
||||
class="mt-1"
|
||||
autocomplete="off"
|
||||
@update:model-value="backup.config.value.accessKeyId = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label
|
||||
for="backup-secret-key"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Secret Access Key
|
||||
</Label>
|
||||
<button
|
||||
v-if="backup.config.value.secretAccessKeyIsSet"
|
||||
type="button"
|
||||
class="text-xs text-muted-foreground hover:text-foreground"
|
||||
:disabled="backup.saving.value"
|
||||
@click="backup.clearS3SecretAccessKey"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative mt-1">
|
||||
<Input
|
||||
id="backup-secret-key"
|
||||
:model-value="backup.config.value.secretAccessKey"
|
||||
masked
|
||||
disable-autofill
|
||||
autocomplete="new-password"
|
||||
:placeholder="backup.config.value.secretAccessKeyIsSet ? '已配置,留空不变' : ''"
|
||||
@update:model-value="backup.config.value.secretAccessKey = String($event)"
|
||||
/>
|
||||
<KeyRound
|
||||
v-if="backup.config.value.secretAccessKeyIsSet && !backup.config.value.secretAccessKey"
|
||||
class="absolute right-10 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-retention"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
最多保留备份数
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-retention"
|
||||
:model-value="backup.config.value.retentionCount"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('retentionCount', $event, 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-4">
|
||||
<div>
|
||||
<Label
|
||||
for="backup-interval"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
周期间隔
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-interval"
|
||||
:model-value="backup.config.value.scheduleInterval"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleInterval', $event, 1)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-unit"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
周期单位
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="backup.config.value.scheduleUnit"
|
||||
@update:model-value="setScheduleUnit"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-unit"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">
|
||||
小时
|
||||
</SelectItem>
|
||||
<SelectItem value="days">
|
||||
天
|
||||
</SelectItem>
|
||||
<SelectItem value="weeks">
|
||||
周
|
||||
</SelectItem>
|
||||
<SelectItem value="months">
|
||||
月
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="px-0"
|
||||
@click="backup.advancedOpen.value = !backup.advancedOpen.value"
|
||||
>
|
||||
<ChevronDown
|
||||
v-if="backup.advancedOpen.value"
|
||||
class="w-3.5 h-3.5 mr-1.5"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
class="w-3.5 h-3.5 mr-1.5"
|
||||
/>
|
||||
高级选项
|
||||
</Button>
|
||||
|
||||
<div
|
||||
v-if="backup.advancedOpen.value"
|
||||
class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label
|
||||
for="backup-region"
|
||||
for="backup-scope"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Region
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-region"
|
||||
:model-value="backup.config.value.region"
|
||||
class="mt-1"
|
||||
placeholder="auto"
|
||||
@update:model-value="backup.config.value.region = String($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-prefix"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Prefix
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-prefix"
|
||||
:model-value="backup.config.value.prefix"
|
||||
class="mt-1"
|
||||
placeholder="aether/backups/"
|
||||
@update:model-value="backup.config.value.prefix = String($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-hour"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
执行小时
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-hour"
|
||||
:model-value="backup.config.value.scheduleHour"
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleHour', $event, 0)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-minute"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
执行分钟
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-minute"
|
||||
:model-value="backup.config.value.scheduleMinute"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleMinute', $event, 0)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="backup.config.value.scheduleUnit === 'weeks'">
|
||||
<Label
|
||||
for="backup-weekday"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
星期
|
||||
备份范围
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="String(backup.config.value.scheduleWeekday)"
|
||||
@update:model-value="setNumber('scheduleWeekday', $event, 1)"
|
||||
:model-value="backup.config.value.scope"
|
||||
@update:model-value="setScope"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-weekday"
|
||||
id="backup-scope"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="item in weekdays"
|
||||
:key="item.value"
|
||||
:value="String(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<SelectItem value="config">
|
||||
配置数据
|
||||
</SelectItem>
|
||||
<SelectItem value="users">
|
||||
用户数据
|
||||
</SelectItem>
|
||||
<SelectItem value="data">
|
||||
完整备份
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="backup.config.value.scheduleUnit === 'months'">
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-month-day"
|
||||
for="backup-bucket"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
每月日期
|
||||
Bucket
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-month-day"
|
||||
:model-value="backup.config.value.scheduleMonthDay"
|
||||
id="backup-bucket"
|
||||
:model-value="backup.config.value.bucket"
|
||||
class="mt-1"
|
||||
placeholder="aether-backups"
|
||||
@update:model-value="backup.config.value.bucket = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-endpoint"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Endpoint
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-endpoint"
|
||||
:model-value="backup.config.value.endpoint"
|
||||
class="mt-1"
|
||||
placeholder="https://s3.example.com"
|
||||
@update:model-value="backup.config.value.endpoint = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-access-key"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Access Key ID
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-access-key"
|
||||
:model-value="backup.config.value.accessKeyId"
|
||||
class="mt-1"
|
||||
autocomplete="off"
|
||||
@update:model-value="backup.config.value.accessKeyId = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label
|
||||
for="backup-secret-key"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Secret Access Key
|
||||
</Label>
|
||||
<button
|
||||
v-if="backup.config.value.secretAccessKeyIsSet"
|
||||
type="button"
|
||||
class="text-xs text-muted-foreground hover:text-foreground"
|
||||
:disabled="backup.saving.value"
|
||||
@click="backup.clearS3SecretAccessKey"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative mt-1">
|
||||
<Input
|
||||
id="backup-secret-key"
|
||||
:model-value="backup.config.value.secretAccessKey"
|
||||
masked
|
||||
disable-autofill
|
||||
autocomplete="new-password"
|
||||
:placeholder="backup.config.value.secretAccessKeyIsSet ? '已配置,留空不变' : ''"
|
||||
@update:model-value="backup.config.value.secretAccessKey = String($event)"
|
||||
/>
|
||||
<KeyRound
|
||||
v-if="backup.config.value.secretAccessKeyIsSet && !backup.config.value.secretAccessKey"
|
||||
class="absolute right-10 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
for="backup-retention"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
最多保留备份数
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-retention"
|
||||
:model-value="backup.config.value.retentionCount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleMonthDay', $event, 1)"
|
||||
@update:model-value="setNumber('retentionCount', $event, 1)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg border border-border p-4">
|
||||
<span class="text-sm font-medium">Path Style</span>
|
||||
<Switch
|
||||
:model-value="backup.config.value.pathStyle"
|
||||
@update:model-value="backup.config.value.pathStyle = $event"
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-4">
|
||||
<div>
|
||||
<Label
|
||||
for="backup-interval"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
周期间隔
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-interval"
|
||||
:model-value="backup.config.value.scheduleInterval"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleInterval', $event, 1)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-compression"
|
||||
for="backup-unit"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
压缩格式
|
||||
周期单位
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="backup.config.value.compression"
|
||||
@update:model-value="backup.config.value.compression = $event"
|
||||
:model-value="backup.config.value.scheduleUnit"
|
||||
@update:model-value="setScheduleUnit"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-compression"
|
||||
id="backup-unit"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zstd">
|
||||
zstd
|
||||
<SelectItem value="hours">
|
||||
小时
|
||||
</SelectItem>
|
||||
<SelectItem value="days">
|
||||
天
|
||||
</SelectItem>
|
||||
<SelectItem value="weeks">
|
||||
周
|
||||
</SelectItem>
|
||||
<SelectItem value="months">
|
||||
月
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="px-0"
|
||||
@click="backup.advancedOpen.value = !backup.advancedOpen.value"
|
||||
>
|
||||
<ChevronDown
|
||||
v-if="backup.advancedOpen.value"
|
||||
class="w-3.5 h-3.5 mr-1.5"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
class="w-3.5 h-3.5 mr-1.5"
|
||||
/>
|
||||
高级选项
|
||||
</Button>
|
||||
|
||||
<div
|
||||
v-if="backup.advancedOpen.value"
|
||||
class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-region"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Region
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-region"
|
||||
:model-value="backup.config.value.region"
|
||||
class="mt-1"
|
||||
placeholder="auto"
|
||||
@update:model-value="backup.config.value.region = String($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-prefix"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
Prefix
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-prefix"
|
||||
:model-value="backup.config.value.prefix"
|
||||
class="mt-1"
|
||||
placeholder="aether/backups/"
|
||||
@update:model-value="backup.config.value.prefix = String($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-hour"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
执行小时
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-hour"
|
||||
:model-value="backup.config.value.scheduleHour"
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleHour', $event, 0)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-minute"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
执行分钟
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-minute"
|
||||
:model-value="backup.config.value.scheduleMinute"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleMinute', $event, 0)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="backup.config.value.scheduleUnit === 'weeks'">
|
||||
<Label
|
||||
for="backup-weekday"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
星期
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="String(backup.config.value.scheduleWeekday)"
|
||||
@update:model-value="setNumber('scheduleWeekday', $event, 1)"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-weekday"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="item in weekdays"
|
||||
:key="item.value"
|
||||
:value="String(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="backup.config.value.scheduleUnit === 'months'">
|
||||
<Label
|
||||
for="backup-month-day"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
每月日期
|
||||
</Label>
|
||||
<Input
|
||||
id="backup-month-day"
|
||||
:model-value="backup.config.value.scheduleMonthDay"
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
class="mt-1"
|
||||
@update:model-value="setNumber('scheduleMonthDay', $event, 1)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg border border-border p-4">
|
||||
<span class="text-sm font-medium">Path Style</span>
|
||||
<Switch
|
||||
:model-value="backup.config.value.pathStyle"
|
||||
@update:model-value="backup.config.value.pathStyle = $event"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label
|
||||
for="backup-compression"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
压缩格式
|
||||
</Label>
|
||||
<Select
|
||||
:model-value="backup.config.value.compression"
|
||||
@update:model-value="backup.config.value.compression = $event"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="backup-compression"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zstd">
|
||||
zstd
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
</PageContainer>
|
||||
|
||||
@@ -111,9 +111,9 @@
|
||||
>
|
||||
{{ apiKey.name }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">
|
||||
创建于 {{ formatDate(apiKey.created_at) }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">
|
||||
创建于 {{ formatDate(apiKey.created_at) }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5 truncate">
|
||||
IP 限制:{{ formatIpRules(apiKey.ip_rules) }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user