mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 11:12:28 +08:00
refactor(frontend): 优化布局和视图页面
- 更新 MainLayout 布局组件 - 优化 admin 视图: 用户、模型、Provider、API Keys 等管理页面 - 改进 shared 视图: Dashboard、Usage 页面 - 调整 user 视图: ModelCatalog、MyApiKeys、Settings、Announcements 页面 - 更新 public 视图: Home、CliSection、LogoColorDemo 页面
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
<!-- 搜索和过滤区域 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h3 class="text-base font-semibold">别名管理</h3>
|
||||
<h3 class="text-base font-semibold">
|
||||
别名管理
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
@@ -20,14 +22,26 @@
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 提供商过滤器 -->
|
||||
<Select v-model:open="aliasProviderSelectOpen" :model-value="aliasProviderFilter" @update:model-value="aliasProviderFilter = $event">
|
||||
<Select
|
||||
v-model:open="aliasProviderSelectOpen"
|
||||
:model-value="aliasProviderFilter"
|
||||
@update:model-value="aliasProviderFilter = $event"
|
||||
>
|
||||
<SelectTrigger class="w-40 h-8 text-xs border-border/60">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部别名</SelectItem>
|
||||
<SelectItem value="global">仅全局别名</SelectItem>
|
||||
<SelectItem v-for="provider in providers" :key="provider.id" :value="provider.id">
|
||||
<SelectItem value="all">
|
||||
全部别名
|
||||
</SelectItem>
|
||||
<SelectItem value="global">
|
||||
仅全局别名
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
>
|
||||
{{ provider.display_name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -40,37 +54,61 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openCreateAliasDialog"
|
||||
title="新建别名"
|
||||
@click="openCreateAliasDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loadingAliases" @click="loadAliases" />
|
||||
<RefreshButton
|
||||
:loading="loadingAliases"
|
||||
@click="loadAliases"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loadingAliases" class="flex items-center justify-center py-12">
|
||||
<div
|
||||
v-if="loadingAliases"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<Loader2 class="w-10 h-10 animate-spin text-primary" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Table class="text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[200px]">别名</TableHead>
|
||||
<TableHead class="w-[280px]">关联模型</TableHead>
|
||||
<TableHead class="w-[70px] text-center">类型</TableHead>
|
||||
<TableHead class="w-[100px] text-center">作用域</TableHead>
|
||||
<TableHead class="w-[70px] text-center">状态</TableHead>
|
||||
<TableHead class="w-[100px] text-center">操作</TableHead>
|
||||
<TableHead class="w-[200px]">
|
||||
别名
|
||||
</TableHead>
|
||||
<TableHead class="w-[280px]">
|
||||
关联模型
|
||||
</TableHead>
|
||||
<TableHead class="w-[70px] text-center">
|
||||
类型
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] text-center">
|
||||
作用域
|
||||
</TableHead>
|
||||
<TableHead class="w-[70px] text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="filteredAliases.length === 0">
|
||||
<TableCell colspan="6" class="text-center py-8 text-muted-foreground">
|
||||
<TableCell
|
||||
colspan="6"
|
||||
class="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
{{ aliasProviderFilter === 'global' ? '暂无全局别名' : '暂无别名' }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="alias in paginatedAliases" :key="alias.id">
|
||||
<TableRow
|
||||
v-for="alias in paginatedAliases"
|
||||
:key="alias.id"
|
||||
>
|
||||
<TableCell>
|
||||
<span class="font-mono font-medium">{{ alias.alias }}</span>
|
||||
</TableCell>
|
||||
@@ -81,7 +119,10 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ alias.mapping_type === 'mapping' ? '映射' : '别名' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -102,7 +143,10 @@
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge :variant="alias.is_active ? 'default' : 'secondary'" class="text-xs">
|
||||
<Badge
|
||||
:variant="alias.is_active ? 'default' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ alias.is_active ? '活跃' : '停用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -112,8 +156,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="openEditAliasDialog(alias)"
|
||||
title="编辑别名"
|
||||
@click="openEditAliasDialog(alias)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -121,8 +165,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="toggleAliasStatus(alias)"
|
||||
:title="alias.is_active ? '停用别名' : '启用别名'"
|
||||
@click="toggleAliasStatus(alias)"
|
||||
>
|
||||
<Power class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -130,8 +174,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@click="confirmDeleteAlias(alias)"
|
||||
title="删除别名"
|
||||
@click="confirmDeleteAlias(alias)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="space-y-6 pb-8">
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="py-16 text-center space-y-4">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="py-16 text-center space-y-4"
|
||||
>
|
||||
<Skeleton class="mx-auto h-10 w-10 rounded-full" />
|
||||
<Skeleton class="mx-auto h-4 w-32" />
|
||||
</div>
|
||||
@@ -11,10 +17,15 @@
|
||||
<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">独立余额 API Keys</h3>
|
||||
<h3 class="text-base font-semibold">
|
||||
独立余额 API Keys
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
活跃 {{ activeKeyCount }} · 禁用 {{ inactiveKeyCount }} · 无限 Key {{ unlimitedKeyCount }}
|
||||
<span v-if="expiringSoonCount > 0" class="text-amber-600"> · 即将到期 {{ expiringSoonCount }}</span>
|
||||
<span
|
||||
v-if="expiringSoonCount > 0"
|
||||
class="text-amber-600"
|
||||
> · 即将到期 {{ expiringSoonCount }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -33,24 +44,38 @@
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<Select v-model="filterStatus" v-model:open="filterStatusOpen">
|
||||
<Select
|
||||
v-model="filterStatus"
|
||||
v-model:open="filterStatusOpen"
|
||||
>
|
||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="status in statusFilters" :key="status.value" :value="status.value">
|
||||
<SelectItem
|
||||
v-for="status in statusFilters"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- 余额类型筛选 -->
|
||||
<Select v-model="filterBalance" v-model:open="filterBalanceOpen">
|
||||
<Select
|
||||
v-model="filterBalance"
|
||||
v-model:open="filterBalanceOpen"
|
||||
>
|
||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="balance in balanceFilters" :key="balance.value" :value="balance.value">
|
||||
<SelectItem
|
||||
v-for="balance in balanceFilters"
|
||||
:key="balance.value"
|
||||
:value="balance.value"
|
||||
>
|
||||
{{ balance.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -64,14 +89,17 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openCreateDialog"
|
||||
title="创建独立 Key"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<RefreshButton :loading="loading" @click="loadApiKeys" />
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="loadApiKeys"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,33 +108,59 @@
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="w-[200px] h-12 font-semibold">密钥信息</TableHead>
|
||||
<TableHead class="w-[160px] h-12 font-semibold">余额 (已用/总额)</TableHead>
|
||||
<TableHead class="w-[130px] h-12 font-semibold">使用统计</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">有效期</TableHead>
|
||||
<TableHead class="w-[140px] h-12 font-semibold">最近使用</TableHead>
|
||||
<TableHead class="w-[70px] h-12 font-semibold text-center">状态</TableHead>
|
||||
<TableHead class="w-[130px] h-12 font-semibold text-center">操作</TableHead>
|
||||
<TableHead class="w-[200px] h-12 font-semibold">
|
||||
密钥信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[160px] h-12 font-semibold">
|
||||
余额 (已用/总额)
|
||||
</TableHead>
|
||||
<TableHead class="w-[130px] h-12 font-semibold">
|
||||
使用统计
|
||||
</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">
|
||||
有效期
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px] h-12 font-semibold">
|
||||
最近使用
|
||||
</TableHead>
|
||||
<TableHead class="w-[70px] h-12 font-semibold text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[130px] h-12 font-semibold text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="filteredApiKeys.length === 0">
|
||||
<TableCell colspan="7" class="h-64 text-center">
|
||||
<TableCell
|
||||
colspan="7"
|
||||
class="h-64 text-center"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center space-y-4">
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||
<Key class="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div v-if="hasActiveFilters">
|
||||
<h3 class="text-lg font-semibold">未找到匹配的 Key</h3>
|
||||
<h3 class="text-lg font-semibold">
|
||||
未找到匹配的 Key
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
尝试调整筛选条件
|
||||
</p>
|
||||
<Button variant="outline" size="sm" class="mt-3" @click="clearFilters">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-3"
|
||||
@click="clearFilters"
|
||||
>
|
||||
清除筛选
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 class="text-lg font-semibold">暂无独立余额 Key</h3>
|
||||
<h3 class="text-lg font-semibold">
|
||||
暂无独立余额 Key
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
点击右上角按钮创建独立余额 Key
|
||||
</p>
|
||||
@@ -114,10 +168,17 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="apiKey in filteredApiKeys" :key="apiKey.id" class="border-b border-border/40 hover:bg-muted/30 transition-colors">
|
||||
<TableRow
|
||||
v-for="apiKey in filteredApiKeys"
|
||||
:key="apiKey.id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<TableCell class="py-4">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-foreground truncate" :title="apiKey.name || '未命名 Key'">
|
||||
<div
|
||||
class="text-sm font-semibold text-foreground truncate"
|
||||
:title="apiKey.name || '未命名 Key'"
|
||||
>
|
||||
{{ apiKey.name || '未命名 Key' }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
@@ -128,8 +189,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
@click="copyKeyPrefix(apiKey)"
|
||||
title="复制完整密钥"
|
||||
@click="copyKeyPrefix(apiKey)"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -159,21 +220,42 @@
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="text-xs">
|
||||
<div v-if="apiKey.expires_at" class="space-y-1">
|
||||
<div class="text-foreground">{{ formatDate(apiKey.expires_at) }}</div>
|
||||
<div class="text-muted-foreground">{{ getRelativeTime(apiKey.expires_at) }}</div>
|
||||
<div
|
||||
v-if="apiKey.expires_at"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="text-foreground">
|
||||
{{ formatDate(apiKey.expires_at) }}
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
{{ getRelativeTime(apiKey.expires_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
永不过期
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground">永不过期</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="text-xs">
|
||||
<span v-if="apiKey.last_used_at" class="text-foreground">{{ formatDate(apiKey.last_used_at) }}</span>
|
||||
<span v-else class="text-muted-foreground">暂无记录</span>
|
||||
<span
|
||||
v-if="apiKey.last_used_at"
|
||||
class="text-foreground"
|
||||
>{{ formatDate(apiKey.last_used_at) }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>暂无记录</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<Badge :variant="apiKey.is_active ? 'success' : 'destructive'" class="font-medium">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -183,8 +265,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="editApiKey(apiKey)"
|
||||
title="编辑"
|
||||
@click="editApiKey(apiKey)"
|
||||
>
|
||||
<SquarePen class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -192,8 +274,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openAddBalanceDialog(apiKey)"
|
||||
title="调整余额"
|
||||
@click="openAddBalanceDialog(apiKey)"
|
||||
>
|
||||
<DollarSign class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -201,8 +283,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="toggleApiKey(apiKey)"
|
||||
:title="apiKey.is_active ? '禁用' : '启用'"
|
||||
@click="toggleApiKey(apiKey)"
|
||||
>
|
||||
<Power class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -210,8 +292,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
title="删除"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -223,11 +305,20 @@
|
||||
</div>
|
||||
|
||||
<div class="xl:hidden divide-y divide-border/40">
|
||||
<div v-if="apiKeys.length === 0" class="p-8 text-center">
|
||||
<div
|
||||
v-if="apiKeys.length === 0"
|
||||
class="p-8 text-center"
|
||||
>
|
||||
<Key class="h-12 w-12 mx-auto mb-3 text-muted-foreground/50" />
|
||||
<p class="text-muted-foreground">暂无独立余额 Key</p>
|
||||
<p class="text-muted-foreground">
|
||||
暂无独立余额 Key
|
||||
</p>
|
||||
</div>
|
||||
<div v-for="apiKey in apiKeys" :key="apiKey.id" class="p-4 sm:p-5 hover:bg-muted/30 transition-colors">
|
||||
<div
|
||||
v-for="apiKey in apiKeys"
|
||||
:key="apiKey.id"
|
||||
class="p-4 sm:p-5 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-2">
|
||||
@@ -239,17 +330,23 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 hover:bg-muted flex-shrink-0"
|
||||
@click="copyKeyPrefix(apiKey)"
|
||||
title="复制完整密钥"
|
||||
@click="copyKeyPrefix(apiKey)"
|
||||
>
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-foreground" :class="{ 'text-muted-foreground': !apiKey.name }">
|
||||
<div
|
||||
class="text-sm font-semibold text-foreground"
|
||||
:class="{ 'text-muted-foreground': !apiKey.name }"
|
||||
>
|
||||
{{ apiKey.name || '未命名 Key' }}
|
||||
</div>
|
||||
</div>
|
||||
<Badge :variant="apiKey.is_active ? 'success' : 'destructive'" class="text-xs flex-shrink-0">
|
||||
<Badge
|
||||
:variant="apiKey.is_active ? 'success' : 'destructive'"
|
||||
class="text-xs flex-shrink-0"
|
||||
>
|
||||
{{ apiKey.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -281,7 +378,10 @@
|
||||
<span>总费用</span>
|
||||
<span>${{ (apiKey.total_cost_usd || 0).toFixed(4) }}</span>
|
||||
</div>
|
||||
<div v-if="isBalanceLimited(apiKey)" class="h-1.5 rounded-full bg-background/40 overflow-hidden">
|
||||
<div
|
||||
v-if="isBalanceLimited(apiKey)"
|
||||
class="h-1.5 rounded-full bg-background/40 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-emerald-500"
|
||||
:style="{ width: `${getBalanceProgress(apiKey)}%` }"
|
||||
@@ -291,19 +391,32 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="p-2 bg-muted/40 rounded-lg">
|
||||
<div class="text-muted-foreground mb-1">速率限制</div>
|
||||
<div class="font-semibold">{{ apiKey.rate_limit ? `${apiKey.rate_limit}/min` : '未设置' }}</div>
|
||||
<div class="text-muted-foreground mb-1">
|
||||
速率限制
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
{{ apiKey.rate_limit ? `${apiKey.rate_limit}/min` : '未设置' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-muted/40 rounded-lg">
|
||||
<div class="text-muted-foreground mb-1">请求次数</div>
|
||||
<div class="font-semibold">{{ (apiKey.total_requests || 0).toLocaleString() }}</div>
|
||||
<div class="text-muted-foreground mb-1">
|
||||
请求次数
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
{{ (apiKey.total_requests || 0).toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-muted/40 rounded-lg col-span-2">
|
||||
<div class="text-muted-foreground mb-1">有效期</div>
|
||||
<div class="text-muted-foreground mb-1">
|
||||
有效期
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
{{ apiKey.expires_at ? formatDate(apiKey.expires_at) : '永不过期' }}
|
||||
</div>
|
||||
<div v-if="apiKey.expires_at" class="text-[11px] text-muted-foreground">
|
||||
<div
|
||||
v-if="apiKey.expires_at"
|
||||
class="text-[11px] text-muted-foreground"
|
||||
>
|
||||
{{ getRelativeTime(apiKey.expires_at) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,10 +426,15 @@
|
||||
<p>创建: {{ formatDate(apiKey.created_at) }}</p>
|
||||
<p>
|
||||
最近使用:
|
||||
<span v-if="apiKey.last_used_at" class="font-medium text-foreground">{{ formatDate(apiKey.last_used_at) }}</span>
|
||||
<span
|
||||
v-if="apiKey.last_used_at"
|
||||
class="font-medium text-foreground"
|
||||
>{{ formatDate(apiKey.last_used_at) }}</span>
|
||||
<span v-else>暂无记录</span>
|
||||
</p>
|
||||
<p v-if="apiKey.expires_at">过期后: {{ apiKey.auto_delete_on_expiry ? '自动删除' : '仅禁用' }}</p>
|
||||
<p v-if="apiKey.expires_at">
|
||||
过期后: {{ apiKey.auto_delete_on_expiry ? '自动删除' : '仅禁用' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@@ -373,15 +491,18 @@
|
||||
|
||||
<!-- 创建/编辑独立Key对话框 -->
|
||||
<StandaloneKeyFormDialog
|
||||
ref="keyFormDialogRef"
|
||||
:open="showKeyFormDialog"
|
||||
:api-key="editingKeyData"
|
||||
@close="closeKeyFormDialog"
|
||||
@submit="handleKeyFormSubmit"
|
||||
ref="keyFormDialogRef"
|
||||
/>
|
||||
|
||||
<!-- 新 Key 显示对话框 -->
|
||||
<Dialog v-model="showNewKeyDialog" size="lg">
|
||||
<Dialog
|
||||
v-model="showNewKeyDialog"
|
||||
size="lg"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -389,8 +510,12 @@
|
||||
<CheckCircle class="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">创建成功</h3>
|
||||
<p class="text-xs text-muted-foreground">请妥善保管, 切勿泄露给他人.</p>
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
创建成功
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
请妥善保管, 切勿泄露给他人.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -401,14 +526,17 @@
|
||||
<Label class="text-sm font-medium">API Key</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
ref="keyInput"
|
||||
type="text"
|
||||
:value="newKeyValue"
|
||||
readonly
|
||||
class="flex-1 font-mono text-sm bg-muted/50 h-11"
|
||||
@click="selectKey"
|
||||
ref="keyInput"
|
||||
/>
|
||||
<Button @click="copyKey" class="h-11">
|
||||
<Button
|
||||
class="h-11"
|
||||
@click="copyKey"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
@@ -416,12 +544,20 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button @click="closeNewKeyDialog" class="h-10 px-5">确定</Button>
|
||||
<Button
|
||||
class="h-10 px-5"
|
||||
@click="closeNewKeyDialog"
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 余额调整对话框 -->
|
||||
<Dialog v-model="showAddBalanceDialog" size="md">
|
||||
<Dialog
|
||||
v-model="showAddBalanceDialog"
|
||||
size="md"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -429,8 +565,12 @@
|
||||
<DollarSign class="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">余额调整</h3>
|
||||
<p class="text-xs text-muted-foreground">增加或扣除 API Key 余额</p>
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
余额调整
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
增加或扣除 API Key 余额
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -438,7 +578,9 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-3 bg-muted/50 rounded-lg text-sm">
|
||||
<div class="font-medium mb-2">当前余额信息</div>
|
||||
<div class="font-medium mb-2">
|
||||
当前余额信息
|
||||
</div>
|
||||
<div class="space-y-1 text-xs text-muted-foreground">
|
||||
<div>已用: <span class="font-semibold text-foreground">${{ (addBalanceKey.balance_used_usd || 0).toFixed(2) }}</span></div>
|
||||
<div>当前余额: <span class="font-semibold text-foreground">${{ (addBalanceKey.current_balance_usd || 0).toFixed(2) }}</span></div>
|
||||
@@ -446,7 +588,10 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="addBalanceAmount" class="text-sm font-medium">调整金额 (USD)</Label>
|
||||
<Label
|
||||
for="addBalanceAmount"
|
||||
class="text-sm font-medium"
|
||||
>调整金额 (USD)</Label>
|
||||
<Input
|
||||
id="addBalanceAmount"
|
||||
:model-value="addBalanceAmount ?? ''"
|
||||
@@ -457,13 +602,22 @@
|
||||
@update:model-value="(v) => addBalanceAmount = parseNumberInput(v, { allowFloat: true })"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
<span v-if="addBalanceAmount && addBalanceAmount > 0" class="text-emerald-600">
|
||||
<span
|
||||
v-if="addBalanceAmount && addBalanceAmount > 0"
|
||||
class="text-emerald-600"
|
||||
>
|
||||
增加 ${{ addBalanceAmount.toFixed(2) }},调整后余额: ${{ ((addBalanceKey.current_balance_usd || 0) + addBalanceAmount).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else-if="addBalanceAmount && addBalanceAmount < 0" class="text-rose-600">
|
||||
<span
|
||||
v-else-if="addBalanceAmount && addBalanceAmount < 0"
|
||||
class="text-rose-600"
|
||||
>
|
||||
扣除 ${{ Math.abs(addBalanceAmount).toFixed(2) }},调整后余额: ${{ Math.max(0, (addBalanceKey.current_balance_usd || 0) + addBalanceAmount).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
输入正数增加余额,负数扣除余额
|
||||
</span>
|
||||
</p>
|
||||
@@ -472,10 +626,18 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<Button variant="outline" @click="showAddBalanceDialog = false" class="h-10 px-5">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-10 px-5"
|
||||
@click="showAddBalanceDialog = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="handleAddBalance" :disabled="addingBalance || !addBalanceAmount || addBalanceAmount === 0" class="h-10 px-5">
|
||||
<Button
|
||||
:disabled="addingBalance || !addBalanceAmount || addBalanceAmount === 0"
|
||||
class="h-10 px-5"
|
||||
@click="handleAddBalance"
|
||||
>
|
||||
{{ addingBalance ? '调整中...' : '确认调整' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<div class="space-y-6 pb-8">
|
||||
<!-- 审计日志列表 -->
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">审计日志</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">查看系统所有操作记录</p>
|
||||
<h3 class="text-base font-semibold">
|
||||
审计日志
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
查看系统所有操作记录
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
@@ -33,17 +40,39 @@
|
||||
<SelectValue placeholder="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">全部类型</SelectItem>
|
||||
<SelectItem value="login_success">登录成功</SelectItem>
|
||||
<SelectItem value="login_failed">登录失败</SelectItem>
|
||||
<SelectItem value="logout">退出登录</SelectItem>
|
||||
<SelectItem value="api_key_created">API密钥创建</SelectItem>
|
||||
<SelectItem value="api_key_deleted">API密钥删除</SelectItem>
|
||||
<SelectItem value="request_success">请求成功</SelectItem>
|
||||
<SelectItem value="request_failed">请求失败</SelectItem>
|
||||
<SelectItem value="user_created">用户创建</SelectItem>
|
||||
<SelectItem value="user_updated">用户更新</SelectItem>
|
||||
<SelectItem value="user_deleted">用户删除</SelectItem>
|
||||
<SelectItem value="__all__">
|
||||
全部类型
|
||||
</SelectItem>
|
||||
<SelectItem value="login_success">
|
||||
登录成功
|
||||
</SelectItem>
|
||||
<SelectItem value="login_failed">
|
||||
登录失败
|
||||
</SelectItem>
|
||||
<SelectItem value="logout">
|
||||
退出登录
|
||||
</SelectItem>
|
||||
<SelectItem value="api_key_created">
|
||||
API密钥创建
|
||||
</SelectItem>
|
||||
<SelectItem value="api_key_deleted">
|
||||
API密钥删除
|
||||
</SelectItem>
|
||||
<SelectItem value="request_success">
|
||||
请求成功
|
||||
</SelectItem>
|
||||
<SelectItem value="request_failed">
|
||||
请求失败
|
||||
</SelectItem>
|
||||
<SelectItem value="user_created">
|
||||
用户创建
|
||||
</SelectItem>
|
||||
<SelectItem value="user_updated">
|
||||
用户更新
|
||||
</SelectItem>
|
||||
<SelectItem value="user_deleted">
|
||||
用户删除
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<!-- 时间范围筛选 -->
|
||||
@@ -56,10 +85,18 @@
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1天</SelectItem>
|
||||
<SelectItem value="7">7天</SelectItem>
|
||||
<SelectItem value="30">30天</SelectItem>
|
||||
<SelectItem value="90">90天</SelectItem>
|
||||
<SelectItem value="1">
|
||||
1天
|
||||
</SelectItem>
|
||||
<SelectItem value="7">
|
||||
7天
|
||||
</SelectItem>
|
||||
<SelectItem value="30">
|
||||
30天
|
||||
</SelectItem>
|
||||
<SelectItem value="90">
|
||||
90天
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<!-- 重置筛选 -->
|
||||
@@ -68,8 +105,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="handleResetFilters"
|
||||
title="重置筛选"
|
||||
@click="handleResetFilters"
|
||||
>
|
||||
<FilterX class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -79,22 +116,31 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="exportLogs"
|
||||
title="导出"
|
||||
@click="exportLogs"
|
||||
>
|
||||
<Download class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<!-- 刷新按钮 -->
|
||||
<RefreshButton :loading="loading" @click="refreshLogs" />
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="refreshLogs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="logs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<div
|
||||
v-else-if="logs.length === 0"
|
||||
class="text-center py-12 text-muted-foreground"
|
||||
>
|
||||
暂无审计记录
|
||||
</div>
|
||||
|
||||
@@ -102,45 +148,81 @@
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="h-12 font-semibold">时间</TableHead>
|
||||
<TableHead class="h-12 font-semibold">用户</TableHead>
|
||||
<TableHead class="h-12 font-semibold">事件类型</TableHead>
|
||||
<TableHead class="h-12 font-semibold">描述</TableHead>
|
||||
<TableHead class="h-12 font-semibold">IP地址</TableHead>
|
||||
<TableHead class="h-12 font-semibold">状态</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
时间
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
用户
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
事件类型
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
描述
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
IP地址
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
状态
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="log in logs" :key="log.id" @mousedown="handleMouseDown" @click="handleRowClick($event, log)" class="cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors">
|
||||
<TableRow
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
class="cursor-pointer border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
@mousedown="handleMouseDown"
|
||||
@click="handleRowClick($event, log)"
|
||||
>
|
||||
<TableCell class="text-xs py-4">
|
||||
{{ formatDateTime(log.created_at) }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<div v-if="log.user_id" class="flex flex-col">
|
||||
<div
|
||||
v-if="log.user_id"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<span class="text-sm font-medium">
|
||||
{{ log.user_email || `用户 ${log.user_id}` }}
|
||||
</span>
|
||||
<span v-if="log.user_username" class="text-xs text-muted-foreground">
|
||||
<span
|
||||
v-if="log.user_username"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{ log.user_username }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-muted-foreground italic">系统</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-muted-foreground italic"
|
||||
>系统</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<Badge :variant="getEventTypeBadgeVariant(log.event_type)">
|
||||
<component :is="getEventTypeIcon(log.event_type)" class="h-3 w-3 mr-1" />
|
||||
<component
|
||||
:is="getEventTypeIcon(log.event_type)"
|
||||
class="h-3 w-3 mr-1"
|
||||
/>
|
||||
{{ getEventTypeLabel(log.event_type) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="max-w-xs truncate py-4" :title="log.description">
|
||||
<TableCell
|
||||
class="max-w-xs truncate py-4"
|
||||
:title="log.description"
|
||||
>
|
||||
{{ log.description || '无描述' }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<span v-if="log.ip_address" class="flex items-center text-sm">
|
||||
<span
|
||||
v-if="log.ip_address"
|
||||
class="flex items-center text-sm"
|
||||
>
|
||||
<Globe class="h-3 w-3 mr-1 text-muted-foreground" />
|
||||
{{ log.ip_address }}
|
||||
</span>
|
||||
@@ -148,7 +230,10 @@
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="py-4">
|
||||
<Badge v-if="log.status_code" :variant="getStatusCodeVariant(log.status_code)">
|
||||
<Badge
|
||||
v-if="log.status_code"
|
||||
:variant="getStatusCodeVariant(log.status_code)"
|
||||
>
|
||||
{{ log.status_code }}
|
||||
</Badge>
|
||||
<span v-else>-</span>
|
||||
@@ -170,12 +255,25 @@
|
||||
</Card>
|
||||
|
||||
<!-- 详情对话框 (使用shadcn Dialog组件) -->
|
||||
<div v-if="selectedLog" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="closeLogDetail">
|
||||
<Card class="max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto" @click.stop>
|
||||
<div
|
||||
v-if="selectedLog"
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
@click="closeLogDetail"
|
||||
>
|
||||
<Card
|
||||
class="max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto"
|
||||
@click.stop
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium">审计日志详情</h3>
|
||||
<Button variant="ghost" size="sm" @click="closeLogDetail">
|
||||
<h3 class="text-lg font-medium">
|
||||
审计日志详情
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="closeLogDetail"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -183,43 +281,64 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label>事件类型</Label>
|
||||
<p class="mt-1 text-sm">{{ getEventTypeLabel(selectedLog.event_type) }}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ getEventTypeLabel(selectedLog.event_type) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<p class="mt-1 text-sm">{{ selectedLog.description || '无描述' }}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ selectedLog.description || '无描述' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>时间</Label>
|
||||
<p class="mt-1 text-sm">{{ formatDateTime(selectedLog.created_at) }}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ formatDateTime(selectedLog.created_at) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog.user_id">
|
||||
<Label>用户信息</Label>
|
||||
<div class="mt-1 text-sm">
|
||||
<p class="font-medium">{{ selectedLog.user_email || `用户 ${selectedLog.user_id}` }}</p>
|
||||
<p v-if="selectedLog.user_username" class="text-muted-foreground">{{ selectedLog.user_username }}</p>
|
||||
<p class="text-xs text-muted-foreground">ID: {{ selectedLog.user_id }}</p>
|
||||
<p class="font-medium">
|
||||
{{ selectedLog.user_email || `用户 ${selectedLog.user_id}` }}
|
||||
</p>
|
||||
<p
|
||||
v-if="selectedLog.user_username"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ selectedLog.user_username }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
ID: {{ selectedLog.user_id }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog.ip_address">
|
||||
<Label>IP地址</Label>
|
||||
<p class="mt-1 text-sm">{{ selectedLog.ip_address }}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ selectedLog.ip_address }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog.status_code">
|
||||
<Label>状态码</Label>
|
||||
<p class="mt-1 text-sm">{{ selectedLog.status_code }}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{{ selectedLog.status_code }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog.error_message">
|
||||
<Label>错误消息</Label>
|
||||
<p class="mt-1 text-sm text-destructive">{{ selectedLog.error_message }}</p>
|
||||
<p class="mt-1 text-sm text-destructive">
|
||||
{{ selectedLog.error_message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog.metadata">
|
||||
@@ -334,7 +453,7 @@ async function loadLogs() {
|
||||
event_type: (filters.value.eventType !== '__all__' ? filters.value.eventType : undefined),
|
||||
days: filters.value.days,
|
||||
limit: pageSize.value,
|
||||
offset: offset
|
||||
offset
|
||||
}
|
||||
|
||||
const data = await auditApi.getAuditLogs(filterParams)
|
||||
|
||||
@@ -285,7 +285,9 @@ onBeforeUnmount(() => {
|
||||
<div class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">缓存监控</h2>
|
||||
<h2 class="text-2xl font-bold">
|
||||
缓存监控
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
管理缓存亲和性,提高 Prompt Caching 命中率
|
||||
</p>
|
||||
@@ -294,7 +296,9 @@ onBeforeUnmount(() => {
|
||||
<!-- 亲和性系统状态 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card class="p-4">
|
||||
<div class="text-xs text-muted-foreground">活跃亲和性</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
活跃亲和性
|
||||
</div>
|
||||
<div class="text-2xl font-bold mt-1">
|
||||
{{ stats?.affinity_stats?.active_affinities || 0 }}
|
||||
</div>
|
||||
@@ -304,8 +308,13 @@ onBeforeUnmount(() => {
|
||||
</Card>
|
||||
|
||||
<Card class="p-4">
|
||||
<div class="text-xs text-muted-foreground">Provider 切换</div>
|
||||
<div class="text-2xl font-bold mt-1" :class="(stats?.affinity_stats?.provider_switches || 0) > 0 ? 'text-destructive' : ''">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Provider 切换
|
||||
</div>
|
||||
<div
|
||||
class="text-2xl font-bold mt-1"
|
||||
:class="(stats?.affinity_stats?.provider_switches || 0) > 0 ? 'text-destructive' : ''"
|
||||
>
|
||||
{{ stats?.affinity_stats?.provider_switches || 0 }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">
|
||||
@@ -314,8 +323,13 @@ onBeforeUnmount(() => {
|
||||
</Card>
|
||||
|
||||
<Card class="p-4">
|
||||
<div class="text-xs text-muted-foreground">缓存失效</div>
|
||||
<div class="text-2xl font-bold mt-1" :class="(stats?.affinity_stats?.cache_invalidations || 0) > 0 ? 'text-warning' : ''">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
缓存失效
|
||||
</div>
|
||||
<div
|
||||
class="text-2xl font-bold mt-1"
|
||||
:class="(stats?.affinity_stats?.cache_invalidations || 0) > 0 ? 'text-warning' : ''"
|
||||
>
|
||||
{{ stats?.affinity_stats?.cache_invalidations || 0 }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">
|
||||
@@ -326,7 +340,13 @@ onBeforeUnmount(() => {
|
||||
<Card class="p-4">
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1">
|
||||
预留比例
|
||||
<Badge v-if="config?.dynamic_reservation?.enabled" variant="outline" class="text-[10px] px-1">动态</Badge>
|
||||
<Badge
|
||||
v-if="config?.dynamic_reservation?.enabled"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1"
|
||||
>
|
||||
动态
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold mt-1">
|
||||
<template v-if="config?.dynamic_reservation?.enabled">
|
||||
@@ -347,7 +367,9 @@ onBeforeUnmount(() => {
|
||||
<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>
|
||||
<h3 class="text-base font-semibold">
|
||||
亲和性列表
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
@@ -368,10 +390,19 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<Button @click="clearAllCache" variant="ghost" size="icon" class="h-8 w-8 text-muted-foreground/70 hover:text-destructive" title="清除全部缓存">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground/70 hover:text-destructive"
|
||||
title="清除全部缓存"
|
||||
@click="clearAllCache"
|
||||
>
|
||||
<Eraser class="h-4 w-4" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loading || listLoading" @click="refreshData" />
|
||||
<RefreshButton
|
||||
:loading="loading || listLoading"
|
||||
@click="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,41 +410,99 @@ onBeforeUnmount(() => {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-36">用户</TableHead>
|
||||
<TableHead class="w-28">Key</TableHead>
|
||||
<TableHead class="w-28">Provider</TableHead>
|
||||
<TableHead class="w-40">模型</TableHead>
|
||||
<TableHead class="w-36">API 格式 / Key</TableHead>
|
||||
<TableHead class="w-20 text-center">剩余</TableHead>
|
||||
<TableHead class="w-14 text-center">次数</TableHead>
|
||||
<TableHead class="w-12 text-right">操作</TableHead>
|
||||
<TableHead class="w-36">
|
||||
用户
|
||||
</TableHead>
|
||||
<TableHead class="w-28">
|
||||
Key
|
||||
</TableHead>
|
||||
<TableHead class="w-28">
|
||||
Provider
|
||||
</TableHead>
|
||||
<TableHead class="w-40">
|
||||
模型
|
||||
</TableHead>
|
||||
<TableHead class="w-36">
|
||||
API 格式 / Key
|
||||
</TableHead>
|
||||
<TableHead class="w-20 text-center">
|
||||
剩余
|
||||
</TableHead>
|
||||
<TableHead class="w-14 text-center">
|
||||
次数
|
||||
</TableHead>
|
||||
<TableHead class="w-12 text-right">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody v-if="!listLoading && affinityList.length">
|
||||
<TableRow v-for="item in paginatedAffinityList" :key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}`">
|
||||
<TableRow
|
||||
v-for="item in paginatedAffinityList"
|
||||
:key="`${item.affinity_key}-${item.endpoint_id}-${item.key_id}`"
|
||||
>
|
||||
<TableCell>
|
||||
<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 max-w-[120px]" :title="item.username ?? undefined">{{ item.username || '未知' }}</span>
|
||||
<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 max-w-[120px]"
|
||||
:title="item.username ?? undefined"
|
||||
>{{ item.username || '未知' }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-sm truncate max-w-[80px]" :title="item.user_api_key_name || undefined">{{ item.user_api_key_name || '未命名' }}</span>
|
||||
<Badge v-if="item.rate_multiplier !== 1.0" variant="outline" class="text-warning border-warning/30 text-[10px] px-2">{{ item.rate_multiplier }}x</Badge>
|
||||
<span
|
||||
class="text-sm truncate max-w-[80px]"
|
||||
:title="item.user_api_key_name || undefined"
|
||||
>{{ item.user_api_key_name || '未命名' }}</span>
|
||||
<Badge
|
||||
v-if="item.rate_multiplier !== 1.0"
|
||||
variant="outline"
|
||||
class="text-warning border-warning/30 text-[10px] px-2"
|
||||
>
|
||||
{{ item.rate_multiplier }}x
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">
|
||||
{{ item.user_api_key_prefix || '---' }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">{{ item.user_api_key_prefix || '---' }}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="text-sm truncate max-w-[100px]" :title="item.provider_name || undefined">{{ item.provider_name || '未知' }}</div>
|
||||
<div
|
||||
class="text-sm truncate max-w-[100px]"
|
||||
:title="item.provider_name || undefined"
|
||||
>
|
||||
{{ item.provider_name || '未知' }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="text-sm truncate max-w-[150px]" :title="item.model_display_name || undefined">{{ item.model_display_name || '---' }}</div>
|
||||
<div class="text-xs text-muted-foreground" :title="item.model_name || undefined">{{ item.model_name || '---' }}</div>
|
||||
<div
|
||||
class="text-sm truncate max-w-[150px]"
|
||||
:title="item.model_display_name || undefined"
|
||||
>
|
||||
{{ item.model_display_name || '---' }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-muted-foreground"
|
||||
:title="item.model_name || undefined"
|
||||
>
|
||||
{{ item.model_name || '---' }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="text-sm">{{ item.endpoint_api_format || '---' }}</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">{{ item.key_prefix || '---' }}</div>
|
||||
<div class="text-sm">
|
||||
{{ item.endpoint_api_format || '---' }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground font-mono">
|
||||
{{ item.key_prefix || '---' }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span class="text-xs">{{ getRemainingTime(item.expire_at) }}</span>
|
||||
@@ -426,9 +515,9 @@ onBeforeUnmount(() => {
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-destructive"
|
||||
@click="clearUserCache(item.affinity_key, item.user_api_key_name || item.affinity_key)"
|
||||
:disabled="clearingRowAffinityKey === item.affinity_key"
|
||||
title="清除缓存"
|
||||
@click="clearUserCache(item.affinity_key, item.user_api_key_name || item.affinity_key)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -437,7 +526,10 @@ onBeforeUnmount(() => {
|
||||
</TableBody>
|
||||
<TableBody v-else>
|
||||
<TableRow>
|
||||
<TableCell colspan="8" class="text-center py-6 text-sm text-muted-foreground">
|
||||
<TableCell
|
||||
colspan="8"
|
||||
class="text-center py-6 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ listLoading ? '加载中...' : '暂无缓存记录' }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -460,7 +552,9 @@ onBeforeUnmount(() => {
|
||||
<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">TTL 分析</h3>
|
||||
<h3 class="text-base font-semibold">
|
||||
TTL 分析
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground">分析用户请求间隔,推荐合适的缓存 TTL</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -483,31 +577,62 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<!-- 缓存命中概览 -->
|
||||
<div v-if="hitAnalysis" class="px-6 py-4 border-b border-border/40 bg-muted/30">
|
||||
<div
|
||||
v-if="hitAnalysis"
|
||||
class="px-6 py-4 border-b border-border/40 bg-muted/30"
|
||||
>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-6">
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground">请求命中率</div>
|
||||
<div class="text-2xl font-bold text-success">{{ hitAnalysis.request_cache_hit_rate }}%</div>
|
||||
<div class="text-xs text-muted-foreground">{{ formatNumber(hitAnalysis.requests_with_cache_hit) }} / {{ formatNumber(hitAnalysis.total_requests) }} 请求</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
请求命中率
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-success">
|
||||
{{ hitAnalysis.request_cache_hit_rate }}%
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ formatNumber(hitAnalysis.requests_with_cache_hit) }} / {{ formatNumber(hitAnalysis.total_requests) }} 请求
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground">Token 命中率</div>
|
||||
<div class="text-2xl font-bold">{{ hitAnalysis.token_cache_hit_rate }}%</div>
|
||||
<div class="text-xs text-muted-foreground">{{ formatTokens(hitAnalysis.total_cache_read_tokens) }} tokens 命中</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Token 命中率
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ hitAnalysis.token_cache_hit_rate }}%
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ formatTokens(hitAnalysis.total_cache_read_tokens) }} tokens 命中
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground">缓存创建费用</div>
|
||||
<div class="text-2xl font-bold">{{ formatCost(hitAnalysis.total_cache_creation_cost_usd) }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ formatTokens(hitAnalysis.total_cache_creation_tokens) }} tokens</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
缓存创建费用
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ formatCost(hitAnalysis.total_cache_creation_cost_usd) }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ formatTokens(hitAnalysis.total_cache_creation_tokens) }} tokens
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground">缓存读取费用</div>
|
||||
<div class="text-2xl font-bold">{{ formatCost(hitAnalysis.total_cache_read_cost_usd) }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ formatTokens(hitAnalysis.total_cache_read_tokens) }} tokens</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
缓存读取费用
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ formatCost(hitAnalysis.total_cache_read_cost_usd) }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ formatTokens(hitAnalysis.total_cache_read_tokens) }} tokens
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground">预估节省</div>
|
||||
<div class="text-2xl font-bold text-success">{{ formatCost(hitAnalysis.estimated_savings_usd) }}</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
预估节省
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-success">
|
||||
{{ formatCost(hitAnalysis.estimated_savings_usd) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -516,24 +641,41 @@ onBeforeUnmount(() => {
|
||||
<Table v-if="ttlAnalysis && ttlAnalysis.users.length > 0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-10"></TableHead>
|
||||
<TableHead class="w-[20%]">用户</TableHead>
|
||||
<TableHead class="w-[15%] text-center">请求数</TableHead>
|
||||
<TableHead class="w-[15%] text-center">使用频率</TableHead>
|
||||
<TableHead class="w-[15%] text-center">推荐 TTL</TableHead>
|
||||
<TableHead class="w-10" />
|
||||
<TableHead class="w-[20%]">
|
||||
用户
|
||||
</TableHead>
|
||||
<TableHead class="w-[15%] text-center">
|
||||
请求数
|
||||
</TableHead>
|
||||
<TableHead class="w-[15%] text-center">
|
||||
使用频率
|
||||
</TableHead>
|
||||
<TableHead class="w-[15%] text-center">
|
||||
推荐 TTL
|
||||
</TableHead>
|
||||
<TableHead>说明</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-for="user in ttlAnalysis.users" :key="user.group_id">
|
||||
<template
|
||||
v-for="user in ttlAnalysis.users"
|
||||
:key="user.group_id"
|
||||
>
|
||||
<TableRow
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
@click="toggleUserExpand(user.group_id)"
|
||||
>
|
||||
<TableCell class="p-2">
|
||||
<button class="p-1 hover:bg-muted rounded">
|
||||
<ChevronDown v-if="expandedUserId === user.group_id" class="h-4 w-4 text-muted-foreground" />
|
||||
<ChevronRight v-else class="h-4 w-4 text-muted-foreground" />
|
||||
<ChevronDown
|
||||
v-if="expandedUserId === user.group_id"
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
class="h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -543,7 +685,10 @@ onBeforeUnmount(() => {
|
||||
<span class="text-sm font-medium">{{ user.request_count }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span class="text-sm" :class="getFrequencyClass(user.recommended_ttl_minutes)">
|
||||
<span
|
||||
class="text-sm"
|
||||
:class="getFrequencyClass(user.recommended_ttl_minutes)"
|
||||
>
|
||||
{{ getFrequencyLabel(user.recommended_ttl_minutes) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -559,27 +704,47 @@ onBeforeUnmount(() => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<!-- 展开行:显示用户散点图 -->
|
||||
<TableRow v-if="expandedUserId === user.group_id" class="bg-muted/30">
|
||||
<TableCell colspan="6" class="p-0">
|
||||
<TableRow
|
||||
v-if="expandedUserId === user.group_id"
|
||||
class="bg-muted/30"
|
||||
>
|
||||
<TableCell
|
||||
colspan="6"
|
||||
class="p-0"
|
||||
>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-medium">请求间隔时间线</h4>
|
||||
<h4 class="text-sm font-medium">
|
||||
请求间隔时间线
|
||||
</h4>
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-green-500"></span> 0-5分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500"></span> 5-15分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-purple-500"></span> 15-30分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-orange-500"></span> 30-60分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-red-500"></span> >60分钟</span>
|
||||
<span v-if="userTimelineData" class="ml-2">共 {{ userTimelineData.total_points }} 个数据点</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-green-500" /> 0-5分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500" /> 5-15分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-purple-500" /> 15-30分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-orange-500" /> 30-60分钟</span>
|
||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-red-500" /> >60分钟</span>
|
||||
<span
|
||||
v-if="userTimelineData"
|
||||
class="ml-2"
|
||||
>共 {{ userTimelineData.total_points }} 个数据点</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="userTimelineLoading" class="h-64 flex items-center justify-center">
|
||||
<div
|
||||
v-if="userTimelineLoading"
|
||||
class="h-64 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="userTimelineData && userTimelineData.points.length > 0" class="h-64">
|
||||
<div
|
||||
v-else-if="userTimelineData && userTimelineData.points.length > 0"
|
||||
class="h-64"
|
||||
>
|
||||
<ScatterChart :data="userTimelineChartData" />
|
||||
</div>
|
||||
<div v-else class="h-64 flex items-center justify-center">
|
||||
<div
|
||||
v-else
|
||||
class="h-64 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">暂无数据</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,7 +755,10 @@ onBeforeUnmount(() => {
|
||||
</Table>
|
||||
|
||||
<!-- 分析完成但无数据 -->
|
||||
<div v-else-if="ttlAnalysis && ttlAnalysis.users.length === 0" class="px-6 py-12 text-center">
|
||||
<div
|
||||
v-else-if="ttlAnalysis && ttlAnalysis.users.length === 0"
|
||||
class="px-6 py-12 text-center"
|
||||
>
|
||||
<BarChart3 class="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
|
||||
<p class="text-sm text-muted-foreground">
|
||||
未找到符合条件的用户数据
|
||||
@@ -601,8 +769,13 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-else-if="ttlAnalysisLoading" class="px-6 py-12 text-center">
|
||||
<p class="text-sm text-muted-foreground">正在分析用户请求数据...</p>
|
||||
<div
|
||||
v-else-if="ttlAnalysisLoading"
|
||||
class="px-6 py-12 text-center"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
正在分析用户请求数据...
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">黑名单 IP 数量</p>
|
||||
<h3 class="text-2xl font-bold mt-2">{{ blacklistStats.total || 0 }}</h3>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
黑名单 IP 数量
|
||||
</p>
|
||||
<h3 class="text-2xl font-bold mt-2">
|
||||
{{ blacklistStats.total || 0 }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center">
|
||||
<ShieldX class="h-6 w-6 text-destructive" />
|
||||
@@ -20,8 +24,12 @@
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">白名单 IP 数量</p>
|
||||
<h3 class="text-2xl font-bold mt-2">{{ whitelistData.total || 0 }}</h3>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
白名单 IP 数量
|
||||
</p>
|
||||
<h3 class="text-2xl font-bold mt-2">
|
||||
{{ whitelistData.total || 0 }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<ShieldCheck class="h-6 w-6 text-primary" />
|
||||
@@ -32,76 +40,119 @@
|
||||
</div>
|
||||
|
||||
<!-- IP 黑名单管理 -->
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">IP 黑名单</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">管理被禁止访问的 IP 地址</p>
|
||||
<h3 class="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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="showAddBlacklistDialog = true"
|
||||
title="添加黑名单"
|
||||
@click="showAddBlacklistDialog = true"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loadingBlacklist" @click="loadBlacklistStats" />
|
||||
<RefreshButton
|
||||
:loading="loadingBlacklist"
|
||||
@click="loadBlacklistStats"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingBlacklist" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div
|
||||
v-if="loadingBlacklist"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
|
||||
<div v-else class="p-6">
|
||||
<div v-if="!blacklistStats.available" class="text-center py-8 text-muted-foreground">
|
||||
<div
|
||||
v-else
|
||||
class="p-6"
|
||||
>
|
||||
<div
|
||||
v-if="!blacklistStats.available"
|
||||
class="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
<AlertCircle class="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>Redis 不可用,无法管理黑名单</p>
|
||||
<p class="text-xs mt-1">{{ blacklistStats.error }}</p>
|
||||
<p class="text-xs mt-1">
|
||||
{{ blacklistStats.error }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="blacklistStats.total === 0" class="text-center py-8 text-muted-foreground">
|
||||
<div
|
||||
v-else-if="blacklistStats.total === 0"
|
||||
class="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
<ShieldX class="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>暂无黑名单 IP</p>
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground">
|
||||
<div
|
||||
v-else
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
当前共有 <span class="font-semibold text-foreground">{{ blacklistStats.total }}</span> 个 IP 在黑名单中
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- IP 白名单管理 -->
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold">IP 白名单</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">管理可信任的 IP 地址(支持 CIDR 格式)</p>
|
||||
<h3 class="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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="showAddWhitelistDialog = true"
|
||||
title="添加白名单"
|
||||
@click="showAddWhitelistDialog = true"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loadingWhitelist" @click="loadWhitelist" />
|
||||
<RefreshButton
|
||||
:loading="loadingWhitelist"
|
||||
@click="loadWhitelist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingWhitelist" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div
|
||||
v-if="loadingWhitelist"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="whitelistData.whitelist.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
<div
|
||||
v-else-if="whitelistData.whitelist.length === 0"
|
||||
class="text-center py-12 text-muted-foreground"
|
||||
>
|
||||
<ShieldCheck class="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>暂无白名单 IP</p>
|
||||
</div>
|
||||
@@ -111,18 +162,25 @@
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>IP 地址 / CIDR</TableHead>
|
||||
<TableHead class="text-right">操作</TableHead>
|
||||
<TableHead class="text-right">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="ip in whitelistData.whitelist" :key="ip">
|
||||
<TableCell class="font-mono text-sm">{{ ip }}</TableCell>
|
||||
<TableRow
|
||||
v-for="ip in whitelistData.whitelist"
|
||||
:key="ip"
|
||||
>
|
||||
<TableCell class="font-mono text-sm">
|
||||
{{ ip }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleRemoveFromWhitelist(ip)"
|
||||
class="h-8 px-3"
|
||||
@click="handleRemoveFromWhitelist(ip)"
|
||||
>
|
||||
<Trash2 class="w-4 h-4 mr-1.5" />
|
||||
移除
|
||||
@@ -174,11 +232,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" @click="showAddBlacklistDialog = false">取消</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="showAddBlacklistDialog = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@click="handleAddToBlacklist"
|
||||
:disabled="!blacklistForm.ip_address || !blacklistForm.reason"
|
||||
@click="handleAddToBlacklist"
|
||||
>
|
||||
添加到黑名单
|
||||
</Button>
|
||||
@@ -209,10 +272,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" @click="showAddWhitelistDialog = false">取消</Button>
|
||||
<Button
|
||||
@click="handleAddToWhitelist"
|
||||
variant="ghost"
|
||||
@click="showAddWhitelistDialog = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="!whitelistForm.ip_address"
|
||||
@click="handleAddToWhitelist"
|
||||
>
|
||||
添加到白名单
|
||||
</Button>
|
||||
|
||||
@@ -4,252 +4,314 @@
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<!-- 模型列表 -->
|
||||
<Card class="overflow-hidden">
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- 左侧:标题 -->
|
||||
<h3 class="text-base font-semibold">模型管理</h3>
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- 左侧:标题 -->
|
||||
<h3 class="text-base font-semibold">
|
||||
模型管理
|
||||
</h3>
|
||||
|
||||
<!-- 右侧:操作区 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
|
||||
<Input
|
||||
id="model-search"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索模型名称..."
|
||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 能力筛选 -->
|
||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.streaming ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="capabilityFilters.streaming = !capabilityFilters.streaming"
|
||||
title="流式输出"
|
||||
>
|
||||
<Zap class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.imageGeneration ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="capabilityFilters.imageGeneration = !capabilityFilters.imageGeneration"
|
||||
title="图像生成"
|
||||
>
|
||||
<Image class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="capabilityFilters.vision = !capabilityFilters.vision"
|
||||
title="视觉理解"
|
||||
>
|
||||
<Eye class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
|
||||
title="工具调用"
|
||||
>
|
||||
<Wrench class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
|
||||
title="深度思考"
|
||||
>
|
||||
<Brain class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openCreateModelDialog"
|
||||
title="创建模型"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loading" @click="refreshData" />
|
||||
</div>
|
||||
<!-- 右侧:操作区 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/70 z-10 pointer-events-none" />
|
||||
<Input
|
||||
id="model-search"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索模型名称..."
|
||||
class="w-44 pl-8 pr-3 h-8 text-sm bg-muted/30 border-border/50 focus:border-primary/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 能力筛选 -->
|
||||
<div class="flex items-center border rounded-md border-border/60 h-8 overflow-hidden">
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.streaming ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="流式输出"
|
||||
@click="capabilityFilters.streaming = !capabilityFilters.streaming"
|
||||
>
|
||||
<Zap class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.imageGeneration ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="图像生成"
|
||||
@click="capabilityFilters.imageGeneration = !capabilityFilters.imageGeneration"
|
||||
>
|
||||
<Image class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.vision ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="视觉理解"
|
||||
@click="capabilityFilters.vision = !capabilityFilters.vision"
|
||||
>
|
||||
<Eye class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.toolUse ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="工具调用"
|
||||
@click="capabilityFilters.toolUse = !capabilityFilters.toolUse"
|
||||
>
|
||||
<Wrench class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-border/60" />
|
||||
<button
|
||||
class="px-2.5 h-full text-xs transition-colors"
|
||||
:class="capabilityFilters.extendedThinking ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'"
|
||||
title="深度思考"
|
||||
@click="capabilityFilters.extendedThinking = !capabilityFilters.extendedThinking"
|
||||
>
|
||||
<Brain class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="创建模型"
|
||||
@click="openCreateModelDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[240px]">模型名称</TableHead>
|
||||
<TableHead class="w-[140px]">能力/偏好</TableHead>
|
||||
<TableHead class="w-[160px] text-center">价格 ($/M)</TableHead>
|
||||
<TableHead class="w-[80px] text-center">提供商</TableHead>
|
||||
<TableHead class="w-[70px] text-center">别名/映射</TableHead>
|
||||
<TableHead class="w-[80px] text-center">调用次数</TableHead>
|
||||
<TableHead class="w-[70px]">状态</TableHead>
|
||||
<TableHead class="w-[140px] text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="loading">
|
||||
<TableCell colspan="8" class="text-center py-8">
|
||||
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-else-if="filteredGlobalModels.length === 0">
|
||||
<TableCell colspan="8" class="text-center py-8 text-muted-foreground">
|
||||
没有找到匹配的模型
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<template v-else>
|
||||
<TableRow
|
||||
v-for="model in paginatedGlobalModels"
|
||||
:key="model.id"
|
||||
class="cursor-pointer hover:bg-muted/50 group"
|
||||
@mousedown="handleMouseDown"
|
||||
@click="handleRowClick($event, model)"
|
||||
>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div class="font-medium">{{ model.display_name }}</div>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<span>{{ model.name }}</span>
|
||||
<button
|
||||
class="p-0.5 rounded hover:bg-muted transition-colors"
|
||||
title="复制模型 ID"
|
||||
@click.stop="copyToClipboard(model.name)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[240px]">
|
||||
模型名称
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px]">
|
||||
能力/偏好
|
||||
</TableHead>
|
||||
<TableHead class="w-[160px] text-center">
|
||||
价格 ($/M)
|
||||
</TableHead>
|
||||
<TableHead class="w-[80px] text-center">
|
||||
提供商
|
||||
</TableHead>
|
||||
<TableHead class="w-[70px] text-center">
|
||||
别名/映射
|
||||
</TableHead>
|
||||
<TableHead class="w-[80px] text-center">
|
||||
调用次数
|
||||
</TableHead>
|
||||
<TableHead class="w-[70px]">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px] text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="loading">
|
||||
<TableCell
|
||||
colspan="8"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<Loader2 class="w-6 h-6 animate-spin mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-else-if="filteredGlobalModels.length === 0">
|
||||
<TableCell
|
||||
colspan="8"
|
||||
class="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
没有找到匹配的模型
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<template v-else>
|
||||
<TableRow
|
||||
v-for="model in paginatedGlobalModels"
|
||||
:key="model.id"
|
||||
class="cursor-pointer hover:bg-muted/50 group"
|
||||
@mousedown="handleMouseDown"
|
||||
@click="handleRowClick($event, model)"
|
||||
>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ model.display_name }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<span>{{ model.name }}</span>
|
||||
<button
|
||||
class="p-0.5 rounded hover:bg-muted transition-colors"
|
||||
title="复制模型 ID"
|
||||
@click.stop="copyToClipboard(model.name)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="space-y-1 w-fit">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Zap
|
||||
v-if="model.default_supports_streaming"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="流式输出"
|
||||
/>
|
||||
<Image
|
||||
v-if="model.default_supports_image_generation"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="图像生成"
|
||||
/>
|
||||
<Eye
|
||||
v-if="model.default_supports_vision"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="视觉理解"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.default_supports_function_calling"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="工具调用"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.default_supports_extended_thinking"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="深度思考"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="model.supported_capabilities?.length">
|
||||
<div class="border-t border-border/50" />
|
||||
<div class="flex flex-wrap gap-0.5">
|
||||
<span
|
||||
v-for="capName in model.supported_capabilities"
|
||||
:key="capName"
|
||||
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
|
||||
:title="getCapabilityDisplayName(capName)"
|
||||
>{{ getCapabilityShortName(capName) }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="space-y-1 w-fit">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<Zap v-if="model.default_supports_streaming" class="w-4 h-4 text-muted-foreground" title="流式输出" />
|
||||
<Image v-if="model.default_supports_image_generation" class="w-4 h-4 text-muted-foreground" title="图像生成" />
|
||||
<Eye v-if="model.default_supports_vision" class="w-4 h-4 text-muted-foreground" title="视觉理解" />
|
||||
<Wrench v-if="model.default_supports_function_calling" class="w-4 h-4 text-muted-foreground" title="工具调用" />
|
||||
<Brain v-if="model.default_supports_extended_thinking" class="w-4 h-4 text-muted-foreground" title="深度思考" />
|
||||
</div>
|
||||
<template v-if="model.supported_capabilities?.length">
|
||||
<div class="border-t border-border/50"></div>
|
||||
<div class="flex flex-wrap gap-0.5">
|
||||
<span
|
||||
v-for="capName in model.supported_capabilities"
|
||||
:key="capName"
|
||||
class="text-[11px] px-1 py-0.5 rounded bg-muted/60 text-muted-foreground"
|
||||
:title="getCapabilityDisplayName(capName)"
|
||||
>{{ getCapabilityShortName(capName) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<div class="text-xs space-y-0.5">
|
||||
<!-- 按 Token 计费 -->
|
||||
<div v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')">
|
||||
<span class="text-muted-foreground">In:</span>
|
||||
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}</span>
|
||||
<span class="text-muted-foreground mx-1">/</span>
|
||||
<span class="text-muted-foreground">Out:</span>
|
||||
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}</span>
|
||||
<!-- 阶梯计费标记 -->
|
||||
<span v-if="hasTieredPricing(model)" class="ml-1 text-muted-foreground" title="阶梯计费">[阶梯]</span>
|
||||
</div>
|
||||
<!-- 按次计费 -->
|
||||
<div v-if="model.default_price_per_request && model.default_price_per_request > 0">
|
||||
<span class="text-muted-foreground">按次:</span>
|
||||
<span class="font-mono ml-1">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
||||
</div>
|
||||
<!-- 无计费配置 -->
|
||||
<div v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request" class="text-muted-foreground">-</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge variant="secondary">{{ model.provider_count || 0 }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge variant="secondary">{{ model.alias_count || 0 }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span class="text-sm font-mono">{{ formatUsageCount(model.usage_count || 0) }}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="model.is_active ? 'default' : 'secondary'">
|
||||
{{ model.is_active ? '活跃' : '停用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click.stop="selectModel(model)"
|
||||
title="查看详情"
|
||||
>
|
||||
<Eye class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click.stop="editModel(model)"
|
||||
title="编辑模型"
|
||||
>
|
||||
<Edit class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click.stop="toggleModelStatus(model)"
|
||||
:title="model.is_active ? '停用模型' : '启用模型'"
|
||||
>
|
||||
<Power class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click.stop="deleteModel(model)"
|
||||
title="删除模型"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</template>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<div class="text-xs space-y-0.5">
|
||||
<!-- 按 Token 计费 -->
|
||||
<div v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')">
|
||||
<span class="text-muted-foreground">In:</span>
|
||||
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'input')?.toFixed(2) || '-' }}</span>
|
||||
<span class="text-muted-foreground mx-1">/</span>
|
||||
<span class="text-muted-foreground">Out:</span>
|
||||
<span class="font-mono ml-1">{{ getFirstTierPrice(model, 'output')?.toFixed(2) || '-' }}</span>
|
||||
<!-- 阶梯计费标记 -->
|
||||
<span
|
||||
v-if="hasTieredPricing(model)"
|
||||
class="ml-1 text-muted-foreground"
|
||||
title="阶梯计费"
|
||||
>[阶梯]</span>
|
||||
</div>
|
||||
<!-- 按次计费 -->
|
||||
<div v-if="model.default_price_per_request && model.default_price_per_request > 0">
|
||||
<span class="text-muted-foreground">按次:</span>
|
||||
<span class="font-mono ml-1">${{ model.default_price_per_request.toFixed(3) }}/次</span>
|
||||
</div>
|
||||
<!-- 无计费配置 -->
|
||||
<div
|
||||
v-if="!getFirstTierPrice(model, 'input') && !getFirstTierPrice(model, 'output') && !model.default_price_per_request"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge variant="secondary">
|
||||
{{ model.provider_count || 0 }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<Badge variant="secondary">
|
||||
{{ model.alias_count || 0 }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span class="text-sm font-mono">{{ formatUsageCount(model.usage_count || 0) }}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="model.is_active ? 'default' : 'secondary'">
|
||||
{{ model.is_active ? '活跃' : '停用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="查看详情"
|
||||
@click.stop="selectModel(model)"
|
||||
>
|
||||
<Eye class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑模型"
|
||||
@click.stop="editModel(model)"
|
||||
>
|
||||
<Edit class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="model.is_active ? '停用模型' : '启用模型'"
|
||||
@click.stop="toggleModelStatus(model)"
|
||||
>
|
||||
<Power class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="删除模型"
|
||||
@click.stop="deleteModel(model)"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="!loading && filteredGlobalModels.length > 0"
|
||||
:current="catalogCurrentPage"
|
||||
:total="filteredGlobalModels.length"
|
||||
:page-size="catalogPageSize"
|
||||
@update:current="catalogCurrentPage = $event"
|
||||
@update:page-size="catalogPageSize = $event"
|
||||
/>
|
||||
</Card>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-if="!loading && filteredGlobalModels.length > 0"
|
||||
:current="catalogCurrentPage"
|
||||
:total="filteredGlobalModels.length"
|
||||
:page-size="catalogPageSize"
|
||||
@update:current="catalogCurrentPage = $event"
|
||||
@update:page-size="catalogPageSize = $event"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑模型对话框 -->
|
||||
@@ -299,22 +361,32 @@
|
||||
<!-- 批量添加关联提供商对话框 -->
|
||||
<Dialog
|
||||
:model-value="batchAddProvidersDialogOpen"
|
||||
@update:model-value="handleBatchAddProvidersDialogUpdate"
|
||||
title="批量添加关联提供商"
|
||||
description="为模型批量添加 Provider 实现, 提供商将自动继承模型的价格和能力, 可在添加后单独修改"
|
||||
:icon="Server"
|
||||
size="4xl"
|
||||
@update:model-value="handleBatchAddProvidersDialogUpdate"
|
||||
>
|
||||
<template #default>
|
||||
<div v-if="selectedModel" class="space-y-4">
|
||||
<div
|
||||
v-if="selectedModel"
|
||||
class="space-y-4"
|
||||
>
|
||||
<!-- 模型信息头部 -->
|
||||
<div class="rounded-lg border bg-muted/30 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="font-semibold text-lg">{{ selectedModel.display_name }}</p>
|
||||
<p class="text-sm text-muted-foreground font-mono">{{ selectedModel.name }}</p>
|
||||
<p class="font-semibold text-lg">
|
||||
{{ selectedModel.display_name }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground font-mono">
|
||||
{{ selectedModel.name }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" class="text-xs">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
当前 {{ selectedModelProviders.length }} 个 Provider
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -326,7 +398,9 @@
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium">可添加</p>
|
||||
<p class="text-sm font-medium">
|
||||
可添加
|
||||
</p>
|
||||
<Button
|
||||
v-if="availableProvidersForBatchAdd.length > 0"
|
||||
variant="ghost"
|
||||
@@ -337,19 +411,33 @@
|
||||
{{ isAllLeftSelected ? '取消全选' : '全选' }}
|
||||
</Button>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ availableProvidersForBatchAdd.length }} 个
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="border rounded-lg h-80 overflow-y-auto">
|
||||
<div v-if="loadingProviderOptions" class="flex items-center justify-center h-full">
|
||||
<div
|
||||
v-if="loadingProviderOptions"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<Loader2 class="w-6 h-6 animate-spin text-primary" />
|
||||
</div>
|
||||
<div v-else-if="availableProvidersForBatchAdd.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<div
|
||||
v-else-if="availableProvidersForBatchAdd.length === 0"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
>
|
||||
<Building2 class="w-10 h-10 mb-2 opacity-30" />
|
||||
<p class="text-sm">所有 Provider 均已关联</p>
|
||||
<p class="text-sm">
|
||||
所有 Provider 均已关联
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="p-2 space-y-1">
|
||||
<div
|
||||
v-else
|
||||
class="p-2 space-y-1"
|
||||
>
|
||||
<div
|
||||
v-for="provider in availableProvidersForBatchAdd"
|
||||
:key="provider.id"
|
||||
@@ -365,8 +453,12 @@
|
||||
@click.stop
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm truncate">{{ provider.display_name || provider.name }}</p>
|
||||
<p class="text-xs text-muted-foreground truncate">{{ provider.name }}</p>
|
||||
<p class="font-medium text-sm truncate">
|
||||
{{ provider.display_name || provider.name }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ provider.name }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'outline' : 'secondary'"
|
||||
@@ -388,11 +480,18 @@
|
||||
class="w-9 h-8"
|
||||
:class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'border-primary' : ''"
|
||||
:disabled="selectedLeftProviderIds.length === 0 || submittingBatchAddProviders"
|
||||
@click="batchAddSelectedProviders"
|
||||
title="添加选中"
|
||||
@click="batchAddSelectedProviders"
|
||||
>
|
||||
<Loader2 v-if="submittingBatchAddProviders" class="w-4 h-4 animate-spin" />
|
||||
<ChevronRight v-else class="w-6 h-6 stroke-[3]" :class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'text-primary' : ''" />
|
||||
<Loader2
|
||||
v-if="submittingBatchAddProviders"
|
||||
class="w-4 h-4 animate-spin"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
class="w-6 h-6 stroke-[3]"
|
||||
:class="selectedLeftProviderIds.length > 0 && !submittingBatchAddProviders ? 'text-primary' : ''"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -400,11 +499,18 @@
|
||||
class="w-9 h-8"
|
||||
:class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'border-primary' : ''"
|
||||
:disabled="selectedRightProviderIds.length === 0 || submittingBatchRemoveProviders"
|
||||
@click="batchRemoveSelectedProviders"
|
||||
title="移除选中"
|
||||
@click="batchRemoveSelectedProviders"
|
||||
>
|
||||
<Loader2 v-if="submittingBatchRemoveProviders" class="w-4 h-4 animate-spin" />
|
||||
<ChevronLeft v-else class="w-6 h-6 stroke-[3]" :class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'text-primary' : ''" />
|
||||
<Loader2
|
||||
v-if="submittingBatchRemoveProviders"
|
||||
class="w-4 h-4 animate-spin"
|
||||
/>
|
||||
<ChevronLeft
|
||||
v-else
|
||||
class="w-6 h-6 stroke-[3]"
|
||||
:class="selectedRightProviderIds.length > 0 && !submittingBatchRemoveProviders ? 'text-primary' : ''"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -412,7 +518,9 @@
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium">已添加</p>
|
||||
<p class="text-sm font-medium">
|
||||
已添加
|
||||
</p>
|
||||
<Button
|
||||
v-if="selectedModelProviders.length > 0"
|
||||
variant="ghost"
|
||||
@@ -423,16 +531,27 @@
|
||||
{{ isAllRightSelected ? '取消全选' : '全选' }}
|
||||
</Button>
|
||||
</div>
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ selectedModelProviders.length }} 个
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="border rounded-lg h-80 overflow-y-auto">
|
||||
<div v-if="selectedModelProviders.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<div
|
||||
v-if="selectedModelProviders.length === 0"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
>
|
||||
<Building2 class="w-10 h-10 mb-2 opacity-30" />
|
||||
<p class="text-sm">暂无关联提供商</p>
|
||||
<p class="text-sm">
|
||||
暂无关联提供商
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="p-2 space-y-1">
|
||||
<div
|
||||
v-else
|
||||
class="p-2 space-y-1"
|
||||
>
|
||||
<!-- 已存在的(可选中删除) -->
|
||||
<div
|
||||
v-for="provider in selectedModelProviders"
|
||||
@@ -449,8 +568,12 @@
|
||||
@click.stop
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm truncate">{{ provider.display_name }}</p>
|
||||
<p class="text-xs text-muted-foreground truncate">{{ provider.identifier }}</p>
|
||||
<p class="font-medium text-sm truncate">
|
||||
{{ provider.display_name }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ provider.identifier }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'outline' : 'secondary'"
|
||||
@@ -485,7 +608,6 @@
|
||||
@update:open="handleEditProviderDialogUpdate"
|
||||
@saved="handleEditProviderSaved"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -623,9 +745,9 @@ const { confirmDanger } = useConfirm()
|
||||
// 格式化调用次数(大数字简化显示)
|
||||
function formatUsageCount(count: number): string {
|
||||
if (count >= 1000000) {
|
||||
return (count / 1000000).toFixed(1) + 'M'
|
||||
return `${(count / 1000000).toFixed(1) }M`
|
||||
} else if (count >= 1000) {
|
||||
return (count / 1000).toFixed(1) + 'K'
|
||||
return `${(count / 1000).toFixed(1) }K`
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- 提供商表格 -->
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<!-- 标题和操作栏 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/50">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- 左侧:标题 -->
|
||||
<h3 class="text-base font-semibold text-foreground">提供商管理</h3>
|
||||
<h3 class="text-base font-semibold text-foreground">
|
||||
提供商管理
|
||||
</h3>
|
||||
|
||||
<!-- 右侧:操作区 -->
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -27,8 +32,8 @@
|
||||
<!-- 调度策略 -->
|
||||
<button
|
||||
class="group inline-flex items-center gap-1.5 px-2.5 h-8 rounded-md border border-border/50 bg-muted/20 hover:bg-muted/40 hover:border-primary/40 transition-all duration-200 text-xs"
|
||||
@click="openPriorityDialog"
|
||||
title="点击调整调度策略"
|
||||
@click="openPriorityDialog"
|
||||
>
|
||||
<span class="text-muted-foreground/80">调度:</span>
|
||||
<span class="font-medium text-foreground/90">{{ priorityModeConfig.label }}</span>
|
||||
@@ -42,23 +47,32 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openAddProviderDialog"
|
||||
title="新增提供商"
|
||||
@click="openAddProviderDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<RefreshButton :loading="loading" @click="loadProviders" />
|
||||
<RefreshButton
|
||||
:loading="loading"
|
||||
@click="loadProviders"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="filteredProviders.length === 0" class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div
|
||||
v-else-if="filteredProviders.length === 0"
|
||||
class="flex flex-col items-center justify-center py-16 text-center"
|
||||
>
|
||||
<div class="text-muted-foreground mb-2">
|
||||
<template v-if="searchQuery">
|
||||
未找到匹配 "{{ searchQuery }}" 的提供商
|
||||
@@ -67,24 +81,48 @@
|
||||
暂无提供商,点击右上角添加
|
||||
</template>
|
||||
</div>
|
||||
<Button v-if="searchQuery" variant="outline" size="sm" @click="searchQuery = ''">
|
||||
<Button
|
||||
v-if="searchQuery"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="searchQuery = ''"
|
||||
>
|
||||
清除搜索
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端表格 -->
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div
|
||||
v-else
|
||||
class="overflow-x-auto"
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/40 hover:bg-transparent">
|
||||
<TableHead class="w-[150px] h-11 font-medium text-foreground/80">提供商信息</TableHead>
|
||||
<TableHead class="w-[100px] h-11 font-medium text-foreground/80">计费类型</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80">官网</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80 text-center">资源统计</TableHead>
|
||||
<TableHead class="w-[240px] h-11 font-medium text-foreground/80">端点健康</TableHead>
|
||||
<TableHead class="w-[140px] h-11 font-medium text-foreground/80">配额/限流</TableHead>
|
||||
<TableHead class="w-[80px] h-11 font-medium text-foreground/80 text-center">状态</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80 text-center">操作</TableHead>
|
||||
<TableHead class="w-[150px] h-11 font-medium text-foreground/80">
|
||||
提供商信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] h-11 font-medium text-foreground/80">
|
||||
计费类型
|
||||
</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80">
|
||||
官网
|
||||
</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
资源统计
|
||||
</TableHead>
|
||||
<TableHead class="w-[240px] h-11 font-medium text-foreground/80">
|
||||
端点健康
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px] h-11 font-medium text-foreground/80">
|
||||
配额/限流
|
||||
</TableHead>
|
||||
<TableHead class="w-[80px] h-11 font-medium text-foreground/80 text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -102,7 +140,10 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5">
|
||||
<Badge variant="outline" class="text-xs font-normal border-border/50">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs font-normal border-border/50"
|
||||
>
|
||||
{{ formatBillingType(provider.billing_type || 'pay_as_you_go') }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -113,12 +154,15 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-primary/80 hover:text-primary hover:underline truncate block max-w-[100px]"
|
||||
@click.stop
|
||||
:title="provider.website"
|
||||
@click.stop
|
||||
>
|
||||
{{ formatWebsiteDisplay(provider.website) }}
|
||||
</a>
|
||||
<span v-else class="text-xs text-muted-foreground/50">-</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground/50"
|
||||
>-</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5 text-center">
|
||||
<div class="space-y-0.5 text-xs">
|
||||
@@ -154,39 +198,60 @@
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:class="getEndpointDotColor(endpoint, provider)"
|
||||
></span>
|
||||
/>
|
||||
{{ endpoint.api_format }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-xs text-muted-foreground/50">暂无端点</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground/50"
|
||||
>暂无端点</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5">
|
||||
<div class="space-y-0.5 text-xs">
|
||||
<div v-if="provider.billing_type === 'monthly_quota'" class="text-muted-foreground/70">
|
||||
配额: <span class="font-semibold" :class="getQuotaUsedColorClass(provider)">${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span> / <span class="font-medium">${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}</span>
|
||||
<div
|
||||
v-if="provider.billing_type === 'monthly_quota'"
|
||||
class="text-muted-foreground/70"
|
||||
>
|
||||
配额: <span
|
||||
class="font-semibold"
|
||||
:class="getQuotaUsedColorClass(provider)"
|
||||
>${{ (provider.monthly_used_usd ?? 0).toFixed(2) }}</span> / <span class="font-medium">${{ (provider.monthly_quota_usd ?? 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-if="rpmUsage(provider)" class="flex items-center gap-1">
|
||||
<div
|
||||
v-if="rpmUsage(provider)"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<span class="text-muted-foreground/70">RPM:</span>
|
||||
<span class="font-medium text-foreground/80">{{ rpmUsage(provider) }}</span>
|
||||
</div>
|
||||
<div v-if="provider.billing_type !== 'monthly_quota' && !rpmUsage(provider)" class="text-muted-foreground/50">
|
||||
<div
|
||||
v-if="provider.billing_type !== 'monthly_quota' && !rpmUsage(provider)"
|
||||
class="text-muted-foreground/50"
|
||||
>
|
||||
无限制
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5 text-center">
|
||||
<Badge :variant="provider.is_active ? 'success' : 'secondary'" class="text-xs">
|
||||
<Badge
|
||||
:variant="provider.is_active ? 'success' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ provider.is_active ? '活跃' : '已停用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5" @click.stop>
|
||||
<TableCell
|
||||
class="py-3.5"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-center justify-center gap-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
@click="openProviderDrawer(provider.id)"
|
||||
title="查看详情"
|
||||
@click="openProviderDrawer(provider.id)"
|
||||
>
|
||||
<Eye class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -194,8 +259,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
@click="openEditProviderDialog(provider)"
|
||||
title="编辑提供商"
|
||||
@click="openEditProviderDialog(provider)"
|
||||
>
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -203,8 +268,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
|
||||
@click="toggleProviderStatus(provider)"
|
||||
:title="provider.is_active ? '停用提供商' : '启用提供商'"
|
||||
@click="toggleProviderStatus(provider)"
|
||||
>
|
||||
<Power class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -212,8 +277,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 text-muted-foreground/70 hover:text-destructive"
|
||||
@click="handleDeleteProvider(provider)"
|
||||
title="删除提供商"
|
||||
@click="handleDeleteProvider(provider)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<PageContainer>
|
||||
<PageHeader title="系统设置" description="管理系统级别的配置和参数">
|
||||
<PageHeader
|
||||
title="系统设置"
|
||||
description="管理系统级别的配置和参数"
|
||||
>
|
||||
<template #actions>
|
||||
<Button @click="saveSystemConfig" :disabled="loading">
|
||||
<Button
|
||||
:disabled="loading"
|
||||
@click="saveSystemConfig"
|
||||
>
|
||||
{{ loading ? '保存中...' : '保存所有配置' }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -10,10 +16,16 @@
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
<!-- 基础配置 -->
|
||||
<CardSection title="基础配置" description="配置系统默认参数">
|
||||
<CardSection
|
||||
title="基础配置"
|
||||
description="配置系统默认参数"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="default-quota" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="default-quota"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
默认用户配额(美元)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -30,7 +42,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="rate-limit" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="rate-limit"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
每分钟请求限制
|
||||
</Label>
|
||||
<Input
|
||||
@@ -44,19 +59,24 @@
|
||||
0 表示不限制
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
<!-- 用户注册配置 -->
|
||||
<CardSection title="用户注册" description="控制用户注册和验证">
|
||||
<CardSection
|
||||
title="用户注册"
|
||||
description="控制用户注册和验证"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="enable-registration"
|
||||
v-model:checked="systemConfig.enable_registration"
|
||||
/>
|
||||
<Label for="enable-registration" class="cursor-pointer">
|
||||
<Label
|
||||
for="enable-registration"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
开放用户注册
|
||||
</Label>
|
||||
</div>
|
||||
@@ -66,7 +86,10 @@
|
||||
id="require-email-verification"
|
||||
v-model:checked="systemConfig.require_email_verification"
|
||||
/>
|
||||
<Label for="require-email-verification" class="cursor-pointer">
|
||||
<Label
|
||||
for="require-email-verification"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
需要邮箱验证
|
||||
</Label>
|
||||
</div>
|
||||
@@ -74,10 +97,16 @@
|
||||
</CardSection>
|
||||
|
||||
<!-- API Key 管理配置 -->
|
||||
<CardSection title="API Key 管理" description="API Key 相关配置">
|
||||
<CardSection
|
||||
title="API Key 管理"
|
||||
description="API Key 相关配置"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="api-key-expire" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="api-key-expire"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
API密钥过期天数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -99,7 +128,10 @@
|
||||
v-model:checked="systemConfig.auto_delete_expired_keys"
|
||||
/>
|
||||
<div>
|
||||
<Label for="auto-delete-expired-keys" class="cursor-pointer">
|
||||
<Label
|
||||
for="auto-delete-expired-keys"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
自动删除过期 Key
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
@@ -112,20 +144,38 @@
|
||||
</CardSection>
|
||||
|
||||
<!-- 日志记录配置 -->
|
||||
<CardSection title="日志记录" description="控制请求日志的记录方式和内容">
|
||||
<CardSection
|
||||
title="日志记录"
|
||||
description="控制请求日志的记录方式和内容"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="request-log-level" class="block text-sm font-medium mb-2">
|
||||
<Label
|
||||
for="request-log-level"
|
||||
class="block text-sm font-medium mb-2"
|
||||
>
|
||||
记录详细程度
|
||||
</Label>
|
||||
<Select v-model="systemConfig.request_log_level" v-model:open="logLevelSelectOpen">
|
||||
<SelectTrigger id="request-log-level" class="mt-1">
|
||||
<Select
|
||||
v-model="systemConfig.request_log_level"
|
||||
v-model:open="logLevelSelectOpen"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="request-log-level"
|
||||
class="mt-1"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="basic">BASIC - 基本信息 (~1KB/条)</SelectItem>
|
||||
<SelectItem value="headers">HEADERS - 含请求头 (~2-3KB/条)</SelectItem>
|
||||
<SelectItem value="full">FULL - 完整请求响应 (~50KB/条)</SelectItem>
|
||||
<SelectItem value="basic">
|
||||
BASIC - 基本信息 (~1KB/条)
|
||||
</SelectItem>
|
||||
<SelectItem value="headers">
|
||||
HEADERS - 含请求头 (~2-3KB/条)
|
||||
</SelectItem>
|
||||
<SelectItem value="full">
|
||||
FULL - 完整请求响应 (~50KB/条)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
@@ -134,7 +184,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="max-request-body-size" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="max-request-body-size"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
最大请求体大小 (KB)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -150,7 +203,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="max-response-body-size" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="max-response-body-size"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
最大响应体大小 (KB)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -166,7 +222,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="sensitive-headers" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="sensitive-headers"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
敏感请求头
|
||||
</Label>
|
||||
<Input
|
||||
@@ -183,7 +242,10 @@
|
||||
</CardSection>
|
||||
|
||||
<!-- 日志清理策略 -->
|
||||
<CardSection title="日志清理策略" description="配置日志的分级保留和自动清理">
|
||||
<CardSection
|
||||
title="日志清理策略"
|
||||
description="配置日志的分级保留和自动清理"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="md:col-span-2">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
@@ -191,7 +253,10 @@
|
||||
id="enable-auto-cleanup"
|
||||
v-model:checked="systemConfig.enable_auto_cleanup"
|
||||
/>
|
||||
<Label for="enable-auto-cleanup" class="cursor-pointer">
|
||||
<Label
|
||||
for="enable-auto-cleanup"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
启用自动清理任务
|
||||
</Label>
|
||||
<span class="text-xs text-muted-foreground ml-2">
|
||||
@@ -201,7 +266,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="detail-log-retention-days" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="detail-log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
详细日志保留天数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -217,7 +285,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="compressed-log-retention-days" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="compressed-log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
压缩日志保留天数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -233,7 +304,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="header-retention-days" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="header-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
请求头保留天数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -249,7 +323,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="log-retention-days" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="log-retention-days"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
完整日志保留天数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -265,7 +342,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label for="cleanup-batch-size" class="block text-sm font-medium">
|
||||
<Label
|
||||
for="cleanup-batch-size"
|
||||
class="block text-sm font-medium"
|
||||
>
|
||||
每批次清理记录数
|
||||
</Label>
|
||||
<Input
|
||||
@@ -283,7 +363,9 @@
|
||||
|
||||
<!-- 清理策略说明 -->
|
||||
<div class="mt-4 p-4 bg-muted/50 rounded-lg">
|
||||
<h4 class="text-sm font-medium mb-2">清理策略说明</h4>
|
||||
<h4 class="text-sm font-medium mb-2">
|
||||
清理策略说明
|
||||
</h4>
|
||||
<div class="text-xs text-muted-foreground space-y-1">
|
||||
<p>1. <strong>详细日志阶段</strong>: 保留完整的 request_body 和 response_body</p>
|
||||
<p>2. <strong>压缩日志阶段</strong>: body 字段被压缩存储,节省空间</p>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<div class="space-y-6 pb-8">
|
||||
<!-- 用户表格 -->
|
||||
<Card variant="default" class="overflow-hidden">
|
||||
<Card
|
||||
variant="default"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<!-- 标题和筛选器 -->
|
||||
<div class="px-6 py-3.5 border-b border-border/60">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h3 class="text-base font-semibold">用户管理</h3>
|
||||
<h3 class="text-base font-semibold">
|
||||
用户管理
|
||||
</h3>
|
||||
|
||||
<!-- 筛选器和操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -25,26 +30,44 @@
|
||||
<div class="h-4 w-px bg-border" />
|
||||
|
||||
<!-- 角色筛选 -->
|
||||
<Select v-model="filterRole" v-model:open="filterRoleOpen">
|
||||
<Select
|
||||
v-model="filterRole"
|
||||
v-model:open="filterRoleOpen"
|
||||
>
|
||||
<SelectTrigger class="w-32 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部角色</SelectItem>
|
||||
<SelectItem value="admin">管理员</SelectItem>
|
||||
<SelectItem value="user">普通用户</SelectItem>
|
||||
<SelectItem value="all">
|
||||
全部角色
|
||||
</SelectItem>
|
||||
<SelectItem value="admin">
|
||||
管理员
|
||||
</SelectItem>
|
||||
<SelectItem value="user">
|
||||
普通用户
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<Select v-model="filterStatus" v-model:open="filterStatusOpen">
|
||||
<Select
|
||||
v-model="filterStatus"
|
||||
v-model:open="filterStatusOpen"
|
||||
>
|
||||
<SelectTrigger class="w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="active">活跃</SelectItem>
|
||||
<SelectItem value="inactive">禁用</SelectItem>
|
||||
<SelectItem value="all">
|
||||
全部状态
|
||||
</SelectItem>
|
||||
<SelectItem value="active">
|
||||
活跃
|
||||
</SelectItem>
|
||||
<SelectItem value="inactive">
|
||||
禁用
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -56,14 +79,17 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="openCreateDialog"
|
||||
title="新增用户"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<RefreshButton :loading="usersStore.loading || loadingStats" @click="refreshUsers" />
|
||||
<RefreshButton
|
||||
:loading="usersStore.loading || loadingStats"
|
||||
@click="refreshUsers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,112 +97,192 @@
|
||||
<!-- 桌面端表格 -->
|
||||
<div class="hidden xl:block overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="w-[200px] h-12 font-semibold">用户信息</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">邮箱</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">使用统计</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">配额(美元)</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">创建时间</TableHead>
|
||||
<TableHead class="w-[90px] h-12 font-semibold text-center">状态</TableHead>
|
||||
<TableHead class="w-[220px] h-12 font-semibold text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="user in paginatedUsers" :key="user.id" class="border-b border-border/40 hover:bg-muted/30 transition-colors">
|
||||
<TableCell class="py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar class="h-10 w-10 ring-2 ring-background shadow-md">
|
||||
<AvatarFallback class="bg-primary text-sm font-bold text-white">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate text-sm font-semibold mb-1" :title="user.username">{{ user.username }}</div>
|
||||
<Badge :variant="user.role === 'admin' ? 'default' : 'secondary'" class="text-xs px-2 py-0.5">
|
||||
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
</Badge>
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="w-[200px] h-12 font-semibold">
|
||||
用户信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">
|
||||
邮箱
|
||||
</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">
|
||||
使用统计
|
||||
</TableHead>
|
||||
<TableHead class="w-[180px] h-12 font-semibold">
|
||||
配额(美元)
|
||||
</TableHead>
|
||||
<TableHead class="w-[110px] h-12 font-semibold">
|
||||
创建时间
|
||||
</TableHead>
|
||||
<TableHead class="w-[90px] h-12 font-semibold text-center">
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[220px] h-12 font-semibold text-center">
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="user in paginatedUsers"
|
||||
:key="user.id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<TableCell class="py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar class="h-10 w-10 ring-2 ring-background shadow-md">
|
||||
<AvatarFallback class="bg-primary text-sm font-bold text-white">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="truncate text-sm font-semibold mb-1"
|
||||
:title="user.username"
|
||||
>
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<Badge
|
||||
:variant="user.role === 'admin' ? 'default' : 'secondary'"
|
||||
class="text-xs px-2 py-0.5"
|
||||
>
|
||||
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<span class="block truncate text-sm text-muted-foreground" :title="user.email || '-'">
|
||||
{{ user.email || '-' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div v-if="userStats[user.id]" class="space-y-1 text-xs">
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">请求:</span>
|
||||
<span class="font-medium text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<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 class="text-xs text-muted-foreground">
|
||||
<span v-if="loadingStats">加载中...</span>
|
||||
<span v-else>无数据</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="space-y-1.5 text-xs">
|
||||
<div v-if="user.quota_usd != null" class="text-muted-foreground">
|
||||
当前: <span class="font-semibold text-foreground">${{ (user.used_usd || 0).toFixed(2) }}</span> / <span class="font-medium">${{ user.quota_usd.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div v-else class="text-muted-foreground">
|
||||
当前: <span class="font-semibold text-foreground">${{ (user.used_usd || 0).toFixed(2) }}</span> / <span class="font-medium text-amber-600">无限制</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground">累计: <span class="font-medium text-foreground">${{ (user.total_usd || 0).toFixed(2) }}</span></div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-xs text-muted-foreground">
|
||||
{{ formatDate(user.created_at) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<Badge :variant="user.is_active ? 'success' : 'destructive'" class="font-medium px-3 py-1">
|
||||
{{ user.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="flex justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="editUser(user)" title="编辑用户">
|
||||
<SquarePen class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="manageApiKeys(user)" title="查看API Keys">
|
||||
<Key class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="toggleUserStatus(user)"
|
||||
:title="user.is_active ? '禁用用户' : '启用用户'"
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<span
|
||||
class="block truncate text-sm text-muted-foreground"
|
||||
:title="user.email || '-'"
|
||||
>
|
||||
<PauseCircle v-if="user.is_active" class="h-4 w-4" />
|
||||
<PlayCircle v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="resetQuota(user)" title="重置配额">
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="deleteUser(user)" title="删除用户">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{{ user.email || '-' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div
|
||||
v-if="userStats[user.id]"
|
||||
class="space-y-1 text-xs"
|
||||
>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<span class="w-14">请求:</span>
|
||||
<span class="font-medium text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center text-muted-foreground">
|
||||
<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
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
<span v-if="loadingStats">加载中...</span>
|
||||
<span v-else>无数据</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="space-y-1.5 text-xs">
|
||||
<div
|
||||
v-if="user.quota_usd != null"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
当前: <span class="font-semibold text-foreground">${{ (user.used_usd || 0).toFixed(2) }}</span> / <span class="font-medium">${{ user.quota_usd.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
当前: <span class="font-semibold text-foreground">${{ (user.used_usd || 0).toFixed(2) }}</span> / <span class="font-medium text-amber-600">无限制</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground">
|
||||
累计: <span class="font-medium text-foreground">${{ (user.total_usd || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-xs text-muted-foreground">
|
||||
{{ formatDate(user.created_at) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<Badge
|
||||
:variant="user.is_active ? 'success' : 'destructive'"
|
||||
class="font-medium px-3 py-1"
|
||||
>
|
||||
{{ user.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<div class="flex justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑用户"
|
||||
@click="editUser(user)"
|
||||
>
|
||||
<SquarePen class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="查看API Keys"
|
||||
@click="manageApiKeys(user)"
|
||||
>
|
||||
<Key class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:title="user.is_active ? '禁用用户' : '启用用户'"
|
||||
@click="toggleUserStatus(user)"
|
||||
>
|
||||
<PauseCircle
|
||||
v-if="user.is_active"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<PlayCircle
|
||||
v-else
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="重置配额"
|
||||
@click="resetQuota(user)"
|
||||
>
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="删除用户"
|
||||
@click="deleteUser(user)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="xl:hidden divide-y divide-border/40">
|
||||
<div v-for="user in paginatedUsers" :key="user.id" class="p-4 sm:p-5 hover:bg-muted/30 transition-colors">
|
||||
<div
|
||||
v-for="user in paginatedUsers"
|
||||
:key="user.id"
|
||||
class="p-4 sm:p-5 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<!-- 用户头部 -->
|
||||
<div class="flex items-start justify-between mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
@@ -186,13 +292,21 @@
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-sm sm:text-base mb-1 truncate">{{ user.username }}</div>
|
||||
<Badge :variant="user.role === 'admin' ? 'default' : 'secondary'" class="text-xs">
|
||||
<div class="font-semibold text-sm sm:text-base mb-1 truncate">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<Badge
|
||||
:variant="user.role === 'admin' ? 'default' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Badge :variant="user.is_active ? 'success' : 'destructive'" class="font-medium text-xs flex-shrink-0">
|
||||
<Badge
|
||||
:variant="user.is_active ? 'success' : 'destructive'"
|
||||
class="font-medium text-xs flex-shrink-0"
|
||||
>
|
||||
{{ user.is_active ? '活跃' : '禁用' }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -204,18 +318,33 @@
|
||||
<span class="ml-2 text-foreground truncate block sm:inline">{{ user.email || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="userStats[user.id]" class="grid grid-cols-2 gap-2 p-2 sm:p-3 bg-muted/50 rounded-lg text-xs">
|
||||
<div
|
||||
v-if="userStats[user.id]"
|
||||
class="grid grid-cols-2 gap-2 p-2 sm:p-3 bg-muted/50 rounded-lg text-xs"
|
||||
>
|
||||
<div>
|
||||
<div class="text-muted-foreground mb-1">请求次数</div>
|
||||
<div class="font-semibold text-sm text-foreground">{{ formatNumber(userStats[user.id]?.request_count) }}</div>
|
||||
<div class="text-muted-foreground mb-1">
|
||||
请求次数
|
||||
</div>
|
||||
<div class="font-semibold text-sm text-foreground">
|
||||
{{ formatNumber(userStats[user.id]?.request_count) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted-foreground mb-1">Tokens</div>
|
||||
<div class="font-semibold text-sm text-foreground">{{ formatTokens(userStats[user.id]?.total_tokens ?? 0) }}</div>
|
||||
<div class="text-muted-foreground mb-1">
|
||||
Tokens
|
||||
</div>
|
||||
<div class="font-semibold text-sm text-foreground">
|
||||
{{ 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 class="text-muted-foreground mb-1">
|
||||
消费金额
|
||||
</div>
|
||||
<div class="font-semibold text-sm text-foreground">
|
||||
${{ formatCurrency(userStats[user.id]?.total_cost) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,11 +370,21 @@
|
||||
|
||||
<!-- 操作按钮 - 响应式布局 -->
|
||||
<div class="grid grid-cols-2 sm:flex sm:flex-wrap gap-1.5 sm:gap-2">
|
||||
<Button variant="outline" size="sm" class="text-xs sm:text-sm h-8 sm:h-9 sm:flex-1 sm:min-w-[90px]" @click="editUser(user)">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-xs sm:text-sm h-8 sm:h-9 sm:flex-1 sm:min-w-[90px]"
|
||||
@click="editUser(user)"
|
||||
>
|
||||
<SquarePen class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<span class="hidden sm:inline">编辑</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="text-xs sm:text-sm h-8 sm:h-9 sm:flex-1 sm:min-w-[100px]" @click="manageApiKeys(user)">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-xs sm:text-sm h-8 sm:h-9 sm:flex-1 sm:min-w-[100px]"
|
||||
@click="manageApiKeys(user)"
|
||||
>
|
||||
<Key class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<span class="hidden sm:inline">API Keys</span>
|
||||
</Button>
|
||||
@@ -256,15 +395,31 @@
|
||||
:class="user.is_active ? 'text-amber-600' : 'text-emerald-600'"
|
||||
@click="toggleUserStatus(user)"
|
||||
>
|
||||
<PauseCircle v-if="user.is_active" class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<PlayCircle v-else class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<PauseCircle
|
||||
v-if="user.is_active"
|
||||
class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5"
|
||||
/>
|
||||
<PlayCircle
|
||||
v-else
|
||||
class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5"
|
||||
/>
|
||||
<span class="hidden sm:inline">{{ user.is_active ? '禁用' : '启用' }}</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="text-xs sm:text-sm h-8 sm:h-9" @click="resetQuota(user)">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-xs sm:text-sm h-8 sm:h-9"
|
||||
@click="resetQuota(user)"
|
||||
>
|
||||
<RotateCcw class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<span class="hidden sm:inline">重置</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="col-span-2 text-xs sm:text-sm h-8 sm:h-9 text-rose-600 sm:col-span-1" @click="deleteUser(user)">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="col-span-2 text-xs sm:text-sm h-8 sm:h-9 text-rose-600 sm:col-span-1"
|
||||
@click="deleteUser(user)"
|
||||
>
|
||||
<Trash2 class="h-3 w-3 sm:h-3.5 sm:w-3.5 sm:mr-1.5" />
|
||||
<span class="hidden sm:inline">删除</span>
|
||||
</Button>
|
||||
@@ -284,15 +439,18 @@
|
||||
|
||||
<!-- 用户表单对话框(创建/编辑共用) -->
|
||||
<UserFormDialog
|
||||
ref="userFormDialogRef"
|
||||
:open="showUserFormDialog"
|
||||
:user="editingUser"
|
||||
@close="closeUserFormDialog"
|
||||
@submit="handleUserFormSubmit"
|
||||
ref="userFormDialogRef"
|
||||
/>
|
||||
|
||||
<!-- API Keys 管理对话框 -->
|
||||
<Dialog v-model="showApiKeysDialog" size="xl">
|
||||
<Dialog
|
||||
v-model="showApiKeysDialog"
|
||||
size="xl"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -300,8 +458,12 @@
|
||||
<Key class="h-5 w-5 text-kraft" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">管理 API Keys</h3>
|
||||
<p class="text-xs text-muted-foreground">查看和管理用户的 API 密钥</p>
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
管理 API Keys
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
查看和管理用户的 API 密钥
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,9 +503,9 @@
|
||||
{{ apiKey.key_display || 'sk-****' }}
|
||||
</code>
|
||||
<button
|
||||
@click="copyFullKey(apiKey)"
|
||||
class="p-0.5 hover:bg-muted rounded transition-colors"
|
||||
title="复制完整密钥"
|
||||
@click="copyFullKey(apiKey)"
|
||||
>
|
||||
<Copy class="w-3 h-3 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -364,8 +526,8 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
title="删除"
|
||||
@click="deleteApiKey(apiKey)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -382,23 +544,40 @@
|
||||
<Key class="h-6 w-6 text-muted-foreground/50" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-base font-semibold text-foreground">暂无 API Keys</p>
|
||||
<p class="text-sm text-muted-foreground">点击下方按钮创建</p>
|
||||
<p class="mb-1 text-base font-semibold text-foreground">
|
||||
暂无 API Keys
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
点击下方按钮创建
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button variant="outline" @click="showApiKeysDialog = false" class="h-10 px-5">取消</Button>
|
||||
<Button @click="createApiKey" class="h-10 px-5" :disabled="creatingApiKey">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-10 px-5"
|
||||
@click="showApiKeysDialog = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
class="h-10 px-5"
|
||||
:disabled="creatingApiKey"
|
||||
@click="createApiKey"
|
||||
>
|
||||
{{ creatingApiKey ? '创建中...' : '创建' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 新 API Key 显示对话框 -->
|
||||
<Dialog v-model="showNewApiKeyDialog" size="lg">
|
||||
<Dialog
|
||||
v-model="showNewApiKeyDialog"
|
||||
size="lg"
|
||||
>
|
||||
<template #header>
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -406,8 +585,12 @@
|
||||
<CheckCircle class="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">创建成功</h3>
|
||||
<p class="text-xs text-muted-foreground">请妥善保管, 切勿泄露给他人.</p>
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
创建成功
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
请妥善保管, 切勿泄露给他人.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -418,14 +601,17 @@
|
||||
<Label class="text-sm font-medium">API Key</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
ref="apiKeyInput"
|
||||
type="text"
|
||||
:value="newApiKey"
|
||||
readonly
|
||||
class="flex-1 font-mono text-sm bg-muted/50 h-11"
|
||||
@click="selectApiKey"
|
||||
ref="apiKeyInput"
|
||||
/>
|
||||
<Button @click="copyApiKey" class="h-11">
|
||||
<Button
|
||||
class="h-11"
|
||||
@click="copyApiKey"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
@@ -433,7 +619,12 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button @click="closeNewApiKeyDialog" class="h-10 px-5">确定</Button>
|
||||
<Button
|
||||
class="h-10 px-5"
|
||||
@click="closeNewApiKeyDialog"
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user