feat(frontend): 用量页面 UI 优化 - 合并格式/类型列、分析面板可折叠

- UsageRecordsTable: 合并 API 格式与类型为单列,缩减列宽使表格更紧凑
- MainLayout: 添加 header 级 Teleport 插入点供子页面注入操作按钮
- Usage: 用量分析面板改为可折叠,折叠状态持久化到 localStorage
This commit is contained in:
fawney19
2026-03-14 13:28:56 +08:00
parent e0286aebe3
commit b15ce9977a
3 changed files with 312 additions and 284 deletions

View File

@@ -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'"

View File

@@ -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"

View File

@@ -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'))