mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
Initial commit
This commit is contained in:
784
frontend/src/features/models/components/ModelDetailDrawer.vue
Normal file
784
frontend/src/features/models/components/ModelDetailDrawer.vue
Normal file
@@ -0,0 +1,784 @@
|
||||
<template>
|
||||
<!-- 模型详情抽屉 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="drawer">
|
||||
<div
|
||||
v-if="open && model"
|
||||
class="fixed inset-0 z-50 flex justify-end"
|
||||
@click.self="handleBackdropClick"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleBackdropClick"></div>
|
||||
|
||||
<!-- 抽屉内容 -->
|
||||
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
||||
<div class="sticky top-0 z-10 bg-background border-b p-6">
|
||||
<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">
|
||||
{{ model.is_active ? '活跃' : '停用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground font-mono">{{ model.name }}</span>
|
||||
<button
|
||||
class="p-0.5 rounded hover:bg-muted transition-colors"
|
||||
title="复制模型 ID"
|
||||
@click="copyToClipboard(model.name)"
|
||||
>
|
||||
<Copy class="w-3 h-3 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="model.description" class="text-xs text-muted-foreground">
|
||||
{{ model.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" @click="$emit('edit-model', model)" title="编辑模型">
|
||||
<Edit class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="$emit('toggle-model-status', model)"
|
||||
:title="model.is_active ? '点击停用' : '点击启用'"
|
||||
>
|
||||
<Power class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" @click="handleClose" title="关闭">
|
||||
<X class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</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 v-show="detailTab === 'basic'" class="space-y-6">
|
||||
<!-- 基础属性 -->
|
||||
<div class="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型能力 -->
|
||||
<div class="space-y-3">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<Badge :variant="model.default_supports_extended_thinking ?? false ? 'default' : 'secondary'" class="text-xs">
|
||||
{{ model.default_supports_extended_thinking ?? false ? '支持' : '不支持' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型偏好 -->
|
||||
<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"
|
||||
:key="cap"
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ getCapabilityDisplayName(cap) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 默认定价 -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="font-semibold text-sm">默认定价</h4>
|
||||
|
||||
<!-- 单阶梯(固定价格)展示 -->
|
||||
<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">
|
||||
<Label class="text-xs text-muted-foreground">输入价格 ($/M)</Label>
|
||||
<p class="text-lg font-semibold mt-1">
|
||||
{{ getFirstTierPrice(model.default_tiered_pricing, 'input_price_per_1m') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg border">
|
||||
<Label class="text-xs text-muted-foreground">输出价格 ($/M)</Label>
|
||||
<p class="text-lg font-semibold mt-1">
|
||||
{{ getFirstTierPrice(model.default_tiered_pricing, 'output_price_per_1m') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg border">
|
||||
<Label class="text-xs text-muted-foreground">缓存创建 ($/M)</Label>
|
||||
<p class="text-sm font-mono mt-1">
|
||||
{{ getFirstTierPrice(model.default_tiered_pricing, 'cache_creation_price_per_1m') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg border">
|
||||
<Label class="text-xs text-muted-foreground">缓存读取 ($/M)</Label>
|
||||
<p class="text-sm font-mono mt-1">
|
||||
{{ getFirstTierPrice(model.default_tiered_pricing, 'cache_read_price_per_1m') }}
|
||||
</p>
|
||||
</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">
|
||||
<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">
|
||||
<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 class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Layers class="w-4 h-4" />
|
||||
<span>阶梯计费 ({{ getTierCount(model.default_tiered_pricing) }} 档)</span>
|
||||
</div>
|
||||
|
||||
<!-- 阶梯价格表格 -->
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="(tier, index) in model.default_tiered_pricing?.tiers || []"
|
||||
:key="index"
|
||||
class="text-xs"
|
||||
>
|
||||
<TableCell class="py-2">
|
||||
<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>
|
||||
{{ index === 0 ? '0' : formatTierLimit((model.default_tiered_pricing?.tiers || [])[index - 1]?.up_to) }} - {{ formatTierLimit(tier.up_to) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-2 text-right font-mono">
|
||||
${{ tier.input_price_per_1m?.toFixed(2) || '0.00' }}
|
||||
</TableCell>
|
||||
<TableCell class="py-2 text-right font-mono">
|
||||
${{ tier.output_price_per_1m?.toFixed(2) || '0.00' }}
|
||||
</TableCell>
|
||||
<TableCell class="py-2 text-right font-mono text-muted-foreground">
|
||||
{{ tier.cache_creation_price_per_1m != null ? `$${tier.cache_creation_price_per_1m.toFixed(2)}` : '-' }}
|
||||
</TableCell>
|
||||
<TableCell class="py-2 text-right font-mono text-muted-foreground">
|
||||
{{ tier.cache_read_price_per_1m != null ? `$${tier.cache_read_price_per_1m.toFixed(2)}` : '-' }}
|
||||
</TableCell>
|
||||
<TableCell class="py-2 text-right font-mono text-muted-foreground">
|
||||
{{ get1hCachePrice(tier) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="space-y-3">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="$emit('add-provider')"
|
||||
title="添加关联"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="$emit('refresh-providers')"
|
||||
title="刷新"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<TableCell class="py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
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="工具调用" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<div class="text-xs font-mono space-y-0.5">
|
||||
<!-- Token 计费:输入/输出 -->
|
||||
<div v-if="(provider.input_price_per_1m || 0) > 0 || (provider.output_price_per_1m || 0) > 0">
|
||||
<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>
|
||||
</div>
|
||||
<!-- 缓存价格 -->
|
||||
<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">
|
||||
<span>1h 缓存:</span>
|
||||
<span class="ml-1">${{ (provider.cache_1h_creation_price_per_1m || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<!-- 按次计费 -->
|
||||
<div v-if="(provider.price_per_request || 0) > 0">
|
||||
<span class="text-muted-foreground">按次:</span>
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('edit-provider', provider)"
|
||||
title="编辑此关联"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('toggle-provider-status', provider)"
|
||||
:title="provider.is_active ? '停用此关联' : '启用此关联'"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('delete-provider', provider)"
|
||||
title="删除此关联"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<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')">
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
添加第一个关联
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="$emit('add-alias')"
|
||||
title="添加别名/映射"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="$emit('refresh-aliases')"
|
||||
title="刷新"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="alias in aliases"
|
||||
:key="alias.id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<TableCell class="py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
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">
|
||||
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-3">
|
||||
<Badge
|
||||
v-if="alias.provider_id"
|
||||
variant="outline"
|
||||
class="text-xs truncate max-w-[90px]"
|
||||
:title="alias.provider_name || 'Provider'"
|
||||
>
|
||||
{{ alias.provider_name || 'Provider' }}
|
||||
</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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('edit-alias', alias)"
|
||||
title="编辑"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('toggle-alias-status', alias)"
|
||||
:title="alias.is_active ? '停用' : '启用'"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="$emit('delete-alias', alias)"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<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')">
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
添加别名/映射
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
X,
|
||||
Eye,
|
||||
Wrench,
|
||||
Brain,
|
||||
Zap,
|
||||
Image,
|
||||
Building2,
|
||||
Tag,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Power,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Layers
|
||||
} from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const { success: showSuccess, error: showError } = useToast()
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import Table from '@/components/ui/table.vue'
|
||||
import TableHeader from '@/components/ui/table-header.vue'
|
||||
import TableBody from '@/components/ui/table-body.vue'
|
||||
import TableRow from '@/components/ui/table-row.vue'
|
||||
import TableHead from '@/components/ui/table-head.vue'
|
||||
import TableCell from '@/components/ui/table-cell.vue'
|
||||
|
||||
// 使用外部类型定义
|
||||
import type { GlobalModelResponse } from '@/api/global-models'
|
||||
import type { ModelAlias } from '@/api/endpoints/aliases'
|
||||
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
||||
import type { CapabilityDefinition } from '@/api/endpoints'
|
||||
|
||||
interface Props {
|
||||
model: GlobalModelResponse | null
|
||||
open: boolean
|
||||
providers: any[]
|
||||
aliases: ModelAlias[]
|
||||
loadingProviders?: boolean
|
||||
loadingAliases?: boolean
|
||||
hasBlockingDialogOpen?: boolean
|
||||
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')
|
||||
|
||||
// 处理背景点击
|
||||
function handleBackdropClick() {
|
||||
if (!props.hasBlockingDialogOpen) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭抽屉
|
||||
function handleClose() {
|
||||
if (!props.hasBlockingDialogOpen) {
|
||||
emit('update:open', false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showSuccess('已复制')
|
||||
} catch {
|
||||
showError('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 从 tiered_pricing 获取第一阶梯的价格
|
||||
function getFirstTierPrice(
|
||||
tieredPricing: TieredPricingConfig | undefined | null,
|
||||
priceKey: 'input_price_per_1m' | 'output_price_per_1m' | 'cache_creation_price_per_1m' | 'cache_read_price_per_1m'
|
||||
): string {
|
||||
if (!tieredPricing?.tiers?.length) return '-'
|
||||
const firstTier = tieredPricing.tiers[0]
|
||||
const value = firstTier[priceKey]
|
||||
if (value == null || value === 0) return '-'
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
|
||||
// 获取阶梯数量
|
||||
function getTierCount(tieredPricing: TieredPricingConfig | undefined | null): number {
|
||||
return tieredPricing?.tiers?.length || 0
|
||||
}
|
||||
|
||||
// 格式化阶梯上限(tokens 数量简化显示)
|
||||
function formatTierLimit(limit: number | null | undefined): string {
|
||||
if (limit == null) return ''
|
||||
if (limit >= 1000000) {
|
||||
return `${(limit / 1000000).toFixed(1)}M`
|
||||
} else if (limit >= 1000) {
|
||||
return `${(limit / 1000).toFixed(0)}K`
|
||||
}
|
||||
return limit.toString()
|
||||
}
|
||||
|
||||
// 获取 1h 缓存价格
|
||||
function get1hCachePrice(tier: PricingTier): string {
|
||||
const ttl1h = tier.cache_ttl_pricing?.find(t => t.ttl_minutes === 60)
|
||||
if (ttl1h) {
|
||||
return `$${ttl1h.cache_creation_price_per_1m.toFixed(2)}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 获取第一阶梯的 1h 缓存价格
|
||||
function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | null): string {
|
||||
if (!tieredPricing?.tiers?.length) return '-'
|
||||
return get1hCachePrice(tieredPricing.tiers[0])
|
||||
}
|
||||
|
||||
// 监听 open 变化,重置 tab
|
||||
watch(() => props.open, (newOpen) => {
|
||||
if (newOpen) {
|
||||
// 直接设置为 basic,不需要先重置为空
|
||||
detailTab.value = 'basic'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 抽屉过渡动画 */
|
||||
.drawer-enter-active,
|
||||
.drawer-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.drawer-enter-active .relative,
|
||||
.drawer-leave-active .relative {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.drawer-enter-from,
|
||||
.drawer-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.drawer-enter-from .relative {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.drawer-leave-to .relative {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.drawer-enter-to .relative,
|
||||
.drawer-leave-from .relative {
|
||||
transform: translateX(0);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user