mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(frontend): 用量页面 UI 优化 - 合并格式/类型列、分析面板可折叠
- UsageRecordsTable: 合并 API 格式与类型为单列,缩减列宽使表格更紧凑 - MainLayout: 添加 header 级 Teleport 插入点供子页面注入操作按钮 - Usage: 用量分析面板改为可折叠,折叠状态持久化到 localStorage
This commit is contained in:
@@ -3,172 +3,167 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<!-- 时间范围筛选 -->
|
<!-- 时间范围筛选 -->
|
||||||
<TimeRangePicker
|
<TimeRangePicker
|
||||||
v-model="timeRangeModel"
|
v-model="timeRangeModel"
|
||||||
:show-granularity="false"
|
:show-granularity="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
|
|
||||||
<!-- 通用搜索 -->
|
<!-- 通用搜索 -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||||
<Input
|
<Input
|
||||||
id="usage-records-search"
|
id="usage-records-search"
|
||||||
v-model="localSearch"
|
v-model="localSearch"
|
||||||
:placeholder="isAdmin ? '搜索用户/密钥' : '搜索密钥/模型'"
|
:placeholder="isAdmin ? '搜索用户/密钥' : '搜索密钥/模型'"
|
||||||
class="w-[7.5rem] sm:w-48 h-8 text-xs border-border/60 pl-8"
|
class="w-[7.5rem] sm:w-48 h-8 text-xs border-border/60 pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户筛选(仅管理员可见) -->
|
<!-- 用户筛选(仅管理员可见) -->
|
||||||
<Select
|
<Select
|
||||||
v-if="isAdmin && availableUsers.length > 0"
|
v-if="isAdmin && availableUsers.length > 0"
|
||||||
|
:model-value="filterUser"
|
||||||
:model-value="filterUser"
|
@update:model-value="$emit('update:filterUser', $event)"
|
||||||
@update:model-value="$emit('update:filterUser', $event)"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-36 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="用户" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">
|
|
||||||
全部用户
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-for="user in availableUsers"
|
|
||||||
:key="user.id"
|
|
||||||
:value="user.id"
|
|
||||||
>
|
>
|
||||||
{{ user.username || user.email }}
|
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-36 h-8 text-xs border-border/60">
|
||||||
</SelectItem>
|
<SelectValue placeholder="用户" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">
|
||||||
|
全部用户
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="user in availableUsers"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
|
{{ user.username || user.email }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<!-- 模型筛选 -->
|
<!-- 模型筛选 -->
|
||||||
<Select
|
<Select
|
||||||
|
:model-value="filterModel"
|
||||||
:model-value="filterModel"
|
@update:model-value="$emit('update:filterModel', $event)"
|
||||||
@update:model-value="$emit('update:filterModel', $event)"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-40 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="模型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">
|
|
||||||
全部模型
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-for="model in availableModels"
|
|
||||||
:key="model"
|
|
||||||
:value="model"
|
|
||||||
>
|
>
|
||||||
{{ model.replace('claude-', '') }}
|
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-40 h-8 text-xs border-border/60">
|
||||||
</SelectItem>
|
<SelectValue placeholder="模型" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">
|
||||||
|
全部模型
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="model in availableModels"
|
||||||
|
:key="model"
|
||||||
|
:value="model"
|
||||||
|
>
|
||||||
|
{{ model.replace('claude-', '') }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<!-- 提供商筛选(仅管理员可见) -->
|
<!-- 提供商筛选(仅管理员可见) -->
|
||||||
<Select
|
<Select
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
|
:model-value="filterProvider"
|
||||||
:model-value="filterProvider"
|
@update:model-value="$emit('update:filterProvider', $event)"
|
||||||
@update:model-value="$emit('update:filterProvider', $event)"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="提供商" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">
|
|
||||||
全部提供商
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-for="provider in availableProviders"
|
|
||||||
:key="provider"
|
|
||||||
:value="provider"
|
|
||||||
>
|
>
|
||||||
{{ provider }}
|
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
|
||||||
</SelectItem>
|
<SelectValue placeholder="提供商" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">
|
||||||
|
全部提供商
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="provider in availableProviders"
|
||||||
|
:key="provider"
|
||||||
|
:value="provider"
|
||||||
|
>
|
||||||
|
{{ provider }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<!-- API格式筛选 -->
|
<!-- API格式筛选 -->
|
||||||
<Select
|
<Select
|
||||||
|
:model-value="filterApiFormat"
|
||||||
:model-value="filterApiFormat"
|
@update:model-value="$emit('update:filterApiFormat', $event)"
|
||||||
@update:model-value="$emit('update:filterApiFormat', $event)"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="格式" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">
|
|
||||||
全部格式
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem
|
|
||||||
v-for="format in availableApiFormats"
|
|
||||||
:key="format.value"
|
|
||||||
:value="format.value"
|
|
||||||
>
|
>
|
||||||
{{ format.label }}
|
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
|
||||||
</SelectItem>
|
<SelectValue placeholder="格式" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">
|
||||||
|
全部格式
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
v-for="format in availableApiFormats"
|
||||||
|
:key="format.value"
|
||||||
|
:value="format.value"
|
||||||
|
>
|
||||||
|
{{ format.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<!-- 状态筛选 -->
|
<!-- 状态筛选 -->
|
||||||
<Select
|
<Select
|
||||||
|
:model-value="filterStatus"
|
||||||
|
@update:model-value="$emit('update:filterStatus', $event)"
|
||||||
|
>
|
||||||
|
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-28 h-8 text-xs border-border/60">
|
||||||
|
<SelectValue placeholder="状态" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">
|
||||||
|
全部状态
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="stream">
|
||||||
|
流式
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="standard">
|
||||||
|
标准
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="active">
|
||||||
|
活跃
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="failed">
|
||||||
|
失败
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="cancelled">
|
||||||
|
已取消
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="has_retry">
|
||||||
|
发生重试
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="has_fallback">
|
||||||
|
发生转移
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
:model-value="filterStatus"
|
<!-- 分隔线 -->
|
||||||
@update:model-value="$emit('update:filterStatus', $event)"
|
<div class="hidden sm:block h-4 w-px bg-border" />
|
||||||
>
|
|
||||||
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-28 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="状态" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">
|
|
||||||
全部状态
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="stream">
|
|
||||||
流式
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="standard">
|
|
||||||
标准
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="active">
|
|
||||||
活跃
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="failed">
|
|
||||||
失败
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="cancelled">
|
|
||||||
已取消
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="has_retry">
|
|
||||||
发生重试
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="has_fallback">
|
|
||||||
发生转移
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 自动刷新按钮 -->
|
||||||
<div class="hidden sm:block h-4 w-px bg-border" />
|
<Button
|
||||||
|
variant="ghost"
|
||||||
<!-- 自动刷新按钮 -->
|
size="icon"
|
||||||
<Button
|
class="h-8 w-8"
|
||||||
variant="ghost"
|
:class="autoRefresh ? 'text-primary' : ''"
|
||||||
size="icon"
|
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每3秒刷新)'"
|
||||||
class="h-8 w-8"
|
@click="$emit('update:autoRefresh', !autoRefresh)"
|
||||||
:class="autoRefresh ? 'text-primary' : ''"
|
>
|
||||||
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每3秒刷新)'"
|
<RefreshCcw
|
||||||
@click="$emit('update:autoRefresh', !autoRefresh)"
|
class="w-3.5 h-3.5"
|
||||||
>
|
:class="autoRefresh ? 'animate-spin' : ''"
|
||||||
<RefreshCcw
|
/>
|
||||||
class="w-3.5 h-3.5"
|
</Button>
|
||||||
:class="autoRefresh ? 'animate-spin' : ''"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 移动端卡片视图 -->
|
<!-- 移动端卡片视图 -->
|
||||||
@@ -288,43 +283,43 @@
|
|||||||
<Table class="hidden md:table">
|
<Table class="hidden md:table">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||||
<TableHead class="h-12 font-semibold w-[70px]">
|
<TableHead class="h-12 font-semibold w-[68px]">
|
||||||
时间
|
时间
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead
|
<TableHead
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="h-12 font-semibold w-[100px]"
|
class="h-12 font-semibold"
|
||||||
>
|
>
|
||||||
用户
|
用户
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead
|
<TableHead
|
||||||
v-if="!isAdmin"
|
v-if="!isAdmin"
|
||||||
class="h-12 font-semibold w-[100px]"
|
class="h-12 font-semibold"
|
||||||
>
|
>
|
||||||
密钥
|
密钥
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[140px]">
|
<TableHead class="h-12 font-semibold">
|
||||||
模型
|
模型
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead
|
<TableHead
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="h-12 font-semibold w-[100px]"
|
class="h-12 font-semibold"
|
||||||
>
|
>
|
||||||
提供商
|
提供商
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[120px]">
|
<TableHead class="h-12 font-semibold w-[120px]">
|
||||||
API格式
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
|
<span>格式</span>
|
||||||
|
<span class="text-muted-foreground font-normal">类型</span>
|
||||||
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[70px] text-center">
|
<TableHead class="h-12 font-semibold w-[110px] text-right">
|
||||||
类型
|
|
||||||
</TableHead>
|
|
||||||
<TableHead class="h-12 font-semibold w-[140px] text-right">
|
|
||||||
Tokens
|
Tokens
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[100px] text-right">
|
<TableHead class="h-12 font-semibold w-[72px] text-right">
|
||||||
费用
|
费用
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="h-12 font-semibold w-[70px] text-right">
|
<TableHead class="h-12 font-semibold w-[68px] text-right">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span>首字</span>
|
<span>首字</span>
|
||||||
<span class="text-muted-foreground font-normal">总耗时</span>
|
<span class="text-muted-foreground font-normal">总耗时</span>
|
||||||
@@ -335,7 +330,7 @@
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-if="records.length === 0">
|
<TableRow v-if="records.length === 0">
|
||||||
<TableCell
|
<TableCell
|
||||||
:colspan="isAdmin ? 9 : 8"
|
:colspan="isAdmin ? 8 : 7"
|
||||||
class="text-center py-12 text-muted-foreground"
|
class="text-center py-12 text-muted-foreground"
|
||||||
>
|
>
|
||||||
暂无请求记录
|
暂无请求记录
|
||||||
@@ -349,12 +344,12 @@
|
|||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@click="handleRowClick($event, record.id)"
|
@click="handleRowClick($event, record.id)"
|
||||||
>
|
>
|
||||||
<TableCell class="text-xs py-4 w-[70px]">
|
<TableCell class="text-xs py-4 w-[68px]">
|
||||||
{{ formatDateTime(record.created_at) }}
|
{{ formatDateTime(record.created_at) }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="py-4 w-[100px] truncate"
|
class="py-4 truncate"
|
||||||
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
|
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col text-xs gap-0.5">
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
@@ -373,7 +368,7 @@
|
|||||||
<!-- 用户页面的密钥列 -->
|
<!-- 用户页面的密钥列 -->
|
||||||
<TableCell
|
<TableCell
|
||||||
v-if="!isAdmin"
|
v-if="!isAdmin"
|
||||||
class="py-4 w-[100px]"
|
class="py-4"
|
||||||
:title="record.api_key?.name || '-'"
|
:title="record.api_key?.name || '-'"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col text-xs gap-0.5">
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
@@ -387,7 +382,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
class="font-medium py-4 w-[140px]"
|
class="font-medium py-4"
|
||||||
:title="getModelTooltip(record)"
|
:title="getModelTooltip(record)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -413,12 +408,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="truncate block"
|
class="truncate block text-xs"
|
||||||
>{{ record.model }}</span>
|
>{{ record.model }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="py-4 w-[60px]"
|
class="py-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex flex-col text-xs gap-0.5">
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
@@ -473,16 +468,17 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<!-- 格式 + 类型(合并列) -->
|
||||||
<TableCell
|
<TableCell
|
||||||
class="py-4 w-[120px]"
|
class="py-4 w-[120px]"
|
||||||
:title="getApiFormatTooltip(record)"
|
:title="getApiFormatTooltip(record)"
|
||||||
>
|
>
|
||||||
<!-- 有格式转换或同族格式差异:两行显示 -->
|
<div class="flex flex-col text-xs gap-0.5">
|
||||||
<div
|
<!-- 格式信息 -->
|
||||||
v-if="shouldShowFormatConversion(record)"
|
<div
|
||||||
class="flex flex-col text-xs gap-0.5"
|
v-if="shouldShowFormatConversion(record)"
|
||||||
>
|
class="flex items-center gap-1 whitespace-nowrap"
|
||||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
>
|
||||||
<span>{{ formatApiFormat(record.api_format!) }}</span>
|
<span>{{ formatApiFormat(record.api_format!) }}</span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -496,65 +492,64 @@
|
|||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<span class="text-muted-foreground">{{ formatApiFormat(record.endpoint_api_format!) }}</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else-if="record.api_format"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>{{ formatApiFormat(record.api_format) }}</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>-</span>
|
||||||
|
<!-- 类型 badge -->
|
||||||
|
<div>
|
||||||
|
<Badge
|
||||||
|
v-if="record.status === 'pending'"
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
等待中
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="record.status === 'streaming'"
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
传输中
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
||||||
|
variant="destructive"
|
||||||
|
class="whitespace-nowrap text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
失败
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="record.status === 'cancelled'"
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap border-amber-500/50 text-amber-600 dark:text-amber-400 text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
已取消
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="record.is_stream"
|
||||||
|
variant="secondary"
|
||||||
|
class="whitespace-nowrap text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
流式
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4"
|
||||||
|
>
|
||||||
|
标准
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-muted-foreground whitespace-nowrap">{{ formatApiFormat(record.endpoint_api_format!) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 无格式转换:单行显示 -->
|
|
||||||
<span
|
|
||||||
v-else-if="record.api_format"
|
|
||||||
class="text-xs whitespace-nowrap"
|
|
||||||
>{{ formatApiFormat(record.api_format) }}</span>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="text-muted-foreground text-xs"
|
|
||||||
>-</span>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-center py-4 w-[70px]">
|
<TableCell class="text-right py-4 w-[110px]">
|
||||||
<!-- 优先显示请求状态 -->
|
|
||||||
<Badge
|
|
||||||
v-if="record.status === 'pending'"
|
|
||||||
variant="outline"
|
|
||||||
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground"
|
|
||||||
>
|
|
||||||
等待中
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else-if="record.status === 'streaming'"
|
|
||||||
variant="outline"
|
|
||||||
class="whitespace-nowrap animate-pulse border-primary/50 text-primary"
|
|
||||||
>
|
|
||||||
传输中
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
|
||||||
variant="destructive"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
失败
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else-if="record.status === 'cancelled'"
|
|
||||||
variant="outline"
|
|
||||||
class="whitespace-nowrap border-amber-500/50 text-amber-600 dark:text-amber-400"
|
|
||||||
>
|
|
||||||
已取消
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else-if="record.is_stream"
|
|
||||||
variant="secondary"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
流式
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
v-else
|
|
||||||
variant="outline"
|
|
||||||
class="whitespace-nowrap border-border/60 text-muted-foreground"
|
|
||||||
>
|
|
||||||
标准
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell class="text-right py-4 w-[140px]">
|
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span>{{ formatTokens(record.input_tokens || 0) }}</span>
|
<span>{{ formatTokens(record.input_tokens || 0) }}</span>
|
||||||
@@ -568,7 +563,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-4 w-[100px]">
|
<TableCell class="text-right py-4 w-[72px]">
|
||||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||||
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -579,7 +574,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right py-4 w-[70px]">
|
<TableCell class="text-right py-4 w-[68px]">
|
||||||
<!-- pending 状态:只显示增长的总时间 -->
|
<!-- pending 状态:只显示增长的总时间 -->
|
||||||
<div
|
<div
|
||||||
v-if="record.status === 'pending'"
|
v-if="record.status === 'pending'"
|
||||||
|
|||||||
@@ -266,6 +266,8 @@
|
|||||||
{{ crumb.label }}
|
{{ crumb.label }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 页面级操作插入点 -->
|
||||||
|
<div id="breadcrumb-actions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -279,6 +281,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Page-level header actions (right side) -->
|
||||||
|
<div id="header-actions-right" class="flex items-center" />
|
||||||
<!-- Theme Toggle -->
|
<!-- Theme Toggle -->
|
||||||
<button
|
<button
|
||||||
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||||
|
|||||||
@@ -1,55 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6 pb-8">
|
<div class="space-y-6 pb-8">
|
||||||
<!-- 活跃度热图 + 请求间隔时间线 -->
|
<!-- 面包屑旁的折叠按钮 -->
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
<Teleport to="#header-actions-right" defer>
|
||||||
<ActivityHeatmapCard
|
<button
|
||||||
:data="activityHeatmapData"
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||||
:title="isAdminPage ? '总体活跃天数' : '我的活跃天数'"
|
:title="statsExpanded ? '收起用量分析' : '展开用量分析'"
|
||||||
:is-loading="isLoadingHeatmap"
|
@click="statsExpanded = !statsExpanded"
|
||||||
:has-error="heatmapError"
|
>
|
||||||
/>
|
<PanelTopClose
|
||||||
<IntervalTimelineCard
|
v-if="statsExpanded"
|
||||||
:title="isAdminPage ? '请求间隔时间线' : '我的请求间隔'"
|
class="h-4 w-4"
|
||||||
:is-admin="isAdminPage"
|
/>
|
||||||
:hours="24"
|
<PanelTopOpen
|
||||||
/>
|
v-else
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- 用量分析面板(可折叠) -->
|
||||||
|
<div
|
||||||
|
v-if="statsExpanded"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<!-- 活跃度热图 + 请求间隔时间线 -->
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
|
<ActivityHeatmapCard
|
||||||
|
:data="activityHeatmapData"
|
||||||
|
:title="isAdminPage ? '总体活跃天数' : '我的活跃天数'"
|
||||||
|
:is-loading="isLoadingHeatmap"
|
||||||
|
:has-error="heatmapError"
|
||||||
|
/>
|
||||||
|
<IntervalTimelineCard
|
||||||
|
:title="isAdminPage ? '请求间隔时间线' : '我的请求间隔'"
|
||||||
|
:is-admin="isAdminPage"
|
||||||
|
:hours="24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分析统计 -->
|
||||||
|
<!-- 管理员:模型 + 提供商 + API格式(3列) -->
|
||||||
|
<div
|
||||||
|
v-if="isAdminPage"
|
||||||
|
class="grid grid-cols-1 lg:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
<UsageModelTable
|
||||||
|
:data="enhancedModelStats"
|
||||||
|
:is-admin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
<UsageProviderTable
|
||||||
|
:data="providerStats"
|
||||||
|
:is-admin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
<UsageApiFormatTable
|
||||||
|
:data="apiFormatStats"
|
||||||
|
:is-admin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 用户:模型 + API格式(2列) -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="grid grid-cols-1 lg:grid-cols-2 gap-4"
|
||||||
|
>
|
||||||
|
<UsageModelTable
|
||||||
|
:data="enhancedModelStats"
|
||||||
|
:is-admin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
<UsageApiFormatTable
|
||||||
|
:data="apiFormatStats"
|
||||||
|
:is-admin="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分析统计 -->
|
<!-- 使用记录 -->
|
||||||
<!-- 管理员:模型 + 提供商 + API格式(3列) -->
|
|
||||||
<div
|
|
||||||
v-if="isAdminPage"
|
|
||||||
class="grid grid-cols-1 lg:grid-cols-3 gap-4"
|
|
||||||
>
|
|
||||||
<UsageModelTable
|
|
||||||
:data="enhancedModelStats"
|
|
||||||
:is-admin="authStore.isAdmin"
|
|
||||||
/>
|
|
||||||
<UsageProviderTable
|
|
||||||
:data="providerStats"
|
|
||||||
:is-admin="authStore.isAdmin"
|
|
||||||
/>
|
|
||||||
<UsageApiFormatTable
|
|
||||||
:data="apiFormatStats"
|
|
||||||
:is-admin="authStore.isAdmin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- 用户:模型 + API格式(2列) -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="grid grid-cols-1 lg:grid-cols-2 gap-4"
|
|
||||||
>
|
|
||||||
<UsageModelTable
|
|
||||||
:data="enhancedModelStats"
|
|
||||||
:is-admin="authStore.isAdmin"
|
|
||||||
/>
|
|
||||||
<UsageApiFormatTable
|
|
||||||
:data="apiFormatStats"
|
|
||||||
:is-admin="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 请求详情 -->
|
|
||||||
<UsageRecordsTable
|
<UsageRecordsTable
|
||||||
:records="displayRecords"
|
:records="displayRecords"
|
||||||
:is-admin="isAdminPage"
|
:is-admin="isAdminPage"
|
||||||
@@ -97,10 +121,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { usageApi } from '@/api/usage'
|
import { usageApi } from '@/api/usage'
|
||||||
import { usersApi } from '@/api/users'
|
import { usersApi } from '@/api/users'
|
||||||
import { meApi } from '@/api/me'
|
import { meApi } from '@/api/me'
|
||||||
|
import { PanelTopClose, PanelTopOpen } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
UsageModelTable,
|
UsageModelTable,
|
||||||
UsageProviderTable,
|
UsageProviderTable,
|
||||||
@@ -127,6 +153,9 @@ const authStore = useAuthStore()
|
|||||||
// 判断是否是管理员页面
|
// 判断是否是管理员页面
|
||||||
const isAdminPage = computed(() => route.path.startsWith('/admin'))
|
const isAdminPage = computed(() => route.path.startsWith('/admin'))
|
||||||
|
|
||||||
|
// 用量分析面板折叠状态(默认展开,持久化到 localStorage)
|
||||||
|
const statsExpanded = useLocalStorage('usage-stats-expanded', true)
|
||||||
|
|
||||||
// 时间范围选择
|
// 时间范围选择
|
||||||
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('today'))
|
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('today'))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user