refactor(frontend): 优化布局和视图页面

- 更新 MainLayout 布局组件
- 优化 admin 视图: 用户、模型、Provider、API Keys 等管理页面
- 改进 shared 视图: Dashboard、Usage 页面
- 调整 user 视图: ModelCatalog、MyApiKeys、Settings、Announcements 页面
- 更新 public 视图: Home、CliSection、LogoColorDemo 页面
This commit is contained in:
fawney19
2025-12-12 16:15:54 +08:00
parent 06c0a47b21
commit 39ea9e8e86
20 changed files with 3062 additions and 1154 deletions

View File

@@ -4,252 +4,314 @@
<div class="flex-1 flex flex-col min-w-0">
<!-- 模型列表 -->
<Card 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">模型管理</h3>
<!-- 标题和操作栏 -->
<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">
模型管理
</h3>
<!-- 右侧操作区 -->
<div class="flex items-center gap-2">
<!-- 搜索框 -->
<div class="relative">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
<Input
id="model-search"
v-model="searchQuery"
type="text"
placeholder="搜索模型名称..."
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
/>
</div>
<div class="h-4 w-px bg-border" />
<!-- 能力筛选 -->
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.streaming ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
@click="capabilityFilters.streaming = !capabilityFilters.streaming"
title="流式输出"
>
<Zap class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.imageGeneration ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
@click="capabilityFilters.imageGeneration = !capabilityFilters.imageGeneration"
title="图像生成"
>
<Image class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
@click="capabilityFilters.vision = !capabilityFilters.vision"
title="视觉理解"
>
<Eye class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
title="工具调用"
>
<Wrench class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
title="深度思考"
>
<Brain class="w-3.5 h-3.5" />
</button>
</div>
<div class="h-4 w-px bg-border" />
<!-- 操作按钮 -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="openCreateModelDialog"
title="创建模型"
>
<Plus class="w-3.5 h-3.5" />
</Button>
<RefreshButton :loading="loading" @click="refreshData" />
</div>
<!-- 右侧操作区 -->
<div class="flex items-center gap-2">
<!-- 搜索框 -->
<div class="relative">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
<Input
id="model-search"
v-model="searchQuery"
type="text"
placeholder="搜索模型名称..."
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
/>
</div>
<div class="h-4 w-px bg-border" />
<!-- 能力筛选 -->
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.streaming ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
title="流式输出"
@click="capabilityFilters.streaming = !capabilityFilters.streaming"
>
<Zap class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.imageGeneration ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
title="图像生成"
@click="capabilityFilters.imageGeneration = !capabilityFilters.imageGeneration"
>
<Image class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
title="视觉理解"
@click="capabilityFilters.vision = !capabilityFilters.vision"
>
<Eye class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
title="工具调用"
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
>
<Wrench class="w-3.5 h-3.5" />
</button>
<div class="w-px h-4 bg-border/60" />
<button
class="px-2.5 h-full text-xs transition-colors"
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
title="深度思考"
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
>
<Brain class="w-3.5 h-3.5" />
</button>
</div>
<div class="h-4 w-px bg-border" />
<!-- 操作按钮 -->
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="创建模型"
@click="openCreateModelDialog"
>
<Plus class="w-3.5 h-3.5" />
</Button>
<RefreshButton
:loading="loading"
@click="refreshData"
/>
</div>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[240px]">模型名称</TableHead>
<TableHead class="w-[140px]">能力/偏好</TableHead>
<TableHead class="w-[160px] text-center">价格 ($/M)</TableHead>
<TableHead class="w-[80px] text-center">提供商</TableHead>
<TableHead class="w-[70px] text-center">别名/映射</TableHead>
<TableHead class="w-[80px] text-center">调用次数</TableHead>
<TableHead class="w-[70px]">状态</TableHead>
<TableHead class="w-[140px] text-center">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell colspan="8" class="text-center py-8">
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
<TableRow v-else-if="filteredGlobalModels.length === 0">
<TableCell colspan="8" class="text-center py-8 text-muted-foreground">
没有找到匹配的模型
</TableCell>
</TableRow>
<template v-else>
<TableRow
v-for="model in paginatedGlobalModels"
:key="model.id"
class="cursor-pointer hover:bg-muted/50 group"
@mousedown="handleMouseDown"
@click="handleRowClick($event, model)"
>
<TableCell>
<div>
<div class="font-medium">{{ model.display_name }}</div>
<div class="text-xs text-muted-foreground flex items-center gap-1">
<span>{{ model.name }}</span>
<button
class="p-0.5 rounded hover:bg-muted transition-colors"
title="复制模型 ID"
@click.stop="copyToClipboard(model.name)"
>
<Copy class="w-3 h-3" />
</button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[240px]">
模型名称
</TableHead>
<TableHead class="w-[140px]">
能力/偏好
</TableHead>
<TableHead class="w-[160px] text-center">
价格 ($/M)
</TableHead>
<TableHead class="w-[80px] text-center">
提供商
</TableHead>
<TableHead class="w-[70px] text-center">
别名/映射
</TableHead>
<TableHead class="w-[80px] text-center">
调用次数
</TableHead>
<TableHead class="w-[70px]">
状态
</TableHead>
<TableHead class="w-[140px] text-center">
操作
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell
colspan="8"
class="text-center py-8"
>
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
<TableRow v-else-if="filteredGlobalModels.length === 0">
<TableCell
colspan="8"
class="text-center py-8 text-muted-foreground"
>
没有找到匹配的模型
</TableCell>
</TableRow>
<template v-else>
<TableRow
v-for="model in paginatedGlobalModels"
:key="model.id"
class="cursor-pointer hover:bg-muted/50 group"
@mousedown="handleMouseDown"
@click="handleRowClick($event, model)"
>
<TableCell>
<div>
<div class="font-medium">
{{ model.display_name }}
</div>
<div class="text-xs text-muted-foreground flex items-center gap-1">
<span>{{ model.name }}</span>
<button
class="p-0.5 rounded hover:bg-muted transition-colors"
title="复制模型 ID"
@click.stop="copyToClipboard(model.name)"
>
<Copy class="w-3 h-3" />
</button>
</div>
</div>
</TableCell>
<TableCell>
<div class="space-y-1 w-fit">
<div class="flex flex-wrap gap-1">
<Zap
v-if="model.default_supports_streaming"
class="w-4 h-4 text-muted-foreground"
title="流式输出"
/>
<Image
v-if="model.default_supports_image_generation"
class="w-4 h-4 text-muted-foreground"
title="图像生成"
/>
<Eye
v-if="model.default_supports_vision"
class="w-4 h-4 text-muted-foreground"
title="视觉理解"
/>
<Wrench
v-if="model.default_supports_function_calling"
class="w-4 h-4 text-muted-foreground"
title="工具调用"
/>
<Brain
v-if="model.default_supports_extended_thinking"
class="w-4 h-4 text-muted-foreground"
title="深度思考"
/>
</div>
<template v-if="model.supported_capabilities?.length">
<div class="border-t border-border/50" />
<div class="flex flex-wrap gap-0.5">
<span
v-for="capName in model.supported_capabilities"
:key="capName"
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
:title="getCapabilityDisplayName(capName)"
>{{ getCapabilityShortName(capName) }}</span>
</div>
</TableCell>
<TableCell>
<div class="space-y-1 w-fit">
<div class="flex flex-wrap gap-1">
<Zap v-if="model.default_supports_streaming" class="w-4 h-4 text-muted-foreground" title="流式输出" />
<Image v-if="model.default_supports_image_generation" class="w-4 h-4 text-muted-foreground" title="图像生成" />
<Eye v-if="model.default_supports_vision" class="w-4 h-4 text-muted-foreground" title="视觉理解" />
<Wrench v-if="model.default_supports_function_calling" class="w-4 h-4 text-muted-foreground" title="工具调用" />
<Brain v-if="model.default_supports_extended_thinking" class="w-4 h-4 text-muted-foreground" title="深度思考" />
</div>
<template v-if="model.supported_capabilities?.length">
<div class="border-t border-border/50"></div>
<div class="flex flex-wrap gap-0.5">
<span
v-for="capName in model.supported_capabilities"
:key="capName"
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
:title="getCapabilityDisplayName(capName)"
>{{ getCapabilityShortName(capName) }}</span>
</div>
</template>
</div>
</TableCell>
<TableCell class="text-center">
<div class="text-xs space-y-0.5">
<!-- Token 计费 -->
<div v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')">
<span class="text-muted-foreground">In:</span>
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}</span>
<span class="text-muted-foreground mx-1">/</span>
<span class="text-muted-foreground">Out:</span>
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}</span>
<!-- 阶梯计费标记 -->
<span v-if="hasTieredPricing(model)" class="ml-1 text-muted-foreground" title="阶梯计费">[阶梯]</span>
</div>
<!-- 按次计费 -->
<div v-if="model.default_price_per_request && model.default_price_per_request > 0">
<span class="text-muted-foreground">按次:</span>
<span class="font-mono ml-1">${{ model.default_price_per_request.toFixed(3) }}/</span>
</div>
<!-- 无计费配置 -->
<div v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request" class="text-muted-foreground">-</div>
</div>
</TableCell>
<TableCell class="text-center">
<Badge variant="secondary">{{ model.provider_count || 0 }}</Badge>
</TableCell>
<TableCell class="text-center">
<Badge variant="secondary">{{ model.alias_count || 0 }}</Badge>
</TableCell>
<TableCell class="text-center">
<span class="text-sm font-mono">{{ formatUsageCount(model.usage_count || 0) }}</span>
</TableCell>
<TableCell>
<Badge :variant="model.is_active ? 'default' : 'secondary'">
{{ model.is_active ? '活跃' : '停用' }}
</Badge>
</TableCell>
<TableCell>
<div class="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click.stop="selectModel(model)"
title="查看详情"
>
<Eye class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click.stop="editModel(model)"
title="编辑模型"
>
<Edit class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click.stop="toggleModelStatus(model)"
:title="model.is_active ? '停用模型' : '启用模型'"
>
<Power class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click.stop="deleteModel(model)"
title="删除模型"
>
<Trash2 class="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</template>
</div>
</TableCell>
<TableCell class="text-center">
<div class="text-xs space-y-0.5">
<!-- Token 计费 -->
<div v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')">
<span class="text-muted-foreground">In:</span>
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}</span>
<span class="text-muted-foreground mx-1">/</span>
<span class="text-muted-foreground">Out:</span>
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}</span>
<!-- 阶梯计费标记 -->
<span
v-if="hasTieredPricing(model)"
class="ml-1 text-muted-foreground"
title="阶梯计费"
>[阶梯]</span>
</div>
<!-- 按次计费 -->
<div v-if="model.default_price_per_request && model.default_price_per_request > 0">
<span class="text-muted-foreground">按次:</span>
<span class="font-mono ml-1">${{ model.default_price_per_request.toFixed(3) }}/</span>
</div>
<!-- 无计费配置 -->
<div
v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request"
class="text-muted-foreground"
>
-
</div>
</div>
</TableCell>
<TableCell class="text-center">
<Badge variant="secondary">
{{ model.provider_count || 0 }}
</Badge>
</TableCell>
<TableCell class="text-center">
<Badge variant="secondary">
{{ model.alias_count || 0 }}
</Badge>
</TableCell>
<TableCell class="text-center">
<span class="text-sm font-mono">{{ formatUsageCount(model.usage_count || 0) }}</span>
</TableCell>
<TableCell>
<Badge :variant="model.is_active ? 'default' : 'secondary'">
{{ model.is_active ? '活跃' : '停用' }}
</Badge>
</TableCell>
<TableCell>
<div class="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="查看详情"
@click.stop="selectModel(model)"
>
<Eye class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="编辑模型"
@click.stop="editModel(model)"
>
<Edit class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
:title="model.is_active ? '停用模型' : '启用模型'"
@click.stop="toggleModelStatus(model)"
>
<Power class="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
title="删除模型"
@click.stop="deleteModel(model)"
>
<Trash2 class="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
<!-- 分页 -->
<Pagination
v-if="!loading && filteredGlobalModels.length > 0"
:current="catalogCurrentPage"
:total="filteredGlobalModels.length"
:page-size="catalogPageSize"
@update:current="catalogCurrentPage = $event"
@update:page-size="catalogPageSize = $event"
/>
</Card>
<!-- 分页 -->
<Pagination
v-if="!loading && filteredGlobalModels.length > 0"
:current="catalogCurrentPage"
:total="filteredGlobalModels.length"
:page-size="catalogPageSize"
@update:current="catalogCurrentPage = $event"
@update:page-size="catalogPageSize = $event"
/>
</Card>
</div>
<!-- 创建/编辑模型对话框 -->
@@ -299,22 +361,32 @@
<!-- 批量添加关联提供商对话框 -->
<Dialog
:model-value="batchAddProvidersDialogOpen"
@update:model-value="handleBatchAddProvidersDialogUpdate"
title="批量添加关联提供商"
description="为模型批量添加 Provider 实现, 提供商将自动继承模型的价格和能力, 可在添加后单独修改"
:icon="Server"
size="4xl"
@update:model-value="handleBatchAddProvidersDialogUpdate"
>
<template #default>
<div v-if="selectedModel" class="space-y-4">
<div
v-if="selectedModel"
class="space-y-4"
>
<!-- 模型信息头部 -->
<div class="rounded-lg border bg-muted/30 p-4">
<div class="flex items-start justify-between">
<div>
<p class="font-semibold text-lg">{{ selectedModel.display_name }}</p>
<p class="text-sm text-muted-foreground font-mono">{{ selectedModel.name }}</p>
<p class="font-semibold text-lg">
{{ selectedModel.display_name }}
</p>
<p class="text-sm text-muted-foreground font-mono">
{{ selectedModel.name }}
</p>
</div>
<Badge variant="outline" class="text-xs">
<Badge
variant="outline"
class="text-xs"
>
当前 {{ selectedModelProviders.length }} Provider
</Badge>
</div>
@@ -326,7 +398,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="availableProvidersForBatchAdd.length > 0"
variant="ghost"
@@ -337,19 +411,33 @@
{{ isAllLeftSelected ? '取消全选' : '全选' }}
</Button>
</div>
<Badge variant="secondary" class="text-xs">
<Badge
variant="secondary"
class="text-xs"
>
{{ availableProvidersForBatchAdd.length }}
</Badge>
</div>
<div class="border rounded-lg h-80 overflow-y-auto">
<div v-if="loadingProviderOptions" class="flex items-center justify-center h-full">
<div
v-if="loadingProviderOptions"
class="flex items-center justify-center h-full"
>
<Loader2 class="w-6 h-6 animate-spin text-primary" />
</div>
<div v-else-if="availableProvidersForBatchAdd.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
<div
v-else-if="availableProvidersForBatchAdd.length === 0"
class="flex flex-col items-center justify-center h-full text-muted-foreground"
>
<Building2 class="w-10 h-10 mb-2 opacity-30" />
<p class="text-sm">所有 Provider 均已关联</p>
<p class="text-sm">
所有 Provider 均已关联
</p>
</div>
<div v-else class="p-2 space-y-1">
<div
v-else
class="p-2 space-y-1"
>
<div
v-for="provider in availableProvidersForBatchAdd"
:key="provider.id"
@@ -365,8 +453,12 @@
@click.stop
/>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">{{ provider.display_name || provider.name }}</p>
<p class="text-xs text-muted-foreground truncate">{{ provider.name }}</p>
<p class="font-medium text-sm truncate">
{{ provider.display_name || provider.name }}
</p>
<p class="text-xs text-muted-foreground truncate">
{{ provider.name }}
</p>
</div>
<Badge
:variant="provider.is_active ? 'outline' : 'secondary'"
@@ -388,11 +480,18 @@
class="w-9 h-8"
:class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'border-primary' : ''"
:disabled="selectedLeftProviderIds.length === 0 || submittingBatchAddProviders"
@click="batchAddSelectedProviders"
title="添加选中"
@click="batchAddSelectedProviders"
>
<Loader2 v-if="submittingBatchAddProviders" class="w-4 h-4 animate-spin" />
<ChevronRight v-else class="w-6 h-6 stroke-[3]" :class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'text-primary' : ''" />
<Loader2
v-if="submittingBatchAddProviders"
class="w-4 h-4 animate-spin"
/>
<ChevronRight
v-else
class="w-6 h-6 stroke-[3]"
:class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'text-primary' : ''"
/>
</Button>
<Button
variant="outline"
@@ -400,11 +499,18 @@
class="w-9 h-8"
:class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'border-primary' : ''"
:disabled="selectedRightProviderIds.length === 0 || submittingBatchRemoveProviders"
@click="batchRemoveSelectedProviders"
title="移除选中"
@click="batchRemoveSelectedProviders"
>
<Loader2 v-if="submittingBatchRemoveProviders" class="w-4 h-4 animate-spin" />
<ChevronLeft v-else class="w-6 h-6 stroke-[3]" :class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'text-primary' : ''" />
<Loader2
v-if="submittingBatchRemoveProviders"
class="w-4 h-4 animate-spin"
/>
<ChevronLeft
v-else
class="w-6 h-6 stroke-[3]"
:class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'text-primary' : ''"
/>
</Button>
</div>
@@ -412,7 +518,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="selectedModelProviders.length > 0"
variant="ghost"
@@ -423,16 +531,27 @@
{{ isAllRightSelected ? '取消全选' : '全选' }}
</Button>
</div>
<Badge variant="secondary" class="text-xs">
<Badge
variant="secondary"
class="text-xs"
>
{{ selectedModelProviders.length }}
</Badge>
</div>
<div class="border rounded-lg h-80 overflow-y-auto">
<div v-if="selectedModelProviders.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
<div
v-if="selectedModelProviders.length === 0"
class="flex flex-col items-center justify-center h-full text-muted-foreground"
>
<Building2 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="provider in selectedModelProviders"
@@ -449,8 +568,12 @@
@click.stop
/>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">{{ provider.display_name }}</p>
<p class="text-xs text-muted-foreground truncate">{{ provider.identifier }}</p>
<p class="font-medium text-sm truncate">
{{ provider.display_name }}
</p>
<p class="text-xs text-muted-foreground truncate">
{{ provider.identifier }}
</p>
</div>
<Badge
:variant="provider.is_active ? 'outline' : 'secondary'"
@@ -485,7 +608,6 @@
@update:open="handleEditProviderDialogUpdate"
@saved="handleEditProviderSaved"
/>
</div>
</template>
@@ -623,9 +745,9 @@ const { confirmDanger } = useConfirm()
// 格式化调用次数(大数字简化显示)
function formatUsageCount(count: number): string {
if (count >= 1000000) {
return (count / 1000000).toFixed(1) + 'M'
return `${(count / 1000000).toFixed(1) }M`
} else if (count >= 1000) {
return (count / 1000).toFixed(1) + 'K'
return `${(count / 1000).toFixed(1) }K`
}
return count.toString()
}