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

@@ -1,18 +1,27 @@
<template>
<div class="space-y-6 pb-8">
<!-- 公告列表卡片 -->
<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">
<div>
<h3 class="text-base font-semibold">公告管理</h3>
<h3 class="text-base font-semibold">
公告管理
</h3>
<p class="text-xs text-muted-foreground mt-0.5">
{{ isAdmin ? '管理系统公告和通知' : '查看系统公告和通知' }}
</p>
</div>
<div class="flex items-center gap-2">
<Badge v-if="unreadCount > 0" variant="default" class="px-3 py-1">
<Badge
v-if="unreadCount > 0"
variant="default"
class="px-3 py-1"
>
{{ unreadCount }} 条未读
</Badge>
<div class="h-4 w-px bg-border" />
@@ -21,39 +30,80 @@
variant="ghost"
size="icon"
class="h-8 w-8"
@click="openCreateDialog"
title="新建公告"
@click="openCreateDialog"
>
<Plus class="w-3.5 h-3.5" />
</Button>
<RefreshButton :loading="loading" @click="loadAnnouncements(currentPage)" />
<RefreshButton
:loading="loading"
@click="loadAnnouncements(currentPage)"
/>
</div>
</div>
</div>
<!-- 内容区域 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<div
v-if="loading"
class="flex items-center justify-center py-12"
>
<Loader2 class="w-8 h-8 animate-spin text-primary" />
</div>
<div v-else-if="announcements.length === 0" class="flex flex-col items-center justify-center py-12 text-center">
<div
v-else-if="announcements.length === 0"
class="flex flex-col items-center justify-center py-12 text-center"
>
<Bell class="h-12 w-12 text-muted-foreground mb-3" />
<h3 class="text-sm font-medium text-foreground">暂无公告</h3>
<p class="text-xs text-muted-foreground mt-1">系统暂时没有发布任何公告</p>
<h3 class="text-sm font-medium text-foreground">
暂无公告
</h3>
<p class="text-xs text-muted-foreground mt-1">
系统暂时没有发布任何公告
</p>
</div>
<div v-else class="overflow-x-auto">
<div
v-else
class="overflow-x-auto"
>
<Table>
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="w-[80px] h-12 font-semibold text-center">类型</TableHead>
<TableHead class="h-12 font-semibold">概要</TableHead>
<TableHead class="w-[120px] h-12 font-semibold">发布者</TableHead>
<TableHead class="w-[140px] h-12 font-semibold">发布时间</TableHead>
<TableHead class="w-[80px] h-12 font-semibold text-center">状态</TableHead>
<TableHead v-if="isAdmin" class="w-[80px] h-12 font-semibold text-center">置顶</TableHead>
<TableHead v-if="isAdmin" class="w-[80px] h-12 font-semibold text-center">启用</TableHead>
<TableHead v-if="isAdmin" class="w-[100px] h-12 font-semibold text-center">操作</TableHead>
<TableHead class="w-[80px] h-12 font-semibold text-center">
类型
</TableHead>
<TableHead class="h-12 font-semibold">
概要
</TableHead>
<TableHead class="w-[120px] h-12 font-semibold">
发布者
</TableHead>
<TableHead class="w-[140px] h-12 font-semibold">
发布时间
</TableHead>
<TableHead class="w-[80px] h-12 font-semibold text-center">
状态
</TableHead>
<TableHead
v-if="isAdmin"
class="w-[80px] h-12 font-semibold text-center"
>
置顶
</TableHead>
<TableHead
v-if="isAdmin"
class="w-[80px] h-12 font-semibold text-center"
>
启用
</TableHead>
<TableHead
v-if="isAdmin"
class="w-[100px] h-12 font-semibold text-center"
>
操作
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -65,8 +115,15 @@
>
<TableCell class="py-4 text-center">
<div class="flex flex-col items-center gap-1">
<component :is="getAnnouncementIcon(announcement.type)" class="w-5 h-5" :class="getIconColor(announcement.type)" />
<span :class="['text-xs font-medium', getTypeTextColor(announcement.type)]">
<component
:is="getAnnouncementIcon(announcement.type)"
class="w-5 h-5"
:class="getIconColor(announcement.type)"
/>
<span
class="text-xs font-medium"
:class="[getTypeTextColor(announcement.type)]"
>
{{ getTypeLabel(announcement.type) }}
</span>
</div>
@@ -75,7 +132,10 @@
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-sm font-medium text-foreground">{{ announcement.title }}</span>
<Pin v-if="announcement.is_pinned" class="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
<Pin
v-if="announcement.is_pinned"
class="w-3.5 h-3.5 text-muted-foreground flex-shrink-0"
/>
</div>
<p class="text-xs text-muted-foreground line-clamp-1">
{{ getPlainText(announcement.content) }}
@@ -89,37 +149,67 @@
{{ formatDate(announcement.created_at) }}
</TableCell>
<TableCell class="py-4 text-center">
<Badge v-if="announcement.is_read" variant="secondary" class="text-xs px-2.5 py-0.5">
<Badge
v-if="announcement.is_read"
variant="secondary"
class="text-xs px-2.5 py-0.5"
>
已读
</Badge>
<Badge v-else variant="default" class="text-xs px-2.5 py-0.5">
<Badge
v-else
variant="default"
class="text-xs px-2.5 py-0.5"
>
未读
</Badge>
</TableCell>
<TableCell v-if="isAdmin" class="py-4" @click.stop>
<TableCell
v-if="isAdmin"
class="py-4"
@click.stop
>
<div class="flex items-center justify-center">
<Switch
:model-value="announcement.is_pinned"
@update:model-value="toggleAnnouncementPin(announcement, $event)"
class="data-[state=checked]:bg-emerald-500"
@update:model-value="toggleAnnouncementPin(announcement, $event)"
/>
</div>
</TableCell>
<TableCell v-if="isAdmin" class="py-4" @click.stop>
<TableCell
v-if="isAdmin"
class="py-4"
@click.stop
>
<div class="flex items-center justify-center">
<Switch
:model-value="announcement.is_active"
@update:model-value="toggleAnnouncementActive(announcement, $event)"
class="data-[state=checked]:bg-primary"
@update:model-value="toggleAnnouncementActive(announcement, $event)"
/>
</div>
</TableCell>
<TableCell v-if="isAdmin" class="py-4" @click.stop>
<TableCell
v-if="isAdmin"
class="py-4"
@click.stop
>
<div class="flex items-center justify-center gap-1">
<Button @click="openEditDialog(announcement)" variant="ghost" size="icon" class="h-8 w-8">
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="openEditDialog(announcement)"
>
<SquarePen class="w-4 h-4" />
</Button>
<Button @click="confirmDelete(announcement)" variant="ghost" size="icon" class="h-9 w-9 hover:bg-rose-500/10 hover:text-rose-600">
<Button
variant="ghost"
size="icon"
class="h-9 w-9 hover:bg-rose-500/10 hover:text-rose-600"
@click="confirmDelete(announcement)"
>
<Trash2 class="w-4 h-4" />
</Button>
</div>
@@ -141,7 +231,10 @@
</Card>
<!-- 创建/编辑公告对话框 -->
<Dialog v-model="dialogOpen" size="xl">
<Dialog
v-model="dialogOpen"
size="xl"
>
<template #header>
<div class="border-b border-border px-6 py-4">
<div class="flex items-center gap-3">
@@ -149,43 +242,96 @@
<Bell class="h-5 w-5 text-primary" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-foreground leading-tight">{{ editingAnnouncement ? '编辑公告' : '新建公告' }}</h3>
<p class="text-xs text-muted-foreground">{{ editingAnnouncement ? '修改公告内容和设置' : '发布新的系统公告' }}</p>
<h3 class="text-lg font-semibold text-foreground leading-tight">
{{ editingAnnouncement ? '编辑公告' : '新建公告' }}
</h3>
<p class="text-xs text-muted-foreground">
{{ editingAnnouncement ? '修改公告内容和设置' : '发布新的系统公告' }}
</p>
</div>
</div>
</div>
</template>
<form @submit.prevent="saveAnnouncement" class="space-y-4">
<form
class="space-y-4"
@submit.prevent="saveAnnouncement"
>
<div class="space-y-2">
<Label for="title" class="text-sm font-medium">标题 *</Label>
<Input id="title" v-model="formData.title" placeholder="输入公告标题" class="h-11" required />
<Label
for="title"
class="text-sm font-medium"
>标题 *</Label>
<Input
id="title"
v-model="formData.title"
placeholder="输入公告标题"
class="h-11"
required
/>
</div>
<div class="space-y-2">
<Label for="content" class="text-sm font-medium">内容 * (支持 Markdown)</Label>
<Textarea id="content" v-model="formData.content" placeholder="输入公告内容,支持 Markdown 格式" rows="10" required />
<Label
for="content"
class="text-sm font-medium"
>内容 * (支持 Markdown)</Label>
<Textarea
id="content"
v-model="formData.content"
placeholder="输入公告内容,支持 Markdown 格式"
rows="10"
required
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="type" class="text-sm font-medium">类型</Label>
<Select v-model="formData.type" v-model:open="typeSelectOpen">
<SelectTrigger id="type" class="h-11">
<Label
for="type"
class="text-sm font-medium"
>类型</Label>
<Select
v-model="formData.type"
v-model:open="typeSelectOpen"
>
<SelectTrigger
id="type"
class="h-11"
>
<SelectValue placeholder="选择类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="info">信息</SelectItem>
<SelectItem value="warning">警告</SelectItem>
<SelectItem value="maintenance">维护</SelectItem>
<SelectItem value="important">重要</SelectItem>
<SelectItem value="info">
信息
</SelectItem>
<SelectItem value="warning">
警告
</SelectItem>
<SelectItem value="maintenance">
维护
</SelectItem>
<SelectItem value="important">
重要
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="priority" class="text-sm font-medium">优先级</Label>
<Input id="priority" v-model.number="formData.priority" type="number" placeholder="0" class="h-11" min="0" max="10" />
<Label
for="priority"
class="text-sm font-medium"
>优先级</Label>
<Input
id="priority"
v-model.number="formData.priority"
type="number"
placeholder="0"
class="h-11"
min="0"
max="10"
/>
</div>
</div>
@@ -196,27 +342,50 @@
v-model="formData.is_pinned"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
/>
<Label for="pinned" class="cursor-pointer text-sm">置顶公告</Label>
>
<Label
for="pinned"
class="cursor-pointer text-sm"
>置顶公告</Label>
</div>
<div v-if="editingAnnouncement" class="flex items-center gap-2">
<div
v-if="editingAnnouncement"
class="flex items-center gap-2"
>
<input
id="active"
v-model="formData.is_active"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
/>
<Label for="active" class="cursor-pointer text-sm">启用</Label>
>
<Label
for="active"
class="cursor-pointer text-sm"
>启用</Label>
</div>
</div>
</form>
<template #footer>
<Button @click="saveAnnouncement" :disabled="saving" class="h-10 px-5">
<Loader2 v-if="saving" class="animate-spin h-4 w-4 mr-2" />
<Button
:disabled="saving"
class="h-10 px-5"
@click="saveAnnouncement"
>
<Loader2
v-if="saving"
class="animate-spin h-4 w-4 mr-2"
/>
{{ editingAnnouncement ? '保存' : '创建' }}
</Button>
<Button variant="outline" @click="dialogOpen = false" type="button" class="h-10 px-5">取消</Button>
<Button
variant="outline"
type="button"
class="h-10 px-5"
@click="dialogOpen = false"
>
取消
</Button>
</template>
</Dialog>
@@ -233,27 +402,40 @@
/>
<!-- 公告详情对话框 -->
<Dialog v-model="detailDialogOpen" size="lg">
<Dialog
v-model="detailDialogOpen"
size="lg"
>
<template #header>
<div class="border-b border-border px-6 py-4">
<div class="flex items-center gap-3">
<div class="flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0" :class="getDialogIconClass(viewingAnnouncement?.type)">
<div
class="flex h-9 w-9 items-center justify-center rounded-lg flex-shrink-0"
:class="getDialogIconClass(viewingAnnouncement?.type)"
>
<component
v-if="viewingAnnouncement"
:is="getAnnouncementIcon(viewingAnnouncement.type)"
v-if="viewingAnnouncement"
class="h-5 w-5"
:class="getIconColor(viewingAnnouncement.type)"
/>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-foreground leading-tight truncate">{{ viewingAnnouncement?.title || '公告详情' }}</h3>
<p class="text-xs text-muted-foreground">系统公告</p>
<h3 class="text-lg font-semibold text-foreground leading-tight truncate">
{{ viewingAnnouncement?.title || '公告详情' }}
</h3>
<p class="text-xs text-muted-foreground">
系统公告
</p>
</div>
</div>
</div>
</template>
<div v-if="viewingAnnouncement" class="space-y-4">
<div
v-if="viewingAnnouncement"
class="space-y-4"
>
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-muted-foreground">
<span>{{ viewingAnnouncement.author.username }}</span>
<span>·</span>
@@ -261,13 +443,20 @@
</div>
<div
v-html="renderMarkdown(viewingAnnouncement.content)"
class="prose prose-sm dark:prose-invert max-w-none"
></div>
v-html="renderMarkdown(viewingAnnouncement.content)"
/>
</div>
<template #footer>
<Button variant="outline" @click="detailDialogOpen = false" type="button" class="h-10 px-5">关闭</Button>
<Button
variant="outline"
type="button"
class="h-10 px-5"
@click="detailDialogOpen = false"
>
关闭
</Button>
</template>
</Dialog>
</div>

View File

@@ -6,7 +6,9 @@
<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>
<h3 class="text-base font-semibold">
可用模型
</h3>
<!-- 右侧操作区 -->
<div class="flex items-center gap-2">
@@ -29,8 +31,8 @@
<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="Vision"
@click="capabilityFilters.vision = !capabilityFilters.vision"
>
<Eye class="w-3.5 h-3.5" />
</button>
@@ -38,8 +40,8 @@
<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="Tool Use"
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
>
<Wrench class="w-3.5 h-3.5" />
</button>
@@ -47,8 +49,8 @@
<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="Extended Thinking"
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
>
<Brain class="w-3.5 h-3.5" />
</button>
@@ -57,7 +59,10 @@
<div class="h-4 w-px bg-border" />
<!-- 刷新按钮 -->
<RefreshButton :loading="loading" @click="refreshData" />
<RefreshButton
:loading="loading"
@click="refreshData"
/>
</div>
</div>
</div>
@@ -66,21 +71,37 @@
<Table class="table-fixed w-full">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="w-[140px] h-12 font-semibold">模型名称</TableHead>
<TableHead class="w-[120px] h-12 font-semibold">模型偏好</TableHead>
<TableHead class="w-[100px] h-12 font-semibold">能力</TableHead>
<TableHead class="w-[140px] h-12 font-semibold text-center">价格 ($/M)</TableHead>
<TableHead class="w-[70px] h-12 font-semibold text-center">状态</TableHead>
<TableHead class="w-[140px] h-12 font-semibold">
模型名称
</TableHead>
<TableHead class="w-[120px] h-12 font-semibold">
模型偏好
</TableHead>
<TableHead class="w-[100px] h-12 font-semibold">
能力
</TableHead>
<TableHead class="w-[140px] h-12 font-semibold text-center">
价格 ($/M)
</TableHead>
<TableHead class="w-[70px] h-12 font-semibold text-center">
状态
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell colspan="5" class="text-center py-12">
<TableCell
colspan="5"
class="text-center py-12"
>
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
<TableRow v-else-if="filteredModels.length === 0">
<TableCell colspan="5" class="text-center py-12 text-muted-foreground">
<TableCell
colspan="5"
class="text-center py-12 text-muted-foreground"
>
没有找到匹配的模型
</TableCell>
</TableRow>
@@ -115,8 +136,8 @@
<button
v-for="cap in getModelSupportedCapabilitiesDetails(model)"
:key="cap.name"
class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all"
:class="[
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all',
isCapabilityEnabled(model.name, cap.name)
? 'bg-primary text-primary-foreground'
: 'bg-transparent text-muted-foreground border border-dashed border-muted-foreground/50 hover:border-primary/50 hover:text-foreground'
@@ -124,19 +145,40 @@
:title="cap.description"
@click.stop="toggleCapability(model.name, cap.name)"
>
<Check v-if="isCapabilityEnabled(model.name, cap.name)" class="w-3 h-3" />
<Plus v-else class="w-3 h-3" />
<Check
v-if="isCapabilityEnabled(model.name, cap.name)"
class="w-3 h-3"
/>
<Plus
v-else
class="w-3 h-3"
/>
{{ cap.short_name || cap.display_name }}
</button>
</template>
<span v-else class="text-muted-foreground text-xs">-</span>
<span
v-else
class="text-muted-foreground text-xs"
>-</span>
</div>
</TableCell>
<TableCell class="py-4">
<div class="flex gap-1.5">
<Eye v-if="model.default_supports_vision" class="w-4 h-4 text-muted-foreground" title="Vision" />
<Wrench v-if="model.default_supports_function_calling" class="w-4 h-4 text-muted-foreground" title="Tool Use" />
<Brain v-if="model.default_supports_extended_thinking" class="w-4 h-4 text-muted-foreground" title="Extended Thinking" />
<Eye
v-if="model.default_supports_vision"
class="w-4 h-4 text-muted-foreground"
title="Vision"
/>
<Wrench
v-if="model.default_supports_function_calling"
class="w-4 h-4 text-muted-foreground"
title="Tool Use"
/>
<Brain
v-if="model.default_supports_extended_thinking"
class="w-4 h-4 text-muted-foreground"
title="Extended Thinking"
/>
</div>
</TableCell>
<TableCell class="py-4 text-center">
@@ -148,7 +190,11 @@
<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>
<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">
@@ -156,7 +202,12 @@
<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
v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request"
class="text-muted-foreground"
>
-
</div>
</div>
</TableCell>
<TableCell class="py-4 text-center">
@@ -177,7 +228,7 @@
:total="filteredModels.length"
:page-size="pageSize"
@update:current="currentPage = $event"
@update:pageSize="pageSize = $event"
@update:page-size="pageSize = $event"
/>
</Card>

View File

@@ -1,11 +1,16 @@
<template>
<div class="space-y-6 pb-8">
<!-- API Keys 表格 -->
<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">我的 API Keys</h3>
<h3 class="text-base font-semibold">
我的 API Keys
</h3>
<!-- 操作按钮 -->
<div class="flex items-center gap-2">
@@ -14,32 +19,45 @@
variant="ghost"
size="icon"
class="h-8 w-8"
@click="showCreateDialog = true"
title="创建新 API Key"
@click="showCreateDialog = true"
>
<Plus class="w-3.5 h-3.5" />
</Button>
<!-- 刷新按钮 -->
<RefreshButton :loading="loading" @click="loadApiKeys" />
<RefreshButton
:loading="loading"
@click="loadApiKeys"
/>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<div
v-if="loading"
class="flex items-center justify-center py-12"
>
<LoadingState message="加载中..." />
</div>
<!-- 空状态 -->
<div v-else-if="apiKeys.length === 0" class="flex items-center justify-center py-12">
<div
v-else-if="apiKeys.length === 0"
class="flex items-center justify-center py-12"
>
<EmptyState
title="暂无 API 密钥"
description="创建你的第一个 API 密钥开始使用"
:icon="Key"
>
<template #actions>
<Button @click="showCreateDialog = true" size="lg" class="shadow-lg shadow-primary/20">
<Button
size="lg"
class="shadow-lg shadow-primary/20"
@click="showCreateDialog = true"
>
<Plus class="mr-2 h-4 w-4" />
创建新 API Key
</Button>
@@ -48,18 +66,37 @@
</div>
<!-- 桌面端表格 -->
<div v-else class="hidden md:block overflow-x-auto">
<div
v-else
class="hidden md:block overflow-x-auto"
>
<Table>
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="min-w-[200px] h-12 font-semibold">密钥名称</TableHead>
<TableHead class="min-w-[80px] h-12 font-semibold">能力</TableHead>
<TableHead class="min-w-[160px] h-12 font-semibold">密钥</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">费用(USD)</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">请求次数</TableHead>
<TableHead class="min-w-[70px] h-12 font-semibold text-center">状态</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">最后使用</TableHead>
<TableHead class="min-w-[80px] h-12 font-semibold text-center">操作</TableHead>
<TableHead class="min-w-[200px] h-12 font-semibold">
密钥名称
</TableHead>
<TableHead class="min-w-[80px] h-12 font-semibold">
能力
</TableHead>
<TableHead class="min-w-[160px] h-12 font-semibold">
密钥
</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">
费用(USD)
</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">
请求次数
</TableHead>
<TableHead class="min-w-[70px] h-12 font-semibold text-center">
状态
</TableHead>
<TableHead class="min-w-[100px] h-12 font-semibold">
最后使用
</TableHead>
<TableHead class="min-w-[80px] h-12 font-semibold text-center">
操作
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -71,7 +108,10 @@
<!-- 密钥名称 -->
<TableCell class="py-4">
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold truncate" :title="apiKey.name">
<div
class="text-sm font-semibold truncate"
:title="apiKey.name"
>
{{ apiKey.name }}
</div>
<div class="text-xs text-muted-foreground mt-0.5">
@@ -87,8 +127,8 @@
<button
v-for="cap in userConfigurableCapabilities"
:key="cap.name"
class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all"
:class="[
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all',
isCapabilityEnabled(apiKey, cap.name)
? 'bg-primary text-primary-foreground'
: 'bg-transparent text-muted-foreground border border-dashed border-muted-foreground/50 hover:border-primary/50 hover:text-foreground'
@@ -96,12 +136,21 @@
:title="getCapabilityTooltip(cap, isCapabilityEnabled(apiKey, cap.name))"
@click.stop="toggleCapability(apiKey, cap.name)"
>
<Check v-if="isCapabilityEnabled(apiKey, cap.name)" class="w-3 h-3" />
<Plus v-else class="w-3 h-3" />
<Check
v-if="isCapabilityEnabled(apiKey, cap.name)"
class="w-3 h-3"
/>
<Plus
v-else
class="w-3 h-3"
/>
{{ cap.short_name || cap.display_name }}
</button>
</template>
<span v-else class="text-muted-foreground text-xs">-</span>
<span
v-else
class="text-muted-foreground text-xs"
>-</span>
</div>
</TableCell>
@@ -112,11 +161,11 @@
{{ apiKey.key_display || 'sk-••••••••' }}
</code>
<Button
@click="copyApiKey(apiKey)"
variant="ghost"
size="icon"
class="h-6 w-6"
title="复制完整密钥"
@click="copyApiKey(apiKey)"
>
<Copy class="h-3.5 w-3.5" />
</Button>
@@ -159,20 +208,20 @@
<TableCell class="py-4">
<div class="flex justify-center gap-1">
<Button
@click="toggleApiKey(apiKey)"
variant="ghost"
size="icon"
class="h-8 w-8"
:title="apiKey.is_active ? '禁用' : '启用'"
@click="toggleApiKey(apiKey)"
>
<Power class="h-4 w-4" />
</Button>
<Button
@click="confirmDelete(apiKey)"
variant="ghost"
size="icon"
class="h-8 w-8"
title="删除"
@click="confirmDelete(apiKey)"
>
<Trash2 class="h-4 w-4" />
</Button>
@@ -184,7 +233,10 @@
</div>
<!-- 移动端卡片列表 -->
<div v-if="!loading && apiKeys.length > 0" class="md:hidden space-y-3 p-4">
<div
v-if="!loading && apiKeys.length > 0"
class="md:hidden space-y-3 p-4"
>
<Card
v-for="apiKey in paginatedApiKeys"
:key="apiKey.id"
@@ -195,7 +247,9 @@
<!-- 第一行名称状态操作 -->
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 min-w-0 flex-1">
<h3 class="text-sm font-semibold text-foreground truncate">{{ apiKey.name }}</h3>
<h3 class="text-sm font-semibold text-foreground truncate">
{{ apiKey.name }}
</h3>
<Badge
:variant="apiKey.is_active ? 'success' : 'secondary'"
class="text-xs px-1.5 py-0"
@@ -205,29 +259,29 @@
</div>
<div class="flex items-center gap-0.5 flex-shrink-0">
<Button
@click="copyApiKey(apiKey)"
variant="ghost"
size="icon"
class="h-7 w-7"
title="复制"
@click="copyApiKey(apiKey)"
>
<Copy class="h-3.5 w-3.5" />
</Button>
<Button
@click="toggleApiKey(apiKey)"
variant="ghost"
size="icon"
class="h-7 w-7"
:title="apiKey.is_active ? '禁用' : '启用'"
@click="toggleApiKey(apiKey)"
>
<Power class="h-3.5 w-3.5" />
</Button>
<Button
@click="confirmDelete(apiKey)"
variant="ghost"
size="icon"
class="h-7 w-7"
title="删除"
@click="confirmDelete(apiKey)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
@@ -264,7 +318,7 @@
:total="apiKeys.length"
:page-size="pageSize"
@update:current="currentPage = $event"
@update:pageSize="pageSize = $event"
@update:page-size="pageSize = $event"
/>
</Card>
@@ -277,8 +331,12 @@
<Key class="h-5 w-5 text-primary" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-foreground leading-tight">创建 API 密钥</h3>
<p class="text-xs text-muted-foreground">创建一个新的密钥用于访问 API 服务</p>
<h3 class="text-lg font-semibold text-foreground leading-tight">
创建 API 密钥
</h3>
<p class="text-xs text-muted-foreground">
创建一个新的密钥用于访问 API 服务
</p>
</div>
</div>
</div>
@@ -286,7 +344,10 @@
<div class="space-y-4">
<div class="space-y-2">
<Label for="key-name" class="text-sm font-semibold">密钥名称</Label>
<Label
for="key-name"
class="text-sm font-semibold"
>密钥名称</Label>
<Input
id="key-name"
v-model="newKeyName"
@@ -295,21 +356,39 @@
autocomplete="off"
required
/>
<p class="text-xs text-muted-foreground">给密钥起一个有意义的名称方便识别</p>
<p class="text-xs text-muted-foreground">
给密钥起一个有意义的名称方便识别
</p>
</div>
</div>
<template #footer>
<Button variant="outline" class="h-11 px-6" @click="showCreateDialog = false">取消</Button>
<Button class="h-11 px-6 shadow-lg shadow-primary/20" @click="createApiKey" :disabled="creating">
<Loader2 v-if="creating" class="animate-spin h-4 w-4 mr-2" />
<Button
variant="outline"
class="h-11 px-6"
@click="showCreateDialog = false"
>
取消
</Button>
<Button
class="h-11 px-6 shadow-lg shadow-primary/20"
:disabled="creating"
@click="createApiKey"
>
<Loader2
v-if="creating"
class="animate-spin h-4 w-4 mr-2"
/>
{{ creating ? '创建中...' : '创建' }}
</Button>
</template>
</Dialog>
<!-- 新密钥创建成功对话框 -->
<Dialog v-model="showKeyDialog" size="lg">
<Dialog
v-model="showKeyDialog"
size="lg"
>
<template #header>
<div class="border-b border-border px-6 py-4">
<div class="flex items-center gap-3">
@@ -317,8 +396,12 @@
<CheckCircle class="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-foreground leading-tight">创建成功</h3>
<p class="text-xs text-muted-foreground">请妥善保管, 切勿泄露给他人</p>
<h3 class="text-lg font-semibold text-foreground leading-tight">
创建成功
</h3>
<p class="text-xs text-muted-foreground">
请妥善保管, 切勿泄露给他人
</p>
</div>
</div>
</div>
@@ -335,7 +418,10 @@
class="flex-1 font-mono text-sm bg-muted/50 h-11"
@click="($event.target as HTMLInputElement)?.select()"
/>
<Button @click="copyTextToClipboard(newKeyValue)" class="h-11">
<Button
class="h-11"
@click="copyTextToClipboard(newKeyValue)"
>
复制
</Button>
</div>
@@ -343,7 +429,12 @@
</div>
<template #footer>
<Button @click="showKeyDialog = false" class="h-10 px-5">确定</Button>
<Button
class="h-10 px-5"
@click="showKeyDialog = false"
>
确定
</Button>
</template>
</Dialog>
@@ -440,7 +531,7 @@ async function loadApiKeys() {
} else if (error.response.status === 401) {
showError('认证失败,请重新登录')
} else {
showError('加载 API 密钥失败:' + (error.response?.data?.detail || error.message))
showError(`加载 API 密钥失败:${ error.response?.data?.detail || error.message}`)
}
} finally {
loading.value = false

View File

@@ -1,22 +1,38 @@
<template>
<div class="container mx-auto px-4 py-8">
<h2 class="text-2xl font-bold text-foreground mb-6">个人设置</h2>
<h2 class="text-2xl font-bold text-foreground mb-6">
个人设置
</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左侧个人信息和密码 -->
<div class="lg:col-span-2 space-y-6">
<!-- 基本信息 -->
<Card class="p-6">
<h3 class="text-lg font-medium text-foreground mb-4">基本信息</h3>
<form @submit.prevent="updateProfile" class="space-y-4">
<h3 class="text-lg font-medium text-foreground mb-4">
基本信息
</h3>
<form
class="space-y-4"
@submit.prevent="updateProfile"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label for="username">用户名</Label>
<Input id="username" v-model="profileForm.username" class="mt-1" />
<Input
id="username"
v-model="profileForm.username"
class="mt-1"
/>
</div>
<div>
<Label for="email">邮箱</Label>
<Input id="email" v-model="profileForm.email" type="email" class="mt-1" />
<Input
id="email"
v-model="profileForm.email"
type="email"
class="mt-1"
/>
</div>
</div>
@@ -32,11 +48,21 @@
<div>
<Label for="avatar">头像 URL</Label>
<Input id="avatar" v-model="preferencesForm.avatar_url" type="url" class="mt-1" />
<p class="mt-1 text-sm text-muted-foreground">输入头像图片的 URL 地址</p>
<Input
id="avatar"
v-model="preferencesForm.avatar_url"
type="url"
class="mt-1"
/>
<p class="mt-1 text-sm text-muted-foreground">
输入头像图片的 URL 地址
</p>
</div>
<Button type="submit" :disabled="savingProfile">
<Button
type="submit"
:disabled="savingProfile"
>
{{ savingProfile ? '保存中...' : '保存修改' }}
</Button>
</form>
@@ -44,21 +70,44 @@
<!-- 修改密码 -->
<Card class="p-6">
<h3 class="text-lg font-medium text-foreground mb-4">修改密码</h3>
<form @submit.prevent="changePassword" class="space-y-4">
<h3 class="text-lg font-medium text-foreground mb-4">
修改密码
</h3>
<form
class="space-y-4"
@submit.prevent="changePassword"
>
<div>
<Label for="old-password">当前密码</Label>
<Input id="old-password" v-model="passwordForm.old_password" type="password" class="mt-1" />
<Input
id="old-password"
v-model="passwordForm.old_password"
type="password"
class="mt-1"
/>
</div>
<div>
<Label for="new-password">新密码</Label>
<Input id="new-password" v-model="passwordForm.new_password" type="password" class="mt-1" />
<Input
id="new-password"
v-model="passwordForm.new_password"
type="password"
class="mt-1"
/>
</div>
<div>
<Label for="confirm-password">确认新密码</Label>
<Input id="confirm-password" v-model="passwordForm.confirm_password" type="password" class="mt-1" />
<Input
id="confirm-password"
v-model="passwordForm.confirm_password"
type="password"
class="mt-1"
/>
</div>
<Button type="submit" :disabled="changingPassword">
<Button
type="submit"
:disabled="changingPassword"
>
{{ changingPassword ? '修改中...' : '修改密码' }}
</Button>
</form>
@@ -66,7 +115,9 @@
<!-- 偏好设置 -->
<Card class="p-6">
<h3 class="text-lg font-medium text-foreground mb-4">偏好设置</h3>
<h3 class="text-lg font-medium text-foreground mb-4">
偏好设置
</h3>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
@@ -76,13 +127,22 @@
v-model:open="themeSelectOpen"
@update:model-value="handleThemeChange"
>
<SelectTrigger id="theme" class="mt-1">
<SelectTrigger
id="theme"
class="mt-1"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">浅色</SelectItem>
<SelectItem value="dark">深色</SelectItem>
<SelectItem value="system">跟随系统</SelectItem>
<SelectItem value="light">
浅色
</SelectItem>
<SelectItem value="dark">
深色
</SelectItem>
<SelectItem value="system">
跟随系统
</SelectItem>
</SelectContent>
</Select>
</div>
@@ -94,62 +154,91 @@
v-model:open="languageSelectOpen"
@update:model-value="handleLanguageChange"
>
<SelectTrigger id="language" class="mt-1">
<SelectTrigger
id="language"
class="mt-1"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-CN">简体中文</SelectItem>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh-CN">
简体中文
</SelectItem>
<SelectItem value="en">
English
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label for="timezone">时区</Label>
<Input id="timezone" v-model="preferencesForm.timezone" placeholder="Asia/Shanghai" class="mt-1" />
<Input
id="timezone"
v-model="preferencesForm.timezone"
placeholder="Asia/Shanghai"
class="mt-1"
/>
</div>
</div>
<div class="space-y-3">
<h4 class="font-medium text-foreground">通知设置</h4>
<h4 class="font-medium text-foreground">
通知设置
</h4>
<div class="space-y-3">
<div class="flex items-center justify-between py-2 border-b border-border/40 last:border-0">
<div class="flex-1">
<Label for="email-notifications" class="text-sm font-medium cursor-pointer">
<Label
for="email-notifications"
class="text-sm font-medium cursor-pointer"
>
邮件通知
</Label>
<p class="text-xs text-muted-foreground mt-1">接收系统重要通知</p>
<p class="text-xs text-muted-foreground mt-1">
接收系统重要通知
</p>
</div>
<Switch
id="email-notifications"
v-model="preferencesForm.notifications.email"
@update:modelValue="updatePreferences"
@update:model-value="updatePreferences"
/>
</div>
<div class="flex items-center justify-between py-2 border-b border-border/40 last:border-0">
<div class="flex-1">
<Label for="usage-alerts" class="text-sm font-medium cursor-pointer">
<Label
for="usage-alerts"
class="text-sm font-medium cursor-pointer"
>
使用提醒
</Label>
<p class="text-xs text-muted-foreground mt-1">当接近配额限制时提醒</p>
<p class="text-xs text-muted-foreground mt-1">
当接近配额限制时提醒
</p>
</div>
<Switch
id="usage-alerts"
v-model="preferencesForm.notifications.usage_alerts"
@update:modelValue="updatePreferences"
@update:model-value="updatePreferences"
/>
</div>
<div class="flex items-center justify-between py-2">
<div class="flex-1">
<Label for="announcement-notifications" class="text-sm font-medium cursor-pointer">
<Label
for="announcement-notifications"
class="text-sm font-medium cursor-pointer"
>
公告通知
</Label>
<p class="text-xs text-muted-foreground mt-1">接收系统公告</p>
<p class="text-xs text-muted-foreground mt-1">
接收系统公告
</p>
</div>
<Switch
id="announcement-notifications"
v-model="preferencesForm.notifications.announcements"
@update:modelValue="updatePreferences"
@update:model-value="updatePreferences"
/>
</div>
</div>
@@ -162,7 +251,9 @@
<div class="space-y-6">
<!-- 账户信息 -->
<Card class="p-6">
<h3 class="text-lg font-medium text-foreground mb-4">账户信息</h3>
<h3 class="text-lg font-medium text-foreground mb-4">
账户信息
</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-muted-foreground">角色</span>
@@ -193,7 +284,9 @@
<!-- 使用配额 -->
<Card class="p-6">
<h3 class="text-lg font-medium text-foreground mb-4">使用配额</h3>
<h3 class="text-lg font-medium text-foreground mb-4">
使用配额
</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-1">
@@ -213,7 +306,7 @@
<div
class="bg-success h-2.5 rounded-full"
:style="`width: ${getUsagePercentage()}%`"
></div>
/>
</div>
</div>
</div>

View File

@@ -7,7 +7,10 @@
@click.self="handleClose"
>
<!-- 背景遮罩 -->
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="handleClose"></div>
<div
class="absolute inset-0 bg-black/30 backdrop-blur-sm"
@click="handleClose"
/>
<!-- 抽屉内容 -->
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
@@ -15,9 +18,14 @@
<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">
<h3 class="text-xl font-bold truncate">{{ model.display_name || model.name }}</h3>
<h3 class="text-xl font-bold truncate">
{{ model.display_name || model.name }}
</h3>
<div class="flex items-center gap-2">
<Badge :variant="model.is_active ? 'default' : 'secondary'" class="text-xs">
<Badge
:variant="model.is_active ? 'default' : 'secondary'"
class="text-xs"
>
{{ model.is_active ? '可用' : '停用' }}
</Badge>
<span class="text-sm text-muted-foreground font-mono">{{ model.name }}</span>
@@ -29,11 +37,19 @@
<Copy class="w-3 h-3 text-muted-foreground" />
</button>
</div>
<p v-if="model.description" class="text-xs text-muted-foreground">
<p
v-if="model.description"
class="text-xs text-muted-foreground"
>
{{ model.description }}
</p>
</div>
<Button variant="ghost" size="icon" @click="handleClose" title="关闭">
<Button
variant="ghost"
size="icon"
title="关闭"
@click="handleClose"
>
<X class="w-4 h-4" />
</Button>
</div>
@@ -42,55 +58,92 @@
<div class="p-6 space-y-6">
<!-- 模型能力 -->
<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">
<ImageIcon 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>
@@ -98,8 +151,13 @@
</div>
<!-- 模型偏好 -->
<div v-if="getModelUserConfigurableCapabilities().length > 0" class="space-y-3">
<h4 class="font-semibold text-sm">模型偏好</h4>
<div
v-if="getModelUserConfigurableCapabilities().length > 0"
class="space-y-3"
>
<h4 class="font-semibold text-sm">
模型偏好
</h4>
<div class="space-y-2">
<div
v-for="cap in getModelUserConfigurableCapabilities()"
@@ -107,12 +165,19 @@
class="flex items-center justify-between p-3 rounded-lg border"
>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium">{{ cap.display_name }}</p>
<p v-if="cap.description" class="text-xs text-muted-foreground truncate">{{ cap.description }}</p>
<p class="text-sm font-medium">
{{ cap.display_name }}
</p>
<p
v-if="cap.description"
class="text-xs text-muted-foreground truncate"
>
{{ cap.description }}
</p>
</div>
<button
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
:class="[
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isCapabilityEnabled(cap.name) ? 'bg-primary' : 'bg-muted'
]"
role="switch"
@@ -120,8 +185,8 @@
@click="handleToggleCapability(cap.name)"
>
<span
class="pointer-events-none inline-block h-4 w-4 transform rounded-full bg-background shadow-lg ring-0 transition duration-200 ease-in-out"
:class="[
'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-background shadow-lg ring-0 transition duration-200 ease-in-out',
isCapabilityEnabled(cap.name) ? 'translate-x-4' : 'translate-x-0'
]"
/>
@@ -132,10 +197,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">
<div class="p-3 rounded-lg border">
<Label class="text-xs text-muted-foreground">输入价格 ($/M)</Label>
@@ -163,19 +233,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>
@@ -186,12 +265,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>
@@ -201,7 +292,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>
@@ -229,7 +323,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>
@@ -269,6 +366,13 @@ import type { PublicGlobalModel } from '@/api/public-models'
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
import type { CapabilityDefinition } from '@/api/endpoints'
const props = defineProps<Props>()
const emit = defineEmits<{
'update:open': [value: boolean]
'toggle-capability': [modelName: string, capName: string]
}>()
const { success: showSuccess, error: showError } = useToast()
interface Props {
@@ -279,13 +383,6 @@ interface Props {
modelCapabilitySettings?: Record<string, Record<string, boolean>>
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:open': [value: boolean]
'toggle-capability': [modelName: string, capName: string]
}>()
// 根据能力名称获取显示名称
function getCapabilityDisplayName(capName: string): string {
const cap = props.capabilities?.find(c => c.name === capName)