From 0f0b464f4e265ffc8016b2bb02c786c2960f558f Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 3 Sep 2026 12:15:52 +0800 Subject: [PATCH] fix(frontend): refine account usage presentation Select quota summaries by the most recently used model, streamline dashboard account identities, and align diagnostic client metadata with successful records. Co-authored-by: Codex --- frontend/src/components/ProviderIconGroup.vue | 7 +- .../AccountQuotaSummaryCell/index.vue | 42 ++++------ .../AccountQuotaSummaryCell/presenter.ts | 74 ++++++++++++++++++ .../components/AccountOverviewCard.vue | 23 ++---- .../components/DashboardAccountIdentity.vue | 78 +++++++++++++++++++ .../views/usage/components/OpsErrorPanel.vue | 10 +++ .../usage/components/UsageClientIpCell.vue | 4 +- frontend/src/views/usage/constants.ts | 4 +- frontend/src/views/usage/utils/records.ts | 4 +- 9 files changed, 193 insertions(+), 53 deletions(-) create mode 100644 frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts create mode 100644 frontend/src/views/dashboard/components/DashboardAccountIdentity.vue diff --git a/frontend/src/components/ProviderIconGroup.vue b/frontend/src/components/ProviderIconGroup.vue index b34d2940c..9d84bc0bd 100644 --- a/frontend/src/components/ProviderIconGroup.vue +++ b/frontend/src/components/ProviderIconGroup.vue @@ -1,5 +1,5 @@ @@ -60,7 +62,8 @@ const authenticationLabel = computed(() => { :class="iconContainerClass" :title="authenticationLabel" > - + + ? diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue index beb67419d..be144720f 100644 --- a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue +++ b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/index.vue @@ -6,6 +6,7 @@ import { groupedAccountQuotaWindows, visibleSummaryQuotaWindows } from '../../co import AccountUsageWindow from '../AccountUsageWindow/index.vue' import { quotaWindowPresentation } from '../AccountUsageWindow/presenter' import AccountQuotaSummaryEntry from './Entry.vue' +import { recentlyUsedQuotaEntry, representativeQuotaWindow } from './presenter' const props = defineProps<{ account: AccountRow @@ -15,26 +16,15 @@ const quotaWindows = computed(() => props.account.quota.windows) const visibleQuotaWindows = computed(() => visibleSummaryQuotaWindows(quotaWindows.value)) const summaryEntries = computed(() => groupedAccountQuotaWindows(visibleQuotaWindows.value)) const hasUsage = computed(() => (props.account.usage.requestCount ?? 0) > 0) -const highestUsageWindow = computed(() => visibleQuotaWindows.value.reduce((highest, window) => { - if (typeof window.usedPercent !== 'number') - return highest - if (!highest || typeof highest.usedPercent !== 'number' || window.usedPercent > highest.usedPercent) - return window - return highest -}, undefined as (typeof visibleQuotaWindows.value)[number] | undefined)) -const highestUsageDisplay = computed(() => highestUsageWindow.value?.usedPercentDisplay ?? '—') -const highestUsageTextClass = computed(() => highestUsageWindow.value - ? quotaWindowPresentation(highestUsageWindow.value, '2px').percentTextClass +const recentUsageEntry = computed(() => recentlyUsedQuotaEntry( + summaryEntries.value, + props.account.usage.models, +)) +const currentUsageWindow = computed(() => representativeQuotaWindow(recentUsageEntry.value)) +const currentUsageDisplay = computed(() => currentUsageWindow.value?.usedPercentDisplay ?? '—') +const currentUsageTextClass = computed(() => currentUsageWindow.value + ? quotaWindowPresentation(currentUsageWindow.value, '2px').percentTextClass : 'text-cp-text-quaternary') -const highestUsageEntry = computed(() => { - const highestWindow = highestUsageWindow.value - if (!highestWindow) - return summaryEntries.value[0] - - return summaryEntries.value.find(entry => - entry.windows.some(window => window.key === highestWindow.key), - ) ?? summaryEntries.value[0] -}) const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length - 1, 0)) @@ -58,20 +48,20 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length - 最高占用 - - {{ highestUsageDisplay }} + 消耗比例 + + {{ currentUsageDisplay }} -
+
diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts new file mode 100644 index 000000000..e8a516eff --- /dev/null +++ b/frontend/src/views/accounts/components/AccountQuotaSummaryCell/presenter.ts @@ -0,0 +1,74 @@ +import type { AccountQuotaWindow, AccountQuotaWindowEntry, AccountRow } from '../../constants' + +type AccountModelUsage = AccountRow['usage']['models'][number] + +/** + * 根据账号最近一次模型请求选择对应额度组。 + * 没有专属额度的模型统一回退到 Codex 通用额度。 + */ +export function recentlyUsedQuotaEntry( + entries: readonly AccountQuotaWindowEntry[], + models: readonly AccountModelUsage[], +) { + const recentModel = latestModelUsage(models) + if (recentModel) { + const modelKey = quotaIdentity(recentModel.model) + const modelEntry = entries.find(entry => quotaEntryIdentities(entry).has(modelKey)) + if (modelEntry) + return modelEntry + } + + return entries.find(entry => entry.windows.some(window => window.limitId === 'codex')) + ?? entries[0] +} + +/** 一个额度组可能包含多个滚动窗口,摘要使用其中当前占用最高的窗口。 */ +export function representativeQuotaWindow(entry: AccountQuotaWindowEntry | undefined) { + return entry?.windows.reduce((representative, window) => { + if (typeof window.usedPercent !== 'number') + return representative + if ( + !representative + || typeof representative.usedPercent !== 'number' + || window.usedPercent > representative.usedPercent + ) { + return window + } + return representative + }, undefined) +} + +function latestModelUsage(models: readonly AccountModelUsage[]) { + let latest: AccountModelUsage | undefined + let latestTimestamp = Number.NEGATIVE_INFINITY + + for (const model of models) { + const timestamp = Date.parse(model.lastUsedAt) + if (!Number.isFinite(timestamp) || timestamp <= latestTimestamp) + continue + latest = model + latestTimestamp = timestamp + } + + return latest +} + +function quotaEntryIdentities(entry: AccountQuotaWindowEntry) { + const identities = new Set() + addQuotaIdentity(identities, entry.label) + for (const window of entry.windows) { + addQuotaIdentity(identities, window.limitId) + addQuotaIdentity(identities, window.limitName) + } + return identities +} + +function addQuotaIdentity(identities: Set, value: string | null) { + const identity = quotaIdentity(value) + if (identity) + identities.add(identity) +} + +function quotaIdentity(value: string | null) { + return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, '') ?? '' +} diff --git a/frontend/src/views/dashboard/components/AccountOverviewCard.vue b/frontend/src/views/dashboard/components/AccountOverviewCard.vue index c948d65eb..63475b29a 100644 --- a/frontend/src/views/dashboard/components/AccountOverviewCard.vue +++ b/frontend/src/views/dashboard/components/AccountOverviewCard.vue @@ -5,11 +5,10 @@ import { CircleCheck, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue' import { computed } from 'vue' import BaseCard from '@/components/base/BaseCard.vue' import BaseEmpty from '@/components/base/BaseEmpty.vue' -import ProviderIconGroup from '@/components/ProviderIconGroup.vue' import { formatCompactNumber } from '@/utils/number' -import AccountIdentityCell from '@/views/accounts/components/AccountIdentityCell.vue' import AccountUsageWindow from '@/views/accounts/components/AccountUsageWindow/index.vue' import { metricToneIconClasses, metricToneValueClasses } from '../constants' +import DashboardAccountIdentity from './DashboardAccountIdentity.vue' type DashboardSnapshot = ReturnType @@ -233,24 +232,12 @@ const statusBars = computed(() => {
- - - + class="min-w-0 xl:pr-4" + /> +import type { dashboardSnapshotView } from '../composables/useDashboard' +import { Key, LinkAlt, Openai, Xai } from '@boxicons/vue' +import { computed } from 'vue' + +import { formatProviderLabel } from '@/utils/providers' +import AccountPlanBadge from '@/views/accounts/components/AccountPlanBadge.vue' +import { stablePresetVisualToneClass } from '@/views/accounts/utils/visualTone' + +type DashboardSnapshot = ReturnType +type DashboardAccount = DashboardSnapshot['accountUsage'][number] + +const props = defineProps<{ + account: DashboardAccount +}>() + +const email = computed(() => props.account.email?.trim() || String(props.account.id)) +const displayTitle = computed(() => email.value.split('@')[0] || email.value) +const normalizedProvider = computed(() => props.account.provider?.trim().toLowerCase() || '') +const normalizedAuthenticationKind = computed(() => + props.account.authenticationKind?.trim().toLowerCase() || '', +) +const providerLabel = computed(() => formatProviderLabel(props.account.provider, '未知平台')) + +const authenticationLabel = computed(() => { + if (normalizedAuthenticationKind.value === 'oauth') + return 'OAuth' + if (normalizedAuthenticationKind.value === 'api_key') + return 'API Key' + return props.account.authenticationKind?.trim() || '未知认证类型' +}) + +const avatarToneClass = computed(() => + stablePresetVisualToneClass(props.account.id || props.account.email || email.value), +) + + + diff --git a/frontend/src/views/usage/components/OpsErrorPanel.vue b/frontend/src/views/usage/components/OpsErrorPanel.vue index 7d58611d7..1be6c86c8 100644 --- a/frontend/src/views/usage/components/OpsErrorPanel.vue +++ b/frontend/src/views/usage/components/OpsErrorPanel.vue @@ -12,7 +12,9 @@ import ProviderIconGroup from '@/components/ProviderIconGroup.vue' import { useOpsErrorsTable } from '../composables/useOpsErrorsTable' import { opsErrorColumns } from '../constants' import { opsErrorSummary } from '../utils/opsErrorPresentation' +import { usageUserAgent } from '../utils/records' import OpsErrorDetailModal from './OpsErrorDetailModal.vue' +import UsageClientIpCell from './UsageClientIpCell.vue' const props = defineProps<{ timeRangeParams: UsageTimeRangeParams @@ -157,6 +159,14 @@ function upstreamSendStateText(value: string | null | undefined) { {{ modelText(row) }} + +