mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 03:02:26 +08:00
refactor(frontend): 优化功能模块组件
- 更新 api-keys 模块: StandaloneKeyFormDialog - 改进 auth 模块: LoginDialog - 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor - 重构 providers 模块: 多个表单和对话框组件 - 更新 usage 模块: 时间线、表格和详情组件 - 调整 users 模块: UserFormDialog
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="open"
|
||||
@update:model-value="$emit('update:open', $event)"
|
||||
title="批量添加关联模型"
|
||||
description="为提供商批量添加模型实现,提供商将自动继承模型的价格和能力,可在添加后单独修改"
|
||||
:icon="Layers"
|
||||
size="4xl"
|
||||
@update:model-value="$emit('update:open', $event)"
|
||||
>
|
||||
<template #default>
|
||||
<div class="space-y-4">
|
||||
@@ -13,10 +13,17 @@
|
||||
<div class="rounded-lg border bg-muted/30 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="font-semibold text-lg">{{ providerName }}</p>
|
||||
<p class="text-sm text-muted-foreground font-mono">{{ providerIdentifier }}</p>
|
||||
<p class="font-semibold text-lg">
|
||||
{{ providerName }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground font-mono">
|
||||
{{ providerIdentifier }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" class="text-xs">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
当前 {{ existingModels.length }} 个模型
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -28,7 +35,9 @@
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium">可添加</p>
|
||||
<p class="text-sm font-medium">
|
||||
可添加
|
||||
</p>
|
||||
<Button
|
||||
v-if="availableModels.length > 0"
|
||||
variant="ghost"
|
||||
@@ -39,19 +48,33 @@
|
||||
{{ isAllLeftSelected ? '取消全选' : '全选' }}
|
||||
</Button>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ availableModels.length }} 个
|
||||
</Badge>
|
||||
</div>
|
||||
<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" />
|
||||
</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" />
|
||||
<p class="text-sm">所有模型均已关联</p>
|
||||
<p class="text-sm">
|
||||
所有模型均已关联
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="p-2 space-y-1">
|
||||
<div
|
||||
v-else
|
||||
class="p-2 space-y-1"
|
||||
>
|
||||
<div
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
@@ -67,8 +90,12 @@
|
||||
@click.stop
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm truncate">{{ model.display_name }}</p>
|
||||
<p class="text-xs text-muted-foreground truncate font-mono">{{ model.name }}</p>
|
||||
<p class="font-medium text-sm truncate">
|
||||
{{ model.display_name }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||
{{ model.name }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="model.is_active ? 'outline' : 'secondary'"
|
||||
@@ -90,11 +117,18 @@
|
||||
class="w-9 h-8"
|
||||
:class="selectedLeftIds.length > 0 && !submittingAdd ? 'border-primary' : ''"
|
||||
:disabled="selectedLeftIds.length === 0 || submittingAdd"
|
||||
@click="batchAddSelected"
|
||||
title="添加选中"
|
||||
@click="batchAddSelected"
|
||||
>
|
||||
<Loader2 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' : ''" />
|
||||
<Loader2
|
||||
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
|
||||
variant="outline"
|
||||
@@ -102,11 +136,18 @@
|
||||
class="w-9 h-8"
|
||||
:class="selectedRightIds.length > 0 && !submittingRemove ? 'border-primary' : ''"
|
||||
:disabled="selectedRightIds.length === 0 || submittingRemove"
|
||||
@click="batchRemoveSelected"
|
||||
title="移除选中"
|
||||
@click="batchRemoveSelected"
|
||||
>
|
||||
<Loader2 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' : ''" />
|
||||
<Loader2
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +155,9 @@
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium">已添加</p>
|
||||
<p class="text-sm font-medium">
|
||||
已添加
|
||||
</p>
|
||||
<Button
|
||||
v-if="existingModels.length > 0"
|
||||
variant="ghost"
|
||||
@@ -125,16 +168,27 @@
|
||||
{{ isAllRightSelected ? '取消全选' : '全选' }}
|
||||
</Button>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ existingModels.length }} 个
|
||||
</Badge>
|
||||
</div>
|
||||
<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" />
|
||||
<p class="text-sm">暂无关联模型</p>
|
||||
<p class="text-sm">
|
||||
暂无关联模型
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="p-2 space-y-1">
|
||||
<div
|
||||
v-else
|
||||
class="p-2 space-y-1"
|
||||
>
|
||||
<div
|
||||
v-for="model in existingModels"
|
||||
:key="'existing-' + model.id"
|
||||
@@ -150,8 +204,12 @@
|
||||
@click.stop
|
||||
/>
|
||||
<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="text-xs text-muted-foreground truncate font-mono">{{ model.provider_model_name }}</p>
|
||||
<p class="font-medium text-sm truncate">
|
||||
{{ 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>
|
||||
<Badge
|
||||
:variant="model.is_active ? 'outline' : 'secondary'"
|
||||
|
||||
@@ -7,11 +7,18 @@
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<form
|
||||
class="space-y-6"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- API 配置 -->
|
||||
<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">
|
||||
<!-- API 格式 -->
|
||||
@@ -24,10 +31,16 @@
|
||||
disabled
|
||||
class="bg-muted"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">API 格式创建后不可修改</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API 格式创建后不可修改
|
||||
</p>
|
||||
</template>
|
||||
<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>
|
||||
<SelectValue placeholder="请选择 API 格式" />
|
||||
</SelectTrigger>
|
||||
@@ -69,7 +82,9 @@
|
||||
|
||||
<!-- 请求配置 -->
|
||||
<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="space-y-2">
|
||||
@@ -117,21 +132,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<Button
|
||||
@click="handleCancel"
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="loading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
:disabled="loading || !form.base_url || (!isEditMode && !form.api_format)"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存修改' : '创建') }}
|
||||
</Button>
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
<div class="w-full space-y-1">
|
||||
<!-- 时间线 -->
|
||||
<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>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="flex-1 h-full rounded-sm transition-all duration-150 cursor-pointer hover:scale-y-110 hover:brightness-110"
|
||||
:class="segment.color"
|
||||
></div>
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" :side-offset="8" class="max-w-xs">
|
||||
<div class="text-xs whitespace-pre-line">{{ segment.tooltip }}</div>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
:side-offset="8"
|
||||
class="max-w-xs"
|
||||
>
|
||||
<div class="text-xs whitespace-pre-line">
|
||||
{{ segment.tooltip }}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,42 +1,74 @@
|
||||
<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="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">
|
||||
<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">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 小时</SelectItem>
|
||||
<SelectItem value="6">6 小时</SelectItem>
|
||||
<SelectItem value="12">12 小时</SelectItem>
|
||||
<SelectItem value="24">24 小时</SelectItem>
|
||||
<SelectItem value="48">48 小时</SelectItem>
|
||||
<SelectItem value="1">
|
||||
1 小时
|
||||
</SelectItem>
|
||||
<SelectItem value="6">
|
||||
6 小时
|
||||
</SelectItem>
|
||||
<SelectItem value="12">
|
||||
12 小时
|
||||
</SelectItem>
|
||||
<SelectItem value="24">
|
||||
24 小时
|
||||
</SelectItem>
|
||||
<SelectItem value="48">
|
||||
48 小时
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<RefreshButton :loading="loading" @click="refreshData" />
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<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" />
|
||||
<span class="ml-2 text-muted-foreground">加载中...</span>
|
||||
</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" />
|
||||
<p>暂无健康监控数据</p>
|
||||
<p class="text-xs mt-1">端点尚未产生请求记录</p>
|
||||
<p class="text-xs mt-1">
|
||||
端点尚未产生请求记录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-else
|
||||
class="space-y-3"
|
||||
>
|
||||
<div
|
||||
v-for="monitor in monitors"
|
||||
:key="monitor.api_format"
|
||||
@@ -48,7 +80,10 @@
|
||||
<div class="w-44 flex-shrink-0 space-y-1.5">
|
||||
<!-- API 格式标签和成功率 -->
|
||||
<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 }}
|
||||
</Badge>
|
||||
<Badge
|
||||
@@ -61,7 +96,10 @@
|
||||
</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 }} 个密钥
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,14 @@
|
||||
>
|
||||
<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="text-xs font-medium text-muted-foreground">已选模型 ({{ selectedModels.length }})</div>
|
||||
<div class="text-xs font-medium text-muted-foreground">
|
||||
已选模型 ({{ selectedModels.length }})
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -43,26 +48,40 @@
|
||||
<!-- 模型列表区域 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between px-1">
|
||||
<div class="text-xs font-medium text-muted-foreground">可选模型列表</div>
|
||||
<div v-if="!loadingModels && availableModels.length > 0" class="text-[10px] text-muted-foreground/60">
|
||||
<div class="text-xs font-medium text-muted-foreground">
|
||||
可选模型列表
|
||||
</div>
|
||||
<div
|
||||
v-if="!loadingModels && availableModels.length > 0"
|
||||
class="text-[10px] text-muted-foreground/60"
|
||||
>
|
||||
共 {{ availableModels.length }} 个模型
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<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"></div>
|
||||
<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>
|
||||
</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" />
|
||||
<span class="text-sm">暂无可选模型</span>
|
||||
</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
|
||||
v-for="model in availableModels"
|
||||
:key="model.global_model_name"
|
||||
@@ -86,7 +105,10 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<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) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -101,9 +123,22 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end gap-2 w-full pt-2">
|
||||
<Button @click="handleCancel" variant="outline" class="h-9">取消</Button>
|
||||
<Button @click="handleSave" :disabled="saving" class="h-9 min-w-[80px]">
|
||||
<Loader2 v-if="saving" class="w-3.5 h-3.5 mr-1.5 animate-spin" />
|
||||
<Button
|
||||
variant="outline"
|
||||
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 ? '保存中' : '保存配置' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,203 +7,239 @@
|
||||
size="2xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<form @submit.prevent="handleSave" class="space-y-5" autocomplete="off">
|
||||
<!-- 基本信息 -->
|
||||
<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="keyNameInputId">密钥名称 *</Label>
|
||||
<Input
|
||||
:id="keyNameInputId"
|
||||
:name="keyNameFieldName"
|
||||
v-model="form.name"
|
||||
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>
|
||||
|
||||
<form
|
||||
class="space-y-5"
|
||||
autocomplete="off"
|
||||
@submit.prevent="handleSave"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<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="apiKeyInputId">API 密钥 {{ editingKey ? '' : '*' }}</Label>
|
||||
<Label :for="keyNameInputId">密钥名称 *</Label>
|
||||
<Input
|
||||
:id="apiKeyInputId"
|
||||
:name="apiKeyFieldName"
|
||||
v-model="form.api_key"
|
||||
:type="apiKeyInputType"
|
||||
:required="!editingKey"
|
||||
:placeholder="editingKey ? editingKey.api_key_masked : 'sk-...'"
|
||||
:class="getApiKeyInputClass()"
|
||||
autocomplete="new-password"
|
||||
:id="keyNameInputId"
|
||||
v-model="form.name"
|
||||
:name="keyNameFieldName"
|
||||
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"
|
||||
@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>
|
||||
<Label for="rate_multiplier">成本倍率 *</Label>
|
||||
<Input
|
||||
id="note"
|
||||
v-model="form.note"
|
||||
placeholder="可选的备注信息"
|
||||
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>
|
||||
<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 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 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="cache_ttl_minutes">缓存 TTL (分钟)</Label>
|
||||
<Input
|
||||
id="cache_ttl_minutes"
|
||||
:model-value="form.cache_ttl_minutes ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="60"
|
||||
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
0 = 禁用缓存亲和性
|
||||
</p>
|
||||
</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>
|
||||
<Label for="max_probe_interval_minutes">熔断探测间隔 (分钟)</Label>
|
||||
<Input
|
||||
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 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="cache_ttl_minutes">缓存 TTL (分钟)</Label>
|
||||
<Input
|
||||
id="cache_ttl_minutes"
|
||||
:model-value="form.cache_ttl_minutes ?? ''"
|
||||
type="number"
|
||||
min="0"
|
||||
max="60"
|
||||
@update:model-value="(v) => form.cache_ttl_minutes = parseNumberInput(v, { min: 0, max: 60 }) ?? 5"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">0 = 禁用缓存亲和性</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="max_probe_interval_minutes">熔断探测间隔 (分钟)</Label>
|
||||
<Input
|
||||
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"
|
||||
<!-- 能力标签配置 -->
|
||||
<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
|
||||
type="checkbox"
|
||||
:checked="form.capabilities[cap.name] || false"
|
||||
class="rounded"
|
||||
@change="form.capabilities[cap.name] = !form.capabilities[cap.name]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="form.capabilities[cap.name] || false"
|
||||
@change="form.capabilities[cap.name] = !form.capabilities[cap.name]"
|
||||
class="rounded"
|
||||
/>
|
||||
<span>{{ cap.display_name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<span>{{ cap.display_name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<Button @click="handleCancel" variant="outline">取消</Button>
|
||||
<Button @click="handleSave" :disabled="saving">
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="saving"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg">
|
||||
<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="[
|
||||
'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'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||
@@ -25,8 +25,8 @@
|
||||
</button>
|
||||
<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="[
|
||||
'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'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||
@@ -41,7 +41,10 @@
|
||||
<!-- 内容区域 -->
|
||||
<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">
|
||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
||||
@@ -49,18 +52,24 @@
|
||||
</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" />
|
||||
<span class="text-sm">暂无提供商</span>
|
||||
</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
|
||||
v-for="(provider, index) in sortedProviders"
|
||||
:key="provider.id"
|
||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||
:class="[
|
||||
'group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200',
|
||||
draggedProvider === index
|
||||
? 'border-primary/50 bg-primary/5 shadow-md scale-[1.01]'
|
||||
: dragOverProvider === index
|
||||
@@ -100,7 +109,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<Info class="w-3.5 h-3.5 shrink-0" />
|
||||
@@ -108,29 +120,38 @@
|
||||
</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="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>
|
||||
</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" />
|
||||
<span class="text-sm">暂无 API Key</span>
|
||||
</div>
|
||||
|
||||
<!-- 左右布局:格式列表 + Key 列表 -->
|
||||
<div v-else class="flex gap-4">
|
||||
<div
|
||||
v-else
|
||||
class="flex gap-4"
|
||||
>
|
||||
<!-- 左侧:API 格式列表 -->
|
||||
<div class="w-32 shrink-0 space-y-1">
|
||||
<button
|
||||
v-for="format in availableFormats"
|
||||
:key="format"
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-xs font-medium rounded-md text-left transition-all duration-200"
|
||||
:class="[
|
||||
'w-full px-3 py-2 text-xs font-medium rounded-md text-left transition-all duration-200',
|
||||
activeFormatTab === format
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
@@ -143,116 +164,147 @@
|
||||
|
||||
<!-- 右侧:Key 列表 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-for="format in availableFormats" :key="format" v-show="activeFormatTab === format">
|
||||
<div v-if="keysByFormat[format]?.length > 0" class="space-y-2 max-h-[380px] overflow-y-auto pr-1">
|
||||
<div
|
||||
v-for="format in availableFormats"
|
||||
v-show="activeFormatTab === format"
|
||||
:key="format"
|
||||
>
|
||||
<div
|
||||
v-for="(key, index) in keysByFormat[format]"
|
||||
:key="key.id"
|
||||
: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)"
|
||||
v-if="keysByFormat[format]?.length > 0"
|
||||
class="space-y-2 max-h-[380px] overflow-y-auto pr-1"
|
||||
>
|
||||
<!-- 拖拽手柄 -->
|
||||
<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">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
<!-- 可编辑序号 -->
|
||||
<div class="shrink-0">
|
||||
<input
|
||||
v-if="editingKeyPriority[format] === key.id"
|
||||
type="number"
|
||||
min="1"
|
||||
:value="key.priority"
|
||||
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"
|
||||
@blur="finishEditKeyPriority(format, key, $event)"
|
||||
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
||||
@keydown.escape="cancelEditKeyPriority(format)"
|
||||
autofocus
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
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"
|
||||
:title="'点击编辑优先级,相同数字为同级(负载均衡)'"
|
||||
@click.stop="startEditKeyPriority(format, key)"
|
||||
>
|
||||
{{ key.priority }}
|
||||
<div
|
||||
v-for="(key, index) in keysByFormat[format]"
|
||||
:key="key.id"
|
||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200"
|
||||
:class="[
|
||||
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 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">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key 信息 -->
|
||||
<div class="flex-1 min-w-0 flex items-center gap-3">
|
||||
<!-- 左侧:名称和来源 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{{ key.name }}</span>
|
||||
<Badge
|
||||
v-if="key.circuit_breaker_open"
|
||||
variant="destructive"
|
||||
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 class="shrink-0">
|
||||
<input
|
||||
v-if="editingKeyPriority[format] === key.id"
|
||||
type="number"
|
||||
min="1"
|
||||
:value="key.priority"
|
||||
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"
|
||||
autofocus
|
||||
@blur="finishEditKeyPriority(format, key, $event)"
|
||||
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
||||
@keydown.escape="cancelEditKeyPriority(format)"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
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"
|
||||
title="点击编辑优先级,相同数字为同级(负载均衡)"
|
||||
@click.stop="startEditKeyPriority(format, key)"
|
||||
>
|
||||
{{ key.priority }}
|
||||
</div>
|
||||
</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',
|
||||
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) }}%
|
||||
<!-- Key 信息 -->
|
||||
<div class="flex-1 min-w-0 flex items-center gap-3">
|
||||
<!-- 左侧:名称和来源 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm">{{ key.name }}</span>
|
||||
<Badge
|
||||
v-if="key.circuit_breaker_open"
|
||||
variant="destructive"
|
||||
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 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 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 class="text-sm font-medium tabular-nums text-primary min-w-[40px] text-right">{{ key.rate_multiplier }}x</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else 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
|
||||
v-else
|
||||
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>
|
||||
@@ -264,11 +316,24 @@
|
||||
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button size="sm" @click="save" :disabled="saving" class="min-w-[72px]">
|
||||
<Loader2 v-if="saving" class="w-3.5 h-3.5 mr-1.5 animate-spin" />
|
||||
<Button
|
||||
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 ? '保存中' : '保存' }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="close" class="min-w-[72px]">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="min-w-[72px]"
|
||||
@click="close"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
@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">
|
||||
<!-- 加载状态 -->
|
||||
<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
|
||||
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>
|
||||
|
||||
<template v-else-if="provider">
|
||||
@@ -23,8 +29,13 @@
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="space-y-1 flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-xl font-bold truncate">{{ provider.display_name }}</h2>
|
||||
<Badge :variant="provider.is_active ? 'default' : 'secondary'" class="text-xs shrink-0">
|
||||
<h2 class="text-xl font-bold truncate">
|
||||
{{ provider.display_name }}
|
||||
</h2>
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'default' : 'secondary'"
|
||||
class="text-xs shrink-0"
|
||||
>
|
||||
{{ provider.is_active ? '活跃' : '已停用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -45,18 +56,28 @@
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="$emit('toggle-status', provider)"
|
||||
:title="provider.is_active ? '点击停用' : '点击启用'"
|
||||
@click="$emit('toggle-status', provider)"
|
||||
>
|
||||
<Power class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" @click="handleClose" title="关闭">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="关闭"
|
||||
@click="handleClose"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -64,53 +85,72 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 p-6">
|
||||
<!-- 配额使用情况 -->
|
||||
<Card v-if="provider.billing_type === 'monthly_quota' && provider.monthly_quota_usd" class="p-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">订阅配额</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">
|
||||
<!-- 配额使用情况 -->
|
||||
<Card
|
||||
v-if="provider.billing_type === 'monthly_quota' && provider.monthly_quota_usd"
|
||||
class="p-4"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">
|
||||
订阅配额
|
||||
</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
|
||||
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>
|
||||
<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">
|
||||
v-if="endpoints.length > 0"
|
||||
class="divide-y divide-border/40"
|
||||
>
|
||||
<div
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
@@ -141,10 +181,16 @@
|
||||
<Key class="w-3 h-3" />
|
||||
{{ endpoint.keys?.filter((k: EndpointAPIKey) => k.is_active).length || 0 }}
|
||||
</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 }}次重试
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
@@ -156,61 +202,70 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-5 w-5 shrink-0"
|
||||
@click.stop="copyToClipboard(endpoint.base_url)"
|
||||
title="复制 Base URL"
|
||||
@click.stop="copyToClipboard(endpoint.base_url)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1" @click.stop>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
@click.stop
|
||||
>
|
||||
<Button
|
||||
v-if="hasUnhealthyKeys(endpoint)"
|
||||
@click="handleRecoverAllKeys(endpoint)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-green-600"
|
||||
title="恢复所有密钥健康状态"
|
||||
:disabled="recoveringEndpointId === endpoint.id"
|
||||
@click="handleRecoverAllKeys(endpoint)"
|
||||
>
|
||||
<Loader2 v-if="recoveringEndpointId === endpoint.id" class="w-3.5 h-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="w-3.5 h-3.5" />
|
||||
<Loader2
|
||||
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
|
||||
@click="handleAddKey(endpoint)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="添加密钥"
|
||||
@click="handleAddKey(endpoint)"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleEditEndpoint(endpoint)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑端点"
|
||||
@click="handleEditEndpoint(endpoint)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="toggleEndpointActive(endpoint)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="togglingEndpointId === endpoint.id"
|
||||
:title="endpoint.is_active ? '点击停用' : '点击启用'"
|
||||
@click="toggleEndpointActive(endpoint)"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleDeleteEndpoint(endpoint)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="删除端点"
|
||||
@click="handleDeleteEndpoint(endpoint)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -225,7 +280,10 @@
|
||||
>
|
||||
<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">
|
||||
<span class="text-muted-foreground">自定义路径:</span>
|
||||
<span class="ml-1 font-mono">{{ endpoint.custom_path }}</span>
|
||||
@@ -238,16 +296,14 @@
|
||||
|
||||
<!-- 密钥列表 -->
|
||||
<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
|
||||
v-for="key in endpoint.keys"
|
||||
:key="key.id"
|
||||
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="{
|
||||
'border-border/40 hover:border-border/80': dragState.targetKeyId !== key.id,
|
||||
@@ -255,6 +311,11 @@
|
||||
'opacity-50': dragState.draggedKeyId === key.id,
|
||||
'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">
|
||||
@@ -276,7 +337,9 @@
|
||||
{{ key.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</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 class="flex items-center gap-1.5 ml-auto shrink-0">
|
||||
<div
|
||||
@@ -309,48 +372,48 @@
|
||||
<div class="flex items-center gap-1 ml-2">
|
||||
<Button
|
||||
v-if="key.circuit_breaker_open || (key.health_score !== undefined && key.health_score < 0.5)"
|
||||
@click="handleRecoverKey(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-green-600"
|
||||
title="刷新健康状态"
|
||||
@click="handleRecoverKey(key)"
|
||||
>
|
||||
<RefreshCw class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleConfigKeyModels(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="配置允许的模型"
|
||||
@click="handleConfigKeyModels(key)"
|
||||
>
|
||||
<Layers class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleEditKey(endpoint, key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="编辑密钥"
|
||||
@click="handleEditKey(endpoint, key)"
|
||||
>
|
||||
<Edit class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="toggleKeyActive(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="togglingKeyId === key.id"
|
||||
:title="key.is_active ? '点击停用' : '点击启用'"
|
||||
@click="toggleKeyActive(key)"
|
||||
>
|
||||
<Power class="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleDeleteKey(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
title="删除密钥"
|
||||
@click="handleDeleteKey(key)"
|
||||
>
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
@@ -371,42 +434,67 @@
|
||||
P {{ key.internal_priority }}
|
||||
</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>
|
||||
<input
|
||||
type="number"
|
||||
ref="priorityInput"
|
||||
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"
|
||||
min="0"
|
||||
@keyup.enter="savePriority(key, endpoint)"
|
||||
@keyup.escape="cancelEditPriority"
|
||||
@blur="savePriority(key, endpoint)"
|
||||
ref="priorityInput"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
<span class="text-muted-foreground" title="成本倍率,实际成本 = 模型价格 × 倍率">
|
||||
<span
|
||||
class="text-muted-foreground"
|
||||
title="成本倍率,实际成本 = 模型价格 × 倍率"
|
||||
>
|
||||
{{ key.rate_multiplier }}x
|
||||
</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 }})
|
||||
</span>
|
||||
</div>
|
||||
<!-- 右侧动态信息 -->
|
||||
<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) }}探测
|
||||
</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
|
||||
</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 }}
|
||||
</span>
|
||||
</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>
|
||||
@@ -416,32 +504,39 @@
|
||||
</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" />
|
||||
<p class="text-sm">暂无端点配置</p>
|
||||
<p class="text-xs mt-1">点击上方"添加端点"按钮创建第一个端点</p>
|
||||
<p class="text-sm">
|
||||
暂无端点配置
|
||||
</p>
|
||||
<p class="text-xs mt-1">
|
||||
点击上方"添加端点"按钮创建第一个端点
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<!-- 模型查看 -->
|
||||
<ModelsTab
|
||||
v-if="provider"
|
||||
:key="`models-${provider.id}`"
|
||||
:provider="provider"
|
||||
@edit-model="handleEditModel"
|
||||
@delete-model="handleDeleteModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
/>
|
||||
<!-- 模型查看 -->
|
||||
<ModelsTab
|
||||
v-if="provider"
|
||||
:key="`models-${provider.id}`"
|
||||
:provider="provider"
|
||||
@edit-model="handleEditModel"
|
||||
@delete-model="handleDeleteModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
/>
|
||||
|
||||
<!-- 模型映射 -->
|
||||
<MappingsTab
|
||||
v-if="provider"
|
||||
:key="`mappings-${provider.id}`"
|
||||
:provider="provider"
|
||||
@refresh="handleRelatedDataRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 模型映射 -->
|
||||
<MappingsTab
|
||||
v-if="provider"
|
||||
:key="`mappings-${provider.id}`"
|
||||
:provider="provider"
|
||||
@refresh="handleRelatedDataRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -7,13 +7,21 @@
|
||||
size="2xl"
|
||||
@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">
|
||||
<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>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -21,7 +29,9 @@
|
||||
placeholder="例如: openai-primary"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">唯一ID,创建后不可修改</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
唯一ID,创建后不可修改
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@@ -58,18 +68,29 @@
|
||||
|
||||
<!-- 计费与限流 -->
|
||||
<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="space-y-2">
|
||||
<Label>计费类型</Label>
|
||||
<Select v-model="form.billing_type" v-model:open="billingTypeSelectOpen">
|
||||
<Select
|
||||
v-model="form.billing_type"
|
||||
v-model:open="billingTypeSelectOpen"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly_quota">月卡额度</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">按量付费</SelectItem>
|
||||
<SelectItem value="free_tier">免费套餐</SelectItem>
|
||||
<SelectItem value="monthly_quota">
|
||||
月卡额度
|
||||
</SelectItem>
|
||||
<SelectItem value="pay_as_you_go">
|
||||
按量付费
|
||||
</SelectItem>
|
||||
<SelectItem value="free_tier">
|
||||
免费套餐
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -86,7 +107,10 @@
|
||||
</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">
|
||||
<Label class="text-xs">周期额度 (USD)</Label>
|
||||
<Input
|
||||
@@ -114,13 +138,25 @@
|
||||
周期开始时间
|
||||
<span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input v-model="form.quota_last_reset_at" type="datetime-local" class="h-9" />
|
||||
<p class="text-xs text-muted-foreground">系统会自动统计从该时间点开始的使用量</p>
|
||||
<Input
|
||||
v-model="form.quota_last_reset_at"
|
||||
type="datetime-local"
|
||||
class="h-9"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
系统会自动统计从该时间点开始的使用量
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">过期时间</Label>
|
||||
<Input v-model="form.quota_expires_at" type="datetime-local" class="h-9" />
|
||||
<p class="text-xs text-muted-foreground">留空表示永久有效</p>
|
||||
<Input
|
||||
v-model="form.quota_expires_at"
|
||||
type="datetime-local"
|
||||
class="h-9"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空表示永久有效
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,14 +166,14 @@
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="handleCancel"
|
||||
:disabled="loading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
:disabled="loading || !form.display_name || (!isEditMode && !form.name)"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? (isEditMode ? '保存中...' : '创建中...') : (isEditMode ? '保存' : '创建') }}
|
||||
</Button>
|
||||
@@ -147,16 +183,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import Textarea from '@/components/ui/textarea.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import Select from '@/components/ui/select.vue'
|
||||
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
||||
import SelectValue from '@/components/ui/select-value.vue'
|
||||
import SelectContent from '@/components/ui/select-content.vue'
|
||||
import SelectItem from '@/components/ui/select-item.vue'
|
||||
import {
|
||||
Dialog,
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '@/components/ui'
|
||||
import { Server, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:model-value="open"
|
||||
@update:model-value="handleClose"
|
||||
:title="isEditing ? '编辑模型配置' : '添加模型'"
|
||||
:description="isEditing ? '修改模型价格和能力配置' : '为此 Provider 添加模型实现'"
|
||||
:icon="isEditing ? SquarePen : Layers"
|
||||
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>
|
||||
<Select
|
||||
v-model:open="globalModelSelectOpen"
|
||||
@@ -30,25 +36,41 @@
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</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
|
||||
</p>
|
||||
</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>
|
||||
<p class="font-semibold text-lg">{{ editingModel?.global_model_display_name || editingModel?.provider_model_name }}</p>
|
||||
<p class="text-sm text-muted-foreground font-mono">{{ editingModel?.provider_model_name }}</p>
|
||||
<p class="font-semibold text-lg">
|
||||
{{ 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 class="space-y-4">
|
||||
<h4 class="font-semibold text-sm border-b pb-2">价格配置</h4>
|
||||
<TieredPricingEditor ref="tieredPricingEditorRef" v-model="tieredPricing" :show-cache1h="showCache1h" />
|
||||
<h4 class="font-semibold text-sm border-b pb-2">
|
||||
价格配置
|
||||
</h4>
|
||||
<TieredPricingEditor
|
||||
ref="tieredPricingEditorRef"
|
||||
v-model="tieredPricing"
|
||||
:show-cache1h="showCache1h"
|
||||
/>
|
||||
|
||||
<!-- 按次计费 -->
|
||||
<div class="flex items-center gap-3 pt-2 border-t">
|
||||
@@ -68,70 +90,80 @@
|
||||
|
||||
<!-- 能力配置 -->
|
||||
<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">
|
||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.supports_streaming"
|
||||
type="checkbox"
|
||||
:indeterminate="form.supports_streaming === undefined"
|
||||
class="rounded"
|
||||
/>
|
||||
>
|
||||
<Zap class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm font-medium">流式输出</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.supports_image_generation"
|
||||
type="checkbox"
|
||||
:indeterminate="form.supports_image_generation === undefined"
|
||||
class="rounded"
|
||||
/>
|
||||
>
|
||||
<Image class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm font-medium">图像生成</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.supports_vision"
|
||||
type="checkbox"
|
||||
:indeterminate="form.supports_vision === undefined"
|
||||
class="rounded"
|
||||
/>
|
||||
>
|
||||
<Eye class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm font-medium">视觉理解</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.supports_function_calling"
|
||||
type="checkbox"
|
||||
:indeterminate="form.supports_function_calling === undefined"
|
||||
class="rounded"
|
||||
/>
|
||||
>
|
||||
<Wrench class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm font-medium">工具调用</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 p-3 rounded-lg border cursor-pointer hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.supports_extended_thinking"
|
||||
type="checkbox"
|
||||
:indeterminate="form.supports_extended_thinking === undefined"
|
||||
class="rounded"
|
||||
/>
|
||||
>
|
||||
<Brain class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span class="text-sm font-medium">深度思考</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<Button variant="outline" @click="handleClose(false)">
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="handleClose(false)"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="handleSubmit" :disabled="submitting || (!isEditing && !form.global_model_id)">
|
||||
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
|
||||
<Button
|
||||
:disabled="submitting || (!isEditing && !form.global_model_id)"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<Loader2
|
||||
v-if="submitting"
|
||||
class="w-4 h-4 mr-2 animate-spin"
|
||||
/>
|
||||
{{ isEditing ? '保存' : '添加' }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -141,10 +173,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Eye, Wrench, Brain, Zap, Loader2, Image, Layers, SquarePen } from 'lucide-vue-next'
|
||||
import { Dialog, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import {
|
||||
Dialog,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { createModel, updateModel, getProviderModels } from '@/api/endpoints/models'
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
</div>
|
||||
<Button
|
||||
v-if="!hideAddButton"
|
||||
@click="openCreateDialog"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5 mr-1.5" />
|
||||
创建别名/映射
|
||||
@@ -22,19 +22,36 @@
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<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
|
||||
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 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">
|
||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-semibold">名称</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>
|
||||
<th class="text-left px-4 py-3 font-semibold">
|
||||
名称
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -50,19 +67,25 @@
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:class="mapping.is_active ? 'bg-green-500' : 'bg-gray-300'"
|
||||
:title="mapping.is_active ? '活跃' : '停用'"
|
||||
></span>
|
||||
/>
|
||||
<span class="font-mono">{{ mapping.alias }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ mapping.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||
</Badge>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ mapping.global_model_display_name || mapping.global_model_name }}
|
||||
</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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -78,8 +101,8 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="togglingId === mapping.id"
|
||||
@click="toggleActive(mapping)"
|
||||
:title="mapping.is_active ? '点击停用' : '点击启用'"
|
||||
@click="toggleActive(mapping)"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -100,10 +123,17 @@
|
||||
</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" />
|
||||
<p class="text-sm">暂无特定别名/映射</p>
|
||||
<p class="text-xs mt-1">点击上方按钮添加</p>
|
||||
<p class="text-sm">
|
||||
暂无特定别名/映射
|
||||
</p>
|
||||
<p class="text-xs mt-1">
|
||||
点击上方按钮添加
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
<h3 class="text-sm font-semibold flex items-center gap-2">
|
||||
模型列表
|
||||
</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" />
|
||||
关联模型
|
||||
</Button>
|
||||
@@ -14,19 +19,33 @@
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<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
|
||||
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 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">
|
||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
<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-[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>
|
||||
<th class="text-left px-4 py-3 font-semibold w-[40%]">
|
||||
模型
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -42,7 +61,7 @@
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:class="getStatusIndicatorClass(model)"
|
||||
:title="getStatusTitle(model)"
|
||||
></div>
|
||||
/>
|
||||
<!-- 模型信息 -->
|
||||
<div class="text-left flex-1 min-w-0">
|
||||
<span class="font-semibold text-sm">
|
||||
@@ -51,9 +70,9 @@
|
||||
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||
<span class="font-mono truncate">{{ model.provider_model_name }}</span>
|
||||
<button
|
||||
@click.stop="copyModelId(model.provider_model_name)"
|
||||
class="p-0.5 hover:bg-muted rounded transition-colors shrink-0"
|
||||
title="复制模型 ID"
|
||||
@click.stop="copyModelId(model.provider_model_name)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</button>
|
||||
@@ -62,17 +81,46 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-top px-4 py-3">
|
||||
<div v-if="hasAnyCapability(model)" class="grid grid-cols-3 gap-1 w-fit">
|
||||
<Zap 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
|
||||
v-if="hasAnyCapability(model)"
|
||||
class="grid grid-cols-3 gap-1 w-fit"
|
||||
>
|
||||
<Zap
|
||||
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>
|
||||
<span v-else class="text-xs text-muted-foreground">—</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>—</span>
|
||||
</td>
|
||||
<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 计费 -->
|
||||
<template v-if="hasTokenPricing(model)">
|
||||
<span class="text-muted-foreground text-right">输入/输出:</span>
|
||||
@@ -112,8 +160,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="editModel(model)"
|
||||
title="编辑"
|
||||
@click="editModel(model)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -122,8 +170,8 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="togglingModelId === model.id"
|
||||
@click="toggleModelActive(model)"
|
||||
:title="model.is_active ? '点击停用' : '点击启用'"
|
||||
@click="toggleModelActive(model)"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -131,8 +179,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
||||
@click="deleteModel(model)"
|
||||
title="删除"
|
||||
@click="deleteModel(model)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -144,10 +192,17 @@
|
||||
</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" />
|
||||
<p class="text-sm">暂无模型</p>
|
||||
<p class="text-xs mt-1">请前往"模型目录"页面添加模型</p>
|
||||
<p class="text-sm">
|
||||
暂无模型
|
||||
</p>
|
||||
<p class="text-xs mt-1">
|
||||
请前往"模型目录"页面添加模型
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user