mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
refactor(frontend): optimize view pages (admin, shared, user)
This commit is contained in:
@@ -2,12 +2,12 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<!-- 搜索和过滤区域 -->
|
<!-- 搜索和过滤区域 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||||
别名管理
|
别名管理
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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 z-10 pointer-events-none" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
id="alias-search"
|
id="alias-search"
|
||||||
v-model="aliasesSearch"
|
v-model="aliasesSearch"
|
||||||
placeholder="搜索别名或关联模型"
|
placeholder="搜索别名或关联模型"
|
||||||
class="w-44 pl-8 pr-3 h-8 text-sm border-border/60 focus-visible:ring-1"
|
class="w-32 sm:w-44 pl-8 pr-3 h-8 text-sm border-border/60 focus-visible:ring-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 提供商过滤器 -->
|
<!-- 提供商过滤器 -->
|
||||||
<Select
|
<Select
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
:model-value="aliasProviderFilter"
|
:model-value="aliasProviderFilter"
|
||||||
@update:model-value="aliasProviderFilter = $event"
|
@update:model-value="aliasProviderFilter = $event"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-28 sm:w-40 h-8 text-xs border-border/60">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<Button
|
<Button
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<Loader2 class="w-10 h-10 animate-spin text-primary" />
|
<Loader2 class="w-10 h-10 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Table class="text-sm">
|
<Table class="hidden xl:table text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-[200px]">
|
<TableHead class="w-[200px]">
|
||||||
@@ -185,6 +185,83 @@
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="filteredAliases.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="alias in paginatedAliases"
|
||||||
|
:key="alias.id"
|
||||||
|
class="p-4 space-y-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-mono font-medium truncate">{{ alias.alias }}</span>
|
||||||
|
<Badge
|
||||||
|
:variant="alias.is_active ? 'default' : 'secondary'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
|
{{ alias.is_active ? '活跃' : '停用' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
<span class="font-medium">{{ alias.global_model_display_name || alias.global_model_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="openEditAliasDialog(alias)"
|
||||||
|
>
|
||||||
|
<Edit class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="toggleAliasStatus(alias)"
|
||||||
|
>
|
||||||
|
<Power class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="confirmDeleteAlias(alias)"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="alias.provider_id"
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ alias.provider_name || 'Provider 特定' }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="default"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
全局
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="!loadingAliases && filteredAliases.length > 0"
|
v-if="!loadingAliases && filteredAliases.length > 0"
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
独立余额 API Keys
|
独立余额 API Keys
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground mt-0.5">
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
> · 即将到期 {{ expiringSoonCount }}</span>
|
> · 即将到期 {{ expiringSoonCount }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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 z-10 pointer-events-none" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
@@ -36,19 +36,19 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索..."
|
placeholder="搜索..."
|
||||||
class="h-8 w-40 pl-8 pr-2 text-xs"
|
class="h-8 w-28 sm:w-40 pl-8 pr-2 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 状态筛选 -->
|
<!-- 状态筛选 -->
|
||||||
<Select
|
<Select
|
||||||
v-model="filterStatus"
|
v-model="filterStatus"
|
||||||
v-model:open="filterStatusOpen"
|
v-model:open="filterStatusOpen"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部状态" />
|
<SelectValue placeholder="全部状态" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
v-model="filterBalance"
|
v-model="filterBalance"
|
||||||
v-model:open="filterBalanceOpen"
|
v-model:open="filterBalanceOpen"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||||
<SelectValue placeholder="全部类型" />
|
<SelectValue placeholder="全部类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 创建独立 Key 按钮 -->
|
<!-- 创建独立 Key 按钮 -->
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
审计日志
|
审计日志
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground mt-0.5">
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
查看系统所有操作记录
|
查看系统所有操作记录
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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 z-10 pointer-events-none" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
@@ -24,19 +24,19 @@
|
|||||||
id="audit-logs-search"
|
id="audit-logs-search"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索用户ID..."
|
placeholder="搜索用户ID..."
|
||||||
class="w-64 h-8 text-sm pl-8"
|
class="w-32 sm:w-64 h-8 text-sm pl-8"
|
||||||
@input="handleSearchChange"
|
@input="handleSearchChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
<!-- 事件类型筛选 -->
|
<!-- 事件类型筛选 -->
|
||||||
<Select
|
<Select
|
||||||
v-model="filters.eventType"
|
v-model="filters.eventType"
|
||||||
v-model:open="eventTypeSelectOpen"
|
v-model:open="eventTypeSelectOpen"
|
||||||
@update:model-value="handleEventTypeChange"
|
@update:model-value="handleEventTypeChange"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-40 h-8 border-border/60">
|
<SelectTrigger class="w-24 sm:w-40 h-8 border-border/60">
|
||||||
<SelectValue placeholder="全部类型" />
|
<SelectValue placeholder="全部类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
v-model:open="daysSelectOpen"
|
v-model:open="daysSelectOpen"
|
||||||
@update:model-value="handleDaysChange"
|
@update:model-value="handleDaysChange"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-28 h-8 border-border/60">
|
<SelectTrigger class="w-20 sm:w-28 h-8 border-border/60">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
>
|
>
|
||||||
<FilterX class="w-3.5 h-3.5" />
|
<FilterX class="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
<!-- 导出按钮 -->
|
<!-- 导出按钮 -->
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Table>
|
<Table class="hidden xl:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="h-12 font-semibold">
|
<TableHead class="h-12 font-semibold">
|
||||||
@@ -242,6 +242,60 @@
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="logs.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="logItem in logs"
|
||||||
|
:key="logItem.id"
|
||||||
|
class="p-4 space-y-2 hover:bg-muted/30 cursor-pointer transition-colors"
|
||||||
|
@click="showLogDetail(logItem)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<Badge :variant="getEventTypeBadgeVariant(logItem.event_type)">
|
||||||
|
<component
|
||||||
|
:is="getEventTypeIcon(logItem.event_type)"
|
||||||
|
class="h-3 w-3 mr-1"
|
||||||
|
/>
|
||||||
|
{{ getEventTypeLabel(logItem.event_type) }}
|
||||||
|
</Badge>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1.5">
|
||||||
|
{{ formatDateTime(logItem.created_at) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
v-if="logItem.status_code"
|
||||||
|
:variant="getStatusCodeVariant(logItem.status_code)"
|
||||||
|
class="shrink-0"
|
||||||
|
>
|
||||||
|
{{ logItem.status_code }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="logItem.user_id"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
{{ logItem.user_email || `用户 ${logItem.user_id}` }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-xs text-muted-foreground truncate"
|
||||||
|
:title="logItem.description"
|
||||||
|
>
|
||||||
|
{{ logItem.description || '无描述' }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="logItem.ip_address"
|
||||||
|
class="flex items-center text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Globe class="h-3 w-3 mr-1" />
|
||||||
|
{{ logItem.ip_address }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 分页控件 -->
|
<!-- 分页控件 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
:current="currentPage"
|
:current="currentPage"
|
||||||
|
|||||||
@@ -365,21 +365,19 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- 缓存亲和性列表 -->
|
<!-- 缓存亲和性列表 -->
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<div class="px-6 py-3 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div class="flex items-center gap-3">
|
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
亲和性列表
|
||||||
亲和性列表
|
</h3>
|
||||||
</h3>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="relative">
|
<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 z-10 pointer-events-none" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
<Input
|
<Input
|
||||||
id="cache-affinity-search"
|
id="cache-affinity-search"
|
||||||
v-model="tableKeyword"
|
v-model="tableKeyword"
|
||||||
placeholder="搜索用户或 Key"
|
placeholder="搜索用户或 Key"
|
||||||
class="w-48 h-8 text-sm pl-8 pr-8"
|
class="w-32 sm:w-48 h-8 text-sm pl-8 pr-8"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="tableKeyword"
|
v-if="tableKeyword"
|
||||||
@@ -390,7 +388,7 @@ onBeforeUnmount(() => {
|
|||||||
<X class="h-3.5 w-3.5" />
|
<X class="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -408,7 +406,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table class="hidden xl:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-36">
|
<TableHead class="w-36">
|
||||||
@@ -537,6 +535,60 @@ onBeforeUnmount(() => {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="!listLoading && affinityList.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in paginatedAffinityList"
|
||||||
|
:key="`m-${item.affinity_key}-${item.endpoint_id}-${item.key_id}`"
|
||||||
|
class="p-4 space-y-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<Badge
|
||||||
|
v-if="item.is_standalone"
|
||||||
|
variant="outline"
|
||||||
|
class="text-warning border-warning/30 text-[10px] px-1"
|
||||||
|
>
|
||||||
|
独立
|
||||||
|
</Badge>
|
||||||
|
<span class="text-sm font-medium truncate">{{ item.username || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{{ item.user_api_key_name || '未命名' }} · {{ item.user_api_key_prefix || '---' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-7 w-7 text-muted-foreground/70 hover:text-destructive shrink-0"
|
||||||
|
:disabled="clearingRowAffinityKey === item.affinity_key"
|
||||||
|
@click="clearUserCache(item.affinity_key, item.user_api_key_name || item.affinity_key)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>{{ item.provider_name || '未知' }}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-muted-foreground">{{ item.endpoint_api_format || '---' }}</span>
|
||||||
|
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!listLoading && affinityList.length === 0"
|
||||||
|
class="xl:hidden text-center py-6 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
暂无缓存记录
|
||||||
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="affinityList.length > 0"
|
v-if="affinityList.length > 0"
|
||||||
:current="currentPage"
|
:current="currentPage"
|
||||||
@@ -549,18 +601,18 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- TTL 分析区域 -->
|
<!-- TTL 分析区域 -->
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<div class="px-6 py-3 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3 shrink-0">
|
||||||
<BarChart3 class="h-5 w-5 text-muted-foreground" />
|
<BarChart3 class="h-5 w-5 text-muted-foreground hidden sm:block" />
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
TTL 分析
|
TTL 分析
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-xs text-muted-foreground">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
<span class="text-xs text-muted-foreground hidden sm:inline">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Select v-model="analysisHours">
|
<Select v-model="analysisHours">
|
||||||
<SelectTrigger class="w-28 h-8">
|
<SelectTrigger class="w-24 sm:w-28 h-8">
|
||||||
<SelectValue placeholder="时间段" />
|
<SelectValue placeholder="时间段" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -44,17 +44,17 @@
|
|||||||
variant="default"
|
variant="default"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
IP 黑名单
|
IP 黑名单
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground mt-0.5">
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
管理被禁止访问的 IP 地址
|
管理被禁止访问的 IP 地址
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -114,17 +114,17 @@
|
|||||||
variant="default"
|
variant="default"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
IP 白名单
|
IP 白名单
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground mt-0.5">
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
管理可信任的 IP 地址(支持 CIDR 格式)
|
管理可信任的 IP 地址(支持 CIDR 格式)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Table>
|
<Table class="hidden sm:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>IP 地址 / CIDR</TableHead>
|
<TableHead>IP 地址 / CIDR</TableHead>
|
||||||
@@ -189,6 +189,25 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div class="sm:hidden divide-y divide-border/40">
|
||||||
|
<div
|
||||||
|
v-for="ip in whitelistData.whitelist"
|
||||||
|
:key="ip"
|
||||||
|
class="p-4 flex items-center justify-between gap-3"
|
||||||
|
>
|
||||||
|
<span class="font-mono text-sm truncate">{{ ip }}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 px-3 shrink-0"
|
||||||
|
@click="handleRemoveFromWhitelist(ip)"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<!-- 左侧:标题 -->
|
<!-- 左侧:标题 -->
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||||
模型管理
|
模型管理
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- 右侧:操作区 -->
|
<!-- 右侧:操作区 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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" />
|
<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" />
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索模型名称..."
|
placeholder="搜索模型名称..."
|
||||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
class="w-32 sm:w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 能力筛选 -->
|
<!-- 能力筛选 -->
|
||||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<Button
|
<Button
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table class="hidden xl:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-[240px]">
|
<TableHead class="w-[240px]">
|
||||||
@@ -302,6 +302,109 @@
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="!loading && filteredGlobalModels.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="model in paginatedGlobalModels"
|
||||||
|
:key="model.id"
|
||||||
|
class="p-4 space-y-3 hover:bg-muted/50 cursor-pointer transition-colors"
|
||||||
|
@click="selectModel(model)"
|
||||||
|
>
|
||||||
|
<!-- 第一行:名称 + 状态 + 操作 -->
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium truncate">{{ model.display_name }}</span>
|
||||||
|
<Badge
|
||||||
|
:variant="model.is_active ? 'default' : 'secondary'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
|
{{ model.is_active ? '活跃' : '停用' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
||||||
|
<span class="font-mono truncate">{{ model.name }}</span>
|
||||||
|
<button
|
||||||
|
class="p-0.5 rounded hover:bg-muted transition-colors shrink-0"
|
||||||
|
@click.stop="copyToClipboard(model.name)"
|
||||||
|
>
|
||||||
|
<Copy class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-0.5 shrink-0"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="editModel(model)"
|
||||||
|
>
|
||||||
|
<Edit class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="toggleModelStatus(model)"
|
||||||
|
>
|
||||||
|
<Power class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="deleteModel(model)"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:能力图标 -->
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<Zap
|
||||||
|
v-if="model.default_supports_streaming"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
v-if="model.default_supports_image_generation"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Eye
|
||||||
|
v-if="model.default_supports_vision"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Wrench
|
||||||
|
v-if="model.default_supports_function_calling"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Brain
|
||||||
|
v-if="model.default_supports_extended_thinking"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三行:统计信息 -->
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>提供商 {{ model.provider_count || 0 }}</span>
|
||||||
|
<span>别名 {{ model.alias_count || 0 }}</span>
|
||||||
|
<span>调用 {{ formatUsageCount(model.usage_count || 0) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')"
|
||||||
|
class="font-mono"
|
||||||
|
>
|
||||||
|
${{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}/${{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="!loading && filteredGlobalModels.length > 0"
|
v-if="!loading && filteredGlobalModels.length > 0"
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/50">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/50">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<!-- 左侧:标题 -->
|
<!-- 左侧:标题 -->
|
||||||
<h3 class="text-base font-semibold text-foreground">
|
<h3 class="text-sm sm:text-base font-semibold text-foreground shrink-0">
|
||||||
提供商管理
|
提供商管理
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- 右侧:操作区 -->
|
<!-- 右侧:操作区 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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" />
|
<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" />
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索提供商..."
|
placeholder="搜索提供商..."
|
||||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
class="w-32 sm:w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 调度策略 -->
|
<!-- 调度策略 -->
|
||||||
<button
|
<button
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
title="点击调整调度策略"
|
title="点击调整调度策略"
|
||||||
@click="openPriorityDialog"
|
@click="openPriorityDialog"
|
||||||
>
|
>
|
||||||
<span class="text-muted-foreground/80">调度:</span>
|
<span class="text-muted-foreground/80 hidden sm:inline">调度:</span>
|
||||||
<span class="font-medium text-foreground/90">{{ priorityModeConfig.label }}</span>
|
<span class="font-medium text-foreground/90">{{ priorityModeConfig.label }}</span>
|
||||||
<ChevronDown class="w-3 h-3 text-muted-foreground/70 group-hover:text-foreground transition-colors" />
|
<ChevronDown class="w-3 h-3 text-muted-foreground/70 group-hover:text-foreground transition-colors" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<Button
|
<Button
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<!-- 桌面端表格 -->
|
<!-- 桌面端表格 -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="overflow-x-auto"
|
class="hidden xl:block overflow-x-auto"
|
||||||
>
|
>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -289,6 +289,118 @@
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="!loading && filteredProviders.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="provider in paginatedProviders"
|
||||||
|
:key="provider.id"
|
||||||
|
class="p-4 space-y-3 hover:bg-muted/20 transition-colors cursor-pointer"
|
||||||
|
@click="openProviderDrawer(provider.id)"
|
||||||
|
>
|
||||||
|
<!-- 第一行:名称 + 状态 + 操作 -->
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-foreground truncate">{{ provider.display_name }}</span>
|
||||||
|
<Badge
|
||||||
|
:variant="provider.is_active ? 'success' : 'secondary'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
|
{{ provider.is_active ? '活跃' : '停用' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-muted-foreground/70 font-mono">{{ provider.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-0.5 shrink-0"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="openEditProviderDialog(provider)"
|
||||||
|
>
|
||||||
|
<Edit class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="toggleProviderStatus(provider)"
|
||||||
|
>
|
||||||
|
<Power class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="handleDeleteProvider(provider)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:计费类型 + 资源统计 -->
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs font-normal border-border/50"
|
||||||
|
>
|
||||||
|
{{ formatBillingType(provider.billing_type || 'pay_as_you_go') }}
|
||||||
|
</Badge>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
端点 {{ provider.active_endpoints }}/{{ provider.total_endpoints }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
密钥 {{ provider.active_keys }}/{{ provider.total_keys }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
模型 {{ provider.active_models }}/{{ provider.total_models }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三行:端点健康 -->
|
||||||
|
<div
|
||||||
|
v-if="provider.endpoint_health_details && provider.endpoint_health_details.length > 0"
|
||||||
|
class="flex flex-wrap gap-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="endpoint in sortEndpoints(provider.endpoint_health_details)"
|
||||||
|
:key="endpoint.api_format"
|
||||||
|
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md border text-[10px] font-medium tracking-wide uppercase leading-none"
|
||||||
|
:class="getEndpointTagClass(endpoint, provider)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full"
|
||||||
|
:class="getEndpointDotColor(endpoint, provider)"
|
||||||
|
/>
|
||||||
|
{{ endpoint.api_format }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第四行:配额/限流 -->
|
||||||
|
<div
|
||||||
|
v-if="provider.billing_type === 'monthly_quota' || rpmUsage(provider)"
|
||||||
|
class="flex items-center gap-3 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span v-if="provider.billing_type === 'monthly_quota'">
|
||||||
|
配额: <span
|
||||||
|
class="font-semibold"
|
||||||
|
:class="getQuotaUsedColorClass(provider)"
|
||||||
|
>${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span> / ${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="rpmUsage(provider)">
|
||||||
|
RPM: {{ rpmUsage(provider) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="!loading && filteredProviders.length > 0"
|
v-if="!loading && filteredProviders.length > 0"
|
||||||
|
|||||||
@@ -6,8 +6,86 @@
|
|||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 标题和筛选器 -->
|
<!-- 标题和筛选器 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<!-- 移动端:标题行 + 筛选器行 -->
|
||||||
|
<div class="flex flex-col gap-3 sm:hidden">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-base font-semibold">
|
||||||
|
用户管理
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 新增用户按钮 -->
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
title="新增用户"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
>
|
||||||
|
<Plus class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
|
<RefreshButton
|
||||||
|
:loading="usersStore.loading || loadingStats"
|
||||||
|
@click="refreshUsers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 筛选器 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
|
<Input
|
||||||
|
id="users-search-mobile"
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索..."
|
||||||
|
class="w-full pl-8 pr-3 h-8 text-sm bg-background/50 border-border/60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
v-model="filterRole"
|
||||||
|
v-model:open="filterRoleOpenMobile"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-24 h-8 text-xs border-border/60">
|
||||||
|
<SelectValue placeholder="角色" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">
|
||||||
|
全部
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="admin">
|
||||||
|
管理员
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="user">
|
||||||
|
用户
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
v-model="filterStatus"
|
||||||
|
v-model:open="filterStatusOpenMobile"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="w-20 h-8 text-xs border-border/60">
|
||||||
|
<SelectValue placeholder="状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">
|
||||||
|
全部
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="active">
|
||||||
|
活跃
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="inactive">
|
||||||
|
禁用
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端:单行布局 -->
|
||||||
|
<div class="hidden sm:flex items-center justify-between gap-4">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-base font-semibold">
|
||||||
用户管理
|
用户管理
|
||||||
</h3>
|
</h3>
|
||||||
@@ -172,10 +250,6 @@
|
|||||||
<span class="w-14">Tokens:</span>
|
<span class="w-14">Tokens:</span>
|
||||||
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
|
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center text-muted-foreground">
|
|
||||||
<span class="w-14">费用:</span>
|
|
||||||
<span class="font-medium text-foreground">${{ formatCurrency(userStats[user.id]?.total_cost) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -338,14 +412,6 @@
|
|||||||
{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}
|
{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
|
||||||
<div class="text-muted-foreground mb-1">
|
|
||||||
消费金额
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold text-sm text-foreground">
|
|
||||||
${{ formatCurrency(userStats[user.id]?.total_cost) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-2 sm:p-3 bg-muted/50 rounded-lg text-xs space-y-1">
|
<div class="p-2 sm:p-3 bg-muted/50 rounded-lg text-xs space-y-1">
|
||||||
@@ -707,6 +773,8 @@ const filterRole = ref('all')
|
|||||||
const filterStatus = ref('all')
|
const filterStatus = ref('all')
|
||||||
const filterRoleOpen = ref(false)
|
const filterRoleOpen = ref(false)
|
||||||
const filterStatusOpen = ref(false)
|
const filterStatusOpen = ref(false)
|
||||||
|
const filterRoleOpenMobile = ref(false)
|
||||||
|
const filterStatusOpenMobile = ref(false)
|
||||||
|
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6 px-4 sm:px-6 lg:px-0">
|
||||||
<!-- 页面头部:统计卡片 + 公告 -->
|
<!-- 页面头部:统计卡片 + 公告 -->
|
||||||
<div class="flex gap-6 items-start">
|
<div class="flex flex-col sm:flex-row gap-6 sm:items-start">
|
||||||
<!-- 左侧统计区域 -->
|
<!-- 左侧统计区域 -->
|
||||||
<div
|
<div
|
||||||
ref="statsPanelRef"
|
ref="statsPanelRef"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<!-- 主要统计卡片 -->
|
<!-- 主要统计卡片 -->
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-2 gap-3 sm:gap-4 xl:grid-cols-4">
|
||||||
<template v-if="loading && stats.length === 0">
|
<template v-if="loading && stats.length === 0">
|
||||||
<Card
|
<Card
|
||||||
v-for="i in 4"
|
v-for="i in 4"
|
||||||
@@ -31,53 +31,55 @@
|
|||||||
v-for="(stat, index) in stats"
|
v-for="(stat, index) in stats"
|
||||||
v-else
|
v-else
|
||||||
:key="stat.name"
|
:key="stat.name"
|
||||||
class="relative overflow-hidden p-5"
|
class="relative overflow-hidden p-3 sm:p-5"
|
||||||
:class="statCardBorders[index % statCardBorders.length]"
|
:class="statCardBorders[index % statCardBorders.length]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute -right-4 -top-6 h-28 w-28 rounded-full blur-3xl opacity-40"
|
class="pointer-events-none absolute -right-4 -top-6 h-28 w-28 rounded-full blur-3xl opacity-40"
|
||||||
:class="statCardGlows[index % statCardGlows.length]"
|
:class="statCardGlows[index % statCardGlows.length]"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-start justify-between relative">
|
<!-- 图标固定在右上角 -->
|
||||||
<div>
|
<div
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-[0.4em] text-muted-foreground">
|
class="absolute top-3 right-3 sm:top-5 sm:right-5 rounded-xl sm:rounded-2xl border border-border bg-card/50 p-2 sm:p-3 shadow-inner backdrop-blur-sm"
|
||||||
{{ stat.name }}
|
:class="getStatIconColor(index)"
|
||||||
</p>
|
>
|
||||||
<p class="mt-4 text-3xl font-semibold text-foreground">
|
<component
|
||||||
{{ stat.value }}
|
:is="stat.icon"
|
||||||
</p>
|
class="h-4 w-4 sm:h-5 sm:w-5"
|
||||||
<p
|
/>
|
||||||
v-if="stat.subValue"
|
</div>
|
||||||
class="mt-1 text-sm text-muted-foreground"
|
<!-- 内容区域 -->
|
||||||
>
|
<div>
|
||||||
{{ stat.subValue }}
|
<p class="text-[9px] sm:text-[11px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.4em] text-muted-foreground pr-10 sm:pr-14">
|
||||||
</p>
|
{{ stat.name }}
|
||||||
<div
|
</p>
|
||||||
v-if="stat.change || stat.extraBadge"
|
<p class="mt-2 sm:mt-4 text-xl sm:text-3xl font-semibold text-foreground">
|
||||||
class="mt-2 flex items-center gap-1.5"
|
{{ stat.value }}
|
||||||
>
|
</p>
|
||||||
<Badge
|
<p
|
||||||
v-if="stat.change"
|
v-if="stat.subValue"
|
||||||
variant="secondary"
|
class="mt-0.5 sm:mt-1 text-[10px] sm:text-sm text-muted-foreground"
|
||||||
>
|
|
||||||
{{ stat.change }}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-if="stat.extraBadge"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{{ stat.extraBadge }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="rounded-2xl border border-border bg-card/50 p-3 shadow-inner backdrop-blur-sm"
|
|
||||||
:class="getStatIconColor(index)"
|
|
||||||
>
|
>
|
||||||
<component
|
{{ stat.subValue }}
|
||||||
:is="stat.icon"
|
</p>
|
||||||
class="h-5 w-5"
|
<div
|
||||||
/>
|
v-if="stat.change || stat.extraBadge"
|
||||||
|
class="mt-1.5 sm:mt-2 flex items-center gap-1 sm:gap-1.5 flex-wrap"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
v-if="stat.change"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[9px] sm:text-xs"
|
||||||
|
>
|
||||||
|
{{ stat.change }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="stat.extraBadge"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[9px] sm:text-xs"
|
||||||
|
>
|
||||||
|
{{ stat.extraBadge }}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -99,70 +101,62 @@
|
|||||||
Monthly
|
Monthly
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
||||||
<Card class="p-4 border-book-cloth/30">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
||||||
<div class="flex items-center justify-between">
|
<Clock class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
平均响应
|
平均响应
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ systemHealth.avg_response_time }}s
|
{{ systemHealth.avg_response_time }}s
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Clock class="h-4 w-4 text-book-cloth" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="p-4 border-kraft/30">
|
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
||||||
<div class="flex items-center justify-between">
|
<AlertTriangle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
错误率
|
错误率
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="mt-2 text-xl font-semibold"
|
class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold"
|
||||||
:class="systemHealth.error_rate > 5 ? 'text-destructive' : 'text-foreground'"
|
:class="systemHealth.error_rate > 5 ? 'text-destructive' : 'text-foreground'"
|
||||||
>
|
>
|
||||||
{{ systemHealth.error_rate }}%
|
{{ systemHealth.error_rate }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<AlertTriangle class="h-4 w-4 text-kraft" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="p-4 border-book-cloth/25">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
||||||
<div class="flex items-center justify-between">
|
<Shuffle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
转移次数
|
转移次数
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ systemHealth.fallback_count }}
|
{{ systemHealth.fallback_count }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Shuffle class="h-4 w-4 text-kraft" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
v-if="costStats"
|
v-if="costStats"
|
||||||
class="p-4 border-manilla/40"
|
class="relative p-3 sm:p-4 border-manilla/40"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<DollarSign class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
实际成本
|
实际成本
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ formatCurrency(costStats.total_actual_cost) }}
|
{{ formatCurrency(costStats.total_actual_cost) }}
|
||||||
</p>
|
</p>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="costStats.cost_savings > 0"
|
v-if="costStats.cost_savings > 0"
|
||||||
variant="success"
|
variant="success"
|
||||||
class="mt-1 text-[10px]"
|
class="mt-1 text-[9px] sm:text-[10px]"
|
||||||
>
|
>
|
||||||
节省 {{ formatCurrency(costStats.cost_savings) }}
|
节省 {{ formatCurrency(costStats.cost_savings) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
|
||||||
<DollarSign class="h-4 w-4 text-book-cloth" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,63 +178,55 @@
|
|||||||
Monthly
|
Monthly
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
|
||||||
<Card class="p-4 border-book-cloth/30">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
|
||||||
<div class="flex items-center justify-between">
|
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存命中率
|
缓存命中率
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ cacheStats.cache_hit_rate || 0 }}%
|
{{ cacheStats.cache_hit_rate || 0 }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Database class="h-4 w-4 text-book-cloth" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="p-4 border-kraft/30">
|
<Card class="relative p-3 sm:p-4 border-kraft/30">
|
||||||
<div class="flex items-center justify-between">
|
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存读取
|
缓存读取
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ formatTokens(cacheStats.cache_read_tokens) }}
|
{{ formatTokens(cacheStats.cache_read_tokens) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Hash class="h-4 w-4 text-kraft" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="p-4 border-book-cloth/25">
|
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
|
||||||
<div class="flex items-center justify-between">
|
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
缓存创建
|
缓存创建
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ formatTokens(cacheStats.cache_creation_tokens) }}
|
{{ formatTokens(cacheStats.cache_creation_tokens) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Database class="h-4 w-4 text-kraft" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
v-if="tokenBreakdown"
|
v-if="tokenBreakdown"
|
||||||
class="p-4 border-manilla/40"
|
class="relative p-3 sm:p-4 border-manilla/40"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
|
||||||
<div>
|
<div class="pr-6">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-[0.3em] text-muted-foreground">
|
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
|
||||||
总Token
|
总Token
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
<p class="mt-1.5 sm:mt-2 text-lg sm:text-xl font-semibold text-foreground">
|
||||||
{{ formatTokens((tokenBreakdown.input || 0) + (tokenBreakdown.output || 0)) }}
|
{{ formatTokens((tokenBreakdown.input || 0) + (tokenBreakdown.output || 0)) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-[10px] text-muted-foreground">
|
<p class="mt-0.5 sm:mt-1 text-[9px] sm:text-[10px] text-muted-foreground">
|
||||||
输入 {{ formatTokens(tokenBreakdown.input || 0) }} / 输出 {{ formatTokens(tokenBreakdown.output || 0) }}
|
输入 {{ formatTokens(tokenBreakdown.input || 0) }} / 输出 {{ formatTokens(tokenBreakdown.output || 0) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Hash class="h-4 w-4 text-book-cloth" />
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +236,7 @@
|
|||||||
<!-- 右侧系统公告 -->
|
<!-- 右侧系统公告 -->
|
||||||
<div
|
<div
|
||||||
id="announcements-section"
|
id="announcements-section"
|
||||||
class="w-[340px] flex-shrink-0 flex flex-col min-h-0"
|
class="w-full sm:w-[260px] md:w-[300px] lg:w-[320px] flex-shrink-0 flex flex-col min-h-0"
|
||||||
:style="announcementsContainerStyle"
|
:style="announcementsContainerStyle"
|
||||||
>
|
>
|
||||||
<div class="mb-3 flex items-center justify-between flex-shrink-0">
|
<div class="mb-3 flex items-center justify-between flex-shrink-0">
|
||||||
@@ -265,7 +251,7 @@
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card class="overflow-hidden p-4 flex flex-col flex-1 min-h-0 h-full">
|
<Card class="overflow-hidden p-4 flex flex-col flex-1 min-h-0 h-full max-h-[280px] sm:max-h-none">
|
||||||
<div
|
<div
|
||||||
v-if="loadingAnnouncements"
|
v-if="loadingAnnouncements"
|
||||||
class="py-8 text-center"
|
class="py-8 text-center"
|
||||||
@@ -420,9 +406,70 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 每日统计表格 -->
|
<!-- 每日统计 -->
|
||||||
<Card class="overflow-hidden mt-6">
|
<Card class="overflow-hidden mt-6">
|
||||||
<Table>
|
<!-- 移动端:卡片列表 -->
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<div class="px-4 py-3 border-b border-border/60">
|
||||||
|
<h3 class="text-sm font-semibold">
|
||||||
|
每日统计
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="loadingDaily"
|
||||||
|
class="flex items-center justify-center py-8"
|
||||||
|
>
|
||||||
|
<Skeleton class="h-5 w-5 rounded-full" />
|
||||||
|
<span class="ml-2 text-muted-foreground text-xs">加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="dailyStats.length === 0"
|
||||||
|
class="py-8 text-center text-muted-foreground text-xs"
|
||||||
|
>
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="divide-y divide-border/60"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="stat in dailyStats.slice().reverse()"
|
||||||
|
:key="stat.date"
|
||||||
|
class="p-4 space-y-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-medium text-sm">{{ formatDate(stat.date) }}</span>
|
||||||
|
<Badge
|
||||||
|
variant="success"
|
||||||
|
class="text-[10px]"
|
||||||
|
>
|
||||||
|
${{ stat.cost.toFixed(4) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">请求</span>
|
||||||
|
<span>{{ stat.requests.toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Tokens</span>
|
||||||
|
<span>{{ formatTokens(stat.tokens) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">响应</span>
|
||||||
|
<span>{{ formatResponseTime(stat.avg_response_time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">模型</span>
|
||||||
|
<span>{{ stat.unique_models }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 桌面端:表格 -->
|
||||||
|
<Table class="hidden sm:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="text-left">
|
<TableHead class="text-left">
|
||||||
@@ -657,13 +704,21 @@ const statsPanelRef = ref<HTMLElement | null>(null)
|
|||||||
const announcementsHeight = ref<number | null>(null)
|
const announcementsHeight = ref<number | null>(null)
|
||||||
const announcementsTimelineRef = ref<HTMLElement | null>(null)
|
const announcementsTimelineRef = ref<HTMLElement | null>(null)
|
||||||
const timelineLineStyle = ref<{ top: string; bottom: string }>({ top: '0px', bottom: '0px' })
|
const timelineLineStyle = ref<{ top: string; bottom: string }>({ top: '0px', bottom: '0px' })
|
||||||
|
const isLargeScreen = ref(false)
|
||||||
|
|
||||||
const announcementsContainerStyle = computed(() => {
|
const announcementsContainerStyle = computed(() => {
|
||||||
if (!announcementsHeight.value) return {}
|
// 移动端不设置固定高度,让内容自然流动
|
||||||
// 设置固定高度,与左侧统计面板保持一致
|
if (!isLargeScreen.value || !announcementsHeight.value) return {}
|
||||||
|
// 桌面端设置固定高度,与左侧统计面板保持一致
|
||||||
return { height: `${announcementsHeight.value}px` }
|
return { height: `${announcementsHeight.value}px` }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function checkScreenSize() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
isLargeScreen.value = window.innerWidth >= 640 // sm breakpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let statsPanelObserver: ResizeObserver | null = null
|
let statsPanelObserver: ResizeObserver | null = null
|
||||||
let announcementsTimelineObserver: ResizeObserver | null = null
|
let announcementsTimelineObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
@@ -698,6 +753,7 @@ function updateTimelineLine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowResize() {
|
function handleWindowResize() {
|
||||||
|
checkScreenSize()
|
||||||
updateAnnouncementsHeight()
|
updateAnnouncementsHeight()
|
||||||
updateTimelineLine()
|
updateTimelineLine()
|
||||||
}
|
}
|
||||||
@@ -984,6 +1040,7 @@ const chartOptions = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
checkScreenSize()
|
||||||
setupResizeObserver()
|
setupResizeObserver()
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('resize', handleWindowResize)
|
window.addEventListener('resize', handleWindowResize)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<IntervalTimelineCard
|
<IntervalTimelineCard
|
||||||
:title="isAdminPage ? '请求间隔时间线' : '我的请求间隔'"
|
:title="isAdminPage ? '请求间隔时间线' : '我的请求间隔'"
|
||||||
:is-admin="isAdminPage"
|
:is-admin="isAdminPage"
|
||||||
:hours="168"
|
:hours="24"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold">
|
||||||
公告管理
|
公告管理
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground mt-0.5">
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
{{ isAdmin ? '管理系统公告和通知' : '查看系统公告和通知' }}
|
{{ isAdmin ? '管理系统公告和通知' : '查看系统公告和通知' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
v-if="unreadCount > 0"
|
v-if="unreadCount > 0"
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
>
|
>
|
||||||
{{ unreadCount }} 条未读
|
{{ unreadCount }} 条未读
|
||||||
</Badge>
|
</Badge>
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
<Button
|
<Button
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
v-else
|
v-else
|
||||||
class="overflow-x-auto"
|
class="overflow-x-auto"
|
||||||
>
|
>
|
||||||
<Table>
|
<Table class="hidden xl:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="w-[80px] h-12 font-semibold text-center">
|
<TableHead class="w-[80px] h-12 font-semibold text-center">
|
||||||
@@ -217,6 +217,91 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="announcements.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="announcement in announcements"
|
||||||
|
:key="announcement.id"
|
||||||
|
:class="[
|
||||||
|
'p-4 space-y-2 cursor-pointer transition-colors',
|
||||||
|
announcement.is_read ? 'hover:bg-muted/30' : 'bg-primary/5 hover:bg-primary/10'
|
||||||
|
]"
|
||||||
|
@click="viewAnnouncementDetail(announcement)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<component
|
||||||
|
:is="getAnnouncementIcon(announcement.type)"
|
||||||
|
class="w-4 h-4 shrink-0"
|
||||||
|
:class="getIconColor(announcement.type)"
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-sm">{{ announcement.title }}</span>
|
||||||
|
<Pin
|
||||||
|
v-if="announcement.is_pinned"
|
||||||
|
class="w-3.5 h-3.5 text-muted-foreground shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
:variant="announcement.is_read ? 'secondary' : 'default'"
|
||||||
|
class="text-xs shrink-0"
|
||||||
|
>
|
||||||
|
{{ announcement.is_read ? '已读' : '未读' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground line-clamp-2">
|
||||||
|
{{ getPlainText(announcement.content) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>{{ announcement.author.username }}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{{ formatDate(announcement.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isAdmin"
|
||||||
|
class="flex items-center gap-4 pt-2"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground">置顶</span>
|
||||||
|
<Switch
|
||||||
|
:model-value="announcement.is_pinned"
|
||||||
|
class="data-[state=checked]:bg-emerald-500 scale-75"
|
||||||
|
@update:model-value="toggleAnnouncementPin(announcement, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground">启用</span>
|
||||||
|
<Switch
|
||||||
|
:model-value="announcement.is_active"
|
||||||
|
class="data-[state=checked]:bg-primary scale-75"
|
||||||
|
@update:model-value="toggleAnnouncementActive(announcement, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 ml-auto">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="openEditDialog(announcement)"
|
||||||
|
>
|
||||||
|
<SquarePen class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7 hover:text-destructive"
|
||||||
|
@click="confirmDelete(announcement)"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<Card class="overflow-hidden">
|
<Card class="overflow-hidden">
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<!-- 左侧:标题 -->
|
<!-- 左侧:标题 -->
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||||
可用模型
|
可用模型
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- 右侧:操作区 -->
|
<!-- 右侧:操作区 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative">
|
<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" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索模型名称..."
|
placeholder="搜索模型名称..."
|
||||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-background/50 border-border/60 focus:border-primary/40 transition-colors"
|
class="w-32 sm:w-44 pl-8 pr-3 h-8 text-sm bg-background/50 border-border/60 focus:border-primary/40 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 能力筛选 -->
|
<!-- 能力筛选 -->
|
||||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 刷新按钮 -->
|
<!-- 刷新按钮 -->
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<Table class="table-fixed w-full">
|
<Table class="hidden xl:table table-fixed w-full">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="w-[140px] h-12 font-semibold">
|
<TableHead class="w-[140px] h-12 font-semibold">
|
||||||
@@ -219,6 +219,91 @@
|
|||||||
</template>
|
</template>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div
|
||||||
|
v-if="!loading && filteredModels.length > 0"
|
||||||
|
class="xl:hidden divide-y divide-border/40"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="model in paginatedModels"
|
||||||
|
:key="model.id"
|
||||||
|
class="p-4 space-y-3 hover:bg-muted/30 cursor-pointer transition-colors"
|
||||||
|
@click="selectedModel = model; drawerOpen = true"
|
||||||
|
>
|
||||||
|
<!-- 第一行:名称 + 状态 -->
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="font-medium truncate block">{{ model.display_name || model.name }}</span>
|
||||||
|
<div class="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
|
||||||
|
<span class="truncate">{{ model.name }}</span>
|
||||||
|
<button
|
||||||
|
class="p-0.5 rounded hover:bg-muted transition-colors shrink-0"
|
||||||
|
@click.stop="copyToClipboard(model.name)"
|
||||||
|
>
|
||||||
|
<Copy class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge :variant="model.is_active ? 'success' : 'secondary'">
|
||||||
|
{{ model.is_active ? '可用' : '停用' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:能力图标 -->
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<Eye
|
||||||
|
v-if="model.default_supports_vision"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Wrench
|
||||||
|
v-if="model.default_supports_function_calling"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<Brain
|
||||||
|
v-if="model.default_supports_extended_thinking"
|
||||||
|
class="w-4 h-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三行:价格 -->
|
||||||
|
<div
|
||||||
|
v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')"
|
||||||
|
class="text-xs text-muted-foreground font-mono"
|
||||||
|
>
|
||||||
|
In: ${{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }} / Out: ${{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第四行:模型偏好按钮 -->
|
||||||
|
<div
|
||||||
|
v-if="getModelSupportedCapabilities(model).length > 0"
|
||||||
|
class="flex gap-1.5 flex-wrap"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<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="[
|
||||||
|
isCapabilityEnabled(model.name, cap.name)
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-transparent text-muted-foreground border border-dashed border-muted-foreground/50'
|
||||||
|
]"
|
||||||
|
@click="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"
|
||||||
|
/>
|
||||||
|
{{ cap.short_name || cap.display_name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 标题和操作栏 -->
|
<!-- 标题和操作栏 -->
|
||||||
<div class="px-6 py-3.5 border-b border-border/60">
|
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||||
<h3 class="text-base font-semibold">
|
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||||
我的 API Keys
|
我的 API Keys
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 抽屉内容 -->
|
<!-- 抽屉内容 -->
|
||||||
<Card class="relative h-full w-[700px] rounded-none shadow-2xl overflow-y-auto">
|
<Card class="relative h-full w-full sm:w-[700px] sm:max-w-[90vw] rounded-none shadow-2xl overflow-y-auto">
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="sticky top-0 z-10 bg-background border-b p-6">
|
<div class="sticky top-0 z-10 bg-background border-b p-4 sm:p-6">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-3 sm:gap-4">
|
||||||
<div class="space-y-1 flex-1 min-w-0">
|
<div class="space-y-1 flex-1 min-w-0">
|
||||||
<h3 class="text-xl font-bold truncate">
|
<h3 class="text-lg sm:text-xl font-bold truncate">
|
||||||
{{ model.display_name || model.name }}
|
{{ model.display_name || model.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -55,13 +55,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-6 space-y-6">
|
<div class="p-4 sm:p-6 space-y-6">
|
||||||
<!-- 模型能力 -->
|
<!-- 模型能力 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h4 class="font-semibold text-sm">
|
<h4 class="font-semibold text-sm">
|
||||||
模型能力
|
模型能力
|
||||||
</h4>
|
</h4>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
<div class="flex items-center gap-2 p-3 rounded-lg border">
|
||||||
<Zap class="w-5 h-5 text-muted-foreground" />
|
<Zap class="w-5 h-5 text-muted-foreground" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user