Merge branch 'fawney19:main' into main

This commit is contained in:
ZheFox
2026-05-19 00:50:52 +08:00
committed by GitHub
29 changed files with 795 additions and 79 deletions

View File

@@ -61,11 +61,17 @@ export interface UsageRecordDetail {
cost: number // 官方费率
actual_cost?: number // 倍率消耗(仅管理员可见)
rate_multiplier?: number // 成本倍率(仅管理员可见)
response_time_ms?: number
response_time_ms?: number | null
first_byte_time_ms?: number | null
is_stream: boolean
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
client_family?: string | null
client_ip?: string | null
user_agent?: string | null
request_path?: string | null
request_path_and_query?: string | null
created_at: string
cache_creation_input_tokens?: number
cache_creation_ephemeral_5m_input_tokens?: number

View File

@@ -24,6 +24,11 @@ export interface UsageRecord {
response_time?: number
created_at: string
has_fallback?: boolean // 🆕 是否发生了 fallback
client_family?: string | null
client_ip?: string | null
user_agent?: string | null
request_path?: string | null
request_path_and_query?: string | null
}
export interface UsageStats {
@@ -107,6 +112,7 @@ export interface UsageFilters {
granularity?: 'hour' | 'day' | 'week' | 'month'
timezone?: string
tz_offset_minutes?: number
client_family?: string
page?: number
page_size?: number
}

View File

@@ -139,6 +139,19 @@
<!-- 分隔线 -->
<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
variant="ghost"
@@ -295,36 +308,42 @@
<!-- 桌面端表格视图 -->
<Table
class="hidden md:table table-fixed w-full"
:class="[isAdmin ? 'min-w-[1120px]' : 'min-w-[960px]']"
:class="[desktopTableMinWidthClass]"
>
<colgroup v-if="isAdmin">
<col class="w-[8%]">
<col class="w-[12%]">
<col class="w-[14%]">
<col class="w-[16%]">
<col class="w-[15%]">
<col class="w-[10%]">
<col class="w-[10%]">
<col class="w-[6%]">
<col class="w-[9%]">
<col v-if="isColumnVisible('time')" class="w-[8%]">
<col v-if="isColumnVisible('user')" class="w-[12%]">
<col v-if="isColumnVisible('model')" class="w-[14%]">
<col v-if="isColumnVisible('provider')" class="w-[16%]">
<col v-if="isColumnVisible('api_format')" class="w-[15%]">
<col v-if="isColumnVisible('status')" class="w-[10%]">
<col v-if="isColumnVisible('tokens')" class="w-[10%]">
<col v-if="isColumnVisible('cost')" class="w-[6%]">
<col v-if="isColumnVisible('performance')" class="w-[9%]">
<col v-if="isColumnVisible('client_family')" class="w-[12%]">
<col v-if="isColumnVisible('client_ip')" class="w-[10%]">
<col v-if="isColumnVisible('user_agent')" class="w-[13%]">
</colgroup>
<colgroup v-else>
<col class="w-[9%]">
<col class="w-[17%]">
<col class="w-[22%]">
<col class="w-[14%]">
<col class="w-[10%]">
<col class="w-[11%]">
<col class="w-[7%]">
<col class="w-[10%]">
<col v-if="isColumnVisible('time')" class="w-[9%]">
<col v-if="isColumnVisible('key')" class="w-[17%]">
<col v-if="isColumnVisible('model')" class="w-[22%]">
<col v-if="isColumnVisible('api_format')" class="w-[14%]">
<col v-if="isColumnVisible('status')" class="w-[10%]">
<col v-if="isColumnVisible('tokens')" class="w-[11%]">
<col v-if="isColumnVisible('cost')" class="w-[7%]">
<col v-if="isColumnVisible('performance')" class="w-[10%]">
<col v-if="isColumnVisible('client_family')" class="w-[12%]">
<col v-if="isColumnVisible('client_ip')" class="w-[10%]">
<col v-if="isColumnVisible('user_agent')" class="w-[13%]">
</colgroup>
<TableHeader>
<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>
<SortableTableHead
v-if="isAdmin"
v-if="isAdmin && isColumnVisible('user')"
class="h-12 font-semibold w-[12%]"
column-key="user"
:sortable="false"
@@ -343,12 +362,13 @@
</template>
</SortableTableHead>
<TableHead
v-if="!isAdmin"
v-if="!isAdmin && isColumnVisible('key')"
class="h-12 font-semibold w-[17%]"
>
密钥
</TableHead>
<SortableTableHead
v-if="isColumnVisible('model')"
class="h-12 font-semibold"
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
column-key="model"
@@ -368,7 +388,7 @@
</template>
</SortableTableHead>
<SortableTableHead
v-if="isAdmin"
v-if="isAdmin && isColumnVisible('provider')"
class="h-12 font-semibold w-[16%]"
column-key="provider"
:sortable="false"
@@ -387,6 +407,7 @@
</template>
</SortableTableHead>
<SortableTableHead
v-if="isColumnVisible('api_format')"
class="h-12 font-semibold"
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
column-key="api_format"
@@ -406,6 +427,7 @@
</template>
</SortableTableHead>
<SortableTableHead
v-if="isColumnVisible('status')"
class="h-12 font-semibold w-[10%] text-center"
column-key="status"
:sortable="false"
@@ -424,24 +446,49 @@
/>
</template>
</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
</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 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">
<span class="whitespace-nowrap">首字/总耗时</span>
<span class="text-muted-foreground font-normal">输出速度</span>
</div>
</TableHead>
<SortableTableHead
v-if="isColumnVisible('client_family')"
class="h-12 font-semibold w-[12%]"
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>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="records.length === 0">
<TableCell
:colspan="isAdmin ? 9 : 8"
:colspan="visibleColumnCount"
class="text-center py-12 text-muted-foreground"
>
暂无请求记录
@@ -455,7 +502,7 @@
@mousedown="handleRowMouseDown($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">
<span class="text-xs text-foreground tabular-nums whitespace-nowrap">
{{ formatRecordTime(record.created_at) }}
@@ -466,7 +513,7 @@
</div>
</TableCell>
<TableCell
v-if="isAdmin"
v-if="isAdmin && isColumnVisible('user')"
class="py-4 w-[12%] truncate"
:title="record.username || record.user_email || (record.user_id ? `User ${record.user_id}` : '已删除用户')"
>
@@ -485,7 +532,7 @@
</TableCell>
<!-- 用户页面的密钥列 -->
<TableCell
v-if="!isAdmin"
v-if="!isAdmin && isColumnVisible('key')"
class="py-4 w-[17%]"
:title="record.api_key?.name || '-'"
>
@@ -500,6 +547,7 @@
</div>
</TableCell>
<TableCell
v-if="isColumnVisible('model')"
class="font-medium py-4"
:class="[isAdmin ? 'w-[14%]' : 'w-[22%]']"
:title="getModelTooltip(record)"
@@ -531,7 +579,7 @@
>{{ record.model }}</span>
</TableCell>
<TableCell
v-if="isAdmin"
v-if="isAdmin && isColumnVisible('provider')"
class="py-4 w-[16%]"
>
<div class="flex min-w-0 items-center gap-1">
@@ -588,6 +636,7 @@
</div>
</TableCell>
<TableCell
v-if="isColumnVisible('api_format')"
class="py-4"
:class="[isAdmin ? 'w-[15%]' : 'w-[14%]']"
:title="getApiFormatTooltip(record)"
@@ -624,7 +673,7 @@
class="text-muted-foreground text-xs"
>-</span>
</TableCell>
<TableCell class="text-center py-4 w-[10%]">
<TableCell v-if="isColumnVisible('status')" class="text-center py-4 w-[10%]">
<!-- 优先显示请求状态 -->
<Badge
v-if="getDisplayStatus(record) === 'pending'"
@@ -675,7 +724,7 @@
{{ getStreamModeLabel(record) }}
</Badge>
</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">
<span class="justify-self-end whitespace-nowrap text-right">
{{ formatTokens(getRecordEffectiveInputTokens(record)) }}
@@ -709,7 +758,7 @@
</span>
</div>
</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">
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
<span
@@ -720,7 +769,7 @@
</span>
</div>
</TableCell>
<TableCell class="text-right py-4 w-[9%]">
<TableCell v-if="isColumnVisible('performance')" class="text-right py-4 w-[9%]">
<!-- pending/streaming 状态首字与动态总耗时保留在同一行 -->
<div
v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'"
@@ -753,6 +802,32 @@
class="text-muted-foreground"
>-</span>
</TableCell>
<TableCell
v-if="isColumnVisible('client_family')"
class="py-4 w-[12%] text-xs"
:title="formatClientFamily(record.client_family)"
>
<Badge
variant="outline"
class="w-fit max-w-full border-border/60 text-muted-foreground"
>
<span class="truncate">{{ formatClientFamily(record.client_family) }}</span>
</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>
</TableRow>
</TableBody>
</Table>
@@ -775,7 +850,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useDebounceFn, useLocalStorage } from '@vueuse/core'
import {
TableCard,
Badge,
@@ -815,7 +890,8 @@ import {
import { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
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 ServerUserSelector from './ServerUserSelector.vue'
@@ -831,6 +907,67 @@ interface FilterOption {
disabled?: boolean
}
type UsageRecordColumnId =
| 'time'
| 'user'
| 'key'
| 'model'
| 'provider'
| 'api_format'
| 'status'
| 'tokens'
| 'cost'
| 'performance'
| 'client_family'
| 'client_ip'
| 'user_agent'
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' },
]
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<{
records: UsageRecord[]
isAdmin: boolean
@@ -845,9 +982,11 @@ const props = defineProps<{
filterProvider: string
filterApiFormat: string
filterStatus: string
filterClientFamily: string
availableUsers: UserOption[]
availableModels: string[]
availableProviders: string[]
availableClientFamilies: string[]
// 分页
currentPage: number
pageSize: number
@@ -865,6 +1004,7 @@ const emit = defineEmits<{
'update:filterProvider': [value: string]
'update:filterApiFormat': [value: string]
'update:filterStatus': [value: string]
'update:filterClientFamily': [value: string]
'update:currentPage': [value: number]
'update:pageSize': [value: number]
'update:autoRefresh': [value: boolean]
@@ -888,6 +1028,74 @@ const 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'
)).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[]>(() => [
{ value: '__all__', label: '全部模型' },
...props.availableModels.map((model) => ({
@@ -904,6 +1112,34 @@ 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 === 'codex_vscode') return 'Codex VS Code'
if (normalized === 'claude_code') return 'Claude Code'
if (normalized === 'opencode') return 'OpenCode'
if (normalized === 'gemini_cli') return 'Gemini CLI'
if (normalized === 'openai_js_sdk') return 'OpenAI JS SDK'
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[]>(() => [
{ value: '__all__', label: '全部格式' },
...availableApiFormats.map((format) => ({
@@ -1055,6 +1291,12 @@ function formatOutputRateTokensPerSecond(outputRate: number | null | undefined):
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
}
// useDebounceFn 自动处理清理,无需 onUnmounted
// 判断是否应该显示格式转换信息

View File

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

View File

@@ -30,6 +30,7 @@ export interface FilterParams {
provider?: string
api_format?: string
status?: string
client_family?: string
}
function isUsageProviderVisible(provider: string | undefined | null): provider is string {
@@ -371,6 +372,9 @@ export function useUsageData(options: UseUsageDataOptions) {
if (filters?.status) {
params.status = filters.status
}
if (filters?.client_family) {
params.client_family = filters.client_family
}
const response = await usageApi.getAllUsageRecords(params)
if (requestId !== loadRecordsRequestId) {

View File

@@ -106,12 +106,17 @@ export interface UsageRecord {
total_tokens: number
cost: number
actual_cost?: number
response_time_ms?: number
first_byte_time_ms?: number // 首字时间 (TTFB)
response_time_ms?: number | null
first_byte_time_ms?: number | null // 首字时间 (TTFB)
is_stream: boolean
upstream_is_stream?: boolean
client_requested_stream?: boolean
client_is_stream?: boolean
client_family?: string | null
client_ip?: string | null
user_agent?: string | null
request_path?: string | null
request_path_and_query?: string | null
status_code?: number
error_message?: string
status?: RequestStatus // 请求状态: pending, streaming, completed, failed
@@ -142,7 +147,8 @@ export type FilterStatusValue =
'active' |
'failed' |
'cancelled' |
'has_fallback'
'has_fallback' |
'has_retry'
// 默认统计状态
export function createDefaultStats(): UsageStatsState {

View File

@@ -272,6 +272,8 @@ function formatClientFamily(family?: string | null): string {
return 'OpenCode'
case 'claude_code':
return 'Claude Code'
case 'openai_js_sdk':
return 'OpenAI JS SDK'
case 'generic':
return '通用'
case undefined:

View File

@@ -90,9 +90,11 @@
:filter-provider="filterProvider"
:filter-api-format="filterApiFormat"
:filter-status="filterStatus"
:filter-client-family="filterClientFamily"
:available-users="availableUsers"
:available-models="availableModels"
:available-providers="availableProviders"
:available-client-families="availableClientFamilies"
:current-page="currentPage"
:page-size="pageSize"
:total-records="effectiveTotalRecords"
@@ -105,6 +107,7 @@
@update:filter-provider="handleFilterProviderChange"
@update:filter-api-format="handleFilterApiFormatChange"
@update:filter-status="handleFilterStatusChange"
@update:filter-client-family="handleFilterClientFamilyChange"
@update:current-page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:auto-refresh="handleAutoRefreshChange"
@@ -226,6 +229,7 @@ const filterModel = ref('__all__')
const filterProvider = ref('__all__')
const filterApiFormat = ref('__all__')
const filterStatus = ref<FilterStatusValue>('__all__')
const filterClientFamily = ref('__all__')
// 用户列表(仅管理员页面使用)
const availableUsers = ref<UserOption[]>([])
@@ -372,9 +376,15 @@ const filteredRecords = computed(() => {
records = records.filter(record => record.status === 'cancelled')
} else if (filterStatus.value === 'has_fallback') {
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 currentRecords.value
@@ -704,6 +714,15 @@ const effectiveTotalRecords = computed(() => {
// 显示的记录
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)
@@ -787,7 +806,8 @@ function getCurrentFilters() {
model: filterModel.value !== '__all__' ? filterModel.value : undefined,
provider: filterProvider.value !== '__all__' ? filterProvider.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() {
if (!isPageVisible.value) return