mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
refactor(frontend): 优化功能模块组件
- 更新 api-keys 模块: StandaloneKeyFormDialog - 改进 auth 模块: LoginDialog - 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor - 重构 providers 模块: 多个表单和对话框组件 - 更新 usage 模块: 时间线、表格和详情组件 - 调整 users 模块: UserFormDialog
This commit is contained in:
@@ -8,8 +8,14 @@
|
|||||||
<div class="border-b border-border px-6 py-4">
|
<div class="border-b border-border px-6 py-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
|
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
|
||||||
<Plus v-if="!isEditMode" class="h-5 w-5 text-primary" />
|
<Plus
|
||||||
<SquarePen v-else class="h-5 w-5 text-primary" />
|
v-if="!isEditMode"
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
/>
|
||||||
|
<SquarePen
|
||||||
|
v-else
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||||
@@ -33,7 +39,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-name" class="text-sm font-medium">密钥名称</Label>
|
<Label
|
||||||
|
for="form-name"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>密钥名称</Label>
|
||||||
<Input
|
<Input
|
||||||
id="form-name"
|
id="form-name"
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
@@ -44,8 +53,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 初始余额 - 仅创建模式显示 -->
|
<!-- 初始余额 - 仅创建模式显示 -->
|
||||||
<div v-if="!isEditMode" class="space-y-2">
|
<div
|
||||||
<Label for="form-balance" class="text-sm font-medium">初始余额 (USD) <span class="text-rose-500">*</span></Label>
|
v-if="!isEditMode"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
for="form-balance"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>初始余额 (USD) <span class="text-rose-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="form-balance"
|
id="form-balance"
|
||||||
:model-value="form.initial_balance_usd ?? ''"
|
:model-value="form.initial_balance_usd ?? ''"
|
||||||
@@ -57,11 +72,16 @@
|
|||||||
class="h-10"
|
class="h-10"
|
||||||
@update:model-value="(v) => form.initial_balance_usd = parseNumberInput(v, { allowFloat: true }) ?? 10"
|
@update:model-value="(v) => form.initial_balance_usd = parseNumberInput(v, { allowFloat: true }) ?? 10"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground">独立Key必须设置余额限制,最小值 $0.01</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
独立Key必须设置余额限制,最小值 $0.01
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-expire-days" class="text-sm font-medium">有效期设置</Label>
|
<Label
|
||||||
|
for="form-expire-days"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>有效期设置</Label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="form-expire-days"
|
id="form-expire-days"
|
||||||
@@ -80,24 +100,32 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer"
|
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer"
|
||||||
@change="onNeverExpireChange"
|
@change="onNeverExpireChange"
|
||||||
/>
|
>
|
||||||
永不过期
|
永不过期
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-1.5 border rounded-md px-2 py-1.5 bg-muted/50 cursor-pointer text-xs whitespace-nowrap" :class="form.never_expire ? 'opacity-50' : ''">
|
<label
|
||||||
|
class="flex items-center gap-1.5 border rounded-md px-2 py-1.5 bg-muted/50 cursor-pointer text-xs whitespace-nowrap"
|
||||||
|
:class="form.never_expire ? 'opacity-50' : ''"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.auto_delete_on_expiry"
|
v-model="form.auto_delete_on_expiry"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer"
|
class="h-3.5 w-3.5 rounded border-gray-300 cursor-pointer"
|
||||||
:disabled="form.never_expire"
|
:disabled="form.never_expire"
|
||||||
/>
|
>
|
||||||
到期删除
|
到期删除
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">不勾选"到期删除"则仅禁用</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
不勾选"到期删除"则仅禁用
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-rate-limit" class="text-sm font-medium">速率限制 (请求/分钟)</Label>
|
<Label
|
||||||
|
for="form-rate-limit"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>速率限制 (请求/分钟)</Label>
|
||||||
<Input
|
<Input
|
||||||
id="form-rate-limit"
|
id="form-rate-limit"
|
||||||
:model-value="form.rate_limit ?? ''"
|
:model-value="form.rate_limit ?? ''"
|
||||||
@@ -131,9 +159,16 @@
|
|||||||
<span :class="form.allowed_providers.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_providers.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_providers.length ? `已选择 ${form.allowed_providers.length} 个` : '全部可用' }}
|
{{ form.allowed_providers.length ? `已选择 ${form.allowed_providers.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="providerDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="providerDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="providerDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="providerDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="providerDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="providerDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="providerDropdownOpen"
|
v-if="providerDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -150,10 +185,13 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_providers', provider.id)"
|
@change="toggleSelection('allowed_providers', provider.id)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ provider.display_name || provider.name }}</span>
|
<span class="text-sm">{{ provider.display_name || provider.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="providers.length === 0" class="px-3 py-2 text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="providers.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无可用 Provider
|
暂无可用 Provider
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,9 +210,16 @@
|
|||||||
<span :class="form.allowed_api_formats.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_api_formats.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_api_formats.length ? `已选择 ${form.allowed_api_formats.length} 个` : '全部可用' }}
|
{{ form.allowed_api_formats.length ? `已选择 ${form.allowed_api_formats.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="apiFormatDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="apiFormatDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="apiFormatDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="apiFormatDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="apiFormatDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="apiFormatDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="apiFormatDropdownOpen"
|
v-if="apiFormatDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -191,7 +236,7 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_api_formats', format)"
|
@change="toggleSelection('allowed_api_formats', format)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ format }}</span>
|
<span class="text-sm">{{ format }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,9 +255,16 @@
|
|||||||
<span :class="form.allowed_models.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_models.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_models.length ? `已选择 ${form.allowed_models.length} 个` : '全部可用' }}
|
{{ form.allowed_models.length ? `已选择 ${form.allowed_models.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="modelDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="modelDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="modelDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="modelDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="modelDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="modelDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="modelDropdownOpen"
|
v-if="modelDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -229,10 +281,13 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_models', model.name)"
|
@change="toggleSelection('allowed_models', model.name)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ model.name }}</span>
|
<span class="text-sm">{{ model.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalModels.length === 0" class="px-3 py-2 text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="globalModels.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无可用模型
|
暂无可用模型
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,8 +298,19 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button variant="outline" @click="handleCancel" type="button" class="h-10 px-5">取消</Button>
|
<Button
|
||||||
<Button @click="handleSubmit" :disabled="saving" class="h-10 px-5">
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
class="h-10 px-5"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="saving"
|
||||||
|
class="h-10 px-5"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
{{ saving ? (isEditMode ? '更新中...' : '创建中...') : (isEditMode ? '更新' : '创建') }}
|
{{ saving ? (isEditMode ? '更新中...' : '创建中...') : (isEditMode ? '更新' : '创建') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="isOpen" size="lg">
|
<Dialog
|
||||||
|
v-model="isOpen"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Logo 和标题 -->
|
<!-- Logo 和标题 -->
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<div class="mb-4 rounded-3xl border border-primary/30 dark:border-[#cc785c]/30 bg-primary/5 dark:bg-transparent p-4 shadow-inner shadow-white/40 dark:shadow-[#cc785c]/10">
|
<div class="mb-4 rounded-3xl border border-primary/30 dark:border-[#cc785c]/30 bg-primary/5 dark:bg-transparent p-4 shadow-inner shadow-white/40 dark:shadow-[#cc785c]/10">
|
||||||
<img src="/aether_adaptive.svg" alt="Logo" class="h-16 w-16" />
|
<img
|
||||||
|
src="/aether_adaptive.svg"
|
||||||
|
alt="Logo"
|
||||||
|
class="h-16 w-16"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">
|
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
欢迎回来
|
欢迎回来
|
||||||
@@ -12,11 +19,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Demo 模式提示 -->
|
<!-- Demo 模式提示 -->
|
||||||
<div v-if="isDemo" class="rounded-lg border border-primary/20 dark:border-primary/30 bg-primary/5 dark:bg-primary/10 p-4">
|
<div
|
||||||
|
v-if="isDemo"
|
||||||
|
class="rounded-lg border border-primary/20 dark:border-primary/30 bg-primary/5 dark:bg-primary/10 p-4"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="flex-shrink-0 text-primary dark:text-primary/90">
|
<div class="flex-shrink-0 text-primary dark:text-primary/90">
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
class="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -29,16 +47,16 @@
|
|||||||
<div class="mt-3 space-y-2">
|
<div class="mt-3 space-y-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="fillDemoAccount('admin')"
|
|
||||||
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
||||||
|
@click="fillDemoAccount('admin')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-primary/20 dark:bg-primary/30 text-primary text-[10px] font-bold group-hover:bg-primary/30 dark:group-hover:bg-primary/40 transition-colors">A</span>
|
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-primary/20 dark:bg-primary/30 text-primary text-[10px] font-bold group-hover:bg-primary/30 dark:group-hover:bg-primary/40 transition-colors">A</span>
|
||||||
<span>管理员:admin@demo.aether.io / demo123</span>
|
<span>管理员:admin@demo.aether.io / demo123</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="fillDemoAccount('user')"
|
|
||||||
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors group"
|
||||||
|
@click="fillDemoAccount('user')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-muted text-muted-foreground text-[10px] font-bold group-hover:bg-muted/80 transition-colors">U</span>
|
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-muted text-muted-foreground text-[10px] font-bold group-hover:bg-muted/80 transition-colors">U</span>
|
||||||
<span>普通用户:user@demo.aether.io / demo123</span>
|
<span>普通用户:user@demo.aether.io / demo123</span>
|
||||||
@@ -49,7 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
<form
|
||||||
|
class="space-y-4"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="login-email">邮箱</Label>
|
<Label for="login-email">邮箱</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -76,7 +97,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<p v-if="!isDemo" class="text-xs text-slate-400 dark:text-muted-foreground/80">
|
<p
|
||||||
|
v-if="!isDemo"
|
||||||
|
class="text-xs text-slate-400 dark:text-muted-foreground/80"
|
||||||
|
>
|
||||||
如需开通账户,请联系管理员配置访问权限
|
如需开通账户,请联系管理员配置访问权限
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
@@ -84,17 +108,17 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button
|
<Button
|
||||||
@click="isOpen = false"
|
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full sm:w-auto border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:text-primary hover:border-primary/50 hover:bg-primary/5 dark:hover:text-primary dark:hover:border-primary/50 dark:hover:bg-primary/10"
|
class="w-full sm:w-auto border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:text-primary hover:border-primary/50 hover:bg-primary/5 dark:hover:text-primary dark:hover:border-primary/50 dark:hover:bg-primary/10"
|
||||||
|
@click="isOpen = false"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleLogin"
|
|
||||||
:disabled="authStore.loading"
|
:disabled="authStore.loading"
|
||||||
class="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white border-0"
|
class="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white border-0"
|
||||||
|
@click="handleLogin"
|
||||||
>
|
>
|
||||||
{{ authStore.loading ? '登录中...' : '登录' }}
|
{{ authStore.loading ? '登录中...' : '登录' }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,42 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:model-value="open"
|
:model-value="open"
|
||||||
@update:model-value="handleDialogUpdate"
|
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
:description="dialogDescription"
|
:description="dialogDescription"
|
||||||
:icon="dialogIcon"
|
:icon="dialogIcon"
|
||||||
size="md"
|
size="md"
|
||||||
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form
|
||||||
|
class="space-y-4"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<!-- 模式选择(仅创建时显示) -->
|
<!-- 模式选择(仅创建时显示) -->
|
||||||
<div v-if="!isEditMode" class="space-y-2">
|
<div
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>创建类型 *</Label>
|
<Label>创建类型 *</Label>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="form.mapping_type = 'alias'"
|
class="p-3 rounded-lg border-2 text-left transition-all"
|
||||||
:class="[
|
:class="[
|
||||||
'p-3 rounded-lg border-2 text-left transition-all',
|
|
||||||
form.mapping_type === 'alias'
|
form.mapping_type === 'alias'
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
: 'border-border hover:border-primary/50'
|
: 'border-border hover:border-primary/50'
|
||||||
]"
|
]"
|
||||||
|
@click="form.mapping_type = 'alias'"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-sm">别名</div>
|
<div class="font-medium text-sm">
|
||||||
<div class="text-xs text-muted-foreground mt-1">名称简写,按目标模型计费</div>
|
别名
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
名称简写,按目标模型计费
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="form.mapping_type = 'mapping'"
|
class="p-3 rounded-lg border-2 text-left transition-all"
|
||||||
:class="[
|
:class="[
|
||||||
'p-3 rounded-lg border-2 text-left transition-all',
|
|
||||||
form.mapping_type === 'mapping'
|
form.mapping_type === 'mapping'
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
: 'border-border hover:border-primary/50'
|
: 'border-border hover:border-primary/50'
|
||||||
]"
|
]"
|
||||||
|
@click="form.mapping_type = 'mapping'"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-sm">映射</div>
|
<div class="font-medium text-sm">
|
||||||
<div class="text-xs text-muted-foreground mt-1">模型降级,按源模型计费</div>
|
映射
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
模型降级,按源模型计费
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,20 +68,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Provider 选择/作用范围 -->
|
<!-- Provider 选择/作用范围 -->
|
||||||
<div v-if="showProviderSelect" class="space-y-2">
|
<div
|
||||||
|
v-if="showProviderSelect"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>作用范围</Label>
|
<Label>作用范围</Label>
|
||||||
<!-- 固定 Provider 时显示只读 -->
|
<!-- 固定 Provider 时显示只读 -->
|
||||||
<div v-if="fixedProvider" class="px-3 py-2 border rounded-md bg-muted/50 text-sm">
|
<div
|
||||||
|
v-if="fixedProvider"
|
||||||
|
class="px-3 py-2 border rounded-md bg-muted/50 text-sm"
|
||||||
|
>
|
||||||
仅 {{ fixedProvider.display_name || fixedProvider.name }}
|
仅 {{ fixedProvider.display_name || fixedProvider.name }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 否则显示可选择的下拉 -->
|
<!-- 否则显示可选择的下拉 -->
|
||||||
<Select v-else v-model:open="providerSelectOpen" :model-value="form.provider_id || 'global'" @update:model-value="handleProviderChange">
|
<Select
|
||||||
|
v-else
|
||||||
|
v-model:open="providerSelectOpen"
|
||||||
|
:model-value="form.provider_id || 'global'"
|
||||||
|
@update:model-value="handleProviderChange"
|
||||||
|
>
|
||||||
<SelectTrigger class="w-full">
|
<SelectTrigger class="w-full">
|
||||||
<SelectValue placeholder="选择作用范围" />
|
<SelectValue placeholder="选择作用范围" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="global">全局(所有 Provider)</SelectItem>
|
<SelectItem value="global">
|
||||||
<SelectItem v-for="p in providers" :key="p.id" :value="p.id">
|
全局(所有 Provider)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="p in providers"
|
||||||
|
:key="p.id"
|
||||||
|
:value="p.id"
|
||||||
|
>
|
||||||
仅 {{ p.display_name || p.name }}
|
仅 {{ p.display_name || p.name }}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -75,7 +106,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 别名模式:别名名称 -->
|
<!-- 别名模式:别名名称 -->
|
||||||
<div v-if="form.mapping_type === 'alias'" class="space-y-2">
|
<div
|
||||||
|
v-if="form.mapping_type === 'alias'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label for="alias-name">别名名称 *</Label>
|
<Label for="alias-name">别名名称 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="alias-name"
|
id="alias-name"
|
||||||
@@ -90,10 +124,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 映射模式:选择源模型 -->
|
<!-- 映射模式:选择源模型 -->
|
||||||
<div v-else class="space-y-2">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label>源模型 (用户请求的模型) *</Label>
|
<Label>源模型 (用户请求的模型) *</Label>
|
||||||
<Select v-model:open="sourceModelSelectOpen" :model-value="form.alias" @update:model-value="form.alias = $event" :disabled="isEditMode">
|
<Select
|
||||||
<SelectTrigger class="w-full" :class="{ 'opacity-50': isEditMode }">
|
v-model:open="sourceModelSelectOpen"
|
||||||
|
:model-value="form.alias"
|
||||||
|
:disabled="isEditMode"
|
||||||
|
@update:model-value="form.alias = $event"
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'opacity-50': isEditMode }"
|
||||||
|
>
|
||||||
<SelectValue placeholder="请选择源模型" />
|
<SelectValue placeholder="请选择源模型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -117,12 +162,20 @@
|
|||||||
{{ form.mapping_type === 'alias' ? '目标模型 *' : '目标模型 (实际处理请求) *' }}
|
{{ form.mapping_type === 'alias' ? '目标模型 *' : '目标模型 (实际处理请求) *' }}
|
||||||
</Label>
|
</Label>
|
||||||
<!-- 固定目标模型时显示只读信息 -->
|
<!-- 固定目标模型时显示只读信息 -->
|
||||||
<div v-if="fixedTargetModel" class="px-3 py-2 border rounded-md bg-muted/50">
|
<div
|
||||||
|
v-if="fixedTargetModel"
|
||||||
|
class="px-3 py-2 border rounded-md bg-muted/50"
|
||||||
|
>
|
||||||
<span class="font-medium">{{ fixedTargetModel.display_name }}</span>
|
<span class="font-medium">{{ fixedTargetModel.display_name }}</span>
|
||||||
<span class="text-muted-foreground ml-1">({{ fixedTargetModel.name }})</span>
|
<span class="text-muted-foreground ml-1">({{ fixedTargetModel.name }})</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 否则显示下拉选择 -->
|
<!-- 否则显示下拉选择 -->
|
||||||
<Select v-else v-model:open="targetModelSelectOpen" :model-value="form.global_model_id" @update:model-value="form.global_model_id = $event">
|
<Select
|
||||||
|
v-else
|
||||||
|
v-model:open="targetModelSelectOpen"
|
||||||
|
:model-value="form.global_model_id"
|
||||||
|
@update:model-value="form.global_model_id = $event"
|
||||||
|
>
|
||||||
<SelectTrigger class="w-full">
|
<SelectTrigger class="w-full">
|
||||||
<SelectValue placeholder="请选择模型" />
|
<SelectValue placeholder="请选择模型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -137,15 +190,24 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button type="button" variant="outline" @click="handleCancel">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleSubmit" :disabled="submitting">
|
<Button
|
||||||
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
|
:disabled="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="submitting"
|
||||||
|
class="w-4 h-4 mr-2 animate-spin"
|
||||||
|
/>
|
||||||
{{ isEditMode ? '保存' : '创建' }}
|
{{ isEditMode ? '保存' : '创建' }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,14 +7,22 @@
|
|||||||
size="xl"
|
size="xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-5 max-h-[70vh] overflow-y-auto pr-1">
|
<form
|
||||||
|
class="space-y-5 max-h-[70vh] overflow-y-auto pr-1"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<!-- 基本信息 -->
|
<!-- 基本信息 -->
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<h4 class="font-medium text-sm">基本信息</h4>
|
<h4 class="font-medium text-sm">
|
||||||
|
基本信息
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="model-name" class="text-xs">模型名称 *</Label>
|
<Label
|
||||||
|
for="model-name"
|
||||||
|
class="text-xs"
|
||||||
|
>模型名称 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="model-name"
|
id="model-name"
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
@@ -22,10 +30,18 @@
|
|||||||
:disabled="isEditMode"
|
:disabled="isEditMode"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p v-if="!isEditMode" class="text-xs text-muted-foreground">创建后不可修改</p>
|
<p
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
创建后不可修改
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="model-display-name" class="text-xs">显示名称 *</Label>
|
<Label
|
||||||
|
for="model-display-name"
|
||||||
|
class="text-xs"
|
||||||
|
>显示名称 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="model-display-name"
|
id="model-display-name"
|
||||||
v-model="form.display_name"
|
v-model="form.display_name"
|
||||||
@@ -36,7 +52,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="model-description" class="text-xs">描述</Label>
|
<Label
|
||||||
|
for="model-description"
|
||||||
|
class="text-xs"
|
||||||
|
>描述</Label>
|
||||||
<Input
|
<Input
|
||||||
id="model-description"
|
id="model-description"
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
@@ -47,30 +66,52 @@
|
|||||||
|
|
||||||
<!-- 能力配置 -->
|
<!-- 能力配置 -->
|
||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
<h4 class="font-medium text-sm">默认能力</h4>
|
<h4 class="font-medium text-sm">
|
||||||
|
默认能力
|
||||||
|
</h4>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
||||||
<input type="checkbox" v-model="form.default_supports_streaming" class="rounded" />
|
<input
|
||||||
|
v-model="form.default_supports_streaming"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
<Zap class="w-3.5 h-3.5 text-muted-foreground" />
|
<Zap class="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<span>流式输出</span>
|
<span>流式输出</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
||||||
<input type="checkbox" v-model="form.default_supports_vision" class="rounded" />
|
<input
|
||||||
|
v-model="form.default_supports_vision"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
<Eye class="w-3.5 h-3.5 text-muted-foreground" />
|
<Eye class="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<span>视觉理解</span>
|
<span>视觉理解</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
||||||
<input type="checkbox" v-model="form.default_supports_function_calling" class="rounded" />
|
<input
|
||||||
|
v-model="form.default_supports_function_calling"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
<Wrench class="w-3.5 h-3.5 text-muted-foreground" />
|
<Wrench class="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<span>工具调用</span>
|
<span>工具调用</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
||||||
<input type="checkbox" v-model="form.default_supports_extended_thinking" class="rounded" />
|
<input
|
||||||
|
v-model="form.default_supports_extended_thinking"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
<Brain class="w-3.5 h-3.5 text-muted-foreground" />
|
<Brain class="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<span>深度思考</span>
|
<span>深度思考</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm">
|
||||||
<input type="checkbox" v-model="form.default_supports_image_generation" class="rounded" />
|
<input
|
||||||
|
v-model="form.default_supports_image_generation"
|
||||||
|
type="checkbox"
|
||||||
|
class="rounded"
|
||||||
|
>
|
||||||
<Image class="w-3.5 h-3.5 text-muted-foreground" />
|
<Image class="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<span>图像生成</span>
|
<span>图像生成</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -78,8 +119,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Key 能力配置 -->
|
<!-- Key 能力配置 -->
|
||||||
<section v-if="availableCapabilities.length > 0" class="space-y-2">
|
<section
|
||||||
<h4 class="font-medium text-sm">Key 能力支持</h4>
|
v-if="availableCapabilities.length > 0"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<h4 class="font-medium text-sm">
|
||||||
|
Key 能力支持
|
||||||
|
</h4>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label
|
<label
|
||||||
v-for="cap in availableCapabilities"
|
v-for="cap in availableCapabilities"
|
||||||
@@ -89,9 +135,9 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="form.supported_capabilities?.includes(cap.name)"
|
:checked="form.supported_capabilities?.includes(cap.name)"
|
||||||
@change="toggleCapability(cap.name)"
|
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
@change="toggleCapability(cap.name)"
|
||||||
|
>
|
||||||
<span>{{ cap.display_name }}</span>
|
<span>{{ cap.display_name }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,8 +145,14 @@
|
|||||||
|
|
||||||
<!-- 价格配置 -->
|
<!-- 价格配置 -->
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<h4 class="font-medium text-sm">价格配置</h4>
|
<h4 class="font-medium text-sm">
|
||||||
<TieredPricingEditor ref="tieredPricingEditorRef" v-model="tieredPricing" :show-cache1h="form.supported_capabilities?.includes('cache_1h')" />
|
价格配置
|
||||||
|
</h4>
|
||||||
|
<TieredPricingEditor
|
||||||
|
ref="tieredPricingEditorRef"
|
||||||
|
v-model="tieredPricing"
|
||||||
|
:show-cache1h="form.supported_capabilities?.includes('cache_1h')"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 按次计费 -->
|
<!-- 按次计费 -->
|
||||||
<div class="flex items-center gap-3 pt-2 border-t">
|
<div class="flex items-center gap-3 pt-2 border-t">
|
||||||
@@ -120,11 +172,21 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button type="button" variant="outline" @click="handleCancel">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleSubmit" :disabled="submitting">
|
<Button
|
||||||
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
|
:disabled="submitting"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="submitting"
|
||||||
|
class="w-4 h-4 mr-2 animate-spin"
|
||||||
|
/>
|
||||||
{{ isEditMode ? '保存' : '创建' }}
|
{{ isEditMode ? '保存' : '创建' }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -138,6 +200,7 @@ import { Dialog, Button, Input, Label } from '@/components/ui'
|
|||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useFormDialog } from '@/composables/useFormDialog'
|
import { useFormDialog } from '@/composables/useFormDialog'
|
||||||
import { parseNumberInput } from '@/utils/form'
|
import { parseNumberInput } from '@/utils/form'
|
||||||
|
import { log } from '@/utils/logger'
|
||||||
import TieredPricingEditor from './TieredPricingEditor.vue'
|
import TieredPricingEditor from './TieredPricingEditor.vue'
|
||||||
import {
|
import {
|
||||||
createGlobalModel,
|
createGlobalModel,
|
||||||
@@ -204,7 +267,7 @@ async function loadCapabilities() {
|
|||||||
try {
|
try {
|
||||||
availableCapabilities.value = await getAllCapabilities()
|
availableCapabilities.value = await getAllCapabilities()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load capabilities:', err)
|
log.error('Failed to load capabilities:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
@click.self="handleBackdropClick"
|
@click.self="handleBackdropClick"
|
||||||
>
|
>
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleBackdropClick"></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||||
|
@click="handleBackdropClick"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 抽屉内容 -->
|
<!-- 抽屉内容 -->
|
||||||
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
||||||
@@ -16,8 +19,13 @@
|
|||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="space-y-1 flex-1 min-w-0">
|
<div class="space-y-1 flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h3 class="text-xl font-bold truncate">{{ model.display_name }}</h3>
|
<h3 class="text-xl font-bold truncate">
|
||||||
<Badge :variant="model.is_active ? 'default' : 'secondary'" class="text-xs shrink-0">
|
{{ model.display_name }}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
:variant="model.is_active ? 'default' : 'secondary'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
{{ model.is_active ? '活跃' : '停用' }}
|
{{ model.is_active ? '活跃' : '停用' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,23 +40,36 @@
|
|||||||
</button>
|
</button>
|
||||||
<template v-if="model.description">
|
<template v-if="model.description">
|
||||||
<span class="shrink-0">·</span>
|
<span class="shrink-0">·</span>
|
||||||
<span class="text-xs truncate" :title="model.description">{{ model.description }}</span>
|
<span
|
||||||
|
class="text-xs truncate"
|
||||||
|
:title="model.description"
|
||||||
|
>{{ model.description }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button variant="ghost" size="icon" @click="$emit('edit-model', model)" title="编辑模型">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="编辑模型"
|
||||||
|
@click="$emit('edit-model', model)"
|
||||||
|
>
|
||||||
<Edit class="w-4 h-4" />
|
<Edit class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="$emit('toggle-model-status', model)"
|
|
||||||
:title="model.is_active ? '点击停用' : '点击启用'"
|
:title="model.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="$emit('toggle-model-status', model)"
|
||||||
>
|
>
|
||||||
<Power class="w-4 h-4" />
|
<Power class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" @click="handleClose" title="关闭">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="关闭"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
<X class="w-4 h-4" />
|
<X class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,110 +77,154 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- 自定义 Tab 切换 -->
|
<!-- 自定义 Tab 切换 -->
|
||||||
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg mb-4">
|
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg mb-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||||
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
:class="[
|
||||||
detailTab === 'basic'
|
detailTab === 'basic'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||||
]"
|
]"
|
||||||
@click="detailTab = 'basic'"
|
@click="detailTab = 'basic'"
|
||||||
>
|
>
|
||||||
基本信息
|
基本信息
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||||
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
:class="[
|
||||||
detailTab === 'providers'
|
detailTab === 'providers'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||||
]"
|
]"
|
||||||
@click="detailTab = 'providers'"
|
@click="detailTab = 'providers'"
|
||||||
>
|
>
|
||||||
关联提供商
|
关联提供商
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||||
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
:class="[
|
||||||
detailTab === 'aliases'
|
detailTab === 'aliases'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||||
]"
|
]"
|
||||||
@click="detailTab = 'aliases'"
|
@click="detailTab = 'aliases'"
|
||||||
>
|
>
|
||||||
别名/映射
|
别名/映射
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 内容 -->
|
<!-- Tab 内容 -->
|
||||||
<div v-show="detailTab === 'basic'" class="space-y-6">
|
<div
|
||||||
|
v-show="detailTab === 'basic'"
|
||||||
|
class="space-y-6"
|
||||||
|
>
|
||||||
<!-- 基础属性 -->
|
<!-- 基础属性 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm">基础属性</h4>
|
<h4 class="font-semibold text-sm">
|
||||||
|
基础属性
|
||||||
|
</h4>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label class="text-xs text-muted-foreground">创建时间</Label>
|
<Label class="text-xs text-muted-foreground">创建时间</Label>
|
||||||
<p class="text-sm mt-1">{{ formatDate(model.created_at) }}</p>
|
<p class="text-sm mt-1">
|
||||||
|
{{ formatDate(model.created_at) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型能力 -->
|
<!-- 模型能力 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h4 class="font-semibold text-sm">模型能力</h4>
|
<h4 class="font-semibold text-sm">
|
||||||
|
模型能力
|
||||||
|
</h4>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Zap class="w-5 h-5 text-muted-foreground" />
|
<Zap class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium">Streaming</p>
|
<p class="text-sm font-medium">
|
||||||
<p class="text-xs text-muted-foreground">流式输出</p>
|
Streaming
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
流式输出
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="model.default_supports_streaming ?? false ? 'default' : 'secondary'" class="text-xs">
|
<Badge
|
||||||
|
:variant="model.default_supports_streaming ?? false ? 'default' : 'secondary'"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ model.default_supports_streaming ?? false ? '支持' : '不支持' }}
|
{{ model.default_supports_streaming ?? false ? '支持' : '不支持' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Image class="w-5 h-5 text-muted-foreground" />
|
<Image class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium">Image Generation</p>
|
<p class="text-sm font-medium">
|
||||||
<p class="text-xs text-muted-foreground">图像生成</p>
|
Image Generation
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
图像生成
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="model.default_supports_image_generation ?? false ? 'default' : 'secondary'" class="text-xs">
|
<Badge
|
||||||
|
:variant="model.default_supports_image_generation ?? false ? 'default' : 'secondary'"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ model.default_supports_image_generation ?? false ? '支持' : '不支持' }}
|
{{ model.default_supports_image_generation ?? false ? '支持' : '不支持' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Eye class="w-5 h-5 text-muted-foreground" />
|
<Eye class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium">Vision</p>
|
<p class="text-sm font-medium">
|
||||||
<p class="text-xs text-muted-foreground">视觉理解</p>
|
Vision
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
视觉理解
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="model.default_supports_vision ?? false ? 'default' : 'secondary'" class="text-xs">
|
<Badge
|
||||||
|
:variant="model.default_supports_vision ?? false ? 'default' : 'secondary'"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ model.default_supports_vision ?? false ? '支持' : '不支持' }}
|
{{ model.default_supports_vision ?? false ? '支持' : '不支持' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Wrench class="w-5 h-5 text-muted-foreground" />
|
<Wrench class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium">Tool Use</p>
|
<p class="text-sm font-medium">
|
||||||
<p class="text-xs text-muted-foreground">工具调用</p>
|
Tool Use
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
工具调用
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="model.default_supports_function_calling ?? false ? 'default' : 'secondary'" class="text-xs">
|
<Badge
|
||||||
|
:variant="model.default_supports_function_calling ?? false ? 'default' : 'secondary'"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ model.default_supports_function_calling ?? false ? '支持' : '不支持' }}
|
{{ model.default_supports_function_calling ?? false ? '支持' : '不支持' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Brain class="w-5 h-5 text-muted-foreground" />
|
<Brain class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium">Extended Thinking</p>
|
<p class="text-sm font-medium">
|
||||||
<p class="text-xs text-muted-foreground">深度思考</p>
|
Extended Thinking
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
深度思考
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="model.default_supports_extended_thinking ?? false ? 'default' : 'secondary'" class="text-xs">
|
<Badge
|
||||||
|
:variant="model.default_supports_extended_thinking ?? false ? 'default' : 'secondary'"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ model.default_supports_extended_thinking ?? false ? '支持' : '不支持' }}
|
{{ model.default_supports_extended_thinking ?? false ? '支持' : '不支持' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,8 +232,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型偏好 -->
|
<!-- 模型偏好 -->
|
||||||
<div v-if="model.supported_capabilities && model.supported_capabilities.length > 0" class="space-y-3">
|
<div
|
||||||
<h4 class="font-semibold text-sm">模型偏好</h4>
|
v-if="model.supported_capabilities && model.supported_capabilities.length > 0"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
|
<h4 class="font-semibold text-sm">
|
||||||
|
模型偏好
|
||||||
|
</h4>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
v-for="cap in model.supported_capabilities"
|
v-for="cap in model.supported_capabilities"
|
||||||
@@ -183,10 +253,15 @@
|
|||||||
|
|
||||||
<!-- 默认定价 -->
|
<!-- 默认定价 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h4 class="font-semibold text-sm">默认定价</h4>
|
<h4 class="font-semibold text-sm">
|
||||||
|
默认定价
|
||||||
|
</h4>
|
||||||
|
|
||||||
<!-- 单阶梯(固定价格)展示 -->
|
<!-- 单阶梯(固定价格)展示 -->
|
||||||
<div v-if="getTierCount(model.default_tiered_pricing) <= 1" class="space-y-3">
|
<div
|
||||||
|
v-if="getTierCount(model.default_tiered_pricing) <= 1"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<!-- 按 Token 计费 -->
|
<!-- 按 Token 计费 -->
|
||||||
<div class="p-3 rounded-lg border">
|
<div class="p-3 rounded-lg border">
|
||||||
@@ -215,19 +290,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 1h 缓存 -->
|
<!-- 1h 缓存 -->
|
||||||
<div v-if="getFirst1hCachePrice(model.default_tiered_pricing) !== '-'" class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20">
|
<div
|
||||||
|
v-if="getFirst1hCachePrice(model.default_tiered_pricing) !== '-'"
|
||||||
|
class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20"
|
||||||
|
>
|
||||||
<Label class="text-xs text-muted-foreground whitespace-nowrap">1h 缓存创建</Label>
|
<Label class="text-xs text-muted-foreground whitespace-nowrap">1h 缓存创建</Label>
|
||||||
<span class="text-sm font-mono">{{ getFirst1hCachePrice(model.default_tiered_pricing) }}</span>
|
<span class="text-sm font-mono">{{ getFirst1hCachePrice(model.default_tiered_pricing) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按次计费 -->
|
<!-- 按次计费 -->
|
||||||
<div v-if="model.default_price_per_request && model.default_price_per_request > 0" class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20">
|
<div
|
||||||
|
v-if="model.default_price_per_request && model.default_price_per_request > 0"
|
||||||
|
class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20"
|
||||||
|
>
|
||||||
<Label class="text-xs text-muted-foreground whitespace-nowrap">按次计费</Label>
|
<Label class="text-xs text-muted-foreground whitespace-nowrap">按次计费</Label>
|
||||||
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 多阶梯计费展示 -->
|
<!-- 多阶梯计费展示 -->
|
||||||
<div v-else class="space-y-3">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Layers class="w-4 h-4" />
|
<Layers class="w-4 h-4" />
|
||||||
<span>阶梯计费 ({{ getTierCount(model.default_tiered_pricing) }} 档)</span>
|
<span>阶梯计费 ({{ getTierCount(model.default_tiered_pricing) }} 档)</span>
|
||||||
@@ -238,12 +322,24 @@
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="bg-muted/30">
|
<TableRow class="bg-muted/30">
|
||||||
<TableHead class="text-xs h-9">阶梯</TableHead>
|
<TableHead class="text-xs h-9">
|
||||||
<TableHead class="text-xs h-9 text-right">输入 ($/M)</TableHead>
|
阶梯
|
||||||
<TableHead class="text-xs h-9 text-right">输出 ($/M)</TableHead>
|
</TableHead>
|
||||||
<TableHead class="text-xs h-9 text-right">缓存创建</TableHead>
|
<TableHead class="text-xs h-9 text-right">
|
||||||
<TableHead class="text-xs h-9 text-right">缓存读取</TableHead>
|
输入 ($/M)
|
||||||
<TableHead class="text-xs h-9 text-right">1h 缓存</TableHead>
|
</TableHead>
|
||||||
|
<TableHead class="text-xs h-9 text-right">
|
||||||
|
输出 ($/M)
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="text-xs h-9 text-right">
|
||||||
|
缓存创建
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="text-xs h-9 text-right">
|
||||||
|
缓存读取
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="text-xs h-9 text-right">
|
||||||
|
1h 缓存
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -253,7 +349,10 @@
|
|||||||
class="text-xs"
|
class="text-xs"
|
||||||
>
|
>
|
||||||
<TableCell class="py-2">
|
<TableCell class="py-2">
|
||||||
<span v-if="tier.up_to === null" class="text-muted-foreground">
|
<span
|
||||||
|
v-if="tier.up_to === null"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
{{ index === 0 ? '所有' : `> ${formatTierLimit((model.default_tiered_pricing?.tiers || [])[index - 1]?.up_to)}` }}
|
{{ index === 0 ? '所有' : `> ${formatTierLimit((model.default_tiered_pricing?.tiers || [])[index - 1]?.up_to)}` }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
@@ -281,7 +380,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 按次计费(多阶梯时也显示) -->
|
<!-- 按次计费(多阶梯时也显示) -->
|
||||||
<div v-if="model.default_price_per_request && model.default_price_per_request > 0" class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20">
|
<div
|
||||||
|
v-if="model.default_price_per_request && model.default_price_per_request > 0"
|
||||||
|
class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20"
|
||||||
|
>
|
||||||
<Label class="text-xs text-muted-foreground whitespace-nowrap">按次计费</Label>
|
<Label class="text-xs text-muted-foreground whitespace-nowrap">按次计费</Label>
|
||||||
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,42 +392,50 @@
|
|||||||
|
|
||||||
<!-- 统计信息 -->
|
<!-- 统计信息 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h4 class="font-semibold text-sm">统计信息</h4>
|
<h4 class="font-semibold text-sm">
|
||||||
|
统计信息
|
||||||
|
</h4>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="p-3 rounded-lg border bg-muted/20">
|
<div class="p-3 rounded-lg border bg-muted/20">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Label class="text-xs text-muted-foreground">关联提供商</Label>
|
<Label class="text-xs text-muted-foreground">关联提供商</Label>
|
||||||
<Building2 class="w-4 h-4 text-muted-foreground" />
|
<Building2 class="w-4 h-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold mt-1">{{ model.provider_count || 0 }}</p>
|
<p class="text-2xl font-bold mt-1">
|
||||||
|
{{ model.provider_count || 0 }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 rounded-lg border bg-muted/20">
|
<div class="p-3 rounded-lg border bg-muted/20">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Label class="text-xs text-muted-foreground">别名数量</Label>
|
<Label class="text-xs text-muted-foreground">别名数量</Label>
|
||||||
<Tag class="w-4 h-4 text-muted-foreground" />
|
<Tag class="w-4 h-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-2xl font-bold mt-1">{{ model.alias_count || 0 }}</p>
|
<p class="text-2xl font-bold mt-1">
|
||||||
|
{{ model.alias_count || 0 }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 2: 关联提供商 -->
|
<!-- Tab 2: 关联提供商 -->
|
||||||
<div v-show="detailTab === 'providers'">
|
<div v-show="detailTab === 'providers'">
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="px-4 py-3 border-b border-border/60">
|
<div class="px-4 py-3 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold">关联提供商列表</h4>
|
<h4 class="text-sm font-semibold">
|
||||||
|
关联提供商列表
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="$emit('add-provider')"
|
|
||||||
title="添加关联"
|
title="添加关联"
|
||||||
|
@click="$emit('add-provider')"
|
||||||
>
|
>
|
||||||
<Plus class="w-3.5 h-3.5" />
|
<Plus class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -333,27 +443,41 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="$emit('refresh-providers')"
|
|
||||||
title="刷新"
|
title="刷新"
|
||||||
|
@click="$emit('refresh-providers')"
|
||||||
>
|
>
|
||||||
<RefreshCw class="w-3.5 h-3.5" :class="loadingProviders ? 'animate-spin' : ''" />
|
<RefreshCw
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
:class="loadingProviders ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格内容 -->
|
<!-- 表格内容 -->
|
||||||
<div v-if="loadingProviders" class="flex items-center justify-center py-12">
|
<div
|
||||||
|
v-if="loadingProviders"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table v-else-if="providers.length > 0">
|
<Table v-else-if="providers.length > 0">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="h-10 font-semibold">Provider</TableHead>
|
<TableHead class="h-10 font-semibold">
|
||||||
<TableHead class="w-[120px] h-10 font-semibold">能力</TableHead>
|
Provider
|
||||||
<TableHead class="w-[180px] h-10 font-semibold">价格 ($/M)</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[80px] h-10 font-semibold text-center">操作</TableHead>
|
<TableHead class="w-[120px] h-10 font-semibold">
|
||||||
|
能力
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="w-[180px] h-10 font-semibold">
|
||||||
|
价格 ($/M)
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="w-[80px] h-10 font-semibold text-center">
|
||||||
|
操作
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -368,15 +492,27 @@
|
|||||||
class="w-2 h-2 rounded-full shrink-0"
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
:class="provider.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
:class="provider.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
||||||
:title="provider.is_active ? '活跃' : '停用'"
|
:title="provider.is_active ? '活跃' : '停用'"
|
||||||
></span>
|
/>
|
||||||
<span class="font-medium truncate">{{ provider.display_name }}</span>
|
<span class="font-medium truncate">{{ provider.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3">
|
<TableCell class="py-3">
|
||||||
<div class="flex gap-0.5">
|
<div class="flex gap-0.5">
|
||||||
<Zap v-if="provider.supports_streaming" class="w-3.5 h-3.5 text-muted-foreground" title="流式输出" />
|
<Zap
|
||||||
<Eye v-if="provider.supports_vision" class="w-3.5 h-3.5 text-muted-foreground" title="视觉理解" />
|
v-if="provider.supports_streaming"
|
||||||
<Wrench v-if="provider.supports_function_calling" class="w-3.5 h-3.5 text-muted-foreground" title="工具调用" />
|
class="w-3.5 h-3.5 text-muted-foreground"
|
||||||
|
title="流式输出"
|
||||||
|
/>
|
||||||
|
<Eye
|
||||||
|
v-if="provider.supports_vision"
|
||||||
|
class="w-3.5 h-3.5 text-muted-foreground"
|
||||||
|
title="视觉理解"
|
||||||
|
/>
|
||||||
|
<Wrench
|
||||||
|
v-if="provider.supports_function_calling"
|
||||||
|
class="w-3.5 h-3.5 text-muted-foreground"
|
||||||
|
title="工具调用"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3">
|
<TableCell class="py-3">
|
||||||
@@ -386,15 +522,25 @@
|
|||||||
<span class="text-muted-foreground">输入/输出:</span>
|
<span class="text-muted-foreground">输入/输出:</span>
|
||||||
<span class="ml-1">${{ (provider.input_price_per_1m || 0).toFixed(1) }}/${{ (provider.output_price_per_1m || 0).toFixed(1) }}</span>
|
<span class="ml-1">${{ (provider.input_price_per_1m || 0).toFixed(1) }}/${{ (provider.output_price_per_1m || 0).toFixed(1) }}</span>
|
||||||
<!-- 阶梯标记 -->
|
<!-- 阶梯标记 -->
|
||||||
<span v-if="(provider.tier_count || 1) > 1" class="ml-1 text-muted-foreground" title="阶梯计费">[阶梯]</span>
|
<span
|
||||||
|
v-if="(provider.tier_count || 1) > 1"
|
||||||
|
class="ml-1 text-muted-foreground"
|
||||||
|
title="阶梯计费"
|
||||||
|
>[阶梯]</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 缓存价格 -->
|
<!-- 缓存价格 -->
|
||||||
<div v-if="(provider.cache_creation_price_per_1m || 0) > 0 || (provider.cache_read_price_per_1m || 0) > 0" class="text-muted-foreground">
|
<div
|
||||||
|
v-if="(provider.cache_creation_price_per_1m || 0) > 0 || (provider.cache_read_price_per_1m || 0) > 0"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
<span>缓存:</span>
|
<span>缓存:</span>
|
||||||
<span class="ml-1">${{ (provider.cache_creation_price_per_1m || 0).toFixed(2) }}/${{ (provider.cache_read_price_per_1m || 0).toFixed(2) }}</span>
|
<span class="ml-1">${{ (provider.cache_creation_price_per_1m || 0).toFixed(2) }}/${{ (provider.cache_read_price_per_1m || 0).toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 1h 缓存价格 -->
|
<!-- 1h 缓存价格 -->
|
||||||
<div v-if="(provider.cache_1h_creation_price_per_1m || 0) > 0" class="text-muted-foreground">
|
<div
|
||||||
|
v-if="(provider.cache_1h_creation_price_per_1m || 0) > 0"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
<span>1h 缓存:</span>
|
<span>1h 缓存:</span>
|
||||||
<span class="ml-1">${{ (provider.cache_1h_creation_price_per_1m || 0).toFixed(2) }}</span>
|
<span class="ml-1">${{ (provider.cache_1h_creation_price_per_1m || 0).toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,7 +550,10 @@
|
|||||||
<span class="ml-1">${{ (provider.price_per_request || 0).toFixed(3) }}/次</span>
|
<span class="ml-1">${{ (provider.price_per_request || 0).toFixed(3) }}/次</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 无定价 -->
|
<!-- 无定价 -->
|
||||||
<span v-if="!(provider.input_price_per_1m || 0) && !(provider.output_price_per_1m || 0) && !(provider.price_per_request || 0)" class="text-muted-foreground">-</span>
|
<span
|
||||||
|
v-if="!(provider.input_price_per_1m || 0) && !(provider.output_price_per_1m || 0) && !(provider.price_per_request || 0)"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>-</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3 text-center">
|
<TableCell class="py-3 text-center">
|
||||||
@@ -413,8 +562,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('edit-provider', provider)"
|
|
||||||
title="编辑此关联"
|
title="编辑此关联"
|
||||||
|
@click="$emit('edit-provider', provider)"
|
||||||
>
|
>
|
||||||
<Edit class="w-3.5 h-3.5" />
|
<Edit class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -422,8 +571,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('toggle-provider-status', provider)"
|
|
||||||
:title="provider.is_active ? '停用此关联' : '启用此关联'"
|
:title="provider.is_active ? '停用此关联' : '启用此关联'"
|
||||||
|
@click="$emit('toggle-provider-status', provider)"
|
||||||
>
|
>
|
||||||
<Power class="w-3.5 h-3.5" />
|
<Power class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -431,8 +580,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('delete-provider', provider)"
|
|
||||||
title="删除此关联"
|
title="删除此关联"
|
||||||
|
@click="$emit('delete-provider', provider)"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -443,33 +592,45 @@
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="text-center py-12">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-center py-12"
|
||||||
|
>
|
||||||
<Building2 class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
<Building2 class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
||||||
<p class="text-sm text-muted-foreground">暂无关联提供商</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
<Button size="sm" variant="outline" class="mt-4" @click="$emit('add-provider')">
|
暂无关联提供商
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="mt-4"
|
||||||
|
@click="$emit('add-provider')"
|
||||||
|
>
|
||||||
<Plus class="w-4 h-4 mr-1" />
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
添加第一个关联
|
添加第一个关联
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 3: 别名 -->
|
<!-- Tab 3: 别名 -->
|
||||||
<div v-show="detailTab === 'aliases'">
|
<div v-show="detailTab === 'aliases'">
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="px-4 py-3 border-b border-border/60">
|
<div class="px-4 py-3 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold">别名与映射</h4>
|
<h4 class="text-sm font-semibold">
|
||||||
|
别名与映射
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="$emit('add-alias')"
|
|
||||||
title="添加别名/映射"
|
title="添加别名/映射"
|
||||||
|
@click="$emit('add-alias')"
|
||||||
>
|
>
|
||||||
<Plus class="w-3.5 h-3.5" />
|
<Plus class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -477,27 +638,41 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="$emit('refresh-aliases')"
|
|
||||||
title="刷新"
|
title="刷新"
|
||||||
|
@click="$emit('refresh-aliases')"
|
||||||
>
|
>
|
||||||
<RefreshCw class="w-3.5 h-3.5" :class="loadingAliases ? 'animate-spin' : ''" />
|
<RefreshCw
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
:class="loadingAliases ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格内容 -->
|
<!-- 表格内容 -->
|
||||||
<div v-if="loadingAliases" class="flex items-center justify-center py-12">
|
<div
|
||||||
|
v-if="loadingAliases"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table v-else-if="aliases.length > 0">
|
<Table v-else-if="aliases.length > 0">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="h-10 font-semibold">别名</TableHead>
|
<TableHead class="h-10 font-semibold">
|
||||||
<TableHead class="w-[80px] h-10 font-semibold">类型</TableHead>
|
别名
|
||||||
<TableHead class="w-[100px] h-10 font-semibold">作用域</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[100px] h-10 font-semibold text-center">操作</TableHead>
|
<TableHead class="w-[80px] h-10 font-semibold">
|
||||||
|
类型
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="w-[100px] h-10 font-semibold">
|
||||||
|
作用域
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="w-[100px] h-10 font-semibold text-center">
|
||||||
|
操作
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -512,12 +687,15 @@
|
|||||||
class="w-2 h-2 rounded-full shrink-0"
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
||||||
:title="alias.is_active ? '活跃' : '停用'"
|
:title="alias.is_active ? '活跃' : '停用'"
|
||||||
></span>
|
/>
|
||||||
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded">{{ alias.alias }}</code>
|
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded">{{ alias.alias }}</code>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3">
|
<TableCell class="py-3">
|
||||||
<Badge variant="secondary" class="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -530,7 +708,13 @@
|
|||||||
>
|
>
|
||||||
{{ alias.provider_name || 'Provider' }}
|
{{ alias.provider_name || 'Provider' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else variant="default" class="text-xs">全局</Badge>
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="default"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
全局
|
||||||
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3 text-center">
|
<TableCell class="py-3 text-center">
|
||||||
<div class="flex items-center justify-center gap-0.5">
|
<div class="flex items-center justify-center gap-0.5">
|
||||||
@@ -538,8 +722,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('edit-alias', alias)"
|
|
||||||
title="编辑"
|
title="编辑"
|
||||||
|
@click="$emit('edit-alias', alias)"
|
||||||
>
|
>
|
||||||
<Edit class="w-3.5 h-3.5" />
|
<Edit class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -547,8 +731,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('toggle-alias-status', alias)"
|
|
||||||
:title="alias.is_active ? '停用' : '启用'"
|
:title="alias.is_active ? '停用' : '启用'"
|
||||||
|
@click="$emit('toggle-alias-status', alias)"
|
||||||
>
|
>
|
||||||
<Power class="w-3.5 h-3.5" />
|
<Power class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -556,8 +740,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
@click="$emit('delete-alias', alias)"
|
|
||||||
title="删除"
|
title="删除"
|
||||||
|
@click="$emit('delete-alias', alias)"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -568,17 +752,27 @@
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="text-center py-12">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-center py-12"
|
||||||
|
>
|
||||||
<Tag class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
<Tag class="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
||||||
<p class="text-sm text-muted-foreground">暂无别名或映射</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
<Button size="sm" variant="outline" class="mt-4" @click="$emit('add-alias')">
|
暂无别名或映射
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="mt-4"
|
||||||
|
@click="$emit('add-alias')"
|
||||||
|
>
|
||||||
<Plus class="w-4 h-4 mr-1" />
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
添加别名/映射
|
添加别名/映射
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -607,6 +801,26 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loadingProviders: false,
|
||||||
|
loadingAliases: false,
|
||||||
|
hasBlockingDialogOpen: false
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
'edit-model': [model: GlobalModelResponse]
|
||||||
|
'toggle-model-status': [model: GlobalModelResponse]
|
||||||
|
'add-provider': []
|
||||||
|
'edit-provider': [provider: any]
|
||||||
|
'delete-provider': [provider: any]
|
||||||
|
'toggle-provider-status': [provider: any]
|
||||||
|
'refresh-providers': []
|
||||||
|
'add-alias': []
|
||||||
|
'edit-alias': [alias: ModelAlias]
|
||||||
|
'toggle-alias-status': [alias: ModelAlias]
|
||||||
|
'delete-alias': [alias: ModelAlias]
|
||||||
|
'refresh-aliases': []
|
||||||
|
}>()
|
||||||
const { success: showSuccess, error: showError } = useToast()
|
const { success: showSuccess, error: showError } = useToast()
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
@@ -636,34 +850,12 @@ interface Props {
|
|||||||
capabilities?: CapabilityDefinition[]
|
capabilities?: CapabilityDefinition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
loadingProviders: false,
|
|
||||||
loadingAliases: false,
|
|
||||||
hasBlockingDialogOpen: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 根据能力名称获取显示名称
|
// 根据能力名称获取显示名称
|
||||||
function getCapabilityDisplayName(capName: string): string {
|
function getCapabilityDisplayName(capName: string): string {
|
||||||
const cap = props.capabilities?.find(c => c.name === capName)
|
const cap = props.capabilities?.find(c => c.name === capName)
|
||||||
return cap?.display_name || capName
|
return cap?.display_name || capName
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:open': [value: boolean]
|
|
||||||
'edit-model': [model: GlobalModelResponse]
|
|
||||||
'toggle-model-status': [model: GlobalModelResponse]
|
|
||||||
'add-provider': []
|
|
||||||
'edit-provider': [provider: any]
|
|
||||||
'delete-provider': [provider: any]
|
|
||||||
'toggle-provider-status': [provider: any]
|
|
||||||
'refresh-providers': []
|
|
||||||
'add-alias': []
|
|
||||||
'edit-alias': [alias: ModelAlias]
|
|
||||||
'toggle-alias-status': [alias: ModelAlias]
|
|
||||||
'delete-alias': [alias: ModelAlias]
|
|
||||||
'refresh-aliases': []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const detailTab = ref('basic')
|
const detailTab = ref('basic')
|
||||||
|
|
||||||
// 处理背景点击
|
// 处理背景点击
|
||||||
|
|||||||
@@ -39,7 +39,10 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="font-medium">无上限</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="font-medium"
|
||||||
|
>无上限</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-if="localTiers.length > 1"
|
v-if="localTiers.length > 1"
|
||||||
@@ -53,7 +56,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 价格输入 -->
|
<!-- 价格输入 -->
|
||||||
<div :class="['grid gap-3', showCache1h ? 'grid-cols-5' : 'grid-cols-4']">
|
<div
|
||||||
|
class="grid gap-3"
|
||||||
|
:class="[showCache1h ? 'grid-cols-5' : 'grid-cols-4']"
|
||||||
|
>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="text-xs">输入 ($/M)</Label>
|
<Label class="text-xs">输入 ($/M)</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -102,7 +108,10 @@
|
|||||||
@update:model-value="(v) => updateCacheRead(index, v)"
|
@update:model-value="(v) => updateCacheRead(index, v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showCache1h" class="space-y-1">
|
<div
|
||||||
|
v-if="showCache1h"
|
||||||
|
class="space-y-1"
|
||||||
|
>
|
||||||
<Label class="text-xs text-muted-foreground">1h 缓存创建</Label>
|
<Label class="text-xs text-muted-foreground">1h 缓存创建</Label>
|
||||||
<Input
|
<Input
|
||||||
:model-value="getCache1hDisplay(index)"
|
:model-value="getCache1hDisplay(index)"
|
||||||
@@ -129,7 +138,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- 验证提示 -->
|
<!-- 验证提示 -->
|
||||||
<p v-if="validationError" class="text-xs text-destructive">
|
<p
|
||||||
|
v-if="validationError"
|
||||||
|
class="text-xs text-destructive"
|
||||||
|
>
|
||||||
{{ validationError }}
|
{{ validationError }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:model-value="open"
|
:model-value="open"
|
||||||
@update:model-value="$emit('update:open', $event)"
|
|
||||||
title="批量添加关联模型"
|
title="批量添加关联模型"
|
||||||
description="为提供商批量添加模型实现,提供商将自动继承模型的价格和能力,可在添加后单独修改"
|
description="为提供商批量添加模型实现,提供商将自动继承模型的价格和能力,可在添加后单独修改"
|
||||||
:icon="Layers"
|
:icon="Layers"
|
||||||
size="4xl"
|
size="4xl"
|
||||||
|
@update:model-value="$emit('update:open', $event)"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@@ -13,10 +13,17 @@
|
|||||||
<div class="rounded-lg border bg-muted/30 p-4">
|
<div class="rounded-lg border bg-muted/30 p-4">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-lg">{{ providerName }}</p>
|
<p class="font-semibold text-lg">
|
||||||
<p class="text-sm text-muted-foreground font-mono">{{ providerIdentifier }}</p>
|
{{ providerName }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-muted-foreground font-mono">
|
||||||
|
{{ providerIdentifier }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" class="text-xs">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
当前 {{ existingModels.length }} 个模型
|
当前 {{ existingModels.length }} 个模型
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +35,9 @@
|
|||||||
<div class="flex-1 space-y-2">
|
<div class="flex-1 space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm font-medium">可添加</p>
|
<p class="text-sm font-medium">
|
||||||
|
可添加
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
v-if="availableModels.length > 0"
|
v-if="availableModels.length > 0"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -39,19 +48,33 @@
|
|||||||
{{ isAllLeftSelected ? '取消全选' : '全选' }}
|
{{ isAllLeftSelected ? '取消全选' : '全选' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" class="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ availableModels.length }} 个
|
{{ availableModels.length }} 个
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded-lg h-80 overflow-y-auto">
|
<div class="border rounded-lg h-80 overflow-y-auto">
|
||||||
<div v-if="loadingGlobalModels" class="flex items-center justify-center h-full">
|
<div
|
||||||
|
v-if="loadingGlobalModels"
|
||||||
|
class="flex items-center justify-center h-full"
|
||||||
|
>
|
||||||
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="availableModels.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
|
<div
|
||||||
|
v-else-if="availableModels.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||||
|
>
|
||||||
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p class="text-sm">所有模型均已关联</p>
|
<p class="text-sm">
|
||||||
|
所有模型均已关联
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="p-2 space-y-1">
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-2 space-y-1"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in availableModels"
|
v-for="model in availableModels"
|
||||||
:key="model.id"
|
:key="model.id"
|
||||||
@@ -67,8 +90,12 @@
|
|||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium text-sm truncate">{{ model.display_name }}</p>
|
<p class="font-medium text-sm truncate">
|
||||||
<p class="text-xs text-muted-foreground truncate font-mono">{{ model.name }}</p>
|
{{ model.display_name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||||
|
{{ model.name }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
:variant="model.is_active ? 'outline' : 'secondary'"
|
:variant="model.is_active ? 'outline' : 'secondary'"
|
||||||
@@ -90,11 +117,18 @@
|
|||||||
class="w-9 h-8"
|
class="w-9 h-8"
|
||||||
:class="selectedLeftIds.length > 0 && !submittingAdd ? 'border-primary' : ''"
|
:class="selectedLeftIds.length > 0 && !submittingAdd ? 'border-primary' : ''"
|
||||||
:disabled="selectedLeftIds.length === 0 || submittingAdd"
|
:disabled="selectedLeftIds.length === 0 || submittingAdd"
|
||||||
@click="batchAddSelected"
|
|
||||||
title="添加选中"
|
title="添加选中"
|
||||||
|
@click="batchAddSelected"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="submittingAdd" class="w-4 h-4 animate-spin" />
|
<Loader2
|
||||||
<ChevronRight v-else class="w-6 h-6 stroke-[3]" :class="selectedLeftIds.length > 0 && !submittingAdd ? 'text-primary' : ''" />
|
v-if="submittingAdd"
|
||||||
|
class="w-4 h-4 animate-spin"
|
||||||
|
/>
|
||||||
|
<ChevronRight
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 stroke-[3]"
|
||||||
|
:class="selectedLeftIds.length > 0 && !submittingAdd ? 'text-primary' : ''"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -102,11 +136,18 @@
|
|||||||
class="w-9 h-8"
|
class="w-9 h-8"
|
||||||
:class="selectedRightIds.length > 0 && !submittingRemove ? 'border-primary' : ''"
|
:class="selectedRightIds.length > 0 && !submittingRemove ? 'border-primary' : ''"
|
||||||
:disabled="selectedRightIds.length === 0 || submittingRemove"
|
:disabled="selectedRightIds.length === 0 || submittingRemove"
|
||||||
@click="batchRemoveSelected"
|
|
||||||
title="移除选中"
|
title="移除选中"
|
||||||
|
@click="batchRemoveSelected"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="submittingRemove" class="w-4 h-4 animate-spin" />
|
<Loader2
|
||||||
<ChevronLeft v-else class="w-6 h-6 stroke-[3]" :class="selectedRightIds.length > 0 && !submittingRemove ? 'text-primary' : ''" />
|
v-if="submittingRemove"
|
||||||
|
class="w-4 h-4 animate-spin"
|
||||||
|
/>
|
||||||
|
<ChevronLeft
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 stroke-[3]"
|
||||||
|
:class="selectedRightIds.length > 0 && !submittingRemove ? 'text-primary' : ''"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,7 +155,9 @@
|
|||||||
<div class="flex-1 space-y-2">
|
<div class="flex-1 space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm font-medium">已添加</p>
|
<p class="text-sm font-medium">
|
||||||
|
已添加
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
v-if="existingModels.length > 0"
|
v-if="existingModels.length > 0"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -125,16 +168,27 @@
|
|||||||
{{ isAllRightSelected ? '取消全选' : '全选' }}
|
{{ isAllRightSelected ? '取消全选' : '全选' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" class="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ existingModels.length }} 个
|
{{ existingModels.length }} 个
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded-lg h-80 overflow-y-auto">
|
<div class="border rounded-lg h-80 overflow-y-auto">
|
||||||
<div v-if="existingModels.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
|
<div
|
||||||
|
v-if="existingModels.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||||
|
>
|
||||||
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p class="text-sm">暂无关联模型</p>
|
<p class="text-sm">
|
||||||
|
暂无关联模型
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="p-2 space-y-1">
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-2 space-y-1"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in existingModels"
|
v-for="model in existingModels"
|
||||||
:key="'existing-' + model.id"
|
:key="'existing-' + model.id"
|
||||||
@@ -150,8 +204,12 @@
|
|||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium text-sm truncate">{{ model.global_model_display_name || model.provider_model_name }}</p>
|
<p class="font-medium text-sm truncate">
|
||||||
<p class="text-xs text-muted-foreground truncate font-mono">{{ model.provider_model_name }}</p>
|
{{ model.global_model_display_name || model.provider_model_name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||||
|
{{ model.provider_model_name }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
:variant="model.is_active ? 'outline' : 'secondary'"
|
:variant="model.is_active ? 'outline' : 'secondary'"
|
||||||
|
|||||||
@@ -7,11 +7,18 @@
|
|||||||
size="xl"
|
size="xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
|
<form
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
class="space-y-6"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<!-- API 配置 -->
|
<!-- API 配置 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 v-if="isEditMode" class="text-sm font-medium">API 配置</h3>
|
<h3
|
||||||
|
v-if="isEditMode"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>
|
||||||
|
API 配置
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<!-- API 格式 -->
|
<!-- API 格式 -->
|
||||||
@@ -24,10 +31,16 @@
|
|||||||
disabled
|
disabled
|
||||||
class="bg-muted"
|
class="bg-muted"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground">API 格式创建后不可修改</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
API 格式创建后不可修改
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Select v-model="form.api_format" v-model:open="selectOpen" required>
|
<Select
|
||||||
|
v-model="form.api_format"
|
||||||
|
v-model:open="selectOpen"
|
||||||
|
required
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="请选择 API 格式" />
|
<SelectValue placeholder="请选择 API 格式" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -69,7 +82,9 @@
|
|||||||
|
|
||||||
<!-- 请求配置 -->
|
<!-- 请求配置 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-sm font-medium">请求配置</h3>
|
<h3 class="text-sm font-medium">
|
||||||
|
请求配置
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -117,21 +132,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button
|
<Button
|
||||||
@click="handleCancel"
|
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleSubmit"
|
|
||||||
:disabled="loading || !form.base_url || (!isEditMode && !form.api_format)"
|
:disabled="loading || !form.base_url || (!isEditMode && !form.api_format)"
|
||||||
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存修改' : '创建') }}
|
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存修改' : '创建') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,16 +2,26 @@
|
|||||||
<div class="w-full space-y-1">
|
<div class="w-full space-y-1">
|
||||||
<!-- 时间线 -->
|
<!-- 时间线 -->
|
||||||
<div class="flex items-center gap-px h-6 w-full">
|
<div class="flex items-center gap-px h-6 w-full">
|
||||||
<TooltipProvider v-for="(segment, index) in segments" :key="index" :delay-duration="100">
|
<TooltipProvider
|
||||||
|
v-for="(segment, index) in segments"
|
||||||
|
:key="index"
|
||||||
|
:delay-duration="100"
|
||||||
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<div
|
<div
|
||||||
class="flex-1 h-full rounded-sm transition-all duration-150 cursor-pointer hover:scale-y-110 hover:brightness-110"
|
class="flex-1 h-full rounded-sm transition-all duration-150 cursor-pointer hover:scale-y-110 hover:brightness-110"
|
||||||
:class="segment.color"
|
:class="segment.color"
|
||||||
></div>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" :side-offset="8" class="max-w-xs">
|
<TooltipContent
|
||||||
<div class="text-xs whitespace-pre-line">{{ segment.tooltip }}</div>
|
side="top"
|
||||||
|
:side-offset="8"
|
||||||
|
class="max-w-xs"
|
||||||
|
>
|
||||||
|
<div class="text-xs whitespace-pre-line">
|
||||||
|
{{ segment.tooltip }}
|
||||||
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -1,42 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card variant="default" class="overflow-hidden">
|
<Card
|
||||||
|
variant="default"
|
||||||
|
class="overflow-hidden"
|
||||||
|
>
|
||||||
<!-- 标题和筛选器 -->
|
<!-- 标题和筛选器 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-6 py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<h3 class="text-base font-semibold">{{ title }}</h3>
|
<h3 class="text-base font-semibold">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Label class="text-xs text-muted-foreground">回溯时间:</Label>
|
<Label class="text-xs text-muted-foreground">回溯时间:</Label>
|
||||||
<Select v-model="lookbackHours" v-model:open="selectOpen">
|
<Select
|
||||||
|
v-model="lookbackHours"
|
||||||
|
v-model:open="selectOpen"
|
||||||
|
>
|
||||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">1 小时</SelectItem>
|
<SelectItem value="1">
|
||||||
<SelectItem value="6">6 小时</SelectItem>
|
1 小时
|
||||||
<SelectItem value="12">12 小时</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="24">24 小时</SelectItem>
|
<SelectItem value="6">
|
||||||
<SelectItem value="48">48 小时</SelectItem>
|
6 小时
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="12">
|
||||||
|
12 小时
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="24">
|
||||||
|
24 小时
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="48">
|
||||||
|
48 小时
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<RefreshButton :loading="loading" @click="refreshData" />
|
<RefreshButton
|
||||||
|
:loading="loading"
|
||||||
|
@click="refreshData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div v-if="loadingMonitors" class="flex items-center justify-center py-12">
|
<div
|
||||||
|
v-if="loadingMonitors"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
|
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
<span class="ml-2 text-muted-foreground">加载中...</span>
|
<span class="ml-2 text-muted-foreground">加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="monitors.length === 0" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
<div
|
||||||
|
v-else-if="monitors.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
||||||
|
>
|
||||||
<Activity class="w-12 h-12 mb-3 opacity-30" />
|
<Activity class="w-12 h-12 mb-3 opacity-30" />
|
||||||
<p>暂无健康监控数据</p>
|
<p>暂无健康监控数据</p>
|
||||||
<p class="text-xs mt-1">端点尚未产生请求记录</p>
|
<p class="text-xs mt-1">
|
||||||
|
端点尚未产生请求记录
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-3">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="monitor in monitors"
|
v-for="monitor in monitors"
|
||||||
:key="monitor.api_format"
|
:key="monitor.api_format"
|
||||||
@@ -48,7 +80,10 @@
|
|||||||
<div class="w-44 flex-shrink-0 space-y-1.5">
|
<div class="w-44 flex-shrink-0 space-y-1.5">
|
||||||
<!-- API 格式标签和成功率 -->
|
<!-- API 格式标签和成功率 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="outline" class="font-mono text-xs">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="font-mono text-xs"
|
||||||
|
>
|
||||||
{{ monitor.api_format }}
|
{{ monitor.api_format }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -61,7 +96,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提供商信息(仅管理员可见) -->
|
<!-- 提供商信息(仅管理员可见) -->
|
||||||
<div v-if="showProviderInfo && 'provider_count' in monitor" class="text-xs text-muted-foreground">
|
<div
|
||||||
|
v-if="showProviderInfo && 'provider_count' in monitor"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
{{ monitor.provider_count }} 个提供商 / {{ monitor.key_count }} 个密钥
|
{{ monitor.provider_count }} 个提供商 / {{ monitor.key_count }} 个密钥
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,14 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-4 py-2">
|
<div class="space-y-4 py-2">
|
||||||
<!-- 已选模型展示 -->
|
<!-- 已选模型展示 -->
|
||||||
<div v-if="selectedModels.length > 0" class="space-y-2">
|
<div
|
||||||
|
v-if="selectedModels.length > 0"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between px-1">
|
<div class="flex items-center justify-between px-1">
|
||||||
<div class="text-xs font-medium text-muted-foreground">已选模型 ({{ selectedModels.length }})</div>
|
<div class="text-xs font-medium text-muted-foreground">
|
||||||
|
已选模型 ({{ selectedModels.length }})
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -43,26 +48,40 @@
|
|||||||
<!-- 模型列表区域 -->
|
<!-- 模型列表区域 -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between px-1">
|
<div class="flex items-center justify-between px-1">
|
||||||
<div class="text-xs font-medium text-muted-foreground">可选模型列表</div>
|
<div class="text-xs font-medium text-muted-foreground">
|
||||||
<div v-if="!loadingModels && availableModels.length > 0" class="text-[10px] text-muted-foreground/60">
|
可选模型列表
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!loadingModels && availableModels.length > 0"
|
||||||
|
class="text-[10px] text-muted-foreground/60"
|
||||||
|
>
|
||||||
共 {{ availableModels.length }} 个模型
|
共 {{ availableModels.length }} 个模型
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loadingModels" class="flex flex-col items-center justify-center py-12 space-y-3">
|
<div
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-2 border-primary/20 border-t-primary"></div>
|
v-if="loadingModels"
|
||||||
|
class="flex flex-col items-center justify-center py-12 space-y-3"
|
||||||
|
>
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-2 border-primary/20 border-t-primary" />
|
||||||
<span class="text-xs text-muted-foreground">正在加载模型列表...</span>
|
<span class="text-xs text-muted-foreground">正在加载模型列表...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无模型 -->
|
<!-- 无模型 -->
|
||||||
<div v-else-if="availableModels.length === 0" class="flex flex-col items-center justify-center py-12 text-muted-foreground border border-dashed rounded-lg bg-muted/10">
|
<div
|
||||||
|
v-else-if="availableModels.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground border border-dashed rounded-lg bg-muted/10"
|
||||||
|
>
|
||||||
<Box class="w-10 h-10 mb-2 opacity-20" />
|
<Box class="w-10 h-10 mb-2 opacity-20" />
|
||||||
<span class="text-sm">暂无可选模型</span>
|
<span class="text-sm">暂无可选模型</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<div v-else class="max-h-[320px] overflow-y-auto pr-1 space-y-1.5 custom-scrollbar">
|
<div
|
||||||
|
v-else
|
||||||
|
class="max-h-[320px] overflow-y-auto pr-1 space-y-1.5 custom-scrollbar"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="model in availableModels"
|
v-for="model in availableModels"
|
||||||
:key="model.global_model_name"
|
:key="model.global_model_name"
|
||||||
@@ -86,7 +105,10 @@
|
|||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-sm font-medium truncate text-foreground/90">{{ model.display_name }}</span>
|
<span class="text-sm font-medium truncate text-foreground/90">{{ model.display_name }}</span>
|
||||||
<span v-if="hasPricing(model)" class="text-[10px] font-mono text-muted-foreground/80 bg-muted/30 px-1.5 py-0.5 rounded border border-border/30 shrink-0">
|
<span
|
||||||
|
v-if="hasPricing(model)"
|
||||||
|
class="text-[10px] font-mono text-muted-foreground/80 bg-muted/30 px-1.5 py-0.5 rounded border border-border/30 shrink-0"
|
||||||
|
>
|
||||||
{{ formatPricingShort(model) }}
|
{{ formatPricingShort(model) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,9 +123,22 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-end gap-2 w-full pt-2">
|
<div class="flex items-center justify-end gap-2 w-full pt-2">
|
||||||
<Button @click="handleCancel" variant="outline" class="h-9">取消</Button>
|
<Button
|
||||||
<Button @click="handleSave" :disabled="saving" class="h-9 min-w-[80px]">
|
variant="outline"
|
||||||
<Loader2 v-if="saving" class="w-3.5 h-3.5 mr-1.5 animate-spin" />
|
class="h-9"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="saving"
|
||||||
|
class="h-9 min-w-[80px]"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="saving"
|
||||||
|
class="w-3.5 h-3.5 mr-1.5 animate-spin"
|
||||||
|
/>
|
||||||
{{ saving ? '保存中' : '保存配置' }}
|
{{ saving ? '保存中' : '保存配置' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,203 +7,239 @@
|
|||||||
size="2xl"
|
size="2xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSave" class="space-y-5" autocomplete="off">
|
<form
|
||||||
<!-- 基本信息 -->
|
class="space-y-5"
|
||||||
<div class="space-y-3">
|
autocomplete="off"
|
||||||
<h3 class="text-sm font-medium border-b pb-2">基本信息</h3>
|
@submit.prevent="handleSave"
|
||||||
<div class="grid grid-cols-2 gap-4">
|
>
|
||||||
<div>
|
<!-- 基本信息 -->
|
||||||
<Label :for="keyNameInputId">密钥名称 *</Label>
|
<div class="space-y-3">
|
||||||
<Input
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
:id="keyNameInputId"
|
基本信息
|
||||||
:name="keyNameFieldName"
|
</h3>
|
||||||
v-model="form.name"
|
<div class="grid grid-cols-2 gap-4">
|
||||||
required
|
|
||||||
placeholder="例如:主 Key、备用 Key 1"
|
|
||||||
maxlength="100"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="none"
|
|
||||||
autocorrect="off"
|
|
||||||
spellcheck="false"
|
|
||||||
data-form-type="other"
|
|
||||||
data-lpignore="true"
|
|
||||||
data-1p-ignore="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label for="rate_multiplier">成本倍率 *</Label>
|
|
||||||
<Input
|
|
||||||
id="rate_multiplier"
|
|
||||||
v-model.number="form.rate_multiplier"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0.01"
|
|
||||||
required
|
|
||||||
placeholder="1.0"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground mt-1">
|
|
||||||
真实成本 = 表面成本 × 倍率
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label :for="apiKeyInputId">API 密钥 {{ editingKey ? '' : '*' }}</Label>
|
<Label :for="keyNameInputId">密钥名称 *</Label>
|
||||||
<Input
|
<Input
|
||||||
:id="apiKeyInputId"
|
:id="keyNameInputId"
|
||||||
:name="apiKeyFieldName"
|
v-model="form.name"
|
||||||
v-model="form.api_key"
|
:name="keyNameFieldName"
|
||||||
:type="apiKeyInputType"
|
required
|
||||||
:required="!editingKey"
|
placeholder="例如:主 Key、备用 Key 1"
|
||||||
:placeholder="editingKey ? editingKey.api_key_masked : 'sk-...'"
|
maxlength="100"
|
||||||
:class="getApiKeyInputClass()"
|
autocomplete="off"
|
||||||
autocomplete="new-password"
|
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
data-form-type="other"
|
data-form-type="other"
|
||||||
data-lpignore="true"
|
data-lpignore="true"
|
||||||
data-1p-ignore="true"
|
data-1p-ignore="true"
|
||||||
@focus="apiKeyFocused = true"
|
|
||||||
@blur="apiKeyFocused = form.api_key.trim().length > 0"
|
|
||||||
/>
|
/>
|
||||||
<p v-if="apiKeyError" class="text-xs text-destructive mt-1">
|
|
||||||
{{ apiKeyError }}
|
|
||||||
</p>
|
|
||||||
<p v-else-if="editingKey" class="text-xs text-muted-foreground mt-1">
|
|
||||||
留空表示不修改,输入新值则覆盖
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label for="note">备注</Label>
|
<Label for="rate_multiplier">成本倍率 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="note"
|
id="rate_multiplier"
|
||||||
v-model="form.note"
|
v-model.number="form.rate_multiplier"
|
||||||
placeholder="可选的备注信息"
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
required
|
||||||
|
placeholder="1.0"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
真实成本 = 表面成本 × 倍率
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label :for="apiKeyInputId">API 密钥 {{ editingKey ? '' : '*' }}</Label>
|
||||||
|
<Input
|
||||||
|
:id="apiKeyInputId"
|
||||||
|
v-model="form.api_key"
|
||||||
|
:name="apiKeyFieldName"
|
||||||
|
:type="apiKeyInputType"
|
||||||
|
:required="!editingKey"
|
||||||
|
:placeholder="editingKey ? editingKey.api_key_masked : 'sk-...'"
|
||||||
|
:class="getApiKeyInputClass()"
|
||||||
|
autocomplete="new-password"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
data-form-type="other"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
@focus="apiKeyFocused = true"
|
||||||
|
@blur="apiKeyFocused = form.api_key.trim().length > 0"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="apiKeyError"
|
||||||
|
class="text-xs text-destructive mt-1"
|
||||||
|
>
|
||||||
|
{{ apiKeyError }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-else-if="editingKey"
|
||||||
|
class="text-xs text-muted-foreground mt-1"
|
||||||
|
>
|
||||||
|
留空表示不修改,输入新值则覆盖
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label for="note">备注</Label>
|
||||||
|
<Input
|
||||||
|
id="note"
|
||||||
|
v-model="form.note"
|
||||||
|
placeholder="可选的备注信息"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调度与限流 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
|
调度与限流
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label for="internal_priority">内部优先级</Label>
|
||||||
|
<Input
|
||||||
|
id="internal_priority"
|
||||||
|
v-model.number="form.internal_priority"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
数字越小越优先
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label for="max_concurrent">最大并发</Label>
|
||||||
|
<Input
|
||||||
|
id="max_concurrent"
|
||||||
|
:model-value="form.max_concurrent ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="留空启用自适应"
|
||||||
|
@update:model-value="(v) => form.max_concurrent = parseNumberInput(v)"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
留空 = 自适应模式
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label for="rate_limit">速率限制(/分钟)</Label>
|
||||||
|
<Input
|
||||||
|
id="rate_limit"
|
||||||
|
:model-value="form.rate_limit ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
@update:model-value="(v) => form.rate_limit = parseNumberInput(v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label for="daily_limit">每日限制</Label>
|
||||||
|
<Input
|
||||||
|
id="daily_limit"
|
||||||
|
:model-value="form.daily_limit ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
@update:model-value="(v) => form.daily_limit = parseNumberInput(v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label for="monthly_limit">每月限制</Label>
|
||||||
|
<Input
|
||||||
|
id="monthly_limit"
|
||||||
|
:model-value="form.monthly_limit ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
@update:model-value="(v) => form.monthly_limit = parseNumberInput(v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 调度与限流 -->
|
<!-- 缓存与熔断 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h3 class="text-sm font-medium border-b pb-2">调度与限流</h3>
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
缓存与熔断
|
||||||
<div>
|
</h3>
|
||||||
<Label for="internal_priority">内部优先级</Label>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<Input
|
<div>
|
||||||
id="internal_priority"
|
<Label for="cache_ttl_minutes">缓存 TTL (分钟)</Label>
|
||||||
v-model.number="form.internal_priority"
|
<Input
|
||||||
type="number"
|
id="cache_ttl_minutes"
|
||||||
min="0"
|
:model-value="form.cache_ttl_minutes ?? ''"
|
||||||
/>
|
type="number"
|
||||||
<p class="text-xs text-muted-foreground mt-1">数字越小越优先</p>
|
min="0"
|
||||||
</div>
|
max="60"
|
||||||
<div>
|
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||||
<Label for="max_concurrent">最大并发</Label>
|
/>
|
||||||
<Input
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
id="max_concurrent"
|
0 = 禁用缓存亲和性
|
||||||
:model-value="form.max_concurrent ?? ''"
|
</p>
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="留空启用自适应"
|
|
||||||
@update:model-value="(v) => form.max_concurrent = parseNumberInput(v)"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground mt-1">留空 = 自适应模式</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<Label for="max_probe_interval_minutes">熔断探测间隔 (分钟)</Label>
|
||||||
<div>
|
<Input
|
||||||
<Label for="rate_limit">速率限制(/分钟)</Label>
|
id="max_probe_interval_minutes"
|
||||||
<Input
|
:model-value="form.max_probe_interval_minutes ?? ''"
|
||||||
id="rate_limit"
|
type="number"
|
||||||
:model-value="form.rate_limit ?? ''"
|
min="2"
|
||||||
type="number"
|
max="32"
|
||||||
min="1"
|
placeholder="32"
|
||||||
@update:model-value="(v) => form.rate_limit = parseNumberInput(v)"
|
@update:model-value="(v) => form.max_probe_interval_minutes = parseNumberInput(v, { min: 2, max: 32 }) ?? 32"
|
||||||
/>
|
/>
|
||||||
</div>
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
<div>
|
范围 2-32 分钟
|
||||||
<Label for="daily_limit">每日限制</Label>
|
</p>
|
||||||
<Input
|
|
||||||
id="daily_limit"
|
|
||||||
:model-value="form.daily_limit ?? ''"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
@update:model-value="(v) => form.daily_limit = parseNumberInput(v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label for="monthly_limit">每月限制</Label>
|
|
||||||
<Input
|
|
||||||
id="monthly_limit"
|
|
||||||
:model-value="form.monthly_limit ?? ''"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
@update:model-value="(v) => form.monthly_limit = parseNumberInput(v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 缓存与熔断 -->
|
<!-- 能力标签配置 -->
|
||||||
<div class="space-y-3">
|
<div
|
||||||
<h3 class="text-sm font-medium border-b pb-2">缓存与熔断</h3>
|
v-if="availableCapabilities.length > 0"
|
||||||
<div class="grid grid-cols-2 gap-4">
|
class="space-y-3"
|
||||||
<div>
|
>
|
||||||
<Label for="cache_ttl_minutes">缓存 TTL (分钟)</Label>
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
<Input
|
能力标签
|
||||||
id="cache_ttl_minutes"
|
</h3>
|
||||||
:model-value="form.cache_ttl_minutes ?? ''"
|
<div class="flex flex-wrap gap-2">
|
||||||
type="number"
|
<label
|
||||||
min="0"
|
v-for="cap in availableCapabilities"
|
||||||
max="60"
|
:key="cap.name"
|
||||||
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm"
|
||||||
/>
|
>
|
||||||
<p class="text-xs text-muted-foreground mt-1">0 = 禁用缓存亲和性</p>
|
<input
|
||||||
</div>
|
type="checkbox"
|
||||||
<div>
|
:checked="form.capabilities[cap.name] || false"
|
||||||
<Label for="max_probe_interval_minutes">熔断探测间隔 (分钟)</Label>
|
class="rounded"
|
||||||
<Input
|
@change="form.capabilities[cap.name] = !form.capabilities[cap.name]"
|
||||||
id="max_probe_interval_minutes"
|
|
||||||
:model-value="form.max_probe_interval_minutes ?? ''"
|
|
||||||
type="number"
|
|
||||||
min="2"
|
|
||||||
max="32"
|
|
||||||
placeholder="32"
|
|
||||||
@update:model-value="(v) => form.max_probe_interval_minutes = parseNumberInput(v, { min: 2, max: 32 }) ?? 32"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground mt-1">范围 2-32 分钟</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 能力标签配置 -->
|
|
||||||
<div v-if="availableCapabilities.length > 0" class="space-y-3">
|
|
||||||
<h3 class="text-sm font-medium border-b pb-2">能力标签</h3>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<label
|
|
||||||
v-for="cap in availableCapabilities"
|
|
||||||
:key="cap.name"
|
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border bg-muted/30 cursor-pointer text-sm"
|
|
||||||
>
|
>
|
||||||
<input
|
<span>{{ cap.display_name }}</span>
|
||||||
type="checkbox"
|
</label>
|
||||||
:checked="form.capabilities[cap.name] || false"
|
|
||||||
@change="form.capabilities[cap.name] = !form.capabilities[cap.name]"
|
|
||||||
class="rounded"
|
|
||||||
/>
|
|
||||||
<span>{{ cap.display_name }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button @click="handleCancel" variant="outline">取消</Button>
|
<Button
|
||||||
<Button @click="handleSave" :disabled="saving">
|
variant="outline"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:disabled="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
{{ saving ? '保存中...' : '保存' }}
|
{{ saving ? '保存中...' : '保存' }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg">
|
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
'flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
|
||||||
activeMainTab === 'provider'
|
activeMainTab === 'provider'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
'flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
|
|
||||||
activeMainTab === 'key'
|
activeMainTab === 'key'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||||
@@ -41,7 +41,10 @@
|
|||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="min-h-[420px]">
|
<div class="min-h-[420px]">
|
||||||
<!-- 提供商优先级 -->
|
<!-- 提供商优先级 -->
|
||||||
<div v-show="activeMainTab === 'provider'" class="space-y-4">
|
<div
|
||||||
|
v-show="activeMainTab === 'provider'"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
||||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
<Info class="w-3.5 h-3.5 shrink-0" />
|
||||||
@@ -49,18 +52,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="sortedProviders.length === 0" class="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<div
|
||||||
|
v-if="sortedProviders.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||||
|
>
|
||||||
<Layers class="w-10 h-10 mb-3 opacity-20" />
|
<Layers class="w-10 h-10 mb-3 opacity-20" />
|
||||||
<span class="text-sm">暂无提供商</span>
|
<span class="text-sm">暂无提供商</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提供商列表 -->
|
<!-- 提供商列表 -->
|
||||||
<div v-else class="space-y-2 max-h-[380px] overflow-y-auto pr-1">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-2 max-h-[380px] overflow-y-auto pr-1"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(provider, index) in sortedProviders"
|
v-for="(provider, index) in sortedProviders"
|
||||||
:key="provider.id"
|
:key="provider.id"
|
||||||
|
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
'group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200',
|
|
||||||
draggedProvider === index
|
draggedProvider === index
|
||||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||||
: dragOverProvider === index
|
: dragOverProvider === index
|
||||||
@@ -100,7 +109,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Key 优先级 -->
|
<!-- Key 优先级 -->
|
||||||
<div v-show="activeMainTab === 'key'" class="space-y-3">
|
<div
|
||||||
|
v-show="activeMainTab === 'key'"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
<div class="flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-md">
|
||||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
<Info class="w-3.5 h-3.5 shrink-0" />
|
||||||
@@ -108,29 +120,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loadingKeys" class="flex items-center justify-center py-20">
|
<div
|
||||||
|
v-if="loadingKeys"
|
||||||
|
class="flex items-center justify-center py-20"
|
||||||
|
>
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<div class="animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-primary"></div>
|
<div class="animate-spin rounded-full h-5 w-5 border-2 border-muted border-t-primary" />
|
||||||
<span class="text-xs text-muted-foreground">加载中...</span>
|
<span class="text-xs text-muted-foreground">加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else-if="availableFormats.length === 0" class="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<div
|
||||||
|
v-else-if="availableFormats.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||||
|
>
|
||||||
<Key class="w-10 h-10 mb-3 opacity-20" />
|
<Key class="w-10 h-10 mb-3 opacity-20" />
|
||||||
<span class="text-sm">暂无 API Key</span>
|
<span class="text-sm">暂无 API Key</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 左右布局:格式列表 + Key 列表 -->
|
<!-- 左右布局:格式列表 + Key 列表 -->
|
||||||
<div v-else class="flex gap-4">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex gap-4"
|
||||||
|
>
|
||||||
<!-- 左侧:API 格式列表 -->
|
<!-- 左侧:API 格式列表 -->
|
||||||
<div class="w-32 shrink-0 space-y-1">
|
<div class="w-32 shrink-0 space-y-1">
|
||||||
<button
|
<button
|
||||||
v-for="format in availableFormats"
|
v-for="format in availableFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
type="button"
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-xs font-medium rounded-md text-left transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
'w-full px-3 py-2 text-xs font-medium rounded-md text-left transition-all duration-200',
|
|
||||||
activeFormatTab === format
|
activeFormatTab === format
|
||||||
? 'bg-primary text-primary-foreground'
|
? 'bg-primary text-primary-foreground'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||||
@@ -143,116 +164,147 @@
|
|||||||
|
|
||||||
<!-- 右侧:Key 列表 -->
|
<!-- 右侧:Key 列表 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div v-for="format in availableFormats" :key="format" v-show="activeFormatTab === format">
|
<div
|
||||||
<div v-if="keysByFormat[format]?.length > 0" class="space-y-2 max-h-[380px] overflow-y-auto pr-1">
|
v-for="format in availableFormats"
|
||||||
|
v-show="activeFormatTab === format"
|
||||||
|
:key="format"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(key, index) in keysByFormat[format]"
|
v-if="keysByFormat[format]?.length > 0"
|
||||||
:key="key.id"
|
class="space-y-2 max-h-[380px] overflow-y-auto pr-1"
|
||||||
:class="[
|
|
||||||
'group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200',
|
|
||||||
draggedKey[format] === index
|
|
||||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
|
||||||
: dragOverKey[format] === index
|
|
||||||
? 'border-primary/30 bg-primary/5'
|
|
||||||
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
|
|
||||||
]"
|
|
||||||
draggable="true"
|
|
||||||
@dragstart="handleKeyDragStart(format, index, $event)"
|
|
||||||
@dragend="handleKeyDragEnd(format)"
|
|
||||||
@dragover.prevent="handleKeyDragOver(format, index)"
|
|
||||||
@dragleave="handleKeyDragLeave(format)"
|
|
||||||
@drop="handleKeyDrop(format, index)"
|
|
||||||
>
|
>
|
||||||
<!-- 拖拽手柄 -->
|
<div
|
||||||
<div class="cursor-grab active:cursor-grabbing p-1 rounded hover:bg-muted text-muted-foreground/40 group-hover:text-muted-foreground transition-colors shrink-0">
|
v-for="(key, index) in keysByFormat[format]"
|
||||||
<GripVertical class="w-4 h-4" />
|
:key="key.id"
|
||||||
</div>
|
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||||
|
:class="[
|
||||||
<!-- 可编辑序号 -->
|
draggedKey[format] === index
|
||||||
<div class="shrink-0">
|
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||||
<input
|
: dragOverKey[format] === index
|
||||||
v-if="editingKeyPriority[format] === key.id"
|
? 'border-primary/30 bg-primary/5'
|
||||||
type="number"
|
: 'border-border/50 bg-background hover:border-border hover:bg-muted/30'
|
||||||
min="1"
|
]"
|
||||||
:value="key.priority"
|
draggable="true"
|
||||||
class="w-8 h-6 rounded-md bg-background border border-primary text-xs font-medium text-center focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
@dragstart="handleKeyDragStart(format, index, $event)"
|
||||||
@blur="finishEditKeyPriority(format, key, $event)"
|
@dragend="handleKeyDragEnd(format)"
|
||||||
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
@dragover.prevent="handleKeyDragOver(format, index)"
|
||||||
@keydown.escape="cancelEditKeyPriority(format)"
|
@dragleave="handleKeyDragLeave(format)"
|
||||||
autofocus
|
@drop="handleKeyDrop(format, index)"
|
||||||
/>
|
>
|
||||||
<div
|
<!-- 拖拽手柄 -->
|
||||||
v-else
|
<div class="cursor-grab active:cursor-grabbing p-1 rounded hover:bg-muted text-muted-foreground/40 group-hover:text-muted-foreground transition-colors shrink-0">
|
||||||
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
<GripVertical class="w-4 h-4" />
|
||||||
:title="'点击编辑优先级,相同数字为同级(负载均衡)'"
|
|
||||||
@click.stop="startEditKeyPriority(format, key)"
|
|
||||||
>
|
|
||||||
{{ key.priority }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Key 信息 -->
|
<!-- 可编辑序号 -->
|
||||||
<div class="flex-1 min-w-0 flex items-center gap-3">
|
<div class="shrink-0">
|
||||||
<!-- 左侧:名称和来源 -->
|
<input
|
||||||
<div class="flex-1 min-w-0">
|
v-if="editingKeyPriority[format] === key.id"
|
||||||
<div class="flex items-center gap-2">
|
type="number"
|
||||||
<span class="font-medium text-sm">{{ key.name }}</span>
|
min="1"
|
||||||
<Badge
|
:value="key.priority"
|
||||||
v-if="key.circuit_breaker_open"
|
class="w-8 h-6 rounded-md bg-background border border-primary text-xs font-medium text-center focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
variant="destructive"
|
autofocus
|
||||||
class="text-[10px] h-5 px-1.5 shrink-0"
|
@blur="finishEditKeyPriority(format, key, $event)"
|
||||||
>
|
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
||||||
熔断
|
@keydown.escape="cancelEditKeyPriority(format)"
|
||||||
</Badge>
|
>
|
||||||
<Badge
|
<div
|
||||||
v-else-if="!key.is_active"
|
v-else
|
||||||
variant="secondary"
|
class="w-6 h-6 rounded-md bg-muted/50 flex items-center justify-center text-xs font-medium text-muted-foreground cursor-pointer hover:bg-primary/10 hover:text-primary transition-colors"
|
||||||
class="text-[10px] h-5 px-1.5 shrink-0"
|
title="点击编辑优先级,相同数字为同级(负载均衡)"
|
||||||
>
|
@click.stop="startEditKeyPriority(format, key)"
|
||||||
停用
|
>
|
||||||
</Badge>
|
{{ key.priority }}
|
||||||
<!-- 能力标签紧跟名称 -->
|
|
||||||
<template v-if="key.capabilities?.length">
|
|
||||||
<span v-for="cap in key.capabilities.slice(0, 2)" :key="cap" class="px-1 py-0.5 bg-muted text-muted-foreground rounded text-[10px]">{{ cap }}</span>
|
|
||||||
<span v-if="key.capabilities.length > 2" class="text-[10px] text-muted-foreground">+{{ key.capabilities.length - 2 }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-1">
|
|
||||||
<span class="text-[10px] font-medium shrink-0">{{ key.provider_name }}</span>
|
|
||||||
<span class="font-mono text-[10px] opacity-60 truncate">{{ key.api_key_masked }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:健康度 + 速率 -->
|
<!-- Key 信息 -->
|
||||||
<div class="shrink-0 flex items-center gap-3">
|
<div class="flex-1 min-w-0 flex items-center gap-3">
|
||||||
<!-- 健康度 -->
|
<!-- 左侧:名称和来源 -->
|
||||||
<div v-if="key.success_rate !== null" class="text-xs text-right">
|
<div class="flex-1 min-w-0">
|
||||||
<div :class="[
|
<div class="flex items-center gap-2">
|
||||||
'font-medium tabular-nums',
|
<span class="font-medium text-sm">{{ key.name }}</span>
|
||||||
key.success_rate >= 0.95 ? 'text-green-600' :
|
<Badge
|
||||||
key.success_rate >= 0.8 ? 'text-yellow-600' : 'text-red-500'
|
v-if="key.circuit_breaker_open"
|
||||||
]">
|
variant="destructive"
|
||||||
{{ (key.success_rate * 100).toFixed(0) }}%
|
class="text-[10px] h-5 px-1.5 shrink-0"
|
||||||
|
>
|
||||||
|
熔断
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="!key.is_active"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] h-5 px-1.5 shrink-0"
|
||||||
|
>
|
||||||
|
停用
|
||||||
|
</Badge>
|
||||||
|
<!-- 能力标签紧跟名称 -->
|
||||||
|
<template v-if="key.capabilities?.length">
|
||||||
|
<span
|
||||||
|
v-for="cap in key.capabilities.slice(0, 2)"
|
||||||
|
:key="cap"
|
||||||
|
class="px-1 py-0.5 bg-muted text-muted-foreground rounded text-[10px]"
|
||||||
|
>{{ cap }}</span>
|
||||||
|
<span
|
||||||
|
v-if="key.capabilities.length > 2"
|
||||||
|
class="text-[10px] text-muted-foreground"
|
||||||
|
>+{{ key.capabilities.length - 2 }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5 text-xs text-muted-foreground mt-1">
|
||||||
|
<span class="text-[10px] font-medium shrink-0">{{ key.provider_name }}</span>
|
||||||
|
<span class="font-mono text-[10px] opacity-60 truncate">{{ key.api_key_masked }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-muted-foreground opacity-70">{{ key.request_count }} reqs</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-xs text-muted-foreground/50 text-right">
|
|
||||||
<div>--</div>
|
<!-- 右侧:健康度 + 速率 -->
|
||||||
<div class="text-[10px]">无数据</div>
|
<div class="shrink-0 flex items-center gap-3">
|
||||||
|
<!-- 健康度 -->
|
||||||
|
<div
|
||||||
|
v-if="key.success_rate !== null"
|
||||||
|
class="text-xs text-right"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="font-medium tabular-nums"
|
||||||
|
:class="[
|
||||||
|
key.success_rate >= 0.95 ? 'text-green-600' :
|
||||||
|
key.success_rate >= 0.8 ? 'text-yellow-600' : 'text-red-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ (key.success_rate * 100).toFixed(0) }}%
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-muted-foreground opacity-70">
|
||||||
|
{{ key.request_count }} reqs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-xs text-muted-foreground/50 text-right"
|
||||||
|
>
|
||||||
|
<div>--</div>
|
||||||
|
<div class="text-[10px]">
|
||||||
|
无数据
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 速率倍数 -->
|
||||||
|
<div class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">
|
||||||
|
{{ key.rate_multiplier }}x
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 速率倍数 -->
|
|
||||||
<div class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">{{ key.rate_multiplier }}x</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
<div
|
||||||
<Key class="w-10 h-10 mb-3 opacity-20" />
|
v-else
|
||||||
<span class="text-sm">暂无 {{ format }} 格式的 Key</span>
|
class="flex flex-col items-center justify-center py-20 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Key class="w-10 h-10 mb-3 opacity-20" />
|
||||||
|
<span class="text-sm">暂无 {{ format }} 格式的 Key</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,11 +316,24 @@
|
|||||||
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button size="sm" @click="save" :disabled="saving" class="min-w-[72px]">
|
<Button
|
||||||
<Loader2 v-if="saving" class="w-3.5 h-3.5 mr-1.5 animate-spin" />
|
size="sm"
|
||||||
|
:disabled="saving"
|
||||||
|
class="min-w-[72px]"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="saving"
|
||||||
|
class="w-3.5 h-3.5 mr-1.5 animate-spin"
|
||||||
|
/>
|
||||||
{{ saving ? '保存中' : '保存' }}
|
{{ saving ? '保存中' : '保存' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" @click="close" class="min-w-[72px]">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="min-w-[72px]"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,13 +8,19 @@
|
|||||||
@click.self="handleBackdropClick"
|
@click.self="handleBackdropClick"
|
||||||
>
|
>
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleBackdropClick"></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||||
|
@click="handleBackdropClick"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 抽屉内容 -->
|
<!-- 抽屉内容 -->
|
||||||
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
<div
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
v-if="loading"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="provider">
|
<template v-else-if="provider">
|
||||||
@@ -23,8 +29,13 @@
|
|||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="space-y-1 flex-1 min-w-0">
|
<div class="space-y-1 flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2 class="text-xl font-bold truncate">{{ provider.display_name }}</h2>
|
<h2 class="text-xl font-bold truncate">
|
||||||
<Badge :variant="provider.is_active ? 'default' : 'secondary'" class="text-xs shrink-0">
|
{{ provider.display_name }}
|
||||||
|
</h2>
|
||||||
|
<Badge
|
||||||
|
:variant="provider.is_active ? 'default' : 'secondary'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
{{ provider.is_active ? '活跃' : '已停用' }}
|
{{ provider.is_active ? '活跃' : '已停用' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,18 +56,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button variant="ghost" size="icon" @click="$emit('edit', provider)" title="编辑提供商">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="编辑提供商"
|
||||||
|
@click="$emit('edit', provider)"
|
||||||
|
>
|
||||||
<Edit class="w-4 h-4" />
|
<Edit class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="$emit('toggle-status', provider)"
|
|
||||||
:title="provider.is_active ? '点击停用' : '点击启用'"
|
:title="provider.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="$emit('toggle-status', provider)"
|
||||||
>
|
>
|
||||||
<Power class="w-4 h-4" />
|
<Power class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" @click="handleClose" title="关闭">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="关闭"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
<X class="w-4 h-4" />
|
<X class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,53 +85,72 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 p-6">
|
<div class="space-y-6 p-6">
|
||||||
<!-- 配额使用情况 -->
|
<!-- 配额使用情况 -->
|
||||||
<Card v-if="provider.billing_type === 'monthly_quota' && provider.monthly_quota_usd" class="p-4">
|
<Card
|
||||||
<div class="space-y-3">
|
v-if="provider.billing_type === 'monthly_quota' && provider.monthly_quota_usd"
|
||||||
<div class="flex items-center justify-between">
|
class="p-4"
|
||||||
<h3 class="text-sm font-semibold">订阅配额</h3>
|
>
|
||||||
<Badge variant="secondary" class="text-xs">
|
<div class="space-y-3">
|
||||||
{{ ((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100).toFixed(1) }}%
|
<div class="flex items-center justify-between">
|
||||||
</Badge>
|
<h3 class="text-sm font-semibold">
|
||||||
</div>
|
订阅配额
|
||||||
<div class="relative w-full h-2 bg-muted rounded-full overflow-hidden">
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ ((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100).toFixed(1) }}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.7,
|
||||||
|
'bg-yellow-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.7 && (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.9,
|
||||||
|
'bg-red-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.9
|
||||||
|
}"
|
||||||
|
:style="{ width: `${Math.min((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100, 100)}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="font-semibold">
|
||||||
|
${{ (provider.monthly_used_usd || 0).toFixed(2) }} / ${{ provider.monthly_quota_usd.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="provider.quota_reset_day"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
|
每月 {{ provider.quota_reset_day }} 号重置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 端点与密钥管理 -->
|
||||||
|
<Card class="overflow-hidden">
|
||||||
|
<div class="p-4 border-b border-border/60">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-sm font-semibold flex items-center gap-2">
|
||||||
|
<span>端点与密钥管理</span>
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8"
|
||||||
|
@click="showAddEndpointDialog"
|
||||||
|
>
|
||||||
|
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
添加端点
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 端点列表 -->
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
v-if="endpoints.length > 0"
|
||||||
:class="{
|
class="divide-y divide-border/40"
|
||||||
'bg-green-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.7,
|
>
|
||||||
'bg-yellow-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.7 && (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.9,
|
|
||||||
'bg-red-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.9
|
|
||||||
}"
|
|
||||||
:style="{ width: `${Math.min((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100, 100)}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between text-xs">
|
|
||||||
<span class="font-semibold">
|
|
||||||
${{ (provider.monthly_used_usd || 0).toFixed(2) }} / ${{ provider.monthly_quota_usd.toFixed(2) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="provider.quota_reset_day" class="text-muted-foreground">
|
|
||||||
每月 {{ provider.quota_reset_day }} 号重置
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- 端点与密钥管理 -->
|
|
||||||
<Card class="overflow-hidden">
|
|
||||||
<div class="p-4 border-b border-border/60">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="text-sm font-semibold flex items-center gap-2">
|
|
||||||
<span>端点与密钥管理</span>
|
|
||||||
</h3>
|
|
||||||
<Button @click="showAddEndpointDialog" variant="outline" size="sm" class="h-8">
|
|
||||||
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
|
||||||
添加端点
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 端点列表 -->
|
|
||||||
<div v-if="endpoints.length > 0" class="divide-y divide-border/40">
|
|
||||||
<div
|
<div
|
||||||
v-for="endpoint in endpoints"
|
v-for="endpoint in endpoints"
|
||||||
:key="endpoint.id"
|
:key="endpoint.id"
|
||||||
@@ -141,10 +181,16 @@
|
|||||||
<Key class="w-3 h-3" />
|
<Key class="w-3 h-3" />
|
||||||
{{ endpoint.keys?.filter((k: EndpointAPIKey) => k.is_active).length || 0 }}
|
{{ endpoint.keys?.filter((k: EndpointAPIKey) => k.is_active).length || 0 }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="endpoint.max_retries" class="text-xs text-muted-foreground">
|
<span
|
||||||
|
v-if="endpoint.max_retries"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
{{ endpoint.max_retries }}次重试
|
{{ endpoint.max_retries }}次重试
|
||||||
</span>
|
</span>
|
||||||
<span v-if="endpoint.timeout" class="text-xs text-muted-foreground">
|
<span
|
||||||
|
v-if="endpoint.timeout"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
{{ endpoint.timeout }}s
|
{{ endpoint.timeout }}s
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,61 +202,70 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-5 w-5 shrink-0"
|
class="h-5 w-5 shrink-0"
|
||||||
@click.stop="copyToClipboard(endpoint.base_url)"
|
|
||||||
title="复制 Base URL"
|
title="复制 Base URL"
|
||||||
|
@click.stop="copyToClipboard(endpoint.base_url)"
|
||||||
>
|
>
|
||||||
<Copy class="w-3 h-3" />
|
<Copy class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1" @click.stop>
|
<div
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-if="hasUnhealthyKeys(endpoint)"
|
v-if="hasUnhealthyKeys(endpoint)"
|
||||||
@click="handleRecoverAllKeys(endpoint)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8 text-green-600"
|
class="h-8 w-8 text-green-600"
|
||||||
title="恢复所有密钥健康状态"
|
title="恢复所有密钥健康状态"
|
||||||
:disabled="recoveringEndpointId === endpoint.id"
|
:disabled="recoveringEndpointId === endpoint.id"
|
||||||
|
@click="handleRecoverAllKeys(endpoint)"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="recoveringEndpointId === endpoint.id" class="w-3.5 h-3.5 animate-spin" />
|
<Loader2
|
||||||
<RefreshCw v-else class="w-3.5 h-3.5" />
|
v-if="recoveringEndpointId === endpoint.id"
|
||||||
|
class="w-3.5 h-3.5 animate-spin"
|
||||||
|
/>
|
||||||
|
<RefreshCw
|
||||||
|
v-else
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleAddKey(endpoint)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
title="添加密钥"
|
title="添加密钥"
|
||||||
|
@click="handleAddKey(endpoint)"
|
||||||
>
|
>
|
||||||
<Plus class="w-4 h-4" />
|
<Plus class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleEditEndpoint(endpoint)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
title="编辑端点"
|
title="编辑端点"
|
||||||
|
@click="handleEditEndpoint(endpoint)"
|
||||||
>
|
>
|
||||||
<Edit class="w-3.5 h-3.5" />
|
<Edit class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="toggleEndpointActive(endpoint)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
:disabled="togglingEndpointId === endpoint.id"
|
:disabled="togglingEndpointId === endpoint.id"
|
||||||
:title="endpoint.is_active ? '点击停用' : '点击启用'"
|
:title="endpoint.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="toggleEndpointActive(endpoint)"
|
||||||
>
|
>
|
||||||
<Power class="w-3.5 h-3.5" />
|
<Power class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleDeleteEndpoint(endpoint)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
title="删除端点"
|
title="删除端点"
|
||||||
|
@click="handleDeleteEndpoint(endpoint)"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -225,7 +280,10 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-3 pt-3">
|
<div class="space-y-3 pt-3">
|
||||||
<!-- 端点配置信息 -->
|
<!-- 端点配置信息 -->
|
||||||
<div v-if="endpoint.custom_path || endpoint.rpm_limit" class="flex flex-wrap gap-x-4 gap-y-1 text-xs">
|
<div
|
||||||
|
v-if="endpoint.custom_path || endpoint.rpm_limit"
|
||||||
|
class="flex flex-wrap gap-x-4 gap-y-1 text-xs"
|
||||||
|
>
|
||||||
<div v-if="endpoint.custom_path">
|
<div v-if="endpoint.custom_path">
|
||||||
<span class="text-muted-foreground">自定义路径:</span>
|
<span class="text-muted-foreground">自定义路径:</span>
|
||||||
<span class="ml-1 font-mono">{{ endpoint.custom_path }}</span>
|
<span class="ml-1 font-mono">{{ endpoint.custom_path }}</span>
|
||||||
@@ -238,16 +296,14 @@
|
|||||||
|
|
||||||
<!-- 密钥列表 -->
|
<!-- 密钥列表 -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-if="endpoint.keys && endpoint.keys.length > 0" class="space-y-2">
|
<div
|
||||||
|
v-if="endpoint.keys && endpoint.keys.length > 0"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="key in endpoint.keys"
|
v-for="key in endpoint.keys"
|
||||||
:key="key.id"
|
:key="key.id"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="handleDragStart($event, key, endpoint)"
|
|
||||||
@dragend="handleDragEnd"
|
|
||||||
@dragover="handleDragOver($event, key)"
|
|
||||||
@dragleave="handleDragLeave"
|
|
||||||
@drop="handleDrop($event, key, endpoint)"
|
|
||||||
class="p-3 bg-background rounded-md border transition-all duration-150 group/key"
|
class="p-3 bg-background rounded-md border transition-all duration-150 group/key"
|
||||||
:class="{
|
:class="{
|
||||||
'border-border/40 hover:border-border/80': dragState.targetKeyId !== key.id,
|
'border-border/40 hover:border-border/80': dragState.targetKeyId !== key.id,
|
||||||
@@ -255,6 +311,11 @@
|
|||||||
'opacity-50': dragState.draggedKeyId === key.id,
|
'opacity-50': dragState.draggedKeyId === key.id,
|
||||||
'cursor-grabbing': dragState.isDragging
|
'cursor-grabbing': dragState.isDragging
|
||||||
}"
|
}"
|
||||||
|
@dragstart="handleDragStart($event, key, endpoint)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
@dragover="handleDragOver($event, key)"
|
||||||
|
@dragleave="handleDragLeave"
|
||||||
|
@drop="handleDrop($event, key, endpoint)"
|
||||||
>
|
>
|
||||||
<!-- 密钥主要信息行 -->
|
<!-- 密钥主要信息行 -->
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@@ -276,7 +337,9 @@
|
|||||||
{{ key.is_active ? '活跃' : '禁用' }}
|
{{ key.is_active ? '活跃' : '禁用' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] font-mono text-muted-foreground truncate">{{ key.api_key_masked }}</div>
|
<div class="text-[10px] font-mono text-muted-foreground truncate">
|
||||||
|
{{ key.api_key_masked }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 ml-auto shrink-0">
|
<div class="flex items-center gap-1.5 ml-auto shrink-0">
|
||||||
<div
|
<div
|
||||||
@@ -309,48 +372,48 @@
|
|||||||
<div class="flex items-center gap-1 ml-2">
|
<div class="flex items-center gap-1 ml-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="key.circuit_breaker_open || (key.health_score !== undefined && key.health_score < 0.5)"
|
v-if="key.circuit_breaker_open || (key.health_score !== undefined && key.health_score < 0.5)"
|
||||||
@click="handleRecoverKey(key)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7 text-green-600"
|
class="h-7 w-7 text-green-600"
|
||||||
title="刷新健康状态"
|
title="刷新健康状态"
|
||||||
|
@click="handleRecoverKey(key)"
|
||||||
>
|
>
|
||||||
<RefreshCw class="w-3 h-3" />
|
<RefreshCw class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleConfigKeyModels(key)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
title="配置允许的模型"
|
title="配置允许的模型"
|
||||||
|
@click="handleConfigKeyModels(key)"
|
||||||
>
|
>
|
||||||
<Layers class="w-3 h-3" />
|
<Layers class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleEditKey(endpoint, key)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
title="编辑密钥"
|
title="编辑密钥"
|
||||||
|
@click="handleEditKey(endpoint, key)"
|
||||||
>
|
>
|
||||||
<Edit class="w-3 h-3" />
|
<Edit class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="toggleKeyActive(key)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
:disabled="togglingKeyId === key.id"
|
:disabled="togglingKeyId === key.id"
|
||||||
:title="key.is_active ? '点击停用' : '点击启用'"
|
:title="key.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="toggleKeyActive(key)"
|
||||||
>
|
>
|
||||||
<Power class="w-3 h-3" />
|
<Power class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleDeleteKey(key)"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
title="删除密钥"
|
title="删除密钥"
|
||||||
|
@click="handleDeleteKey(key)"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3 h-3" />
|
<Trash2 class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -371,42 +434,67 @@
|
|||||||
P {{ key.internal_priority }}
|
P {{ key.internal_priority }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 编辑模式 -->
|
<!-- 编辑模式 -->
|
||||||
<span v-else class="flex items-center gap-1">
|
<span
|
||||||
|
v-else
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground">P</span>
|
<span class="text-muted-foreground">P</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
ref="priorityInput"
|
||||||
v-model.number="editingPriorityValue"
|
v-model.number="editingPriorityValue"
|
||||||
|
type="number"
|
||||||
class="w-12 h-5 px-1 text-[11px] border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
class="w-12 h-5 px-1 text-[11px] border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
min="0"
|
min="0"
|
||||||
@keyup.enter="savePriority(key, endpoint)"
|
@keyup.enter="savePriority(key, endpoint)"
|
||||||
@keyup.escape="cancelEditPriority"
|
@keyup.escape="cancelEditPriority"
|
||||||
@blur="savePriority(key, endpoint)"
|
@blur="savePriority(key, endpoint)"
|
||||||
ref="priorityInput"
|
>
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground" title="成本倍率,实际成本 = 模型价格 × 倍率">
|
<span
|
||||||
|
class="text-muted-foreground"
|
||||||
|
title="成本倍率,实际成本 = 模型价格 × 倍率"
|
||||||
|
>
|
||||||
{{ key.rate_multiplier }}x
|
{{ key.rate_multiplier }}x
|
||||||
</span>
|
</span>
|
||||||
<span v-if="key.success_rate !== undefined" class="text-muted-foreground" title="成功率 = 成功次数 / 总请求数">
|
<span
|
||||||
|
v-if="key.success_rate !== undefined"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
title="成功率 = 成功次数 / 总请求数"
|
||||||
|
>
|
||||||
{{ (key.success_rate * 100).toFixed(1) }}% ({{ key.success_count }}/{{ key.request_count }})
|
{{ (key.success_rate * 100).toFixed(1) }}% ({{ key.success_count }}/{{ key.request_count }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧动态信息 -->
|
<!-- 右侧动态信息 -->
|
||||||
<div class="flex items-center gap-2 ml-auto">
|
<div class="flex items-center gap-2 ml-auto">
|
||||||
<span v-if="key.next_probe_at" class="text-amber-600 dark:text-amber-400" title="熔断器探测恢复时间">
|
<span
|
||||||
|
v-if="key.next_probe_at"
|
||||||
|
class="text-amber-600 dark:text-amber-400"
|
||||||
|
title="熔断器探测恢复时间"
|
||||||
|
>
|
||||||
{{ formatProbeTime(key.next_probe_at) }}探测
|
{{ formatProbeTime(key.next_probe_at) }}探测
|
||||||
</span>
|
</span>
|
||||||
<span v-if="key.rate_limit" class="text-muted-foreground" title="每分钟请求数限制">
|
<span
|
||||||
|
v-if="key.rate_limit"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
title="每分钟请求数限制"
|
||||||
|
>
|
||||||
{{ key.rate_limit }}rpm
|
{{ key.rate_limit }}rpm
|
||||||
</span>
|
</span>
|
||||||
<span v-if="key.max_concurrent || key.is_adaptive" class="text-muted-foreground" :title="key.is_adaptive ? `自适应并发限制(学习值: ${key.learned_max_concurrent ?? '未学习'})` : '固定并发限制'">
|
<span
|
||||||
|
v-if="key.max_concurrent || key.is_adaptive"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
:title="key.is_adaptive ? `自适应并发限制(学习值: ${key.learned_max_concurrent ?? '未学习'})` : '固定并发限制'"
|
||||||
|
>
|
||||||
{{ key.is_adaptive ? '自适应' : '固定' }}并发: {{ key.learned_max_concurrent || key.max_concurrent || 3 }}
|
{{ key.is_adaptive ? '自适应' : '固定' }}并发: {{ key.learned_max_concurrent || key.max_concurrent || 3 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-xs text-muted-foreground text-center py-4">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-xs text-muted-foreground text-center py-4"
|
||||||
|
>
|
||||||
暂无密钥
|
暂无密钥
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,32 +504,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="p-8 text-center text-muted-foreground">
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-8 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
<Server class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<Server class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p class="text-sm">暂无端点配置</p>
|
<p class="text-sm">
|
||||||
<p class="text-xs mt-1">点击上方"添加端点"按钮创建第一个端点</p>
|
暂无端点配置
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1">
|
||||||
|
点击上方"添加端点"按钮创建第一个端点
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 模型查看 -->
|
<!-- 模型查看 -->
|
||||||
<ModelsTab
|
<ModelsTab
|
||||||
v-if="provider"
|
v-if="provider"
|
||||||
:key="`models-${provider.id}`"
|
:key="`models-${provider.id}`"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
@edit-model="handleEditModel"
|
@edit-model="handleEditModel"
|
||||||
@delete-model="handleDeleteModel"
|
@delete-model="handleDeleteModel"
|
||||||
@batch-assign="handleBatchAssign"
|
@batch-assign="handleBatchAssign"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 模型映射 -->
|
<!-- 模型映射 -->
|
||||||
<MappingsTab
|
<MappingsTab
|
||||||
v-if="provider"
|
v-if="provider"
|
||||||
:key="`mappings-${provider.id}`"
|
:key="`mappings-${provider.id}`"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
@refresh="handleRelatedDataRefresh"
|
@refresh="handleRelatedDataRefresh"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -7,13 +7,21 @@
|
|||||||
size="2xl"
|
size="2xl"
|
||||||
@update:model-value="handleDialogUpdate"
|
@update:model-value="handleDialogUpdate"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<!-- 基本信息 -->
|
<!-- 基本信息 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-sm font-medium border-b pb-2">基本信息</h3>
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
|
基本信息
|
||||||
|
</h3>
|
||||||
|
|
||||||
<!-- 添加模式显示提供商标识 -->
|
<!-- 添加模式显示提供商标识 -->
|
||||||
<div v-if="!isEditMode" class="space-y-2">
|
<div
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label for="name">提供商标识 *</Label>
|
<Label for="name">提供商标识 *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
@@ -21,7 +29,9 @@
|
|||||||
placeholder="例如: openai-primary"
|
placeholder="例如: openai-primary"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground">唯一ID,创建后不可修改</p>
|
<p class="text-xs text-muted-foreground">
|
||||||
|
唯一ID,创建后不可修改
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
@@ -58,18 +68,29 @@
|
|||||||
|
|
||||||
<!-- 计费与限流 -->
|
<!-- 计费与限流 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-sm font-medium border-b pb-2">计费与限流</h3>
|
<h3 class="text-sm font-medium border-b pb-2">
|
||||||
|
计费与限流
|
||||||
|
</h3>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>计费类型</Label>
|
<Label>计费类型</Label>
|
||||||
<Select v-model="form.billing_type" v-model:open="billingTypeSelectOpen">
|
<Select
|
||||||
|
v-model="form.billing_type"
|
||||||
|
v-model:open="billingTypeSelectOpen"
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="monthly_quota">月卡额度</SelectItem>
|
<SelectItem value="monthly_quota">
|
||||||
<SelectItem value="pay_as_you_go">按量付费</SelectItem>
|
月卡额度
|
||||||
<SelectItem value="free_tier">免费套餐</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value="pay_as_you_go">
|
||||||
|
按量付费
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="free_tier">
|
||||||
|
免费套餐
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +107,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 月卡配置 -->
|
<!-- 月卡配置 -->
|
||||||
<div v-if="form.billing_type === 'monthly_quota'" class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50">
|
<div
|
||||||
|
v-if="form.billing_type === 'monthly_quota'"
|
||||||
|
class="grid grid-cols-2 gap-4 p-3 border rounded-lg bg-muted/50"
|
||||||
|
>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-xs">周期额度 (USD)</Label>
|
<Label class="text-xs">周期额度 (USD)</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -114,13 +138,25 @@
|
|||||||
周期开始时间
|
周期开始时间
|
||||||
<span class="text-red-500">*</span>
|
<span class="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input v-model="form.quota_last_reset_at" type="datetime-local" class="h-9" />
|
<Input
|
||||||
<p class="text-xs text-muted-foreground">系统会自动统计从该时间点开始的使用量</p>
|
v-model="form.quota_last_reset_at"
|
||||||
|
type="datetime-local"
|
||||||
|
class="h-9"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
系统会自动统计从该时间点开始的使用量
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-xs">过期时间</Label>
|
<Label class="text-xs">过期时间</Label>
|
||||||
<Input v-model="form.quota_expires_at" type="datetime-local" class="h-9" />
|
<Input
|
||||||
<p class="text-xs text-muted-foreground">留空表示永久有效</p>
|
v-model="form.quota_expires_at"
|
||||||
|
type="datetime-local"
|
||||||
|
class="h-9"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
留空表示永久有效
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,14 +166,14 @@
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="handleCancel"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@click="handleSubmit"
|
|
||||||
:disabled="loading || !form.display_name || (!isEditMode && !form.name)"
|
:disabled="loading || !form.display_name || (!isEditMode && !form.name)"
|
||||||
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存' : '创建') }}
|
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存' : '创建') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -147,16 +183,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Dialog } from '@/components/ui'
|
import {
|
||||||
import Button from '@/components/ui/button.vue'
|
Dialog,
|
||||||
import Input from '@/components/ui/input.vue'
|
Button,
|
||||||
import Textarea from '@/components/ui/textarea.vue'
|
Input,
|
||||||
import Label from '@/components/ui/label.vue'
|
Textarea,
|
||||||
import Select from '@/components/ui/select.vue'
|
Label,
|
||||||
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
Select,
|
||||||
import SelectValue from '@/components/ui/select-value.vue'
|
SelectTrigger,
|
||||||
import SelectContent from '@/components/ui/select-content.vue'
|
SelectValue,
|
||||||
import SelectItem from '@/components/ui/select-item.vue'
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from '@/components/ui'
|
||||||
import { Server, SquarePen } from 'lucide-vue-next'
|
import { Server, SquarePen } from 'lucide-vue-next'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useFormDialog } from '@/composables/useFormDialog'
|
import { useFormDialog } from '@/composables/useFormDialog'
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:model-value="open"
|
:model-value="open"
|
||||||
@update:model-value="handleClose"
|
|
||||||
:title="isEditing ? '编辑模型配置' : '添加模型'"
|
:title="isEditing ? '编辑模型配置' : '添加模型'"
|
||||||
:description="isEditing ? '修改模型价格和能力配置' : '为此 Provider 添加模型实现'"
|
:description="isEditing ? '修改模型价格和能力配置' : '为此 Provider 添加模型实现'"
|
||||||
:icon="isEditing ? SquarePen : Layers"
|
:icon="isEditing ? SquarePen : Layers"
|
||||||
size="xl"
|
size="xl"
|
||||||
|
@update:model-value="handleClose"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form
|
||||||
|
class="space-y-4"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<!-- 添加模式:选择全局模型 -->
|
<!-- 添加模式:选择全局模型 -->
|
||||||
<div v-if="!isEditing" class="space-y-2">
|
<div
|
||||||
|
v-if="!isEditing"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<Label for="global-model">选择模型 *</Label>
|
<Label for="global-model">选择模型 *</Label>
|
||||||
<Select
|
<Select
|
||||||
v-model:open="globalModelSelectOpen"
|
v-model:open="globalModelSelectOpen"
|
||||||
@@ -30,25 +36,41 @@
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p v-if="availableGlobalModels.length === 0 && !loadingGlobalModels" class="text-xs text-muted-foreground">
|
<p
|
||||||
|
v-if="availableGlobalModels.length === 0 && !loadingGlobalModels"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
所有全局模型已添加到此 Provider
|
所有全局模型已添加到此 Provider
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑模式:显示模型信息 -->
|
<!-- 编辑模式:显示模型信息 -->
|
||||||
<div v-else class="rounded-lg border bg-muted/30 p-4">
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded-lg border bg-muted/30 p-4"
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-lg">{{ editingModel?.global_model_display_name || editingModel?.provider_model_name }}</p>
|
<p class="font-semibold text-lg">
|
||||||
<p class="text-sm text-muted-foreground font-mono">{{ editingModel?.provider_model_name }}</p>
|
{{ editingModel?.global_model_display_name || editingModel?.provider_model_name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-muted-foreground font-mono">
|
||||||
|
{{ editingModel?.provider_model_name }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 价格配置 -->
|
<!-- 价格配置 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm border-b pb-2">价格配置</h4>
|
<h4 class="font-semibold text-sm border-b pb-2">
|
||||||
<TieredPricingEditor ref="tieredPricingEditorRef" v-model="tieredPricing" :show-cache1h="showCache1h" />
|
价格配置
|
||||||
|
</h4>
|
||||||
|
<TieredPricingEditor
|
||||||
|
ref="tieredPricingEditorRef"
|
||||||
|
v-model="tieredPricing"
|
||||||
|
:show-cache1h="showCache1h"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 按次计费 -->
|
<!-- 按次计费 -->
|
||||||
<div class="flex items-center gap-3 pt-2 border-t">
|
<div class="flex items-center gap-3 pt-2 border-t">
|
||||||
@@ -68,70 +90,80 @@
|
|||||||
|
|
||||||
<!-- 能力配置 -->
|
<!-- 能力配置 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm border-b pb-2">能力配置</h4>
|
<h4 class="font-semibold text-sm border-b pb-2">
|
||||||
|
能力配置
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.supports_streaming"
|
v-model="form.supports_streaming"
|
||||||
|
type="checkbox"
|
||||||
:indeterminate="form.supports_streaming === undefined"
|
:indeterminate="form.supports_streaming === undefined"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
>
|
||||||
<Zap class="w-4 h-4 text-muted-foreground shrink-0" />
|
<Zap class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<span class="text-sm font-medium">流式输出</span>
|
<span class="text-sm font-medium">流式输出</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.supports_image_generation"
|
v-model="form.supports_image_generation"
|
||||||
|
type="checkbox"
|
||||||
:indeterminate="form.supports_image_generation === undefined"
|
:indeterminate="form.supports_image_generation === undefined"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
>
|
||||||
<Image class="w-4 h-4 text-muted-foreground shrink-0" />
|
<Image class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<span class="text-sm font-medium">图像生成</span>
|
<span class="text-sm font-medium">图像生成</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.supports_vision"
|
v-model="form.supports_vision"
|
||||||
|
type="checkbox"
|
||||||
:indeterminate="form.supports_vision === undefined"
|
:indeterminate="form.supports_vision === undefined"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
>
|
||||||
<Eye class="w-4 h-4 text-muted-foreground shrink-0" />
|
<Eye class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<span class="text-sm font-medium">视觉理解</span>
|
<span class="text-sm font-medium">视觉理解</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.supports_function_calling"
|
v-model="form.supports_function_calling"
|
||||||
|
type="checkbox"
|
||||||
:indeterminate="form.supports_function_calling === undefined"
|
:indeterminate="form.supports_function_calling === undefined"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
>
|
||||||
<Wrench class="w-4 h-4 text-muted-foreground shrink-0" />
|
<Wrench class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<span class="text-sm font-medium">工具调用</span>
|
<span class="text-sm font-medium">工具调用</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.supports_extended_thinking"
|
v-model="form.supports_extended_thinking"
|
||||||
|
type="checkbox"
|
||||||
:indeterminate="form.supports_extended_thinking === undefined"
|
:indeterminate="form.supports_extended_thinking === undefined"
|
||||||
class="rounded"
|
class="rounded"
|
||||||
/>
|
>
|
||||||
<Brain class="w-4 h-4 text-muted-foreground shrink-0" />
|
<Brain class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<span class="text-sm font-medium">深度思考</span>
|
<span class="text-sm font-medium">深度思考</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button variant="outline" @click="handleClose(false)">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
@click="handleClose(false)"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleSubmit" :disabled="submitting || (!isEditing && !form.global_model_id)">
|
<Button
|
||||||
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
|
:disabled="submitting || (!isEditing && !form.global_model_id)"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<Loader2
|
||||||
|
v-if="submitting"
|
||||||
|
class="w-4 h-4 mr-2 animate-spin"
|
||||||
|
/>
|
||||||
{{ isEditing ? '保存' : '添加' }}
|
{{ isEditing ? '保存' : '添加' }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,10 +173,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Eye, Wrench, Brain, Zap, Loader2, Image, Layers, SquarePen } from 'lucide-vue-next'
|
import { Eye, Wrench, Brain, Zap, Loader2, Image, Layers, SquarePen } from 'lucide-vue-next'
|
||||||
import { Dialog, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
|
import {
|
||||||
import Button from '@/components/ui/button.vue'
|
Dialog,
|
||||||
import Input from '@/components/ui/input.vue'
|
Button,
|
||||||
import Label from '@/components/ui/label.vue'
|
Input,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from '@/components/ui'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { parseNumberInput } from '@/utils/form'
|
import { parseNumberInput } from '@/utils/form'
|
||||||
import { createModel, updateModel, getProviderModels } from '@/api/endpoints/models'
|
import { createModel, updateModel, getProviderModels } from '@/api/endpoints/models'
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-if="!hideAddButton"
|
v-if="!hideAddButton"
|
||||||
@click="openCreateDialog"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
||||||
创建别名/映射
|
创建别名/映射
|
||||||
@@ -22,19 +22,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
<div
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
v-if="loading"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 别名列表 -->
|
<!-- 别名列表 -->
|
||||||
<div v-else-if="mappings.length > 0" class="overflow-x-auto">
|
<div
|
||||||
|
v-else-if="mappings.length > 0"
|
||||||
|
class="overflow-x-auto"
|
||||||
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-3 font-semibold">名称</th>
|
<th class="text-left px-4 py-3 font-semibold">
|
||||||
<th class="text-left px-4 py-3 font-semibold w-24">类型</th>
|
名称
|
||||||
<th class="text-left px-4 py-3 font-semibold">指向模型</th>
|
</th>
|
||||||
<th v-if="!hideAddButton" class="px-4 py-3 font-semibold w-28 text-center">操作</th>
|
<th class="text-left px-4 py-3 font-semibold w-24">
|
||||||
|
类型
|
||||||
|
</th>
|
||||||
|
<th class="text-left px-4 py-3 font-semibold">
|
||||||
|
指向模型
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-if="!hideAddButton"
|
||||||
|
class="px-4 py-3 font-semibold w-28 text-center"
|
||||||
|
>
|
||||||
|
操作
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -50,19 +67,25 @@
|
|||||||
class="w-2 h-2 rounded-full shrink-0"
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
:class="mapping.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
:class="mapping.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
||||||
:title="mapping.is_active ? '活跃' : '停用'"
|
:title="mapping.is_active ? '活跃' : '停用'"
|
||||||
></span>
|
/>
|
||||||
<span class="font-mono">{{ mapping.alias }}</span>
|
<span class="font-mono">{{ mapping.alias }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<Badge variant="secondary" class="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
{{ mapping.mapping_type === 'mapping' ? '映射' : '别名' }}
|
{{ mapping.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
{{ mapping.global_model_display_name || mapping.global_model_name }}
|
{{ mapping.global_model_display_name || mapping.global_model_name }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="!hideAddButton" class="px-4 py-3">
|
<td
|
||||||
|
v-if="!hideAddButton"
|
||||||
|
class="px-4 py-3"
|
||||||
|
>
|
||||||
<div class="flex justify-center gap-1.5">
|
<div class="flex justify-center gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -78,8 +101,8 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
:disabled="togglingId === mapping.id"
|
:disabled="togglingId === mapping.id"
|
||||||
@click="toggleActive(mapping)"
|
|
||||||
:title="mapping.is_active ? '点击停用' : '点击启用'"
|
:title="mapping.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="toggleActive(mapping)"
|
||||||
>
|
>
|
||||||
<Power class="w-3.5 h-3.5" />
|
<Power class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -100,10 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="p-8 text-center text-muted-foreground">
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-8 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
<ArrowLeftRight class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<ArrowLeftRight class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p class="text-sm">暂无特定别名/映射</p>
|
<p class="text-sm">
|
||||||
<p class="text-xs mt-1">点击上方按钮添加</p>
|
暂无特定别名/映射
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1">
|
||||||
|
点击上方按钮添加
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
<h3 class="text-sm font-semibold flex items-center gap-2">
|
<h3 class="text-sm font-semibold flex items-center gap-2">
|
||||||
模型列表
|
模型列表
|
||||||
</h3>
|
</h3>
|
||||||
<Button @click="openBatchAssignDialog" variant="outline" size="sm" class="h-8">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8"
|
||||||
|
@click="openBatchAssignDialog"
|
||||||
|
>
|
||||||
<Layers class="w-3.5 h-3.5 mr-1.5" />
|
<Layers class="w-3.5 h-3.5 mr-1.5" />
|
||||||
关联模型
|
关联模型
|
||||||
</Button>
|
</Button>
|
||||||
@@ -14,19 +19,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
<div
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
v-if="loading"
|
||||||
|
class="flex items-center justify-center py-12"
|
||||||
|
>
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<div v-else-if="models.length > 0" class="overflow-hidden">
|
<div
|
||||||
|
v-else-if="models.length > 0"
|
||||||
|
class="overflow-hidden"
|
||||||
|
>
|
||||||
<table class="w-full text-sm table-fixed">
|
<table class="w-full text-sm table-fixed">
|
||||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-3 font-semibold w-[40%]">模型</th>
|
<th class="text-left px-4 py-3 font-semibold w-[40%]">
|
||||||
<th class="text-left px-4 py-3 font-semibold w-[15%]">能力</th>
|
模型
|
||||||
<th class="text-left px-4 py-3 font-semibold w-[25%]">价格 ($/M)</th>
|
</th>
|
||||||
<th class="text-center px-4 py-3 font-semibold w-[20%]">操作</th>
|
<th class="text-left px-4 py-3 font-semibold w-[15%]">
|
||||||
|
能力
|
||||||
|
</th>
|
||||||
|
<th class="text-left px-4 py-3 font-semibold w-[25%]">
|
||||||
|
价格 ($/M)
|
||||||
|
</th>
|
||||||
|
<th class="text-center px-4 py-3 font-semibold w-[20%]">
|
||||||
|
操作
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -42,7 +61,7 @@
|
|||||||
class="w-2 h-2 rounded-full shrink-0"
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
:class="getStatusIndicatorClass(model)"
|
:class="getStatusIndicatorClass(model)"
|
||||||
:title="getStatusTitle(model)"
|
:title="getStatusTitle(model)"
|
||||||
></div>
|
/>
|
||||||
<!-- 模型信息 -->
|
<!-- 模型信息 -->
|
||||||
<div class="text-left flex-1 min-w-0">
|
<div class="text-left flex-1 min-w-0">
|
||||||
<span class="font-semibold text-sm">
|
<span class="font-semibold text-sm">
|
||||||
@@ -51,9 +70,9 @@
|
|||||||
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||||
<span class="font-mono truncate">{{ model.provider_model_name }}</span>
|
<span class="font-mono truncate">{{ model.provider_model_name }}</span>
|
||||||
<button
|
<button
|
||||||
@click.stop="copyModelId(model.provider_model_name)"
|
|
||||||
class="p-0.5 hover:bg-muted rounded transition-colors shrink-0"
|
class="p-0.5 hover:bg-muted rounded transition-colors shrink-0"
|
||||||
title="复制模型 ID"
|
title="复制模型 ID"
|
||||||
|
@click.stop="copyModelId(model.provider_model_name)"
|
||||||
>
|
>
|
||||||
<Copy class="w-3 h-3" />
|
<Copy class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -62,17 +81,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-top px-4 py-3">
|
<td class="align-top px-4 py-3">
|
||||||
<div v-if="hasAnyCapability(model)" class="grid grid-cols-3 gap-1 w-fit">
|
<div
|
||||||
<Zap v-if="model.effective_supports_streaming ?? model.supports_streaming" class="w-4 h-4 text-muted-foreground" title="流式输出" />
|
v-if="hasAnyCapability(model)"
|
||||||
<Image v-if="model.effective_supports_image_generation ?? model.supports_image_generation" class="w-4 h-4 text-muted-foreground" title="图像生成" />
|
class="grid grid-cols-3 gap-1 w-fit"
|
||||||
<Eye v-if="model.effective_supports_vision ?? model.supports_vision" class="w-4 h-4 text-muted-foreground" title="视觉理解" />
|
>
|
||||||
<Wrench v-if="model.effective_supports_function_calling ?? model.supports_function_calling" class="w-4 h-4 text-muted-foreground" title="工具调用" />
|
<Zap
|
||||||
<Brain v-if="model.effective_supports_extended_thinking ?? model.supports_extended_thinking" class="w-4 h-4 text-muted-foreground" title="深度思考" />
|
v-if="model.effective_supports_streaming ?? model.supports_streaming"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
title="流式输出"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
v-if="model.effective_supports_image_generation ?? model.supports_image_generation"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
title="图像生成"
|
||||||
|
/>
|
||||||
|
<Eye
|
||||||
|
v-if="model.effective_supports_vision ?? model.supports_vision"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
title="视觉理解"
|
||||||
|
/>
|
||||||
|
<Wrench
|
||||||
|
v-if="model.effective_supports_function_calling ?? model.supports_function_calling"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
title="工具调用"
|
||||||
|
/>
|
||||||
|
<Brain
|
||||||
|
v-if="model.effective_supports_extended_thinking ?? model.supports_extended_thinking"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
title="深度思考"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="text-xs text-muted-foreground">—</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-top px-4 py-3 text-xs whitespace-nowrap">
|
<td class="align-top px-4 py-3 text-xs whitespace-nowrap">
|
||||||
<div class="grid gap-1" style="grid-template-columns: auto 1fr;">
|
<div
|
||||||
|
class="grid gap-1"
|
||||||
|
style="grid-template-columns: auto 1fr;"
|
||||||
|
>
|
||||||
<!-- 按 Token 计费 -->
|
<!-- 按 Token 计费 -->
|
||||||
<template v-if="hasTokenPricing(model)">
|
<template v-if="hasTokenPricing(model)">
|
||||||
<span class="text-muted-foreground text-right">输入/输出:</span>
|
<span class="text-muted-foreground text-right">输入/输出:</span>
|
||||||
@@ -112,8 +160,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="editModel(model)"
|
|
||||||
title="编辑"
|
title="编辑"
|
||||||
|
@click="editModel(model)"
|
||||||
>
|
>
|
||||||
<Edit class="w-3.5 h-3.5" />
|
<Edit class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -122,8 +170,8 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
:disabled="togglingModelId === model.id"
|
:disabled="togglingModelId === model.id"
|
||||||
@click="toggleModelActive(model)"
|
|
||||||
:title="model.is_active ? '点击停用' : '点击启用'"
|
:title="model.is_active ? '点击停用' : '点击启用'"
|
||||||
|
@click="toggleModelActive(model)"
|
||||||
>
|
>
|
||||||
<Power class="w-3.5 h-3.5" />
|
<Power class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -131,8 +179,8 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
@click="deleteModel(model)"
|
|
||||||
title="删除"
|
title="删除"
|
||||||
|
@click="deleteModel(model)"
|
||||||
>
|
>
|
||||||
<Trash2 class="w-3.5 h-3.5" />
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -144,10 +192,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else class="p-8 text-center text-muted-foreground">
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-8 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
<Box class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<Box class="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p class="text-sm">暂无模型</p>
|
<p class="text-sm">
|
||||||
<p class="text-xs mt-1">请前往"模型目录"页面添加模型</p>
|
暂无模型
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1">
|
||||||
|
请前往"模型目录"页面添加模型
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="p-4 !overflow-visible">
|
<Card class="p-4 !overflow-visible">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-sm font-semibold">{{ title }}</p>
|
<p class="text-sm font-semibold">
|
||||||
<div v-if="hasData" class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0">
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="hasData"
|
||||||
|
class="flex items-center gap-1 text-[11px] text-muted-foreground flex-shrink-0"
|
||||||
|
>
|
||||||
<span class="flex-shrink-0">少</span>
|
<span class="flex-shrink-0">少</span>
|
||||||
<div
|
<div
|
||||||
v-for="(level, index) in legendLevels"
|
v-for="(level, index) in legendLevels"
|
||||||
@@ -18,7 +23,10 @@
|
|||||||
:data="data"
|
:data="data"
|
||||||
:show-header="false"
|
:show-header="false"
|
||||||
/>
|
/>
|
||||||
<div v-else class="h-full min-h-[160px] flex items-center justify-center text-sm text-muted-foreground">
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-full min-h-[160px] flex items-center justify-center text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无活跃数据
|
暂无活跃数据
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,25 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="minimal-request-timeline">
|
<div class="minimal-request-timeline">
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="py-4">
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="py-4"
|
||||||
|
>
|
||||||
<Skeleton class="h-32 w-full" />
|
<Skeleton class="h-32 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<Card v-else-if="error" class="border-red-200 dark:border-red-800">
|
<Card
|
||||||
|
v-else-if="error"
|
||||||
|
class="border-red-200 dark:border-red-800"
|
||||||
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
<p class="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Timeline Content -->
|
<!-- Timeline Content -->
|
||||||
<div v-else-if="trace && trace.candidates.length > 0" class="space-y-0">
|
<div
|
||||||
|
v-else-if="trace && trace.candidates.length > 0"
|
||||||
|
class="space-y-0"
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- 概览信息 -->
|
<!-- 概览信息 -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h4 class="text-sm font-semibold">请求链路追踪</h4>
|
<h4 class="text-sm font-semibold">
|
||||||
|
请求链路追踪
|
||||||
|
</h4>
|
||||||
<Badge :variant="getFinalStatusBadgeVariant(computedFinalStatus)">
|
<Badge :variant="getFinalStatusBadgeVariant(computedFinalStatus)">
|
||||||
{{ getFinalStatusLabel(computedFinalStatus) }}
|
{{ getFinalStatusLabel(computedFinalStatus) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -46,7 +59,9 @@
|
|||||||
<!-- 节点容器 -->
|
<!-- 节点容器 -->
|
||||||
<div class="node-container">
|
<div class="node-container">
|
||||||
<!-- 节点名称(在节点上方) -->
|
<!-- 节点名称(在节点上方) -->
|
||||||
<div class="node-label">{{ group.providerName }}</div>
|
<div class="node-label">
|
||||||
|
{{ group.providerName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 主节点(代表首次请求) -->
|
<!-- 主节点(代表首次请求) -->
|
||||||
<div
|
<div
|
||||||
@@ -56,10 +71,13 @@
|
|||||||
{ 'is-first-selected': isGroupSelected(group) && selectedAttemptIndex === 0 }
|
{ 'is-first-selected': isGroupSelected(group) && selectedAttemptIndex === 0 }
|
||||||
]"
|
]"
|
||||||
@click.stop="selectFirstAttempt(group)"
|
@click.stop="selectFirstAttempt(group)"
|
||||||
></div>
|
/>
|
||||||
|
|
||||||
<!-- 子节点(同提供商的其他尝试,不包含首次) -->
|
<!-- 子节点(同提供商的其他尝试,不包含首次) -->
|
||||||
<div v-if="group.retryCount > 0 && isGroupSelected(group)" class="sub-dots">
|
<div
|
||||||
|
v-if="group.retryCount > 0 && isGroupSelected(group)"
|
||||||
|
class="sub-dots"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-for="(attempt, idx) in group.allAttempts.slice(1)"
|
v-for="(attempt, idx) in group.allAttempts.slice(1)"
|
||||||
:key="attempt.id"
|
:key="attempt.id"
|
||||||
@@ -68,23 +86,32 @@
|
|||||||
getStatusColorClass(attempt.status),
|
getStatusColorClass(attempt.status),
|
||||||
{ active: selectedAttemptIndex === idx + 1 }
|
{ active: selectedAttemptIndex === idx + 1 }
|
||||||
]"
|
]"
|
||||||
@click.stop="selectedAttemptIndex = idx + 1"
|
|
||||||
:title="attempt.key_name || `Key ${idx + 2}`"
|
:title="attempt.key_name || `Key ${idx + 2}`"
|
||||||
></button>
|
@click.stop="selectedAttemptIndex = idx + 1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 连接线 -->
|
<!-- 连接线 -->
|
||||||
<div class="node-line" v-if="groupIndex < groupedTimeline.length - 1"></div>
|
<div
|
||||||
|
v-if="groupIndex < groupedTimeline.length - 1"
|
||||||
|
class="node-line"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选中详情面板 -->
|
<!-- 选中详情面板 -->
|
||||||
<Transition name="slide-up">
|
<Transition name="slide-up">
|
||||||
<div v-if="selectedGroup && currentAttempt" class="detail-panel">
|
<div
|
||||||
|
v-if="selectedGroup && currentAttempt"
|
||||||
|
class="detail-panel"
|
||||||
|
>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<span class="title-dot" :class="getStatusColorClass(currentAttempt.status)"></span>
|
<span
|
||||||
|
class="title-dot"
|
||||||
|
:class="getStatusColorClass(currentAttempt.status)"
|
||||||
|
/>
|
||||||
<span class="title-text">{{ selectedGroup.providerName }}</span>
|
<span class="title-text">{{ selectedGroup.providerName }}</span>
|
||||||
<a
|
<a
|
||||||
v-if="currentAttempt.provider_website"
|
v-if="currentAttempt.provider_website"
|
||||||
@@ -96,7 +123,10 @@
|
|||||||
>
|
>
|
||||||
<ExternalLink class="w-3 h-3" />
|
<ExternalLink class="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
<span class="status-tag" :class="getStatusColorClass(currentAttempt.status)">
|
<span
|
||||||
|
class="status-tag"
|
||||||
|
:class="getStatusColorClass(currentAttempt.status)"
|
||||||
|
>
|
||||||
{{ currentAttempt.status_code || getStatusLabel(currentAttempt.status) }}
|
{{ currentAttempt.status_code || getStatusLabel(currentAttempt.status) }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 多 Key 标识 -->
|
<!-- 多 Key 标识 -->
|
||||||
@@ -126,31 +156,45 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
||||||
<!-- 核心信息网格 -->
|
<!-- 核心信息网格 -->
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item" v-if="currentAttempt.started_at">
|
<div
|
||||||
|
v-if="currentAttempt.started_at"
|
||||||
|
class="info-item"
|
||||||
|
>
|
||||||
<span class="info-label">时间范围</span>
|
<span class="info-label">时间范围</span>
|
||||||
<span class="info-value mono time-range-value">
|
<span class="info-value mono time-range-value">
|
||||||
{{ formatTime(currentAttempt.started_at) }}
|
{{ formatTime(currentAttempt.started_at) }}
|
||||||
<span class="time-arrow-container">
|
<span class="time-arrow-container">
|
||||||
<span class="time-duration" v-if="currentAttempt.finished_at">+{{ formatDuration(currentAttempt.started_at, currentAttempt.finished_at) }}</span>
|
<span
|
||||||
|
v-if="currentAttempt.finished_at"
|
||||||
|
class="time-duration"
|
||||||
|
>+{{ formatDuration(currentAttempt.started_at, currentAttempt.finished_at) }}</span>
|
||||||
<span class="time-arrow">→</span>
|
<span class="time-arrow">→</span>
|
||||||
</span>
|
</span>
|
||||||
{{ currentAttempt.finished_at ? formatTime(currentAttempt.finished_at) : '进行中' }}
|
{{ currentAttempt.finished_at ? formatTime(currentAttempt.finished_at) : '进行中' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item" v-if="currentAttempt.key_name || currentAttempt.key_id">
|
<div
|
||||||
|
v-if="currentAttempt.key_name || currentAttempt.key_id"
|
||||||
|
class="info-item"
|
||||||
|
>
|
||||||
<span class="info-label">密钥</span>
|
<span class="info-label">密钥</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="key-name">{{ currentAttempt.key_name || '未知' }}</span>
|
<span class="key-name">{{ currentAttempt.key_name || '未知' }}</span>
|
||||||
<template v-if="currentAttempt.key_preview">
|
<template v-if="currentAttempt.key_preview">
|
||||||
<Separator orientation="vertical" class="h-3 mx-2" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-3 mx-2"
|
||||||
|
/>
|
||||||
<code class="key-preview">{{ currentAttempt.key_preview }}</code>
|
<code class="key-preview">{{ currentAttempt.key_preview }}</code>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item" v-if="mergedCapabilities.length > 0">
|
<div
|
||||||
|
v-if="mergedCapabilities.length > 0"
|
||||||
|
class="info-item"
|
||||||
|
>
|
||||||
<span class="info-label">能力</span>
|
<span class="info-label">能力</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="capability-tags">
|
<span class="capability-tags">
|
||||||
@@ -166,7 +210,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用量与费用(仅成功节点显示) -->
|
<!-- 用量与费用(仅成功节点显示) -->
|
||||||
<div v-if="currentAttempt.status === 'success' && usageData" class="usage-section">
|
<div
|
||||||
|
v-if="currentAttempt.status === 'success' && usageData"
|
||||||
|
class="usage-section"
|
||||||
|
>
|
||||||
<div class="usage-grid">
|
<div class="usage-grid">
|
||||||
<!-- 输入 输出 -->
|
<!-- 输入 输出 -->
|
||||||
<div class="usage-row">
|
<div class="usage-row">
|
||||||
@@ -175,7 +222,7 @@
|
|||||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.input) }}</span>
|
<span class="usage-tokens">{{ formatNumber(usageData.tokens.input) }}</span>
|
||||||
<span class="usage-cost">${{ usageData.cost.input.toFixed(6) }}</span>
|
<span class="usage-cost">${{ usageData.cost.input.toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-divider"></div>
|
<div class="usage-divider" />
|
||||||
<div class="usage-item">
|
<div class="usage-item">
|
||||||
<span class="usage-label">输出</span>
|
<span class="usage-label">输出</span>
|
||||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.output) }}</span>
|
<span class="usage-tokens">{{ formatNumber(usageData.tokens.output) }}</span>
|
||||||
@@ -183,13 +230,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 缓存创建 缓存读取(仅在有缓存数据时显示) -->
|
<!-- 缓存创建 缓存读取(仅在有缓存数据时显示) -->
|
||||||
<div v-if="usageData.tokens.cache_creation || usageData.tokens.cache_read" class="usage-row">
|
<div
|
||||||
|
v-if="usageData.tokens.cache_creation || usageData.tokens.cache_read"
|
||||||
|
class="usage-row"
|
||||||
|
>
|
||||||
<div class="usage-item">
|
<div class="usage-item">
|
||||||
<span class="usage-label">缓存创建</span>
|
<span class="usage-label">缓存创建</span>
|
||||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_creation || 0) }}</span>
|
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_creation || 0) }}</span>
|
||||||
<span class="usage-cost">${{ (usageData.cost.cache_creation || 0).toFixed(6) }}</span>
|
<span class="usage-cost">${{ (usageData.cost.cache_creation || 0).toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-divider"></div>
|
<div class="usage-divider" />
|
||||||
<div class="usage-item">
|
<div class="usage-item">
|
||||||
<span class="usage-label">缓存读取</span>
|
<span class="usage-label">缓存读取</span>
|
||||||
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_read || 0) }}</span>
|
<span class="usage-tokens">{{ formatNumber(usageData.tokens.cache_read || 0) }}</span>
|
||||||
@@ -200,20 +250,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 跳过原因 -->
|
<!-- 跳过原因 -->
|
||||||
<div v-if="currentAttempt.skip_reason" class="skip-reason">
|
<div
|
||||||
|
v-if="currentAttempt.skip_reason"
|
||||||
|
class="skip-reason"
|
||||||
|
>
|
||||||
<span class="reason-label">跳过原因</span>
|
<span class="reason-label">跳过原因</span>
|
||||||
<span class="reason-value">{{ currentAttempt.skip_reason }}</span>
|
<span class="reason-value">{{ currentAttempt.skip_reason }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误信息 -->
|
<!-- 错误信息 -->
|
||||||
<div v-if="currentAttempt.status === 'failed' && (currentAttempt.error_message || currentAttempt.error_type)" class="error-block">
|
<div
|
||||||
<div class="error-type">{{ currentAttempt.error_type || '错误' }}</div>
|
v-if="currentAttempt.status === 'failed' && (currentAttempt.error_message || currentAttempt.error_type)"
|
||||||
<div class="error-msg">{{ currentAttempt.error_message || '未知错误' }}</div>
|
class="error-block"
|
||||||
|
>
|
||||||
|
<div class="error-type">
|
||||||
|
{{ currentAttempt.error_type || '错误' }}
|
||||||
|
</div>
|
||||||
|
<div class="error-msg">
|
||||||
|
{{ currentAttempt.error_message || '未知错误' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 额外数据 -->
|
<!-- 额外数据 -->
|
||||||
<details v-if="currentAttempt.extra_data && Object.keys(currentAttempt.extra_data).length > 0" class="extra-block">
|
<details
|
||||||
<summary class="extra-toggle">额外信息</summary>
|
v-if="currentAttempt.extra_data && Object.keys(currentAttempt.extra_data).length > 0"
|
||||||
|
class="extra-block"
|
||||||
|
>
|
||||||
|
<summary class="extra-toggle">
|
||||||
|
额外信息
|
||||||
|
</summary>
|
||||||
<pre class="extra-json">{{ JSON.stringify(currentAttempt.extra_data, null, 2) }}</pre>
|
<pre class="extra-json">{{ JSON.stringify(currentAttempt.extra_data, null, 2) }}</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,9 +289,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<Card v-else class="border-dashed">
|
<Card
|
||||||
|
v-else
|
||||||
|
class="border-dashed"
|
||||||
|
>
|
||||||
<div class="p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<p class="text-sm text-muted-foreground">暂无追踪数据</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
暂无追踪数据
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="p-4">
|
<Card class="p-4">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-sm font-semibold">{{ title }}</p>
|
<p class="text-sm font-semibold">
|
||||||
<div v-if="displayLegendItems.length > 0" class="flex items-center gap-2 flex-wrap justify-end text-[11px]">
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="displayLegendItems.length > 0"
|
||||||
|
class="flex items-center gap-2 flex-wrap justify-end text-[11px]"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in displayLegendItems"
|
v-for="item in displayLegendItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -14,18 +19,35 @@
|
|||||||
/>
|
/>
|
||||||
<span class="text-muted-foreground">{{ item.name }}</span>
|
<span class="text-muted-foreground">{{ item.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="hiddenLegendCount > 0" class="text-muted-foreground">
|
<span
|
||||||
|
v-if="hiddenLegendCount > 0"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
+{{ hiddenLegendCount }} 更多
|
+{{ hiddenLegendCount }} 更多
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" class="h-[160px] flex items-center justify-center">
|
<div
|
||||||
<div class="text-sm text-muted-foreground">Loading...</div>
|
v-if="loading"
|
||||||
|
class="h-[160px] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasData" class="h-[160px]">
|
<div
|
||||||
<ScatterChart :data="chartData" :options="chartOptions" />
|
v-else-if="hasData"
|
||||||
|
class="h-[160px]"
|
||||||
|
>
|
||||||
|
<ScatterChart
|
||||||
|
:data="chartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="h-[160px] flex items-center justify-center text-sm text-muted-foreground">
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-[160px] flex items-center justify-center text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无请求间隔数据
|
暂无请求间隔数据
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -147,7 +169,7 @@ function formatModelName(model: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 其他模型保持原样但截断
|
// 其他模型保持原样但截断
|
||||||
return model.length > 20 ? model.slice(0, 17) + '...' : model
|
return model.length > 20 ? `${model.slice(0, 17) }...` : model
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建图表数据
|
// 构建图表数据
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
@click.self="handleClose"
|
@click.self="handleClose"
|
||||||
>
|
>
|
||||||
<!-- 背景遮罩 -->
|
<!-- 背景遮罩 -->
|
||||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleClose"></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
|
||||||
|
@click="handleClose"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 抽屉内容 -->
|
<!-- 抽屉内容 -->
|
||||||
<Card class="relative h-full w-[800px] max-w-[90vw] rounded-none shadow-2xl flex flex-col">
|
<Card class="relative h-full w-[800px] max-w-[90vw] rounded-none shadow-2xl flex flex-col">
|
||||||
@@ -17,19 +20,46 @@
|
|||||||
<!-- 第一行:标题、模型、状态、操作按钮 -->
|
<!-- 第一行:标题、模型、状态、操作按钮 -->
|
||||||
<div class="flex items-center justify-between gap-4 mb-3">
|
<div class="flex items-center justify-between gap-4 mb-3">
|
||||||
<div class="flex items-center gap-3 flex-wrap">
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
<h3 class="text-lg font-semibold">请求详情</h3>
|
<h3 class="text-lg font-semibold">
|
||||||
|
请求详情
|
||||||
|
</h3>
|
||||||
<div class="flex items-center gap-1 text-sm font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
<div class="flex items-center gap-1 text-sm font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
||||||
<span>{{ detail?.model || '-' }}</span>
|
<span>{{ detail?.model || '-' }}</span>
|
||||||
<template v-if="detail?.target_model">
|
<template v-if="detail?.target_model">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 flex-shrink-0">
|
<svg
|
||||||
<path fill-rule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clip-rule="evenodd" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-3 h-3 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ detail.target_model }}</span>
|
<span>{{ detail.target_model }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<Badge v-if="detail?.status_code === 200" variant="success">{{ detail.status_code }}</Badge>
|
<Badge
|
||||||
<Badge v-else-if="detail" variant="destructive">{{ detail.status_code }}</Badge>
|
v-if="detail?.status_code === 200"
|
||||||
<Badge variant="outline" class="text-xs" v-if="detail">{{ detail.is_stream ? '流式' : '标准' }}</Badge>
|
variant="success"
|
||||||
|
>
|
||||||
|
{{ detail.status_code }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="detail"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
{{ detail.status_code }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="detail"
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ detail.is_stream ? '流式' : '标准' }}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
@@ -37,18 +67,30 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="refreshDetail"
|
|
||||||
title="刷新"
|
title="刷新"
|
||||||
|
@click="refreshDetail"
|
||||||
>
|
>
|
||||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': loading }" />
|
<RefreshCw
|
||||||
|
class="w-4 h-4"
|
||||||
|
:class="{ 'animate-spin': loading }"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="handleClose" title="关闭">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
title="关闭"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
<X class="w-4 h-4" />
|
<X class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 第二行:关键元信息 -->
|
<!-- 第二行:关键元信息 -->
|
||||||
<div v-if="detail" class="flex items-center flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
<div
|
||||||
|
v-if="detail"
|
||||||
|
class="flex items-center flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<span class="font-medium text-foreground">ID:</span>
|
<span class="font-medium text-foreground">ID:</span>
|
||||||
<span class="font-mono">{{ detail.request_id || detail.id }}</span>
|
<span class="font-mono">{{ detail.request_id || detail.id }}</span>
|
||||||
@@ -67,21 +109,32 @@
|
|||||||
<!-- 可滚动内容区域 -->
|
<!-- 可滚动内容区域 -->
|
||||||
<div class="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-stable">
|
<div class="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-stable">
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="py-8 space-y-4">
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="py-8 space-y-4"
|
||||||
|
>
|
||||||
<Skeleton class="h-8 w-full" />
|
<Skeleton class="h-8 w-full" />
|
||||||
<Skeleton class="h-32 w-full" />
|
<Skeleton class="h-32 w-full" />
|
||||||
<Skeleton class="h-64 w-full" />
|
<Skeleton class="h-64 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<Card v-else-if="error" class="border-red-200 dark:border-red-800">
|
<Card
|
||||||
|
v-else-if="error"
|
||||||
|
class="border-red-200 dark:border-red-800"
|
||||||
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
<p class="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Detail Content -->
|
<!-- Detail Content -->
|
||||||
<div v-else-if="detail" class="space-y-4">
|
<div
|
||||||
|
v-else-if="detail"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
<!-- 费用与性能概览 -->
|
<!-- 费用与性能概览 -->
|
||||||
<Card>
|
<Card>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@@ -93,7 +146,10 @@
|
|||||||
${{ ((typeof detail.cost === 'object' ? detail.cost?.total : detail.cost) || detail.total_cost || 0).toFixed(6) }}
|
${{ ((typeof detail.cost === 'object' ? detail.cost?.total : detail.cost) || detail.total_cost || 0).toFixed(6) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" class="h-6 mx-6" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-6 mx-6"
|
||||||
|
/>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-xs text-muted-foreground w-[56px]">响应时间</span>
|
<span class="text-xs text-muted-foreground w-[56px]">响应时间</span>
|
||||||
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
|
<span class="text-lg font-bold">{{ detail.response_time_ms ? formatResponseTime(detail.response_time_ms).value : 'N/A' }}</span>
|
||||||
@@ -111,7 +167,11 @@
|
|||||||
<span class="text-foreground">|</span>
|
<span class="text-foreground">|</span>
|
||||||
<span>总输入上下文: <span class="font-mono font-medium text-foreground">{{ formatNumber(totalInputContext) }}</span></span>
|
<span>总输入上下文: <span class="font-mono font-medium text-foreground">{{ formatNumber(totalInputContext) }}</span></span>
|
||||||
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ formatNumber(detail.cache_creation_input_tokens || 0) }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
<span class="text-muted-foreground/60">(输入 {{ formatNumber(detail.tokens?.input || detail.input_tokens || 0) }} + 缓存创建 {{ formatNumber(detail.cache_creation_input_tokens || 0) }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
||||||
<Badge v-if="displayTiers.length > 1" variant="outline" class="text-[10px] px-1.5 py-0 h-4">
|
<Badge
|
||||||
|
v-if="displayTiers.length > 1"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] px-1.5 py-0 h-4"
|
||||||
|
>
|
||||||
命中第 {{ currentTierIndex + 1 }} 阶
|
命中第 {{ currentTierIndex + 1 }} 阶
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,13 +189,20 @@
|
|||||||
<!-- 阶梯标题行 -->
|
<!-- 阶梯标题行 -->
|
||||||
<div class="flex items-center justify-between text-xs">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium" :class="index === currentTierIndex ? 'text-primary' : 'text-muted-foreground'">
|
<span
|
||||||
|
class="font-medium"
|
||||||
|
:class="index === currentTierIndex ? 'text-primary' : 'text-muted-foreground'"
|
||||||
|
>
|
||||||
第 {{ index + 1 }} 阶
|
第 {{ index + 1 }} 阶
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground">
|
<span class="text-muted-foreground">
|
||||||
{{ getTierRangeText(tier, index, displayTiers) }}
|
{{ getTierRangeText(tier, index, displayTiers) }}
|
||||||
</span>
|
</span>
|
||||||
<Badge v-if="index === currentTierIndex" variant="default" class="text-[10px] px-1.5 py-0 h-4">
|
<Badge
|
||||||
|
v-if="index === currentTierIndex"
|
||||||
|
variant="default"
|
||||||
|
class="text-[10px] px-1.5 py-0 h-4"
|
||||||
|
>
|
||||||
当前
|
当前
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +228,10 @@
|
|||||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.input || detail.input_tokens || 0 }}</span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.input || detail.input_tokens || 0 }}</span>
|
||||||
<span class="text-xs font-mono">${{ (detail.cost?.input || detail.input_cost || 0).toFixed(6) }}</span>
|
<span class="text-xs font-mono">${{ (detail.cost?.input || detail.input_cost || 0).toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" class="h-4 mx-4" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-4 mx-4"
|
||||||
|
/>
|
||||||
<div class="flex items-center flex-1">
|
<div class="flex items-center flex-1">
|
||||||
<span class="text-xs text-muted-foreground w-[56px]">输出</span>
|
<span class="text-xs text-muted-foreground w-[56px]">输出</span>
|
||||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.output || detail.output_tokens || 0 }}</span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.output || detail.output_tokens || 0 }}</span>
|
||||||
@@ -175,7 +245,10 @@
|
|||||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_creation_input_tokens || 0 }}</span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_creation_input_tokens || 0 }}</span>
|
||||||
<span class="text-xs font-mono">${{ (detail.cache_creation_cost || 0).toFixed(6) }}</span>
|
<span class="text-xs font-mono">${{ (detail.cache_creation_cost || 0).toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" class="h-4 mx-4" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-4 mx-4"
|
||||||
|
/>
|
||||||
<div class="flex items-center flex-1">
|
<div class="flex items-center flex-1">
|
||||||
<span class="text-xs text-muted-foreground w-[56px]">缓存读取</span>
|
<span class="text-xs text-muted-foreground w-[56px]">缓存读取</span>
|
||||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_read_input_tokens || 0 }}</span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_read_input_tokens || 0 }}</span>
|
||||||
@@ -183,13 +256,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按次计费 -->
|
<!-- 按次计费 -->
|
||||||
<div v-if="detail.request_cost" class="flex items-center">
|
<div
|
||||||
|
v-if="detail.request_cost"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
<div class="flex items-center flex-1">
|
<div class="flex items-center flex-1">
|
||||||
<span class="text-xs text-muted-foreground w-[56px]">按次计费</span>
|
<span class="text-xs text-muted-foreground w-[56px]">按次计费</span>
|
||||||
<span class="text-sm font-semibold font-mono flex-1 text-center"></span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center" />
|
||||||
<span class="text-xs font-mono">${{ detail.request_cost.toFixed(6) }}</span>
|
<span class="text-xs font-mono">${{ detail.request_cost.toFixed(6) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" class="h-4 mx-4 invisible" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-4 mx-4 invisible"
|
||||||
|
/>
|
||||||
<div class="flex items-center flex-1 invisible">
|
<div class="flex items-center flex-1 invisible">
|
||||||
<span class="text-xs text-muted-foreground w-[56px]">占位</span>
|
<span class="text-xs text-muted-foreground w-[56px]">占位</span>
|
||||||
<span class="text-sm font-semibold font-mono flex-1 text-center">0</span>
|
<span class="text-sm font-semibold font-mono flex-1 text-center">0</span>
|
||||||
@@ -211,11 +290,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误信息卡片 -->
|
<!-- 错误信息卡片 -->
|
||||||
<Card v-if="detail.error_message" class="border-red-200 dark:border-red-800">
|
<Card
|
||||||
|
v-if="detail.error_message"
|
||||||
|
class="border-red-200 dark:border-red-800"
|
||||||
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">错误信息</h4>
|
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
|
||||||
|
错误信息
|
||||||
|
</h4>
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
<div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
||||||
<p class="text-sm text-red-800 dark:text-red-300">{{ detail.error_message }}</p>
|
<p class="text-sm text-red-800 dark:text-red-300">
|
||||||
|
{{ detail.error_message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -223,7 +309,10 @@
|
|||||||
<!-- Tabs 区域 -->
|
<!-- Tabs 区域 -->
|
||||||
<Card>
|
<Card>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<Tabs v-model="activeTab" :default-value="activeTab">
|
<Tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
:default-value="activeTab"
|
||||||
|
>
|
||||||
<!-- Tab + 图标工具栏同行 -->
|
<!-- Tab + 图标工具栏同行 -->
|
||||||
<div class="flex items-center justify-between border-b pb-2 mb-3">
|
<div class="flex items-center justify-between border-b pb-2 mb-3">
|
||||||
<!-- 左侧 Tab -->
|
<!-- 左侧 Tab -->
|
||||||
@@ -231,11 +320,11 @@
|
|||||||
<button
|
<button
|
||||||
v-for="tab in visibleTabs"
|
v-for="tab in visibleTabs"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
@click="activeTab = tab.name"
|
|
||||||
class="px-3 py-1.5 text-sm transition-colors border-b-2 -mb-[9px]"
|
class="px-3 py-1.5 text-sm transition-colors border-b-2 -mb-[9px]"
|
||||||
:class="activeTab === tab.name
|
:class="activeTab === tab.name
|
||||||
? 'border-primary text-foreground font-medium'
|
? 'border-primary text-foreground font-medium'
|
||||||
: 'border-transparent text-muted-foreground hover:text-foreground'"
|
: 'border-transparent text-muted-foreground hover:text-foreground'"
|
||||||
|
@click="activeTab = tab.name"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</button>
|
</button>
|
||||||
@@ -245,56 +334,71 @@
|
|||||||
<!-- 请求头专用:对比/客户端/提供商 切换组 -->
|
<!-- 请求头专用:对比/客户端/提供商 切换组 -->
|
||||||
<template v-if="activeTab === 'request-headers' && hasProviderHeaders">
|
<template v-if="activeTab === 'request-headers' && hasProviderHeaders">
|
||||||
<button
|
<button
|
||||||
:title="'对比'"
|
title="对比"
|
||||||
@click="viewMode = 'compare'"
|
|
||||||
class="p-1.5 rounded transition-colors"
|
class="p-1.5 rounded transition-colors"
|
||||||
:class="viewMode === 'compare' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
:class="viewMode === 'compare' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||||
|
@click="viewMode = 'compare'"
|
||||||
>
|
>
|
||||||
<Columns2 class="w-4 h-4" />
|
<Columns2 class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:title="'客户端'"
|
title="客户端"
|
||||||
@click="viewMode = 'formatted'; dataSource = 'client'"
|
|
||||||
class="p-1.5 rounded transition-colors"
|
class="p-1.5 rounded transition-colors"
|
||||||
:class="viewMode === 'formatted' && dataSource === 'client' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
:class="viewMode === 'formatted' && dataSource === 'client' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||||
|
@click="viewMode = 'formatted'; dataSource = 'client'"
|
||||||
>
|
>
|
||||||
<Monitor class="w-4 h-4" />
|
<Monitor class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:title="'提供商'"
|
title="提供商"
|
||||||
@click="viewMode = 'formatted'; dataSource = 'provider'"
|
|
||||||
class="p-1.5 rounded transition-colors"
|
class="p-1.5 rounded transition-colors"
|
||||||
:class="viewMode === 'formatted' && dataSource === 'provider' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
:class="viewMode === 'formatted' && dataSource === 'provider' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'"
|
||||||
|
@click="viewMode = 'formatted'; dataSource = 'provider'"
|
||||||
>
|
>
|
||||||
<Server class="w-4 h-4" />
|
<Server class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<Separator orientation="vertical" class="h-4 mx-1" />
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-4 mx-1"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- 展开/收缩 -->
|
<!-- 展开/收缩 -->
|
||||||
<button
|
<button
|
||||||
:title="currentExpandDepth === 0 ? '展开全部' : '收缩全部'"
|
:title="currentExpandDepth === 0 ? '展开全部' : '收缩全部'"
|
||||||
@click="currentExpandDepth === 0 ? expandAll() : collapseAll()"
|
|
||||||
class="p-1.5 rounded transition-colors"
|
class="p-1.5 rounded transition-colors"
|
||||||
:class="viewMode === 'compare'
|
:class="viewMode === 'compare'
|
||||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
? 'text-muted-foreground/40 cursor-not-allowed'
|
||||||
: 'text-muted-foreground hover:bg-muted'"
|
: 'text-muted-foreground hover:bg-muted'"
|
||||||
:disabled="viewMode === 'compare'"
|
:disabled="viewMode === 'compare'"
|
||||||
|
@click="currentExpandDepth === 0 ? expandAll() : collapseAll()"
|
||||||
>
|
>
|
||||||
<Maximize2 v-if="currentExpandDepth === 0" class="w-4 h-4" />
|
<Maximize2
|
||||||
<Minimize2 v-else class="w-4 h-4" />
|
v-if="currentExpandDepth === 0"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<Minimize2
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<!-- 复制 -->
|
<!-- 复制 -->
|
||||||
<button
|
<button
|
||||||
:title="copiedStates[activeTab] ? '已复制' : '复制'"
|
:title="copiedStates[activeTab] ? '已复制' : '复制'"
|
||||||
@click="copyJsonToClipboard(activeTab)"
|
|
||||||
class="p-1.5 rounded transition-colors"
|
class="p-1.5 rounded transition-colors"
|
||||||
:class="viewMode === 'compare'
|
:class="viewMode === 'compare'
|
||||||
? 'text-muted-foreground/40 cursor-not-allowed'
|
? 'text-muted-foreground/40 cursor-not-allowed'
|
||||||
: 'text-muted-foreground hover:bg-muted'"
|
: 'text-muted-foreground hover:bg-muted'"
|
||||||
:disabled="viewMode === 'compare'"
|
:disabled="viewMode === 'compare'"
|
||||||
|
@click="copyJsonToClipboard(activeTab)"
|
||||||
>
|
>
|
||||||
<Check v-if="copiedStates[activeTab]" class="w-4 h-4 text-green-500" />
|
<Check
|
||||||
<Copy v-else class="w-4 h-4" />
|
v-if="copiedStates[activeTab]"
|
||||||
|
class="w-4 h-4 text-green-500"
|
||||||
|
/>
|
||||||
|
<Copy
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,9 +717,9 @@ function formatApiFormat(format: string | null | undefined): string {
|
|||||||
|
|
||||||
function formatNumber(num: number): string {
|
function formatNumber(num: number): string {
|
||||||
if (num >= 1_000_000) {
|
if (num >= 1_000_000) {
|
||||||
return (num / 1_000_000).toFixed(1) + 'M'
|
return `${(num / 1_000_000).toFixed(1) }M`
|
||||||
} else if (num >= 1_000) {
|
} else if (num >= 1_000) {
|
||||||
return (num / 1_000).toFixed(1) + 'K'
|
return `${(num / 1_000).toFixed(1) }K`
|
||||||
}
|
}
|
||||||
return num.toLocaleString()
|
return num.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!data || (typeof data === 'object' && Object.keys(data).length === 0)" class="text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="!data || (typeof data === 'object' && Object.keys(data).length === 0)"
|
||||||
|
class="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
{{ emptyMessage }}
|
{{ emptyMessage }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 纯字符串数据(非 JSON 对象) -->
|
<!-- 纯字符串数据(非 JSON 对象) -->
|
||||||
<Card v-else-if="typeof data === 'string'" class="bg-muted/30 overflow-hidden">
|
<Card
|
||||||
|
v-else-if="typeof data === 'string'"
|
||||||
|
class="bg-muted/30 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ data }}</pre>
|
<pre class="text-xs font-mono whitespace-pre-wrap">{{ data }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
||||||
<Card v-else-if="data.raw_response && data.metadata?.parse_error" class="bg-muted/30 overflow-hidden">
|
<Card
|
||||||
|
v-else-if="data.raw_response && data.metadata?.parse_error"
|
||||||
|
class="bg-muted/30 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
||||||
@@ -21,12 +30,24 @@
|
|||||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
<Card
|
||||||
|
v-else
|
||||||
|
class="bg-muted/30 overflow-hidden"
|
||||||
|
>
|
||||||
<!-- JSON 查看器 -->
|
<!-- JSON 查看器 -->
|
||||||
<div class="json-viewer" :class="{ 'theme-dark': isDark }">
|
<div
|
||||||
|
class="json-viewer"
|
||||||
|
:class="{ 'theme-dark': isDark }"
|
||||||
|
>
|
||||||
<div class="json-lines">
|
<div class="json-lines">
|
||||||
<template v-for="line in visibleLines" :key="line.displayId">
|
<template
|
||||||
<div class="json-line" :class="{ 'has-fold': line.canFold }">
|
v-for="line in visibleLines"
|
||||||
|
:key="line.displayId"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="json-line"
|
||||||
|
:class="{ 'has-fold': line.canFold }"
|
||||||
|
>
|
||||||
<!-- 行号区域(包含折叠按钮) -->
|
<!-- 行号区域(包含折叠按钮) -->
|
||||||
<div class="line-number-area">
|
<div class="line-number-area">
|
||||||
<span
|
<span
|
||||||
@@ -34,22 +55,31 @@
|
|||||||
class="fold-button"
|
class="fold-button"
|
||||||
@click="toggleFold(line.blockId)"
|
@click="toggleFold(line.blockId)"
|
||||||
>
|
>
|
||||||
<ChevronRight v-if="collapsedBlocks.has(line.blockId)" class="fold-icon" />
|
<ChevronRight
|
||||||
<ChevronDown v-else class="fold-icon" />
|
v-if="collapsedBlocks.has(line.blockId)"
|
||||||
|
class="fold-icon"
|
||||||
|
/>
|
||||||
|
<ChevronDown
|
||||||
|
v-else
|
||||||
|
class="fold-icon"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="line-number">{{ line.displayLineNumber }}</span>
|
<span class="line-number">{{ line.displayLineNumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="line-content-area">
|
<div class="line-content-area">
|
||||||
<!-- 缩进 -->
|
<!-- 缩进 -->
|
||||||
<span class="indent" :style="{ width: `${line.indent * 16}px` }"></span>
|
<span
|
||||||
|
class="indent"
|
||||||
|
:style="{ width: `${line.indent * 16}px` }"
|
||||||
|
/>
|
||||||
<!-- 内容 -->
|
<!-- 内容 -->
|
||||||
<span
|
<span
|
||||||
class="line-content"
|
class="line-content"
|
||||||
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
||||||
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
||||||
v-html="getDisplayHtml(line)"
|
v-html="getDisplayHtml(line)"
|
||||||
></span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
<div>
|
<div>
|
||||||
<!-- 对比模式 - 并排 Diff -->
|
<!-- 对比模式 - 并排 Diff -->
|
||||||
<div v-show="viewMode === 'compare'">
|
<div v-show="viewMode === 'compare'">
|
||||||
<div v-if="!detail.request_headers && !detail.provider_request_headers" class="text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="!detail.request_headers && !detail.provider_request_headers"
|
||||||
|
class="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
无请求头信息
|
无请求头信息
|
||||||
</div>
|
</div>
|
||||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
<Card
|
||||||
|
v-else
|
||||||
|
class="bg-muted/30 overflow-hidden"
|
||||||
|
>
|
||||||
<!-- Diff 头部 -->
|
<!-- Diff 头部 -->
|
||||||
<div class="flex border-b bg-muted/50">
|
<div class="flex border-b bg-muted/50">
|
||||||
<div class="flex-1 px-3 py-2 text-xs text-muted-foreground border-r flex items-center justify-between">
|
<div class="flex-1 px-3 py-2 text-xs text-muted-foreground border-r flex items-center justify-between">
|
||||||
@@ -23,25 +29,40 @@
|
|||||||
<div class="flex font-mono text-xs">
|
<div class="flex font-mono text-xs">
|
||||||
<!-- 左侧:客户端 -->
|
<!-- 左侧:客户端 -->
|
||||||
<div class="flex-1 border-r">
|
<div class="flex-1 border-r">
|
||||||
<template v-for="entry in sortedEntries" :key="'left-' + entry.key">
|
<template
|
||||||
|
v-for="entry in sortedEntries"
|
||||||
|
:key="'left-' + entry.key"
|
||||||
|
>
|
||||||
<!-- 删除的行 -->
|
<!-- 删除的行 -->
|
||||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-destructive/10 px-3 py-0.5">
|
<div
|
||||||
|
v-if="entry.status === 'removed'"
|
||||||
|
class="flex items-start bg-destructive/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-destructive">
|
<span class="text-destructive">
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 修改的行 - 旧值 -->
|
<!-- 修改的行 - 旧值 -->
|
||||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
<div
|
||||||
|
v-else-if="entry.status === 'modified'"
|
||||||
|
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-amber-600 dark:text-amber-400">
|
<span class="text-amber-600 dark:text-amber-400">
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 新增的行 - 左侧空白占位 -->
|
<!-- 新增的行 - 左侧空白占位 -->
|
||||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
<div
|
||||||
|
v-else-if="entry.status === 'added'"
|
||||||
|
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground/30 italic">(无)</span>
|
<span class="text-muted-foreground/30 italic">(无)</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 未变化的行 -->
|
<!-- 未变化的行 -->
|
||||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground">
|
<span class="text-muted-foreground">
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
</span>
|
</span>
|
||||||
@@ -50,27 +71,42 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 右侧:提供商 -->
|
<!-- 右侧:提供商 -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<template v-for="entry in sortedEntries" :key="'right-' + entry.key">
|
<template
|
||||||
|
v-for="entry in sortedEntries"
|
||||||
|
:key="'right-' + entry.key"
|
||||||
|
>
|
||||||
<!-- 删除的行 - 右侧空白占位 -->
|
<!-- 删除的行 - 右侧空白占位 -->
|
||||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
<div
|
||||||
|
v-if="entry.status === 'removed'"
|
||||||
|
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground/50 line-through">
|
<span class="text-muted-foreground/50 line-through">
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 修改的行 - 新值 -->
|
<!-- 修改的行 - 新值 -->
|
||||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
<div
|
||||||
|
v-else-if="entry.status === 'modified'"
|
||||||
|
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-amber-600 dark:text-amber-400">
|
<span class="text-amber-600 dark:text-amber-400">
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 新增的行 -->
|
<!-- 新增的行 -->
|
||||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-green-500/10 px-3 py-0.5">
|
<div
|
||||||
|
v-else-if="entry.status === 'added'"
|
||||||
|
class="flex items-start bg-green-500/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
<span class="text-green-600 dark:text-green-400">
|
<span class="text-green-600 dark:text-green-400">
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 未变化的行 -->
|
<!-- 未变化的行 -->
|
||||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground">
|
<span class="text-muted-foreground">
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
</span>
|
</span>
|
||||||
@@ -95,10 +131,16 @@
|
|||||||
|
|
||||||
<!-- 原始模式 -->
|
<!-- 原始模式 -->
|
||||||
<div v-show="viewMode === 'raw'">
|
<div v-show="viewMode === 'raw'">
|
||||||
<div v-if="!currentHeaderData || Object.keys(currentHeaderData).length === 0" class="text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="!currentHeaderData || Object.keys(currentHeaderData).length === 0"
|
||||||
|
class="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
无请求头信息
|
无请求头信息
|
||||||
</div>
|
</div>
|
||||||
<Card v-else class="bg-muted/30">
|
<Card
|
||||||
|
v-else
|
||||||
|
class="bg-muted/30"
|
||||||
|
>
|
||||||
<div class="p-4 overflow-x-auto">
|
<div class="p-4 overflow-x-auto">
|
||||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ JSON.stringify(currentHeaderData, null, 2) }}</pre>
|
<pre class="text-xs font-mono whitespace-pre-wrap">{{ JSON.stringify(currentHeaderData, null, 2) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,41 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<div class="px-3 py-2 border-b">
|
<div class="px-3 py-2 border-b">
|
||||||
<h3 class="text-sm font-medium">按API格式分析</h3>
|
<h3 class="text-sm font-medium">
|
||||||
|
按API格式分析
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Table class="text-sm">
|
<Table class="text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="h-8 px-2">API格式</TableHead>
|
<TableHead class="h-8 px-2">
|
||||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
API格式
|
||||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
<TableHead class="h-8 px-2 text-right">
|
||||||
<TableHead class="h-8 px-2 text-right">平均响应</TableHead>
|
请求数
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
Tokens
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
费用
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
平均响应
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="data.length === 0">
|
<TableRow v-if="data.length === 0">
|
||||||
<TableCell :colspan="5" class="text-center py-6 text-muted-foreground px-2">
|
<TableCell
|
||||||
|
:colspan="5"
|
||||||
|
class="text-center py-6 text-muted-foreground px-2"
|
||||||
|
>
|
||||||
暂无API格式统计数据
|
暂无API格式统计数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-for="item in data" :key="item.api_format">
|
<TableRow
|
||||||
|
v-for="item in data"
|
||||||
|
:key="item.api_format"
|
||||||
|
>
|
||||||
<TableCell class="font-medium py-2 px-2">
|
<TableCell class="font-medium py-2 px-2">
|
||||||
{{ formatApiFormat(item.api_format) }}
|
{{ formatApiFormat(item.api_format) }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">{{ item.request_count }}</TableCell>
|
<TableCell class="text-right py-2 px-2">
|
||||||
|
{{ item.request_count }}
|
||||||
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<span>{{ formatTokens(item.total_tokens) }}</span>
|
<span>{{ formatTokens(item.total_tokens) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span class="text-primary font-medium">{{ formatCurrency(item.total_cost) }}</span>
|
<span class="text-primary font-medium">{{ formatCurrency(item.total_cost) }}</span>
|
||||||
<span v-if="isAdmin && item.actual_cost !== undefined" class="text-muted-foreground text-[10px]">
|
<span
|
||||||
|
v-if="isAdmin && item.actual_cost !== undefined"
|
||||||
|
class="text-muted-foreground text-[10px]"
|
||||||
|
>
|
||||||
{{ formatCurrency(item.actual_cost) }}
|
{{ formatCurrency(item.actual_cost) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ item.avgResponseTime }}</TableCell>
|
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||||
|
{{ item.avgResponseTime }}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -54,6 +79,11 @@ import TableCell from '@/components/ui/table-cell.vue'
|
|||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import type { ApiFormatStatsItem } from '../types'
|
import type { ApiFormatStatsItem } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: ApiFormatStatsItem[]
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
// 格式化 API 格式显示名称
|
// 格式化 API 格式显示名称
|
||||||
function formatApiFormat(format: string): string {
|
function formatApiFormat(format: string): string {
|
||||||
const formatMap: Record<string, string> = {
|
const formatMap: Record<string, string> = {
|
||||||
@@ -67,8 +97,4 @@ function formatApiFormat(format: string): string {
|
|||||||
return formatMap[format.toUpperCase()] || format
|
return formatMap[format.toUpperCase()] || format
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
data: ApiFormatStatsItem[]
|
|
||||||
isAdmin: boolean
|
|
||||||
}>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,41 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<div class="px-3 py-2 border-b">
|
<div class="px-3 py-2 border-b">
|
||||||
<h3 class="text-sm font-medium">按模型分析</h3>
|
<h3 class="text-sm font-medium">
|
||||||
|
按模型分析
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Table class="text-sm">
|
<Table class="text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="h-8 px-2">模型</TableHead>
|
<TableHead class="h-8 px-2">
|
||||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
模型
|
||||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
<TableHead class="h-8 px-2 text-right">
|
||||||
<TableHead class="h-8 px-2 text-right">效率</TableHead>
|
请求数
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
Tokens
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
费用
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
效率
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="data.length === 0">
|
<TableRow v-if="data.length === 0">
|
||||||
<TableCell :colspan="5" class="text-center py-6 text-muted-foreground px-2">
|
<TableCell
|
||||||
|
:colspan="5"
|
||||||
|
class="text-center py-6 text-muted-foreground px-2"
|
||||||
|
>
|
||||||
暂无模型统计数据
|
暂无模型统计数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-for="model in data" :key="model.model">
|
<TableRow
|
||||||
|
v-for="model in data"
|
||||||
|
:key="model.model"
|
||||||
|
>
|
||||||
<TableCell class="font-medium py-2 px-2">
|
<TableCell class="font-medium py-2 px-2">
|
||||||
{{ model.model.replace('claude-', '') }}
|
{{ model.model.replace('claude-', '') }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">{{ model.request_count }}</TableCell>
|
<TableCell class="text-right py-2 px-2">
|
||||||
|
{{ model.request_count }}
|
||||||
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<span>{{ formatTokens(model.total_tokens) }}</span>
|
<span>{{ formatTokens(model.total_tokens) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span class="text-primary font-medium">{{ formatCurrency(model.total_cost) }}</span>
|
<span class="text-primary font-medium">{{ formatCurrency(model.total_cost) }}</span>
|
||||||
<span v-if="isAdmin && model.actual_cost !== undefined" class="text-muted-foreground text-[10px]">
|
<span
|
||||||
|
v-if="isAdmin && model.actual_cost !== undefined"
|
||||||
|
class="text-muted-foreground text-[10px]"
|
||||||
|
>
|
||||||
{{ formatCurrency(model.actual_cost) }}
|
{{ formatCurrency(model.actual_cost) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ model.costPerToken }}</TableCell>
|
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||||
|
{{ model.costPerToken }}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,35 +1,62 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<div class="px-3 py-2 border-b">
|
<div class="px-3 py-2 border-b">
|
||||||
<h3 class="text-sm font-medium">按提供商分析</h3>
|
<h3 class="text-sm font-medium">
|
||||||
|
按提供商分析
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Table class="text-sm">
|
<Table class="text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="h-8 px-2">提供商</TableHead>
|
<TableHead class="h-8 px-2">
|
||||||
<TableHead class="h-8 px-2 text-right">请求数</TableHead>
|
提供商
|
||||||
<TableHead class="h-8 px-2 text-right">Tokens</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-8 px-2 text-right">费用</TableHead>
|
<TableHead class="h-8 px-2 text-right">
|
||||||
<TableHead class="h-8 px-2 text-right">成功率</TableHead>
|
请求数
|
||||||
<TableHead class="h-8 px-2 text-right">平均响应</TableHead>
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
Tokens
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
费用
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
成功率
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-8 px-2 text-right">
|
||||||
|
平均响应
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="data.length === 0">
|
<TableRow v-if="data.length === 0">
|
||||||
<TableCell :colspan="6" class="text-center py-6 text-muted-foreground px-2">
|
<TableCell
|
||||||
|
:colspan="6"
|
||||||
|
class="text-center py-6 text-muted-foreground px-2"
|
||||||
|
>
|
||||||
暂无提供商统计数据
|
暂无提供商统计数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-for="provider in data" :key="provider.provider">
|
<TableRow
|
||||||
<TableCell class="font-medium py-2 px-2">{{ provider.provider }}</TableCell>
|
v-for="provider in data"
|
||||||
<TableCell class="text-right py-2 px-2">{{ provider.requests }}</TableCell>
|
:key="provider.provider"
|
||||||
|
>
|
||||||
|
<TableCell class="font-medium py-2 px-2">
|
||||||
|
{{ provider.provider }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-right py-2 px-2">
|
||||||
|
{{ provider.requests }}
|
||||||
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<span>{{ formatTokens(provider.totalTokens) }}</span>
|
<span>{{ formatTokens(provider.totalTokens) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span class="text-primary font-medium">{{ formatCurrency(provider.totalCost) }}</span>
|
<span class="text-primary font-medium">{{ formatCurrency(provider.totalCost) }}</span>
|
||||||
<span v-if="isAdmin && provider.actualCost !== undefined" class="text-muted-foreground text-[10px]">
|
<span
|
||||||
|
v-if="isAdmin && provider.actualCost !== undefined"
|
||||||
|
class="text-muted-foreground text-[10px]"
|
||||||
|
>
|
||||||
{{ formatCurrency(provider.actualCost) }}
|
{{ formatCurrency(provider.actualCost) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +64,9 @@
|
|||||||
<TableCell class="text-right py-2 px-2">
|
<TableCell class="text-right py-2 px-2">
|
||||||
<span :class="getSuccessRateClass(provider.successRate)">{{ provider.successRate }}%</span>
|
<span :class="getSuccessRateClass(provider.successRate)">{{ provider.successRate }}%</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right text-muted-foreground py-2 px-2">{{ provider.avgResponseTime }}</TableCell>
|
<TableCell class="text-right text-muted-foreground py-2 px-2">
|
||||||
|
{{ provider.avgResponseTime }}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -56,14 +85,15 @@ import TableCell from '@/components/ui/table-cell.vue'
|
|||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import type { ProviderStatsItem } from '../types'
|
import type { ProviderStatsItem } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: ProviderStatsItem[]
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
// 成功率样式 - 简化为两种状态
|
// 成功率样式 - 简化为两种状态
|
||||||
function getSuccessRateClass(rate: number): string {
|
function getSuccessRateClass(rate: number): string {
|
||||||
if (rate < 90) return 'text-destructive'
|
if (rate < 90) return 'text-destructive'
|
||||||
return '' // 默认颜色
|
return '' // 默认颜色
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
data: ProviderStatsItem[]
|
|
||||||
isAdmin: boolean
|
|
||||||
}>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,19 +3,29 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<!-- 时间段筛选 -->
|
<!-- 时间段筛选 -->
|
||||||
<Select
|
<Select
|
||||||
:model-value="selectedPeriod"
|
|
||||||
v-model:open="periodSelectOpen"
|
v-model:open="periodSelectOpen"
|
||||||
|
:model-value="selectedPeriod"
|
||||||
@update:model-value="$emit('update:selectedPeriod', $event)"
|
@update:model-value="$emit('update:selectedPeriod', $event)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="选择时间段" />
|
<SelectValue placeholder="选择时间段" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="today">今天</SelectItem>
|
<SelectItem value="today">
|
||||||
<SelectItem value="yesterday">昨天</SelectItem>
|
今天
|
||||||
<SelectItem value="last7days">最近7天</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="last30days">最近30天</SelectItem>
|
<SelectItem value="yesterday">
|
||||||
<SelectItem value="last90days">最近90天</SelectItem>
|
昨天
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="last7days">
|
||||||
|
最近7天
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="last30days">
|
||||||
|
最近30天
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="last90days">
|
||||||
|
最近90天
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -25,16 +35,22 @@
|
|||||||
<!-- 用户筛选(仅管理员可见) -->
|
<!-- 用户筛选(仅管理员可见) -->
|
||||||
<Select
|
<Select
|
||||||
v-if="isAdmin && availableUsers.length > 0"
|
v-if="isAdmin && availableUsers.length > 0"
|
||||||
:model-value="filterUser"
|
|
||||||
v-model:open="filterUserSelectOpen"
|
v-model:open="filterUserSelectOpen"
|
||||||
|
:model-value="filterUser"
|
||||||
@update:model-value="$emit('update:filterUser', $event)"
|
@update:model-value="$emit('update:filterUser', $event)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-36 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-36 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部用户" />
|
<SelectValue placeholder="全部用户" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__all__">全部用户</SelectItem>
|
<SelectItem value="__all__">
|
||||||
<SelectItem v-for="user in availableUsers" :key="user.id" :value="user.id">
|
全部用户
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="user in availableUsers"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
{{ user.username || user.email }}
|
{{ user.username || user.email }}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -42,16 +58,22 @@
|
|||||||
|
|
||||||
<!-- 模型筛选 -->
|
<!-- 模型筛选 -->
|
||||||
<Select
|
<Select
|
||||||
:model-value="filterModel"
|
|
||||||
v-model:open="filterModelSelectOpen"
|
v-model:open="filterModelSelectOpen"
|
||||||
|
:model-value="filterModel"
|
||||||
@update:model-value="$emit('update:filterModel', $event)"
|
@update:model-value="$emit('update:filterModel', $event)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部模型" />
|
<SelectValue placeholder="全部模型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__all__">全部模型</SelectItem>
|
<SelectItem value="__all__">
|
||||||
<SelectItem v-for="model in availableModels" :key="model" :value="model">
|
全部模型
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="model in availableModels"
|
||||||
|
:key="model"
|
||||||
|
:value="model"
|
||||||
|
>
|
||||||
{{ model.replace('claude-', '') }}
|
{{ model.replace('claude-', '') }}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -59,16 +81,22 @@
|
|||||||
|
|
||||||
<!-- 提供商筛选 -->
|
<!-- 提供商筛选 -->
|
||||||
<Select
|
<Select
|
||||||
:model-value="filterProvider"
|
|
||||||
v-model:open="filterProviderSelectOpen"
|
v-model:open="filterProviderSelectOpen"
|
||||||
|
:model-value="filterProvider"
|
||||||
@update:model-value="$emit('update:filterProvider', $event)"
|
@update:model-value="$emit('update:filterProvider', $event)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部提供商" />
|
<SelectValue placeholder="全部提供商" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__all__">全部提供商</SelectItem>
|
<SelectItem value="__all__">
|
||||||
<SelectItem v-for="provider in availableProviders" :key="provider" :value="provider">
|
全部提供商
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="provider in availableProviders"
|
||||||
|
:key="provider"
|
||||||
|
:value="provider"
|
||||||
|
>
|
||||||
{{ provider }}
|
{{ provider }}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -76,20 +104,32 @@
|
|||||||
|
|
||||||
<!-- 状态筛选 -->
|
<!-- 状态筛选 -->
|
||||||
<Select
|
<Select
|
||||||
:model-value="filterStatus"
|
|
||||||
v-model:open="filterStatusSelectOpen"
|
v-model:open="filterStatusSelectOpen"
|
||||||
|
:model-value="filterStatus"
|
||||||
@update:model-value="$emit('update:filterStatus', $event)"
|
@update:model-value="$emit('update:filterStatus', $event)"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部状态" />
|
<SelectValue placeholder="全部状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__all__">全部状态</SelectItem>
|
<SelectItem value="__all__">
|
||||||
<SelectItem value="active">进行中</SelectItem>
|
全部状态
|
||||||
<SelectItem value="pending">等待中</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="streaming">流式传输</SelectItem>
|
<SelectItem value="active">
|
||||||
<SelectItem value="completed">已完成</SelectItem>
|
进行中
|
||||||
<SelectItem value="failed">已失败</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value="pending">
|
||||||
|
等待中
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="streaming">
|
||||||
|
流式传输
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="completed">
|
||||||
|
已完成
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="failed">
|
||||||
|
已失败
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -97,58 +137,113 @@
|
|||||||
<div class="h-4 w-px bg-border" />
|
<div class="h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 刷新按钮 -->
|
||||||
<RefreshButton :loading="loading" @click="$emit('refresh')" />
|
<RefreshButton
|
||||||
|
:loading="loading"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="h-12 font-semibold w-[70px]">时间</TableHead>
|
<TableHead class="h-12 font-semibold w-[70px]">
|
||||||
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">用户</TableHead>
|
时间
|
||||||
<TableHead class="h-12 font-semibold w-[140px]">模型</TableHead>
|
</TableHead>
|
||||||
<TableHead v-if="isAdmin" class="h-12 font-semibold w-[100px]">提供商</TableHead>
|
<TableHead
|
||||||
<TableHead class="h-12 font-semibold w-[80px]">API格式</TableHead>
|
v-if="isAdmin"
|
||||||
<TableHead class="h-12 font-semibold w-[50px] text-center">类型</TableHead>
|
class="h-12 font-semibold w-[100px]"
|
||||||
<TableHead class="h-12 font-semibold w-[140px] text-right">Tokens</TableHead>
|
>
|
||||||
<TableHead class="h-12 font-semibold w-[100px] text-right">费用</TableHead>
|
用户
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-12 font-semibold w-[140px]">
|
||||||
|
模型
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
v-if="isAdmin"
|
||||||
|
class="h-12 font-semibold w-[100px]"
|
||||||
|
>
|
||||||
|
提供商
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-12 font-semibold w-[80px]">
|
||||||
|
API格式
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-12 font-semibold w-[50px] text-center">
|
||||||
|
类型
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-12 font-semibold w-[140px] text-right">
|
||||||
|
Tokens
|
||||||
|
</TableHead>
|
||||||
|
<TableHead class="h-12 font-semibold w-[100px] text-right">
|
||||||
|
费用
|
||||||
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[70px] text-right">
|
<TableHead class="h-12 font-semibold w-[70px] text-right">
|
||||||
<div class="inline-block max-w-[2rem] leading-tight">响应时间</div>
|
<div class="inline-block max-w-[2rem] leading-tight">
|
||||||
|
响应时间
|
||||||
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="records.length === 0">
|
<TableRow v-if="records.length === 0">
|
||||||
<TableCell :colspan="isAdmin ? 9 : 7" class="text-center py-12 text-muted-foreground">
|
<TableCell
|
||||||
|
:colspan="isAdmin ? 9 : 7"
|
||||||
|
class="text-center py-12 text-muted-foreground"
|
||||||
|
>
|
||||||
暂无请求记录
|
暂无请求记录
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
v-else
|
|
||||||
v-for="record in records"
|
v-for="record in records"
|
||||||
|
v-else
|
||||||
:key="record.id"
|
:key="record.id"
|
||||||
|
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@click="handleRowClick($event, record.id)"
|
@click="handleRowClick($event, record.id)"
|
||||||
:class="isAdmin ? 'cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]' : 'border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]'"
|
|
||||||
>
|
>
|
||||||
<TableCell class="text-xs py-4 w-[70px]">
|
<TableCell class="text-xs py-4 w-[70px]">
|
||||||
{{ formatDateTime(record.created_at) }}
|
{{ formatDateTime(record.created_at) }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell v-if="isAdmin" class="py-4 w-[100px] truncate" :title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')">
|
<TableCell
|
||||||
|
v-if="isAdmin"
|
||||||
|
class="py-4 w-[100px] truncate"
|
||||||
|
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
|
||||||
|
>
|
||||||
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
|
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="font-medium py-4 w-[140px]" :title="getModelTooltip(record)">
|
<TableCell
|
||||||
<div v-if="getActualModel(record)" class="flex flex-col text-xs gap-0.5">
|
class="font-medium py-4 w-[140px]"
|
||||||
|
:title="getModelTooltip(record)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="getActualModel(record)"
|
||||||
|
class="flex flex-col text-xs gap-0.5"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-1 truncate">
|
<div class="flex items-center gap-1 truncate">
|
||||||
<span class="truncate">{{ record.model }}</span>
|
<span class="truncate">{{ record.model }}</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 text-muted-foreground flex-shrink-0">
|
<svg
|
||||||
<path fill-rule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clip-rule="evenodd" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-3 h-3 text-muted-foreground flex-shrink-0"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-muted-foreground truncate">{{ getActualModel(record) }}</span>
|
<span class="text-muted-foreground truncate">{{ getActualModel(record) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="truncate block">{{ record.model }}</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="truncate block"
|
||||||
|
>{{ record.model }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell v-if="isAdmin" class="py-4 w-[60px]">
|
<TableCell
|
||||||
|
v-if="isAdmin"
|
||||||
|
class="py-4 w-[60px]"
|
||||||
|
>
|
||||||
<div class="flex flex-col text-xs gap-0.5">
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span>{{ record.provider }}</span>
|
<span>{{ record.provider }}</span>
|
||||||
@@ -157,14 +252,30 @@
|
|||||||
class="inline-flex items-center justify-center w-4 h-4 text-xs text-amber-600 dark:text-amber-400"
|
class="inline-flex items-center justify-center w-4 h-4 text-xs text-amber-600 dark:text-amber-400"
|
||||||
title="此请求发生了 Provider 切换"
|
title="此请求发生了 Provider 切换"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
<svg
|
||||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="record.api_key_name" class="text-muted-foreground truncate" :title="record.api_key_name">
|
<span
|
||||||
|
v-if="record.api_key_name"
|
||||||
|
class="text-muted-foreground truncate"
|
||||||
|
:title="record.api_key_name"
|
||||||
|
>
|
||||||
{{ record.api_key_name }}
|
{{ record.api_key_name }}
|
||||||
<span v-if="record.rate_multiplier && record.rate_multiplier !== 1.0" class="text-foreground/60">({{ record.rate_multiplier }}x)</span>
|
<span
|
||||||
|
v-if="record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||||
|
class="text-foreground/60"
|
||||||
|
>({{ record.rate_multiplier }}x)</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -176,23 +287,46 @@
|
|||||||
>
|
>
|
||||||
{{ formatApiFormat(record.api_format) }}
|
{{ formatApiFormat(record.api_format) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted-foreground text-xs">-</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted-foreground text-xs"
|
||||||
|
>-</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-center py-4 w-[50px]">
|
<TableCell class="text-center py-4 w-[50px]">
|
||||||
<!-- 优先显示请求状态 -->
|
<!-- 优先显示请求状态 -->
|
||||||
<Badge v-if="record.status === 'pending'" variant="outline" class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground">
|
<Badge
|
||||||
|
v-if="record.status === 'pending'"
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground"
|
||||||
|
>
|
||||||
等待中
|
等待中
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else-if="record.status === 'streaming'" variant="outline" class="whitespace-nowrap animate-pulse border-primary/50 text-primary">
|
<Badge
|
||||||
|
v-else-if="record.status === 'streaming'"
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap animate-pulse border-primary/50 text-primary"
|
||||||
|
>
|
||||||
传输中
|
传输中
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message" variant="destructive" class="whitespace-nowrap">
|
<Badge
|
||||||
|
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
||||||
|
variant="destructive"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>
|
||||||
失败
|
失败
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else-if="record.is_stream" variant="secondary" class="whitespace-nowrap">
|
<Badge
|
||||||
|
v-else-if="record.is_stream"
|
||||||
|
variant="secondary"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>
|
||||||
流式
|
流式
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else variant="outline" class="whitespace-nowrap border-border/60 text-muted-foreground">
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap border-border/60 text-muted-foreground"
|
||||||
|
>
|
||||||
标准
|
标准
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -213,7 +347,10 @@
|
|||||||
<TableCell class="text-right py-4 w-[100px]">
|
<TableCell class="text-right py-4 w-[100px]">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||||
<span v-if="showActualCost && record.actual_cost !== undefined" class="text-muted-foreground">
|
<span
|
||||||
|
v-if="showActualCost && record.actual_cost !== undefined"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>
|
||||||
{{ formatCurrency(record.actual_cost) }}
|
{{ formatCurrency(record.actual_cost) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +365,10 @@
|
|||||||
<span v-else-if="record.response_time_ms">
|
<span v-else-if="record.response_time_ms">
|
||||||
{{ (record.response_time_ms / 1000).toFixed(2) }}s
|
{{ (record.response_time_ms / 1000).toFixed(2) }}s
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-muted-foreground">-</span>
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>-</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -251,21 +391,23 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onUnmounted, watch } from 'vue'
|
import { ref, computed, onUnmounted, watch } from 'vue'
|
||||||
import TableCard from '@/components/ui/table-card.vue'
|
import {
|
||||||
import Button from '@/components/ui/button.vue'
|
TableCard,
|
||||||
import Badge from '@/components/ui/badge.vue'
|
Badge,
|
||||||
import Select from '@/components/ui/select.vue'
|
Select,
|
||||||
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
SelectTrigger,
|
||||||
import SelectValue from '@/components/ui/select-value.vue'
|
SelectValue,
|
||||||
import SelectContent from '@/components/ui/select-content.vue'
|
SelectContent,
|
||||||
import SelectItem from '@/components/ui/select-item.vue'
|
SelectItem,
|
||||||
import Table from '@/components/ui/table.vue'
|
Table,
|
||||||
import TableHeader from '@/components/ui/table-header.vue'
|
TableHeader,
|
||||||
import TableBody from '@/components/ui/table-body.vue'
|
TableBody,
|
||||||
import TableRow from '@/components/ui/table-row.vue'
|
TableRow,
|
||||||
import TableHead from '@/components/ui/table-head.vue'
|
TableHead,
|
||||||
import TableCell from '@/components/ui/table-cell.vue'
|
TableCell,
|
||||||
import { Pagination, RefreshButton } from '@/components/ui'
|
Pagination,
|
||||||
|
RefreshButton,
|
||||||
|
} from '@/components/ui'
|
||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { formatDateTime } from '../composables'
|
import { formatDateTime } from '../composables'
|
||||||
import { useRowClick } from '@/composables/useRowClick'
|
import { useRowClick } from '@/composables/useRowClick'
|
||||||
|
|||||||
@@ -8,8 +8,14 @@
|
|||||||
<div class="border-b border-border px-6 py-4">
|
<div class="border-b border-border px-6 py-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
|
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
|
||||||
<UserPlus v-if="!isEditMode" class="h-5 w-5 text-primary" />
|
<UserPlus
|
||||||
<SquarePen v-else class="h-5 w-5 text-primary" />
|
v-if="!isEditMode"
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
/>
|
||||||
|
<SquarePen
|
||||||
|
v-else
|
||||||
|
class="h-5 w-5 text-primary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||||
@@ -23,7 +29,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" autocomplete="off">
|
<form
|
||||||
|
autocomplete="off"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-2 gap-0">
|
<div class="grid grid-cols-2 gap-0">
|
||||||
<!-- 左侧:基础设置 -->
|
<!-- 左侧:基础设置 -->
|
||||||
<div class="pr-6 space-y-4">
|
<div class="pr-6 space-y-4">
|
||||||
@@ -32,7 +41,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-username" class="text-sm font-medium">用户名 <span class="text-muted-foreground">*</span></Label>
|
<Label
|
||||||
|
for="form-username"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>用户名 <span class="text-muted-foreground">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="form-username"
|
id="form-username"
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
@@ -46,14 +58,15 @@
|
|||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label class="text-sm font-medium">
|
<Label class="text-sm font-medium">
|
||||||
{{ isEditMode ? '新密码 (留空保持不变)' : '密码' }} <span v-if="!isEditMode" class="text-muted-foreground">*</span>
|
{{ isEditMode ? '新密码 (留空保持不变)' : '密码' }} <span
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
:id="`pwd-${formNonce}`"
|
:id="`pwd-${formNonce}`"
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
:type="passwordFocused ? 'password' : 'text'"
|
:type="passwordFocused ? 'password' : 'text'"
|
||||||
@focus="passwordFocused = true"
|
|
||||||
@blur="passwordFocused = form.password.length > 0"
|
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
data-form-type="other"
|
data-form-type="other"
|
||||||
data-lpignore="true"
|
data-lpignore="true"
|
||||||
@@ -62,12 +75,22 @@
|
|||||||
minlength="6"
|
minlength="6"
|
||||||
:placeholder="isEditMode ? '留空保持原密码' : '至少6个字符'"
|
:placeholder="isEditMode ? '留空保持原密码' : '至少6个字符'"
|
||||||
:class="!passwordFocused && form.password.length === 0 ? 'h-10 text-transparent' : 'h-10'"
|
:class="!passwordFocused && form.password.length === 0 ? 'h-10 text-transparent' : 'h-10'"
|
||||||
|
@focus="passwordFocused = true"
|
||||||
|
@blur="passwordFocused = form.password.length > 0"
|
||||||
/>
|
/>
|
||||||
<p v-if="!isEditMode" class="text-xs text-muted-foreground">密码至少需要6个字符</p>
|
<p
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
密码至少需要6个字符
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-email" class="text-sm font-medium">邮箱 <span class="text-muted-foreground">*</span></Label>
|
<Label
|
||||||
|
for="form-email"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>邮箱 <span class="text-muted-foreground">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="form-email"
|
id="form-email"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
@@ -80,7 +103,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-quota" class="text-sm font-medium">配额(美元)</Label>
|
<Label
|
||||||
|
for="form-quota"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>配额(美元)</Label>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<Input
|
<Input
|
||||||
id="form-quota"
|
id="form-quota"
|
||||||
@@ -98,32 +124,55 @@
|
|||||||
v-model="form.unlimited"
|
v-model="form.unlimited"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
/>
|
>
|
||||||
<Label for="form-unlimited" class="whitespace-nowrap cursor-pointer text-sm">无限制</Label>
|
<Label
|
||||||
|
for="form-unlimited"
|
||||||
|
class="whitespace-nowrap cursor-pointer text-sm"
|
||||||
|
>无限制</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="form-role" class="text-sm font-medium">用户角色</Label>
|
<Label
|
||||||
|
for="form-role"
|
||||||
|
class="text-sm font-medium"
|
||||||
|
>用户角色</Label>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Select v-model="form.role" v-model:open="roleSelectOpen" class="flex-1">
|
<Select
|
||||||
<SelectTrigger id="form-role" class="h-10">
|
v-model="form.role"
|
||||||
|
v-model:open="roleSelectOpen"
|
||||||
|
class="flex-1"
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="form-role"
|
||||||
|
class="h-10"
|
||||||
|
>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="user">普通用户</SelectItem>
|
<SelectItem value="user">
|
||||||
<SelectItem value="admin">管理员</SelectItem>
|
普通用户
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="admin">
|
||||||
|
管理员
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div v-if="!isEditMode" class="flex items-center justify-center gap-2 border rounded-lg px-3 py-2 bg-muted/50 w-24">
|
<div
|
||||||
|
v-if="!isEditMode"
|
||||||
|
class="flex items-center justify-center gap-2 border rounded-lg px-3 py-2 bg-muted/50 w-24"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="form-active"
|
id="form-active"
|
||||||
v-model="form.is_active"
|
v-model="form.is_active"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
/>
|
>
|
||||||
<Label for="form-active" class="whitespace-nowrap cursor-pointer text-sm">启用用户</Label>
|
<Label
|
||||||
|
for="form-active"
|
||||||
|
class="whitespace-nowrap cursor-pointer text-sm"
|
||||||
|
>启用用户</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,9 +197,16 @@
|
|||||||
<span :class="form.allowed_providers.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_providers.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_providers.length ? `已选择 ${form.allowed_providers.length} 个` : '全部可用' }}
|
{{ form.allowed_providers.length ? `已选择 ${form.allowed_providers.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="providerDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="providerDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="providerDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="providerDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="providerDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="providerDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="providerDropdownOpen"
|
v-if="providerDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -167,10 +223,13 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_providers', provider.id)"
|
@change="toggleSelection('allowed_providers', provider.id)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ provider.display_name || provider.name }}</span>
|
<span class="text-sm">{{ provider.display_name || provider.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="providers.length === 0" class="px-3 py-2 text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="providers.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无可用 Provider
|
暂无可用 Provider
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,9 +248,16 @@
|
|||||||
<span :class="form.allowed_endpoints.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_endpoints.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_endpoints.length ? `已选择 ${form.allowed_endpoints.length} 个` : '全部可用' }}
|
{{ form.allowed_endpoints.length ? `已选择 ${form.allowed_endpoints.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="endpointDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="endpointDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="endpointDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="endpointDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="endpointDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="endpointDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="endpointDropdownOpen"
|
v-if="endpointDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -208,10 +274,13 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_endpoints', format.value)"
|
@change="toggleSelection('allowed_endpoints', format.value)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ format.label }}</span>
|
<span class="text-sm">{{ format.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="apiFormats.length === 0" class="px-3 py-2 text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="apiFormats.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无可用 API 格式
|
暂无可用 API 格式
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,9 +299,16 @@
|
|||||||
<span :class="form.allowed_models.length ? 'text-foreground' : 'text-muted-foreground'">
|
<span :class="form.allowed_models.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||||
{{ form.allowed_models.length ? `已选择 ${form.allowed_models.length} 个` : '全部可用' }}
|
{{ form.allowed_models.length ? `已选择 ${form.allowed_models.length} 个` : '全部可用' }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="modelDropdownOpen ? 'rotate-180' : ''" />
|
<ChevronDown
|
||||||
|
class="h-4 w-4 text-muted-foreground transition-transform"
|
||||||
|
:class="modelDropdownOpen ? 'rotate-180' : ''"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="modelDropdownOpen" class="fixed inset-0 z-[80]" @click.stop="modelDropdownOpen = false"></div>
|
<div
|
||||||
|
v-if="modelDropdownOpen"
|
||||||
|
class="fixed inset-0 z-[80]"
|
||||||
|
@click.stop="modelDropdownOpen = false"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="modelDropdownOpen"
|
v-if="modelDropdownOpen"
|
||||||
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
class="absolute z-[90] w-full mt-1 bg-popover border rounded-lg shadow-lg max-h-48 overflow-y-auto"
|
||||||
@@ -249,10 +325,13 @@
|
|||||||
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="toggleSelection('allowed_models', model.name)"
|
@change="toggleSelection('allowed_models', model.name)"
|
||||||
/>
|
>
|
||||||
<span class="text-sm">{{ model.name }}</span>
|
<span class="text-sm">{{ model.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalModels.length === 0" class="px-3 py-2 text-sm text-muted-foreground">
|
<div
|
||||||
|
v-if="globalModels.length === 0"
|
||||||
|
class="px-3 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
暂无可用模型
|
暂无可用模型
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,8 +342,19 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button variant="outline" @click="handleCancel" type="button" class="h-10 px-5">取消</Button>
|
<Button
|
||||||
<Button @click="handleSubmit" class="h-10 px-5" :disabled="saving || !isFormValid">
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
class="h-10 px-5"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="h-10 px-5"
|
||||||
|
:disabled="saving || !isFormValid"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
{{ saving ? '处理中...' : (isEditMode ? '更新' : '创建') }}
|
{{ saving ? '处理中...' : (isEditMode ? '更新' : '创建') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user