Merge pull request #69 from AoaoMH/feature/Record-optimization

feat: add usage statistics and records feature with new API routes, f…
This commit is contained in:
fawney19
2026-01-05 19:31:59 +08:00
committed by GitHub
8 changed files with 112 additions and 25 deletions

View File

@@ -166,6 +166,7 @@ export const usageApi = {
end_date?: string
user_id?: string // UUID
username?: string
user_api_key_name?: string
model?: string
provider?: string
status?: string // 'stream' | 'standard' | 'error'

View File

@@ -56,6 +56,20 @@
</SelectContent>
</Select>
<!-- Key 名称筛选请求使用的用户 Key仅管理员可见 -->
<div
v-if="isAdmin"
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" />
<Input
id="usage-records-key-name"
v-model="localKeyName"
placeholder="搜索密钥名称"
class="w-24 sm:w-40 h-8 text-xs border-border/60 pl-8"
/>
</div>
<!-- 模型筛选 -->
<Select
v-model:open="filterModelSelectOpen"
@@ -218,7 +232,18 @@
class="py-4 w-[100px] truncate"
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
>
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
<div class="flex flex-col text-xs gap-0.5">
<span class="truncate">
{{ record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户') }}
</span>
<span
v-if="record.api_key?.name"
class="text-muted-foreground truncate"
:title="record.api_key.name"
>
{{ record.api_key.name }}
</span>
</div>
</TableCell>
<TableCell
class="font-medium py-4 w-[140px]"
@@ -438,6 +463,7 @@ import {
TableCard,
Badge,
Button,
Input,
Select,
SelectTrigger,
SelectValue,
@@ -451,7 +477,7 @@ import {
TableCell,
Pagination,
} from '@/components/ui'
import { RefreshCcw } from 'lucide-vue-next'
import { RefreshCcw, Search } from 'lucide-vue-next'
import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables'
import { useRowClick } from '@/composables/useRowClick'
@@ -472,6 +498,7 @@ const props = defineProps<{
selectedPeriod: string
// 筛选
filterUser: string
filterKeyName: string
filterModel: string
filterProvider: string
filterStatus: string
@@ -490,6 +517,7 @@ const props = defineProps<{
const emit = defineEmits<{
'update:selectedPeriod': [value: string]
'update:filterUser': [value: string]
'update:filterKeyName': [value: string]
'update:filterModel': [value: string]
'update:filterProvider': [value: string]
'update:filterStatus': [value: string]
@@ -507,6 +535,23 @@ const filterModelSelectOpen = ref(false)
const filterProviderSelectOpen = ref(false)
const filterStatusSelectOpen = ref(false)
// Key 名称筛选(输入防抖)
const localKeyName = ref(props.filterKeyName)
let keyNameDebounceTimer: ReturnType<typeof setTimeout> | null = null
watch(() => props.filterKeyName, (value) => {
if (value !== localKeyName.value) {
localKeyName.value = value
}
})
watch(localKeyName, (value) => {
if (keyNameDebounceTimer) clearTimeout(keyNameDebounceTimer)
keyNameDebounceTimer = setTimeout(() => {
emit('update:filterKeyName', value)
}, 300)
})
// 动态计时器相关
const now = ref(Date.now())
let timerInterval: ReturnType<typeof setInterval> | null = null
@@ -574,6 +619,10 @@ function handleRowClick(event: MouseEvent, id: string) {
// 组件卸载时清理
onUnmounted(() => {
stopTimer()
if (keyNameDebounceTimer) {
clearTimeout(keyNameDebounceTimer)
keyNameDebounceTimer = null
}
})
// 格式化 API 格式显示名称

View File

@@ -24,6 +24,7 @@ export interface PaginationParams {
export interface FilterParams {
user_id?: string
user_api_key_name?: string
model?: string
provider?: string
status?: string
@@ -255,6 +256,9 @@ export function useUsageData(options: UseUsageDataOptions) {
if (filters?.user_id) {
params.user_id = filters.user_id
}
if (filters?.user_api_key_name) {
params.user_api_key_name = filters.user_api_key_name
}
if (filters?.model) {
params.model = filters.model
}

View File

@@ -61,6 +61,11 @@ export interface UsageRecord {
user_id?: string
username?: string
user_email?: string
api_key?: {
id: string | null
name: string | null
display: string | null
} | null
provider: string
api_key_name?: string
rate_multiplier?: number

View File

@@ -367,6 +367,11 @@ function generateMockUsageRecords(count: number = 100) {
user_id: user.id,
username: user.username,
user_email: user.email,
api_key: {
id: `key-${user.id}-${Math.ceil(Math.random() * 2)}`,
name: `${user.username} Key ${Math.ceil(Math.random() * 3)}`,
display: `sk-ae...${String(1000 + Math.floor(Math.random() * 9000))}`
},
provider: model.provider,
api_key_name: `${model.provider}-key-${Math.ceil(Math.random() * 3)}`,
rate_multiplier: 1.0,
@@ -835,10 +840,17 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
'GET /api/admin/usage/records': async (config) => {
await delay()
requireAdmin()
const records = getUsageRecords()
let records = getUsageRecords()
const params = config.params || {}
const limit = parseInt(params.limit) || 20
const offset = parseInt(params.offset) || 0
// 用户 API Key 名称筛选(注意:不是 Provider Key
if (typeof params.user_api_key_name === 'string' && params.user_api_key_name.trim()) {
const keyword = params.user_api_key_name.trim().toLowerCase()
records = records.filter(r => (r.api_key?.name || '').toLowerCase().includes(keyword))
}
return createMockResponse({
records: records.slice(offset, offset + limit),
total: records.length,

View File

@@ -57,6 +57,7 @@
:loading="isLoadingRecords"
:selected-period="selectedPeriod"
:filter-user="filterUser"
:filter-key-name="filterKeyName"
:filter-model="filterModel"
:filter-provider="filterProvider"
:filter-status="filterStatus"
@@ -70,6 +71,7 @@
:auto-refresh="globalAutoRefresh"
@update:selected-period="handlePeriodChange"
@update:filter-user="handleFilterUserChange"
@update:filter-key-name="handleFilterKeyNameChange"
@update:filter-model="handleFilterModelChange"
@update:filter-provider="handleFilterProviderChange"
@update:filter-status="handleFilterStatusChange"
@@ -134,6 +136,7 @@ const pageSizeOptions = [10, 20, 50, 100]
// 筛选状态
const filterUser = ref('__all__')
const filterKeyName = ref('')
const filterModel = ref('__all__')
const filterProvider = ref('__all__')
const filterStatus = ref<FilterStatusValue>('__all__')
@@ -439,6 +442,7 @@ async function handlePageSizeChange(size: number) {
function getCurrentFilters() {
return {
user_id: filterUser.value !== '__all__' ? filterUser.value : undefined,
user_api_key_name: filterKeyName.value.trim() ? filterKeyName.value.trim() : undefined,
model: filterModel.value !== '__all__' ? filterModel.value : undefined,
provider: filterProvider.value !== '__all__' ? filterProvider.value : undefined,
status: filterStatus.value !== '__all__' ? filterStatus.value : undefined
@@ -455,6 +459,15 @@ async function handleFilterUserChange(value: string) {
}
}
async function handleFilterKeyNameChange(value: string) {
filterKeyName.value = value
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
}
}
async function handleFilterModelChange(value: string) {
filterModel.value = value
currentPage.value = 1 // 重置到第一页