feat(admin): add paginated API key usage leaderboard

This commit is contained in:
beilo
2026-05-19 17:38:42 +08:00
parent 63202a63ef
commit fe3a848eb5
5 changed files with 259 additions and 48 deletions

View File

@@ -7,23 +7,12 @@
<SelectValue placeholder="选择时间段" />
</SelectTrigger>
<SelectContent :searchable="false">
<SelectItem value="today">
今天
</SelectItem>
<SelectItem value="yesterday">
昨天
</SelectItem>
<SelectItem value="last7days">
最近7天
</SelectItem>
<SelectItem value="last30days">
最近30天
</SelectItem>
<SelectItem value="last90days">
最近90天
</SelectItem>
<SelectItem value="custom">
自定义
<SelectItem
v-for="preset in activePresetOptions"
:key="preset"
:value="preset"
>
{{ presetLabels[preset] }}
</SelectItem>
</SelectContent>
</Select>
@@ -85,27 +74,51 @@ import {
} from '@/components/ui'
import type { DateRangeParams } from '@/features/usage/types'
const props = defineProps<{
const selectablePresets = ['today', 'yesterday', 'last7days', 'last30days', 'last90days', 'custom'] as const
type SelectablePreset = typeof selectablePresets[number]
const presetLabels: Record<SelectablePreset, string> = {
today: '今天',
yesterday: '昨天',
last7days: '最近7天',
last30days: '最近30天',
last90days: '最近90天',
custom: '自定义'
}
const props = withDefaults(defineProps<{
modelValue: DateRangeParams
showGranularity?: boolean
allowHourly?: boolean
}>()
presetOptions?: SelectablePreset[]
}>(), {
presetOptions: () => ['today', 'yesterday', 'last7days', 'last30days', 'last90days', 'custom']
})
const emit = defineEmits<{
'update:modelValue': [value: DateRangeParams]
}>()
const selectablePresets = ['today', 'yesterday', 'last7days', 'last30days', 'last90days', 'custom'] as const
type SelectablePreset = typeof selectablePresets[number]
const activePresetOptions = computed<SelectablePreset[]>(() => {
const unique = new Set(props.presetOptions)
const filtered = selectablePresets.filter((preset) => unique.has(preset))
return filtered.length > 0 ? filtered : [...selectablePresets]
})
function defaultPreset(): SelectablePreset {
const options = activePresetOptions.value
if (options.includes('last7days')) return 'last7days'
return options[0] ?? 'last7days'
}
function normalizePreset(value: DateRangeParams): SelectablePreset {
if (value.preset && selectablePresets.includes(value.preset as SelectablePreset)) {
if (value.preset && activePresetOptions.value.includes(value.preset as SelectablePreset)) {
return value.preset as SelectablePreset
}
if (!value.preset && (value.start_date || value.end_date)) {
if (!value.preset && (value.start_date || value.end_date) && activePresetOptions.value.includes('custom')) {
return 'custom'
}
return 'last7days'
return defaultPreset()
}
const selectedPreset = ref<SelectablePreset>(normalizePreset(props.modelValue))
@@ -168,6 +181,12 @@ watch(() => props.modelValue, (value) => {
lastEmittedValue = getValueKey(value)
}, { deep: true })
watch(activePresetOptions, () => {
if (!activePresetOptions.value.includes(selectedPreset.value)) {
selectedPreset.value = normalizePreset(props.modelValue)
}
})
watch([selectedPreset, startDate, endDate, selectedGranularity], () => {
if (!allowHourly.value || !canUseHourly.value) {
if (selectedGranularity.value === 'hour') {

View File

@@ -0,0 +1,70 @@
<template>
<div class="flex flex-wrap items-center gap-2">
<TimeRangePicker
:model-value="timeRange"
:show-granularity="false"
:preset-options="timeRangePresetOptions"
@update:model-value="$emit('update:timeRange', $event)"
/>
<Select
:model-value="metric"
@update:model-value="emitMetric"
>
<SelectTrigger class="h-8 text-xs w-28">
<SelectValue placeholder="指标" />
</SelectTrigger>
<SelectContent>
<SelectItem value="requests">
请求数
</SelectItem>
<SelectItem value="tokens">
Tokens
</SelectItem>
<SelectItem value="cost">
成本
</SelectItem>
</SelectContent>
</Select>
</div>
</template>
<script setup lang="ts">
import { TimeRangePicker } from '@/components/common'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui'
import type { DateRangeParams } from '@/features/usage/types'
type LeaderboardMetric = 'requests' | 'tokens' | 'cost'
type LeaderboardTimeRangePreset = 'today' | 'yesterday' | 'last7days' | 'last30days' | 'last90days' | 'custom'
defineProps<{
metric: LeaderboardMetric
timeRange: DateRangeParams
}>()
const emit = defineEmits<{
(e: 'update:metric', value: LeaderboardMetric): void
(e: 'update:timeRange', value: DateRangeParams): void
}>()
const timeRangePresetOptions: LeaderboardTimeRangePreset[] = [
'today',
'yesterday',
'last7days',
'last30days',
'last90days',
'custom'
]
function emitMetric(value: string) {
if (value === 'requests' || value === 'tokens' || value === 'cost') {
emit('update:metric', value)
}
}
</script>

View File

@@ -1,26 +1,28 @@
<template>
<TableCard :title="title">
<template #actions>
<Select
v-if="showMetricSelect"
:model-value="metric"
@update:model-value="emitMetric"
>
<SelectTrigger class="h-8 text-xs w-28">
<SelectValue placeholder="指标" />
</SelectTrigger>
<SelectContent>
<SelectItem value="requests">
请求数
</SelectItem>
<SelectItem value="tokens">
Tokens
</SelectItem>
<SelectItem value="cost">
成本
</SelectItem>
</SelectContent>
</Select>
<slot name="actions">
<Select
v-if="showMetricSelect"
:model-value="metric"
@update:model-value="emitMetric"
>
<SelectTrigger class="h-8 text-xs w-28">
<SelectValue placeholder="指标" />
</SelectTrigger>
<SelectContent>
<SelectItem value="requests">
请求数
</SelectItem>
<SelectItem value="tokens">
Tokens
</SelectItem>
<SelectItem value="cost">
成本
</SelectItem>
</SelectContent>
</Select>
</slot>
</template>
<div
@@ -77,6 +79,8 @@
</TableRow>
</TableBody>
</Table>
<slot name="pagination" />
</TableCard>
</template>

View File

@@ -1,4 +1,5 @@
export { default as ActivityHeatmap } from './ActivityHeatmap.vue'
export { default as LeaderboardControls } from './LeaderboardControls.vue'
export { default as LeaderboardTable } from './LeaderboardTable.vue'
export { default as CostForecastChart } from './CostForecastChart.vue'
export { default as QuotaProgressCard } from './QuotaProgressCard.vue'