refactor(frontend): 优化功能模块组件

- 更新 api-keys 模块: StandaloneKeyFormDialog
- 改进 auth 模块: LoginDialog
- 优化 models 模块: AliasDialog, GlobalModelFormDialog, ModelDetailDrawer, TieredPricingEditor
- 重构 providers 模块: 多个表单和对话框组件
- 更新 usage 模块: 时间线、表格和详情组件
- 调整 users 模块: UserFormDialog
This commit is contained in:
fawney19
2025-12-12 16:15:36 +08:00
parent e9a6233655
commit 06c0a47b21
29 changed files with 2572 additions and 1051 deletions

View File

@@ -1,42 +1,56 @@
<template>
<Dialog
:model-value="open"
@update:model-value="handleDialogUpdate"
:title="dialogTitle"
:description="dialogDescription"
:icon="dialogIcon"
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>
<div class="grid grid-cols-2 gap-3">
<button
type="button"
@click="form.mapping_type = 'alias'"
class="p-3 rounded-lg border-2 text-left transition-all"
:class="[
'p-3 rounded-lg border-2 text-left transition-all',
form.mapping_type === 'alias'
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
]"
@click="form.mapping_type = 'alias'"
>
<div class="font-medium text-sm">别名</div>
<div class="text-xs text-muted-foreground mt-1">名称简写按目标模型计费</div>
<div class="font-medium text-sm">
别名
</div>
<div class="text-xs text-muted-foreground mt-1">
名称简写按目标模型计费
</div>
</button>
<button
type="button"
@click="form.mapping_type = 'mapping'"
class="p-3 rounded-lg border-2 text-left transition-all"
:class="[
'p-3 rounded-lg border-2 text-left transition-all',
form.mapping_type === 'mapping'
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
]"
@click="form.mapping_type = 'mapping'"
>
<div class="font-medium text-sm">映射</div>
<div class="text-xs text-muted-foreground mt-1">模型降级按源模型计费</div>
<div class="font-medium text-sm">
映射
</div>
<div class="text-xs text-muted-foreground mt-1">
模型降级按源模型计费
</div>
</button>
</div>
</div>
@@ -54,20 +68,37 @@
</div>
<!-- Provider 选择/作用范围 -->
<div v-if="showProviderSelect" class="space-y-2">
<div
v-if="showProviderSelect"
class="space-y-2"
>
<Label>作用范围</Label>
<!-- 固定 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 }}
</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">
<SelectValue placeholder="选择作用范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="global">全局所有 Provider</SelectItem>
<SelectItem v-for="p in providers" :key="p.id" :value="p.id">
<SelectItem value="global">
全局所有 Provider
</SelectItem>
<SelectItem
v-for="p in providers"
:key="p.id"
:value="p.id"
>
{{ p.display_name || p.name }}
</SelectItem>
</SelectContent>
@@ -75,7 +106,10 @@
</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>
<Input
id="alias-name"
@@ -90,10 +124,21 @@
</div>
<!-- 映射模式选择源模型 -->
<div v-else class="space-y-2">
<div
v-else
class="space-y-2"
>
<Label>源模型 (用户请求的模型) *</Label>
<Select v-model:open="sourceModelSelectOpen" :model-value="form.alias" @update:model-value="form.alias = $event" :disabled="isEditMode">
<SelectTrigger class="w-full" :class="{ 'opacity-50': isEditMode }">
<Select
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="请选择源模型" />
</SelectTrigger>
<SelectContent>
@@ -117,12 +162,20 @@
{{ form.mapping_type === 'alias' ? '目标模型 *' : '目标模型 (实际处理请求) *' }}
</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="text-muted-foreground ml-1">({{ fixedTargetModel.name }})</span>
</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">
<SelectValue placeholder="请选择模型" />
</SelectTrigger>
@@ -137,15 +190,24 @@
</SelectContent>
</Select>
</div>
</form>
<template #footer>
<Button type="button" variant="outline" @click="handleCancel">
<Button
type="button"
variant="outline"
@click="handleCancel"
>
取消
</Button>
<Button @click="handleSubmit" :disabled="submitting">
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
<Button
:disabled="submitting"
@click="handleSubmit"
>
<Loader2
v-if="submitting"
class="w-4 h-4 mr-2 animate-spin"
/>
{{ isEditMode ? '保存' : '创建' }}
</Button>
</template>

View File

@@ -7,14 +7,22 @@
size="xl"
@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">
<h4 class="font-medium text-sm">基本信息</h4>
<h4 class="font-medium text-sm">
基本信息
</h4>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1.5">
<Label for="model-name" class="text-xs">模型名称 *</Label>
<Label
for="model-name"
class="text-xs"
>模型名称 *</Label>
<Input
id="model-name"
v-model="form.name"
@@ -22,10 +30,18 @@
:disabled="isEditMode"
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 class="space-y-1.5">
<Label for="model-display-name" class="text-xs">显示名称 *</Label>
<Label
for="model-display-name"
class="text-xs"
>显示名称 *</Label>
<Input
id="model-display-name"
v-model="form.display_name"
@@ -36,7 +52,10 @@
</div>
<div class="space-y-1.5">
<Label for="model-description" class="text-xs">描述</Label>
<Label
for="model-description"
class="text-xs"
>描述</Label>
<Input
id="model-description"
v-model="form.description"
@@ -47,30 +66,52 @@
<!-- 能力配置 -->
<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">
<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" />
<span>流式输出</span>
</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">
<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" />
<span>视觉理解</span>
</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">
<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" />
<span>工具调用</span>
</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">
<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" />
<span>深度思考</span>
</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">
<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" />
<span>图像生成</span>
</label>
@@ -78,8 +119,13 @@
</section>
<!-- Key 能力配置 -->
<section v-if="availableCapabilities.length > 0" class="space-y-2">
<h4 class="font-medium text-sm">Key 能力支持</h4>
<section
v-if="availableCapabilities.length > 0"
class="space-y-2"
>
<h4 class="font-medium text-sm">
Key 能力支持
</h4>
<div class="flex flex-wrap gap-2">
<label
v-for="cap in availableCapabilities"
@@ -89,9 +135,9 @@
<input
type="checkbox"
:checked="form.supported_capabilities?.includes(cap.name)"
@change="toggleCapability(cap.name)"
class="rounded"
/>
@change="toggleCapability(cap.name)"
>
<span>{{ cap.display_name }}</span>
</label>
</div>
@@ -99,8 +145,14 @@
<!-- 价格配置 -->
<section class="space-y-3">
<h4 class="font-medium text-sm">价格配置</h4>
<TieredPricingEditor ref="tieredPricingEditorRef" v-model="tieredPricing" :show-cache1h="form.supported_capabilities?.includes('cache_1h')" />
<h4 class="font-medium text-sm">
价格配置
</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">
@@ -120,11 +172,21 @@
</form>
<template #footer>
<Button type="button" variant="outline" @click="handleCancel">
<Button
type="button"
variant="outline"
@click="handleCancel"
>
取消
</Button>
<Button @click="handleSubmit" :disabled="submitting">
<Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
<Button
:disabled="submitting"
@click="handleSubmit"
>
<Loader2
v-if="submitting"
class="w-4 h-4 mr-2 animate-spin"
/>
{{ isEditMode ? '保存' : '创建' }}
</Button>
</template>
@@ -138,6 +200,7 @@ import { Dialog, Button, Input, Label } from '@/components/ui'
import { useToast } from '@/composables/useToast'
import { useFormDialog } from '@/composables/useFormDialog'
import { parseNumberInput } from '@/utils/form'
import { log } from '@/utils/logger'
import TieredPricingEditor from './TieredPricingEditor.vue'
import {
createGlobalModel,
@@ -204,7 +267,7 @@ async function loadCapabilities() {
try {
availableCapabilities.value = await getAllCapabilities()
} catch (err) {
console.error('Failed to load capabilities:', err)
log.error('Failed to load capabilities:', err)
}
}

View File

@@ -8,7 +8,10 @@
@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">
@@ -16,8 +19,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">
<h3 class="text-xl font-bold truncate">{{ model.display_name }}</h3>
<Badge :variant="model.is_active ? 'default' : 'secondary'" class="text-xs shrink-0">
<h3 class="text-xl font-bold truncate">
{{ model.display_name }}
</h3>
<Badge
:variant="model.is_active ? 'default' : 'secondary'"
class="text-xs shrink-0"
>
{{ model.is_active ? '活跃' : '停用' }}
</Badge>
</div>
@@ -32,23 +40,36 @@
</button>
<template v-if="model.description">
<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>
</div>
</div>
<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" />
</Button>
<Button
variant="ghost"
size="icon"
@click="$emit('toggle-model-status', model)"
:title="model.is_active ? '点击停用' : '点击启用'"
@click="$emit('toggle-model-status', model)"
>
<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>
@@ -56,110 +77,154 @@
</div>
<div class="p-6">
<!-- 自定义 Tab 切换 -->
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg mb-4">
<button
type="button"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
detailTab === 'basic'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'basic'"
>
基本信息
</button>
<button
type="button"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
detailTab === 'providers'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'providers'"
>
关联提供商
</button>
<button
type="button"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
detailTab === 'aliases'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'aliases'"
>
别名/映射
</button>
</div>
<!-- 自定义 Tab 切换 -->
<div class="flex gap-1 p-1 bg-muted/40 rounded-lg mb-4">
<button
type="button"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
:class="[
detailTab === 'basic'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'basic'"
>
基本信息
</button>
<button
type="button"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
:class="[
detailTab === 'providers'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'providers'"
>
关联提供商
</button>
<button
type="button"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200"
:class="[
detailTab === 'aliases'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
]"
@click="detailTab = 'aliases'"
>
别名/映射
</button>
</div>
<!-- Tab 内容 -->
<div v-show="detailTab === 'basic'" class="space-y-6">
<!-- Tab 内容 -->
<div
v-show="detailTab === 'basic'"
class="space-y-6"
>
<!-- 基础属性 -->
<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>
<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 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="flex items-center gap-2 p-3 rounded-lg border">
<Zap class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">Streaming</p>
<p class="text-xs text-muted-foreground">流式输出</p>
<p class="text-sm font-medium">
Streaming
</p>
<p class="text-xs text-muted-foreground">
流式输出
</p>
</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 ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Image class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">Image Generation</p>
<p class="text-xs text-muted-foreground">图像生成</p>
<p class="text-sm font-medium">
Image Generation
</p>
<p class="text-xs text-muted-foreground">
图像生成
</p>
</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 ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Eye class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">Vision</p>
<p class="text-xs text-muted-foreground">视觉理解</p>
<p class="text-sm font-medium">
Vision
</p>
<p class="text-xs text-muted-foreground">
视觉理解
</p>
</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 ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Wrench class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">Tool Use</p>
<p class="text-xs text-muted-foreground">工具调用</p>
<p class="text-sm font-medium">
Tool Use
</p>
<p class="text-xs text-muted-foreground">
工具调用
</p>
</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 ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Brain class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">Extended Thinking</p>
<p class="text-xs text-muted-foreground">深度思考</p>
<p class="text-sm font-medium">
Extended Thinking
</p>
<p class="text-xs text-muted-foreground">
深度思考
</p>
</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 ? '支持' : '不支持' }}
</Badge>
</div>
@@ -167,8 +232,13 @@
</div>
<!-- 模型偏好 -->
<div v-if="model.supported_capabilities && model.supported_capabilities.length > 0" class="space-y-3">
<h4 class="font-semibold text-sm">模型偏好</h4>
<div
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">
<Badge
v-for="cap in model.supported_capabilities"
@@ -183,10 +253,15 @@
<!-- 默认定价 -->
<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">
<!-- Token 计费 -->
<div class="p-3 rounded-lg border">
@@ -215,19 +290,28 @@
</div>
</div>
<!-- 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>
<span class="text-sm font-mono">{{ getFirst1hCachePrice(model.default_tiered_pricing) }}</span>
</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>
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/</span>
</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">
<Layers class="w-4 h-4" />
<span>阶梯计费 ({{ getTierCount(model.default_tiered_pricing) }} )</span>
@@ -238,12 +322,24 @@
<Table>
<TableHeader>
<TableRow class="bg-muted/30">
<TableHead class="text-xs h-9">阶梯</TableHead>
<TableHead class="text-xs h-9 text-right">输入 ($/M)</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>
<TableHead class="text-xs h-9">
阶梯
</TableHead>
<TableHead class="text-xs h-9 text-right">
输入 ($/M)
</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>
</TableHeader>
<TableBody>
@@ -253,7 +349,10 @@
class="text-xs"
>
<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)}` }}
</span>
<span v-else>
@@ -281,7 +380,10 @@
</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>
<span class="text-sm font-mono">${{ model.default_price_per_request.toFixed(3) }}/</span>
</div>
@@ -290,42 +392,50 @@
<!-- 统计信息 -->
<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="p-3 rounded-lg border bg-muted/20">
<div class="flex items-center justify-between">
<Label class="text-xs text-muted-foreground">关联提供商</Label>
<Building2 class="w-4 h-4 text-muted-foreground" />
</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 class="p-3 rounded-lg border bg-muted/20">
<div class="flex items-center justify-between">
<Label class="text-xs text-muted-foreground">别名数量</Label>
<Tag class="w-4 h-4 text-muted-foreground" />
</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>
<!-- Tab 2: 关联提供商 -->
<div v-show="detailTab === 'providers'">
<!-- Tab 2: 关联提供商 -->
<div v-show="detailTab === 'providers'">
<Card class="overflow-hidden">
<!-- 标题栏 -->
<div class="px-4 py-3 border-b border-border/60">
<div class="flex items-center justify-between gap-4">
<div>
<h4 class="text-sm font-semibold">关联提供商列表</h4>
<h4 class="text-sm font-semibold">
关联提供商列表
</h4>
</div>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="$emit('add-provider')"
title="添加关联"
@click="$emit('add-provider')"
>
<Plus class="w-3.5 h-3.5" />
</Button>
@@ -333,27 +443,41 @@
variant="ghost"
size="icon"
class="h-8 w-8"
@click="$emit('refresh-providers')"
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>
</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" />
</div>
<Table v-else-if="providers.length > 0">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="h-10 font-semibold">Provider</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>
<TableHead class="h-10 font-semibold">
Provider
</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>
</TableHeader>
<TableBody>
@@ -368,15 +492,27 @@
class="w-2 h-2 rounded-full shrink-0"
:class="provider.is_active ? 'bg-green-500' : 'bg-gray-300'"
:title="provider.is_active ? '活跃' : '停用'"
></span>
/>
<span class="font-medium truncate">{{ provider.display_name }}</span>
</div>
</TableCell>
<TableCell class="py-3">
<div class="flex gap-0.5">
<Zap v-if="provider.supports_streaming" 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="工具调用" />
<Zap
v-if="provider.supports_streaming"
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>
</TableCell>
<TableCell class="py-3">
@@ -386,15 +522,25 @@
<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 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 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 class="ml-1">${{ (provider.cache_creation_price_per_1m || 0).toFixed(2) }}/${{ (provider.cache_read_price_per_1m || 0).toFixed(2) }}</span>
</div>
<!-- 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 class="ml-1">${{ (provider.cache_1h_creation_price_per_1m || 0).toFixed(2) }}</span>
</div>
@@ -404,7 +550,10 @@
<span class="ml-1">${{ (provider.price_per_request || 0).toFixed(3) }}/</span>
</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>
</TableCell>
<TableCell class="py-3 text-center">
@@ -413,8 +562,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('edit-provider', provider)"
title="编辑此关联"
@click="$emit('edit-provider', provider)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
@@ -422,8 +571,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('toggle-provider-status', provider)"
:title="provider.is_active ? '停用此关联' : '启用此关联'"
@click="$emit('toggle-provider-status', provider)"
>
<Power class="w-3.5 h-3.5" />
</Button>
@@ -431,8 +580,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('delete-provider', provider)"
title="删除此关联"
@click="$emit('delete-provider', provider)"
>
<Trash2 class="w-3.5 h-3.5" />
</Button>
@@ -443,33 +592,45 @@
</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" />
<p class="text-sm text-muted-foreground">暂无关联提供商</p>
<Button size="sm" variant="outline" class="mt-4" @click="$emit('add-provider')">
<p class="text-sm text-muted-foreground">
暂无关联提供商
</p>
<Button
size="sm"
variant="outline"
class="mt-4"
@click="$emit('add-provider')"
>
<Plus class="w-4 h-4 mr-1" />
添加第一个关联
</Button>
</div>
</Card>
</div>
</div>
<!-- Tab 3: 别名 -->
<div v-show="detailTab === 'aliases'">
<!-- Tab 3: 别名 -->
<div v-show="detailTab === 'aliases'">
<Card class="overflow-hidden">
<!-- 标题栏 -->
<div class="px-4 py-3 border-b border-border/60">
<div class="flex items-center justify-between gap-4">
<div>
<h4 class="text-sm font-semibold">别名与映射</h4>
<h4 class="text-sm font-semibold">
别名与映射
</h4>
</div>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="$emit('add-alias')"
title="添加别名/映射"
@click="$emit('add-alias')"
>
<Plus class="w-3.5 h-3.5" />
</Button>
@@ -477,27 +638,41 @@
variant="ghost"
size="icon"
class="h-8 w-8"
@click="$emit('refresh-aliases')"
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>
</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" />
</div>
<Table v-else-if="aliases.length > 0">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="h-10 font-semibold">别名</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>
<TableHead class="h-10 font-semibold">
别名
</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>
</TableHeader>
<TableBody>
@@ -512,12 +687,15 @@
class="w-2 h-2 rounded-full shrink-0"
:class="alias.is_active ? 'bg-green-500' : 'bg-gray-300'"
:title="alias.is_active ? '活跃' : '停用'"
></span>
/>
<code class="text-sm font-medium bg-muted px-1.5 py-0.5 rounded">{{ alias.alias }}</code>
</div>
</TableCell>
<TableCell class="py-3">
<Badge variant="secondary" class="text-xs">
<Badge
variant="secondary"
class="text-xs"
>
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
</Badge>
</TableCell>
@@ -530,7 +708,13 @@
>
{{ alias.provider_name || 'Provider' }}
</Badge>
<Badge v-else variant="default" class="text-xs">全局</Badge>
<Badge
v-else
variant="default"
class="text-xs"
>
全局
</Badge>
</TableCell>
<TableCell class="py-3 text-center">
<div class="flex items-center justify-center gap-0.5">
@@ -538,8 +722,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('edit-alias', alias)"
title="编辑"
@click="$emit('edit-alias', alias)"
>
<Edit class="w-3.5 h-3.5" />
</Button>
@@ -547,8 +731,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('toggle-alias-status', alias)"
:title="alias.is_active ? '停用' : '启用'"
@click="$emit('toggle-alias-status', alias)"
>
<Power class="w-3.5 h-3.5" />
</Button>
@@ -556,8 +740,8 @@
variant="ghost"
size="icon"
class="h-7 w-7"
@click="$emit('delete-alias', alias)"
title="删除"
@click="$emit('delete-alias', alias)"
>
<Trash2 class="w-3.5 h-3.5" />
</Button>
@@ -568,17 +752,27 @@
</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" />
<p class="text-sm text-muted-foreground">暂无别名或映射</p>
<Button size="sm" variant="outline" class="mt-4" @click="$emit('add-alias')">
<p class="text-sm text-muted-foreground">
暂无别名或映射
</p>
<Button
size="sm"
variant="outline"
class="mt-4"
@click="$emit('add-alias')"
>
<Plus class="w-4 h-4 mr-1" />
添加别名/映射
</Button>
</div>
</Card>
</div>
</div>
</div>
</Card>
</div>
</Transition>
@@ -607,6 +801,26 @@ import {
} from 'lucide-vue-next'
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()
import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue'
@@ -636,34 +850,12 @@ interface Props {
capabilities?: CapabilityDefinition[]
}
const props = withDefaults(defineProps<Props>(), {
loadingProviders: false,
loadingAliases: false,
hasBlockingDialogOpen: false
})
// 根据能力名称获取显示名称
function getCapabilityDisplayName(capName: string): string {
const cap = props.capabilities?.find(c => c.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')
// 处理背景点击

View File

@@ -39,7 +39,10 @@
</option>
</select>
</template>
<span v-else class="font-medium">无上限</span>
<span
v-else
class="font-medium"
>无上限</span>
</div>
<Button
v-if="localTiers.length > 1"
@@ -53,7 +56,10 @@
</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">
<Label class="text-xs">输入 ($/M)</Label>
<Input
@@ -102,7 +108,10 @@
@update:model-value="(v) => updateCacheRead(index, v)"
/>
</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>
<Input
:model-value="getCache1hDisplay(index)"
@@ -129,7 +138,10 @@
</Button>
<!-- 验证提示 -->
<p v-if="validationError" class="text-xs text-destructive">
<p
v-if="validationError"
class="text-xs text-destructive"
>
{{ validationError }}
</p>
</div>