mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 11:12:28 +08:00
refactor(frontend): optimize view pages (admin, shared, user)
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
<div class="flex flex-col">
|
||||
<Card class="overflow-hidden">
|
||||
<!-- 搜索和过滤区域 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h3 class="text-base font-semibold">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||
别名管理
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
@@ -15,11 +15,11 @@
|
||||
id="alias-search"
|
||||
v-model="aliasesSearch"
|
||||
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 class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 提供商过滤器 -->
|
||||
<Select
|
||||
@@ -27,7 +27,7 @@
|
||||
:model-value="aliasProviderFilter"
|
||||
@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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -47,7 +47,7 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
@@ -73,7 +73,7 @@
|
||||
<Loader2 class="w-10 h-10 animate-spin text-primary" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Table class="text-sm">
|
||||
<Table class="hidden xl:table text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[200px]">
|
||||
@@ -185,6 +185,83 @@
|
||||
</TableBody>
|
||||
</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
|
||||
v-if="!loadingAliases && filteredAliases.length > 0"
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<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">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div class="shrink-0">
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
独立余额 API Keys
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
@@ -28,7 +28,7 @@
|
||||
> · 即将到期 {{ expiringSoonCount }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
@@ -36,19 +36,19 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
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 class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<Select
|
||||
v-model="filterStatus"
|
||||
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="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -67,7 +67,7 @@
|
||||
v-model="filterBalance"
|
||||
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="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -82,7 +82,7 @@
|
||||
</Select>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 创建独立 Key 按钮 -->
|
||||
<Button
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
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">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div class="shrink-0">
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
审计日志
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
查看系统所有操作记录
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
@@ -24,19 +24,19 @@
|
||||
id="audit-logs-search"
|
||||
v-model="searchQuery"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
<!-- 事件类型筛选 -->
|
||||
<Select
|
||||
v-model="filters.eventType"
|
||||
v-model:open="eventTypeSelectOpen"
|
||||
@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="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -81,7 +81,7 @@
|
||||
v-model:open="daysSelectOpen"
|
||||
@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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -110,7 +110,7 @@
|
||||
>
|
||||
<FilterX class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
<!-- 导出按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -145,7 +145,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<Table>
|
||||
<Table class="hidden xl:table">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="h-12 font-semibold">
|
||||
@@ -242,6 +242,60 @@
|
||||
</TableBody>
|
||||
</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
|
||||
:current="currentPage"
|
||||
|
||||
@@ -365,21 +365,19 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 缓存亲和性列表 -->
|
||||
<Card class="overflow-hidden">
|
||||
<div class="px-6 py-3 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-base font-semibold">
|
||||
亲和性列表
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<h3 class="text-sm sm:text-base font-semibold shrink-0">
|
||||
亲和性列表
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
<Input
|
||||
id="cache-affinity-search"
|
||||
v-model="tableKeyword"
|
||||
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
|
||||
v-if="tableKeyword"
|
||||
@@ -390,7 +388,7 @@ onBeforeUnmount(() => {
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -408,7 +406,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<Table class="hidden xl:table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-36">
|
||||
@@ -537,6 +535,60 @@ onBeforeUnmount(() => {
|
||||
</TableBody>
|
||||
</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
|
||||
v-if="affinityList.length > 0"
|
||||
:current="currentPage"
|
||||
@@ -549,18 +601,18 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- TTL 分析区域 -->
|
||||
<Card class="overflow-hidden">
|
||||
<div class="px-6 py-3 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<BarChart3 class="h-5 w-5 text-muted-foreground" />
|
||||
<h3 class="text-base font-semibold">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<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 shrink-0">
|
||||
<BarChart3 class="h-5 w-5 text-muted-foreground hidden sm:block" />
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
TTL 分析
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
||||
<span class="text-xs text-muted-foreground hidden sm:inline">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Select v-model="analysisHours">
|
||||
<SelectTrigger class="w-28 h-8">
|
||||
<SelectTrigger class="w-24 sm:w-28 h-8">
|
||||
<SelectValue placeholder="时间段" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -44,17 +44,17 @@
|
||||
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">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div class="shrink-0">
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
IP 黑名单
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
管理被禁止访问的 IP 地址
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -114,17 +114,17 @@
|
||||
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">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div class="shrink-0">
|
||||
<h3 class="text-sm sm:text-base font-semibold">
|
||||
IP 白名单
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
管理可信任的 IP 地址(支持 CIDR 格式)
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -158,7 +158,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<Table>
|
||||
<Table class="hidden sm:table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>IP 地址 / CIDR</TableHead>
|
||||
@@ -189,6 +189,25 @@
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<!-- 模型列表 -->
|
||||
<Card class="overflow-hidden">
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
|
||||
<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>
|
||||
|
||||
<!-- 右侧:操作区 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
|
||||
@@ -22,11 +22,11 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索模型名称..."
|
||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||
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 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">
|
||||
@@ -76,7 +76,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<Table class="hidden xl:table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[240px]">
|
||||
@@ -302,6 +302,109 @@
|
||||
</TableBody>
|
||||
</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
|
||||
v-if="!loading && filteredGlobalModels.length > 0"
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/50">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/50">
|
||||
<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>
|
||||
|
||||
<!-- 右侧:操作区 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
|
||||
@@ -23,11 +23,11 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索提供商..."
|
||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||
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 class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 调度策略 -->
|
||||
<button
|
||||
@@ -35,12 +35,12 @@
|
||||
title="点击调整调度策略"
|
||||
@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>
|
||||
<ChevronDown class="w-3 h-3 text-muted-foreground/70 group-hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
@@ -94,7 +94,7 @@
|
||||
<!-- 桌面端表格 -->
|
||||
<div
|
||||
v-else
|
||||
class="overflow-x-auto"
|
||||
class="hidden xl:block overflow-x-auto"
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -289,6 +289,118 @@
|
||||
</Table>
|
||||
</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
|
||||
v-if="!loading && filteredProviders.length > 0"
|
||||
|
||||
@@ -6,8 +6,86 @@
|
||||
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 class="px-4 sm:px-6 py-3.5 border-b border-border/60">
|
||||
<!-- 移动端:标题行 + 筛选器行 -->
|
||||
<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>
|
||||
@@ -172,10 +250,6 @@
|
||||
<span class="w-14">Tokens:</span>
|
||||
<span class="font-medium text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</span>
|
||||
</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
|
||||
v-else
|
||||
@@ -338,14 +412,6 @@
|
||||
{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}
|
||||
</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 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 filterRoleOpen = ref(false)
|
||||
const filterStatusOpen = ref(false)
|
||||
const filterRoleOpenMobile = ref(false)
|
||||
const filterStatusOpenMobile = ref(false)
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
Reference in New Issue
Block a user