Add usage record column controls

This commit is contained in:
RWDai
2026-05-16 15:55:38 +08:00
parent 4f584a71df
commit ba661f1b3c
3 changed files with 332 additions and 37 deletions

View File

@@ -139,6 +139,19 @@
<!-- 分隔线 --> <!-- 分隔线 -->
<div class="hidden sm:block h-4 w-px bg-border" /> <div class="hidden sm:block h-4 w-px bg-border" />
<!-- 列显示配置桌面端 -->
<MultiSelect
v-model="visibleColumnIds"
:options="columnSelectOptions"
placeholder="显示列"
trigger-class="hidden md:flex w-40 h-8 text-xs border-border/60"
dropdown-min-width="14rem"
:searchable="false"
/>
<!-- 分隔线 -->
<div class="hidden sm:block h-4 w-px bg-border" />
<!-- 自动刷新按钮 --> <!-- 自动刷新按钮 -->
<Button <Button
variant="ghost" variant="ghost"
@@ -293,35 +306,43 @@
</div> </div>
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
<Table :class="['hidden md:table table-fixed w-full', isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']"> <Table :class="['hidden md:table table-fixed w-full', desktopTableMinWidthClass]">
<colgroup v-if="isAdmin"> <colgroup v-if="isAdmin">
<col class="w-[8%]"> <col v-if="isColumnVisible('time')" class="w-[8%]">
<col class="w-[12%]"> <col v-if="isColumnVisible('user')" class="w-[12%]">
<col class="w-[14%]"> <col v-if="isColumnVisible('model')" class="w-[14%]">
<col class="w-[16%]"> <col v-if="isColumnVisible('provider')" class="w-[16%]">
<col class="w-[15%]"> <col v-if="isColumnVisible('api_format')" class="w-[15%]">
<col class="w-[10%]"> <col v-if="isColumnVisible('status')" class="w-[10%]">
<col class="w-[10%]"> <col v-if="isColumnVisible('tokens')" class="w-[10%]">
<col class="w-[6%]"> <col v-if="isColumnVisible('cost')" class="w-[6%]">
<col class="w-[9%]"> <col v-if="isColumnVisible('performance')" class="w-[9%]">
<col v-if="isColumnVisible('client_family')" class="w-[9%]">
<col v-if="isColumnVisible('client_ip')" class="w-[10%]">
<col v-if="isColumnVisible('user_agent')" class="w-[13%]">
<col v-if="isColumnVisible('request_path')" class="w-[13%]">
</colgroup> </colgroup>
<colgroup v-else> <colgroup v-else>
<col class="w-[9%]"> <col v-if="isColumnVisible('time')" class="w-[9%]">
<col class="w-[17%]"> <col v-if="isColumnVisible('key')" class="w-[17%]">
<col class="w-[22%]"> <col v-if="isColumnVisible('model')" class="w-[22%]">
<col class="w-[14%]"> <col v-if="isColumnVisible('api_format')" class="w-[14%]">
<col class="w-[10%]"> <col v-if="isColumnVisible('status')" class="w-[10%]">
<col class="w-[11%]"> <col v-if="isColumnVisible('tokens')" class="w-[11%]">
<col class="w-[7%]"> <col v-if="isColumnVisible('cost')" class="w-[7%]">
<col class="w-[10%]"> <col v-if="isColumnVisible('performance')" class="w-[10%]">
<col v-if="isColumnVisible('client_family')" class="w-[9%]">
<col v-if="isColumnVisible('client_ip')" class="w-[10%]">
<col v-if="isColumnVisible('user_agent')" class="w-[13%]">
<col v-if="isColumnVisible('request_path')" class="w-[13%]">
</colgroup> </colgroup>
<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-[8%]"> <TableHead v-if="isColumnVisible('time')" class="h-12 font-semibold w-[8%]">
时间 时间
</TableHead> </TableHead>
<SortableTableHead <SortableTableHead
v-if="isAdmin" v-if="isAdmin && isColumnVisible('user')"
class="h-12 font-semibold w-[12%]" class="h-12 font-semibold w-[12%]"
column-key="user" column-key="user"
:sortable="false" :sortable="false"
@@ -340,12 +361,13 @@
</template> </template>
</SortableTableHead> </SortableTableHead>
<TableHead <TableHead
v-if="!isAdmin" v-if="!isAdmin && isColumnVisible('key')"
class="h-12 font-semibold w-[17%]" class="h-12 font-semibold w-[17%]"
> >
密钥 密钥
</TableHead> </TableHead>
<SortableTableHead <SortableTableHead
v-if="isColumnVisible('model')"
:class="['h-12 font-semibold', isAdmin ? 'w-[14%]' : 'w-[22%]']" :class="['h-12 font-semibold', isAdmin ? 'w-[14%]' : 'w-[22%]']"
column-key="model" column-key="model"
:sortable="false" :sortable="false"
@@ -364,7 +386,7 @@
</template> </template>
</SortableTableHead> </SortableTableHead>
<SortableTableHead <SortableTableHead
v-if="isAdmin" v-if="isAdmin && isColumnVisible('provider')"
class="h-12 font-semibold w-[16%]" class="h-12 font-semibold w-[16%]"
column-key="provider" column-key="provider"
:sortable="false" :sortable="false"
@@ -383,6 +405,7 @@
</template> </template>
</SortableTableHead> </SortableTableHead>
<SortableTableHead <SortableTableHead
v-if="isColumnVisible('api_format')"
:class="['h-12 font-semibold', isAdmin ? 'w-[15%]' : 'w-[14%]']" :class="['h-12 font-semibold', isAdmin ? 'w-[15%]' : 'w-[14%]']"
column-key="api_format" column-key="api_format"
:sortable="false" :sortable="false"
@@ -401,6 +424,7 @@
</template> </template>
</SortableTableHead> </SortableTableHead>
<SortableTableHead <SortableTableHead
v-if="isColumnVisible('status')"
class="h-12 font-semibold w-[10%] text-center" class="h-12 font-semibold w-[10%] text-center"
column-key="status" column-key="status"
:sortable="false" :sortable="false"
@@ -419,24 +443,52 @@
/> />
</template> </template>
</SortableTableHead> </SortableTableHead>
<TableHead class="h-12 font-semibold w-[10%] text-center"> <TableHead v-if="isColumnVisible('tokens')" class="h-12 font-semibold w-[10%] text-center">
Tokens Tokens
</TableHead> </TableHead>
<TableHead class="h-12 font-semibold w-[6%] text-right"> <TableHead v-if="isColumnVisible('cost')" class="h-12 font-semibold w-[6%] text-right">
费用 费用
</TableHead> </TableHead>
<TableHead class="h-12 font-semibold w-[9%] text-right"> <TableHead v-if="isColumnVisible('performance')" class="h-12 font-semibold w-[9%] 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 class="whitespace-nowrap">首字/总耗时</span> <span class="whitespace-nowrap">首字/总耗时</span>
<span class="text-muted-foreground font-normal">输出速度</span> <span class="text-muted-foreground font-normal">输出速度</span>
</div> </div>
</TableHead> </TableHead>
<SortableTableHead
v-if="isColumnVisible('client_family')"
class="h-12 font-semibold w-[9%]"
column-key="client_family"
:sortable="false"
:filter-active="filterClientFamily !== '__all__'"
filter-title="筛选客户端"
filter-content-class="w-44 p-1 rounded-2xl border-border bg-card text-foreground shadow-2xl backdrop-blur-xl"
>
客户端
<template #filter="{ close }">
<TableFilterMenu
:model-value="filterClientFamily"
:options="clientFamilyFilterOptions"
@update:model-value="$emit('update:filterClientFamily', $event)"
@select="close"
/>
</template>
</SortableTableHead>
<TableHead v-if="isColumnVisible('client_ip')" class="h-12 font-semibold w-[10%]">
IP 地址
</TableHead>
<TableHead v-if="isColumnVisible('user_agent')" class="h-12 font-semibold w-[13%]">
User-Agent
</TableHead>
<TableHead v-if="isColumnVisible('request_path')" class="h-12 font-semibold w-[13%]">
请求路径
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="records.length === 0"> <TableRow v-if="records.length === 0">
<TableCell <TableCell
:colspan="isAdmin ? 9 : 8" :colspan="visibleColumnCount"
class="text-center py-12 text-muted-foreground" class="text-center py-12 text-muted-foreground"
> >
暂无请求记录 暂无请求记录
@@ -450,7 +502,7 @@
@mousedown="handleRowMouseDown($event, record.id)" @mousedown="handleRowMouseDown($event, record.id)"
@click="handleRowClick($event, record.id)" @click="handleRowClick($event, record.id)"
> >
<TableCell class="py-4 w-[8%] align-top"> <TableCell v-if="isColumnVisible('time')" class="py-4 w-[8%] align-top">
<div class="flex flex-col gap-0.5 leading-tight"> <div class="flex flex-col gap-0.5 leading-tight">
<span class="text-xs text-foreground tabular-nums whitespace-nowrap"> <span class="text-xs text-foreground tabular-nums whitespace-nowrap">
{{ formatRecordTime(record.created_at) }} {{ formatRecordTime(record.created_at) }}
@@ -461,7 +513,7 @@
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell
v-if="isAdmin" v-if="isAdmin && isColumnVisible('user')"
class="py-4 w-[12%] truncate" class="py-4 w-[12%] 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}` : '已删除用户')"
> >
@@ -480,7 +532,7 @@
</TableCell> </TableCell>
<!-- 用户页面的密钥列 --> <!-- 用户页面的密钥列 -->
<TableCell <TableCell
v-if="!isAdmin" v-if="!isAdmin && isColumnVisible('key')"
class="py-4 w-[17%]" class="py-4 w-[17%]"
:title="record.api_key?.name || '-'" :title="record.api_key?.name || '-'"
> >
@@ -495,6 +547,7 @@
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell
v-if="isColumnVisible('model')"
:class="['font-medium py-4', isAdmin ? 'w-[14%]' : 'w-[22%]']" :class="['font-medium py-4', isAdmin ? 'w-[14%]' : 'w-[22%]']"
:title="getModelTooltip(record)" :title="getModelTooltip(record)"
> >
@@ -525,7 +578,7 @@
>{{ record.model }}</span> >{{ record.model }}</span>
</TableCell> </TableCell>
<TableCell <TableCell
v-if="isAdmin" v-if="isAdmin && isColumnVisible('provider')"
class="py-4 w-[16%]" class="py-4 w-[16%]"
> >
<div class="flex min-w-0 items-center gap-1"> <div class="flex min-w-0 items-center gap-1">
@@ -582,6 +635,7 @@
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell
v-if="isColumnVisible('api_format')"
:class="['py-4', isAdmin ? 'w-[15%]' : 'w-[14%]']" :class="['py-4', isAdmin ? 'w-[15%]' : 'w-[14%]']"
:title="getApiFormatTooltip(record)" :title="getApiFormatTooltip(record)"
> >
@@ -617,7 +671,7 @@
class="text-muted-foreground text-xs" class="text-muted-foreground text-xs"
>-</span> >-</span>
</TableCell> </TableCell>
<TableCell class="text-center py-4 w-[10%]"> <TableCell v-if="isColumnVisible('status')" class="text-center py-4 w-[10%]">
<!-- 优先显示请求状态 --> <!-- 优先显示请求状态 -->
<Badge <Badge
v-if="getDisplayStatus(record) === 'pending'" v-if="getDisplayStatus(record) === 'pending'"
@@ -668,7 +722,7 @@
{{ getStreamModeLabel(record) }} {{ getStreamModeLabel(record) }}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell class="py-4 w-[10%]"> <TableCell v-if="isColumnVisible('tokens')" class="py-4 w-[10%]">
<div class="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-x-1 text-xs leading-tight tabular-nums"> <div class="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-x-1 text-xs leading-tight tabular-nums">
<span class="justify-self-end whitespace-nowrap text-right"> <span class="justify-self-end whitespace-nowrap text-right">
{{ formatTokens(getRecordEffectiveInputTokens(record)) }} {{ formatTokens(getRecordEffectiveInputTokens(record)) }}
@@ -702,7 +756,7 @@
</span> </span>
</div> </div>
</TableCell> </TableCell>
<TableCell class="text-right py-4 w-[6%]"> <TableCell v-if="isColumnVisible('cost')" class="text-right py-4 w-[6%]">
<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
@@ -713,7 +767,7 @@
</span> </span>
</div> </div>
</TableCell> </TableCell>
<TableCell class="text-right py-4 w-[9%]"> <TableCell v-if="isColumnVisible('performance')" class="text-right py-4 w-[9%]">
<!-- pending/streaming 状态首字与动态总耗时保留在同一行 --> <!-- pending/streaming 状态首字与动态总耗时保留在同一行 -->
<div <div
v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'" v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'"
@@ -746,6 +800,39 @@
class="text-muted-foreground" class="text-muted-foreground"
>-</span> >-</span>
</TableCell> </TableCell>
<TableCell
v-if="isColumnVisible('client_family')"
class="py-4 w-[9%] text-xs"
:title="record.client_family || '-'"
>
<Badge
variant="outline"
class="whitespace-nowrap border-border/60 text-muted-foreground"
>
{{ formatClientFamily(record.client_family) }}
</Badge>
</TableCell>
<TableCell
v-if="isColumnVisible('client_ip')"
class="py-4 w-[10%] text-xs truncate"
:title="record.client_ip || '-'"
>
{{ record.client_ip || '-' }}
</TableCell>
<TableCell
v-if="isColumnVisible('user_agent')"
class="py-4 w-[13%] text-xs truncate"
:title="record.user_agent || '-'"
>
{{ formatUserAgent(record.user_agent) }}
</TableCell>
<TableCell
v-if="isColumnVisible('request_path')"
class="py-4 w-[13%] text-xs truncate"
:title="getRequestPath(record) || '-'"
>
{{ getRequestPath(record) || '-' }}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
@@ -768,7 +855,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn, useLocalStorage } from '@vueuse/core'
import { import {
TableCard, TableCard,
Badge, Badge,
@@ -808,7 +895,8 @@ import {
import { useRowClick } from '@/composables/useRowClick' import { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format' import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { DateRangeParams, UsageRecord } from '../types' import type { DateRangeParams, UsageRecord } from '../types'
import { TimeRangePicker } from '@/components/common' import { MultiSelect, TimeRangePicker } from '@/components/common'
import type { MultiSelectOption } from '@/components/common/MultiSelect.vue'
import ElapsedTimeText from './ElapsedTimeText.vue' import ElapsedTimeText from './ElapsedTimeText.vue'
import ServerUserSelector from './ServerUserSelector.vue' import ServerUserSelector from './ServerUserSelector.vue'
@@ -824,6 +912,69 @@ interface FilterOption {
disabled?: boolean disabled?: boolean
} }
type UsageRecordColumnId =
| 'time'
| 'user'
| 'key'
| 'model'
| 'provider'
| 'api_format'
| 'status'
| 'tokens'
| 'cost'
| 'performance'
| 'client_family'
| 'client_ip'
| 'user_agent'
| 'request_path'
interface UsageRecordColumnOption {
id: UsageRecordColumnId
label: string
adminOnly?: boolean
userOnly?: boolean
}
const USAGE_RECORD_COLUMN_OPTIONS: UsageRecordColumnOption[] = [
{ id: 'time', label: '时间' },
{ id: 'user', label: '用户', adminOnly: true },
{ id: 'key', label: '密钥', userOnly: true },
{ id: 'model', label: '模型' },
{ id: 'provider', label: '提供商', adminOnly: true },
{ id: 'api_format', label: 'API格式' },
{ id: 'status', label: '类型/状态' },
{ id: 'tokens', label: 'Tokens' },
{ id: 'cost', label: '费用' },
{ id: 'performance', label: '耗时/速度' },
{ id: 'client_family', label: '客户端类型' },
{ id: 'client_ip', label: 'IP 地址' },
{ id: 'user_agent', label: 'User-Agent' },
{ id: 'request_path', label: '请求路径' },
]
const DEFAULT_ADMIN_COLUMNS: UsageRecordColumnId[] = [
'time',
'user',
'model',
'provider',
'api_format',
'status',
'tokens',
'cost',
'performance',
]
const DEFAULT_USER_COLUMNS: UsageRecordColumnId[] = [
'time',
'key',
'model',
'api_format',
'status',
'tokens',
'cost',
'performance',
]
const props = defineProps<{ const props = defineProps<{
records: UsageRecord[] records: UsageRecord[]
isAdmin: boolean isAdmin: boolean
@@ -838,9 +989,11 @@ const props = defineProps<{
filterProvider: string filterProvider: string
filterApiFormat: string filterApiFormat: string
filterStatus: string filterStatus: string
filterClientFamily: string
availableUsers: UserOption[] availableUsers: UserOption[]
availableModels: string[] availableModels: string[]
availableProviders: string[] availableProviders: string[]
availableClientFamilies: string[]
// 分页 // 分页
currentPage: number currentPage: number
pageSize: number pageSize: number
@@ -858,6 +1011,7 @@ const emit = defineEmits<{
'update:filterProvider': [value: string] 'update:filterProvider': [value: string]
'update:filterApiFormat': [value: string] 'update:filterApiFormat': [value: string]
'update:filterStatus': [value: string] 'update:filterStatus': [value: string]
'update:filterClientFamily': [value: string]
'update:currentPage': [value: number] 'update:currentPage': [value: number]
'update:pageSize': [value: number] 'update:pageSize': [value: number]
'update:autoRefresh': [value: boolean] 'update:autoRefresh': [value: boolean]
@@ -881,6 +1035,75 @@ const AVAILABLE_API_FORMATS = [
// 使用模块级常量 // 使用模块级常量
const availableApiFormats = AVAILABLE_API_FORMATS const availableApiFormats = AVAILABLE_API_FORMATS
const adminVisibleColumnIds = useLocalStorage<UsageRecordColumnId[]>(
'usage-records-visible-columns-admin',
DEFAULT_ADMIN_COLUMNS,
)
const userVisibleColumnIds = useLocalStorage<UsageRecordColumnId[]>(
'usage-records-visible-columns-user',
DEFAULT_USER_COLUMNS,
)
const roleColumnOptions = computed(() => USAGE_RECORD_COLUMN_OPTIONS.filter((column) => {
if (column.adminOnly && !props.isAdmin) return false
if (column.userOnly && props.isAdmin) return false
return true
}))
const roleColumnIds = computed(() => new Set(roleColumnOptions.value.map(column => column.id)))
function sanitizeColumnIds(
ids: readonly string[],
fallback: readonly UsageRecordColumnId[],
): UsageRecordColumnId[] {
const seen = new Set<UsageRecordColumnId>()
const sanitized = ids.filter((id): id is UsageRecordColumnId => {
if (!roleColumnIds.value.has(id as UsageRecordColumnId)) return false
if (seen.has(id as UsageRecordColumnId)) return false
seen.add(id as UsageRecordColumnId)
return true
})
return sanitized.length > 0 ? sanitized : [...fallback]
}
const visibleColumnIds = computed<UsageRecordColumnId[]>({
get: () => sanitizeColumnIds(
props.isAdmin ? adminVisibleColumnIds.value : userVisibleColumnIds.value,
props.isAdmin ? DEFAULT_ADMIN_COLUMNS : DEFAULT_USER_COLUMNS,
),
set: (value) => {
const sanitized = sanitizeColumnIds(value, props.isAdmin ? DEFAULT_ADMIN_COLUMNS : DEFAULT_USER_COLUMNS)
if (props.isAdmin) {
adminVisibleColumnIds.value = sanitized
} else {
userVisibleColumnIds.value = sanitized
}
},
})
const visibleColumnSet = computed(() => new Set<UsageRecordColumnId>(visibleColumnIds.value))
const visibleColumnCount = computed(() => visibleColumnIds.value.length)
const desktopTableMinWidthClass = computed(() => {
const metadataColumnCount = visibleColumnIds.value.filter(column => (
column === 'client_family' ||
column === 'client_ip' ||
column === 'user_agent' ||
column === 'request_path'
)).length
if (metadataColumnCount >= 3) return 'min-w-[1520px]'
if (metadataColumnCount > 0) return 'min-w-[1320px]'
return props.isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]'
})
const columnSelectOptions = computed<MultiSelectOption[]>(() => roleColumnOptions.value.map(column => ({
value: column.id,
label: column.label,
})))
function isColumnVisible(column: UsageRecordColumnId): boolean {
return visibleColumnSet.value.has(column)
}
const modelFilterOptions = computed<FilterOption[]>(() => [ const modelFilterOptions = computed<FilterOption[]>(() => [
{ value: '__all__', label: '全部模型' }, { value: '__all__', label: '全部模型' },
...props.availableModels.map((model) => ({ ...props.availableModels.map((model) => ({
@@ -897,6 +1120,31 @@ const providerFilterOptions = computed<FilterOption[]>(() => [
})), })),
]) ])
function formatClientFamily(value: string | null | undefined): string {
const normalized = value?.trim().toLowerCase()
if (!normalized) return '-'
if (normalized === 'codex') return 'Codex'
if (normalized === 'claude_code') return 'Claude Code'
if (normalized === 'opencode') return 'OpenCode'
if (normalized === 'generic') return '通用客户端'
return value?.trim() || '-'
}
const clientFamilyFilterOptions = computed<FilterOption[]>(() => {
const families = new Set<string>(props.availableClientFamilies)
props.records.forEach((record) => {
const family = record.client_family?.trim()
if (family) families.add(family)
})
return [
{ value: '__all__', label: '全部客户端' },
...Array.from(families).sort().map((family) => ({
value: family,
label: formatClientFamily(family),
})),
]
})
const apiFormatFilterOptions = computed<FilterOption[]>(() => [ const apiFormatFilterOptions = computed<FilterOption[]>(() => [
{ value: '__all__', label: '全部格式' }, { value: '__all__', label: '全部格式' },
...availableApiFormats.map((format) => ({ ...availableApiFormats.map((format) => ({
@@ -1048,6 +1296,16 @@ function formatOutputRateTokensPerSecond(outputRate: number | null | undefined):
return `${value} tokens/s` return `${value} tokens/s`
} }
function formatUserAgent(value: string | null | undefined): string {
const userAgent = value?.trim()
if (!userAgent) return '-'
return userAgent.length > 48 ? `${userAgent.slice(0, 45)}...` : userAgent
}
function getRequestPath(record: UsageRecord): string | null {
return record.request_path_and_query?.trim() || record.request_path?.trim() || null
}
// useDebounceFn 自动处理清理,无需 onUnmounted // useDebounceFn 自动处理清理,无需 onUnmounted
// 判断是否应该显示格式转换信息 // 判断是否应该显示格式转换信息

View File

@@ -55,6 +55,12 @@ vi.mock('@/components/common', async () => {
const { defineComponent, h } = await import('vue') const { defineComponent, h } = await import('vue')
return { return {
MultiSelect: defineComponent({
name: 'MultiSelectStub',
setup() {
return () => h('div')
},
}),
TimeRangePicker: defineComponent({ TimeRangePicker: defineComponent({
name: 'TimeRangePickerStub', name: 'TimeRangePickerStub',
setup() { setup() {
@@ -135,9 +141,11 @@ function mountUsageRecordsTable(records: UsageRecord[], overrides: Record<string
filterProvider: '__all__', filterProvider: '__all__',
filterApiFormat: '__all__', filterApiFormat: '__all__',
filterStatus: '__all__', filterStatus: '__all__',
filterClientFamily: '__all__',
availableUsers: [], availableUsers: [],
availableModels: [], availableModels: [],
availableProviders: [], availableProviders: [],
availableClientFamilies: [],
currentPage: 1, currentPage: 1,
pageSize: 20, pageSize: 20,
totalRecords: records.length, totalRecords: records.length,

View File

@@ -90,9 +90,11 @@
:filter-provider="filterProvider" :filter-provider="filterProvider"
:filter-api-format="filterApiFormat" :filter-api-format="filterApiFormat"
:filter-status="filterStatus" :filter-status="filterStatus"
:filter-client-family="filterClientFamily"
:available-users="availableUsers" :available-users="availableUsers"
:available-models="availableModels" :available-models="availableModels"
:available-providers="availableProviders" :available-providers="availableProviders"
:available-client-families="availableClientFamilies"
:current-page="currentPage" :current-page="currentPage"
:page-size="pageSize" :page-size="pageSize"
:total-records="effectiveTotalRecords" :total-records="effectiveTotalRecords"
@@ -105,6 +107,7 @@
@update:filter-provider="handleFilterProviderChange" @update:filter-provider="handleFilterProviderChange"
@update:filter-api-format="handleFilterApiFormatChange" @update:filter-api-format="handleFilterApiFormatChange"
@update:filter-status="handleFilterStatusChange" @update:filter-status="handleFilterStatusChange"
@update:filter-client-family="handleFilterClientFamilyChange"
@update:current-page="handlePageChange" @update:current-page="handlePageChange"
@update:page-size="handlePageSizeChange" @update:page-size="handlePageSizeChange"
@update:auto-refresh="handleAutoRefreshChange" @update:auto-refresh="handleAutoRefreshChange"
@@ -226,6 +229,7 @@ const filterModel = ref('__all__')
const filterProvider = ref('__all__') const filterProvider = ref('__all__')
const filterApiFormat = ref('__all__') const filterApiFormat = ref('__all__')
const filterStatus = ref<FilterStatusValue>('__all__') const filterStatus = ref<FilterStatusValue>('__all__')
const filterClientFamily = ref('__all__')
// 用户列表(仅管理员页面使用) // 用户列表(仅管理员页面使用)
const availableUsers = ref<UserOption[]>([]) const availableUsers = ref<UserOption[]>([])
@@ -372,9 +376,15 @@ const filteredRecords = computed(() => {
records = records.filter(record => record.status === 'cancelled') records = records.filter(record => record.status === 'cancelled')
} else if (filterStatus.value === 'has_fallback') { } else if (filterStatus.value === 'has_fallback') {
records = records.filter(record => hasUsageFallback(record)) records = records.filter(record => hasUsageFallback(record))
} else if (filterStatus.value === 'has_retry') {
records = records.filter(record => record.has_retry === true)
} }
} }
if (filterClientFamily.value !== '__all__') {
records = records.filter(record => record.client_family === filterClientFamily.value)
}
return records return records
} }
return currentRecords.value return currentRecords.value
@@ -704,6 +714,15 @@ const effectiveTotalRecords = computed(() => {
// 显示的记录 // 显示的记录
const displayRecords = computed(() => paginatedRecords.value) const displayRecords = computed(() => paginatedRecords.value)
const availableClientFamilies = computed(() => {
const families = new Set<string>()
currentRecords.value.forEach((record) => {
const family = record.client_family?.trim()
if (family) families.add(family)
})
return Array.from(families).sort()
})
// 详情弹窗状态 // 详情弹窗状态
const detailModalOpen = ref(false) const detailModalOpen = ref(false)
@@ -787,7 +806,8 @@ function getCurrentFilters() {
model: filterModel.value !== '__all__' ? filterModel.value : undefined, model: filterModel.value !== '__all__' ? filterModel.value : undefined,
provider: filterProvider.value !== '__all__' ? filterProvider.value : undefined, provider: filterProvider.value !== '__all__' ? filterProvider.value : undefined,
api_format: filterApiFormat.value !== '__all__' ? filterApiFormat.value : undefined, api_format: filterApiFormat.value !== '__all__' ? filterApiFormat.value : undefined,
status: filterStatus.value !== '__all__' ? filterStatus.value : undefined status: filterStatus.value !== '__all__' ? filterStatus.value : undefined,
client_family: filterClientFamily.value !== '__all__' ? filterClientFamily.value : undefined
} }
} }
@@ -848,6 +868,15 @@ async function handleFilterStatusChange(value: string) {
} }
} }
async function handleFilterClientFamilyChange(value: string) {
filterClientFamily.value = value
currentPage.value = 1
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
// 刷新数据 // 刷新数据
async function refreshData() { async function refreshData() {
if (!isPageVisible.value) return if (!isPageVisible.value) return