feat(admin): 用量记录页新增"用户 Token 排行"面板

- 在 /admin/usage 新增按用户聚合的 Token 排行表(输入/输出/缓存/总 Token、请求数、费用)
- 支持按列排序、邮箱搜索、Top N (20/50/100/200),点击行下钻到该用户
- 复用现有筛选(用户/时间/模型/分组等)与既有 KPI 卡片、逐条日志
- 后端对 /admin/dashboard/user-breakdown 做加法扩展(向后兼容):
  - UserBreakdownItem 增加 input_tokens/output_tokens/cache_tokens
  - 新增 sort_by(白名单排序,防注入,缺省仍按 actual_cost)
- 新增 sort_by 转发单元测试;更新 UsageView.spec 稳定桩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BirditchandClaude Opus 4.8 committed 2026-07-09 06:04:22 -04:00
1 parent 64fdc11ec4
commit b062b36646
11 files changed
+277 -11

No files matched your search

+15
View File
@@ -109,6 +109,14 @@
</button>
</div>
<div v-show="activeTab === 'usage'">
<UserTokenRanking
class="mb-4"
:start-date="startDate"
:end-date="endDate"
:filters="breakdownFilters"
:model="filters.model"
@select-user="handleRankingSelectUser"
/>
<UsageTable
:data="usageLogs"
:loading="loading"
@@ -167,6 +175,7 @@ import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usag
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'; import DateRangePicker from '@/components/common/DateRangePicker.vue'
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
import UserTokenRanking from '@/components/admin/usage/UserTokenRanking.vue'
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
import UserBalanceHistoryModal from '@/components/admin/user/UserBalanceHistoryModal.vue'
import OpsErrorLogTable from '@/views/admin/ops/components/OpsErrorLogTable.vue'
@@ -235,6 +244,12 @@ const handleUserClick = async (userId: number) => {
}
}
// Drill down from the per-user token ranking: scope the whole usage view to that user.
const handleRankingSelectUser = (userId: number) => {
filters.value = { ...filters.value, user_id: userId }
applyFilters()
}
const granularityOptions = computed(() => [{ value: 'day', label: t('admin.dashboard.day') }, { value: 'hour', label: t('admin.dashboard.hour') }])
// Use local timezone to avoid UTC timezone issues
const formatLD = (d: Date) => {
@@ -163,7 +163,7 @@ describe('admin UsageView distribution metric toggles', () => {
UserBalanceHistoryModal: true, AuditLogModal: true, Pagination: true, Select: true,
DateRangePicker: true, Icon: true, TokenUsageTrend: true,
ModelDistributionChart: ModelDistributionChartStub, GroupDistributionChart: GroupDistributionChartStub,
EndpointDistributionChart: true,
EndpointDistributionChart: true, UserTokenRanking: true,
} },
})
vi.advanceTimersByTime(120)
@@ -201,6 +201,7 @@ describe('admin UsageView distribution metric toggles', () => {
TokenUsageTrend: true,
ModelDistributionChart: ModelDistributionChartStub,
GroupDistributionChart: GroupDistributionChartStub,
UserTokenRanking: true,
},
},
})
@@ -281,6 +282,7 @@ describe('admin UsageView handleUserClick', () => {
ModelDistributionChart: true,
GroupDistributionChart: true,
EndpointDistributionChart: true,
UserTokenRanking: true,
},
},
})
@@ -326,7 +328,7 @@ describe('admin UsageView errors tab filter forwarding', () => {
UserBalanceHistoryModal: true, AuditLogModal: true, Pagination: true, Select: true,
DateRangePicker: true, Icon: true, TokenUsageTrend: true,
ModelDistributionChart: true, GroupDistributionChart: true, EndpointDistributionChart: true,
OpsErrorLogTable: true, OpsErrorDetailModal: true,
UserTokenRanking: true, OpsErrorLogTable: true, OpsErrorDetailModal: true,
} },
})
vi.advanceTimersByTime(120)