From eb20561893554dade039379f64a0ada8bf47142f Mon Sep 17 00:00:00 2001 From: Zyy Date: Fri, 21 Aug 2026 13:58:02 +0800 Subject: [PATCH] fix(admin): align xAI quota usage displays Co-authored-by: Codex --- .../components/AccountQuotaPanelEntry.vue | 121 ++++++++++++------ .../components/AccountQuotaSummaryEntry.vue | 89 ++++++++----- .../AccountRequestTimeline.vue | 35 +++++ .../components/AccountUsageWindow/index.vue | 24 +--- .../AccountUsageWindow/presenter.ts | 25 +++- 5 files changed, 209 insertions(+), 85 deletions(-) create mode 100644 frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue diff --git a/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue b/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue index 4818f9ac0..4368cd381 100644 --- a/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue +++ b/frontend/src/views/accounts/components/AccountQuotaPanelEntry.vue @@ -2,9 +2,12 @@ import type { AccountQuotaWindow } from '../constants' import { computed } from 'vue' +import { useUiClock } from '@/composables/useUiClock' +import AccountRequestTimeline from './AccountUsageWindow/AccountRequestTimeline.vue' import { quotaWindowLocalUsageDisplay, quotaWindowPresentation, + resolveAccountUsageWindowPresentation, } from './AccountUsageWindow/presenter' const props = defineProps<{ @@ -12,12 +15,25 @@ const props = defineProps<{ windows: AccountQuotaWindow[] }>() +const now = useUiClock() const grouped = computed(() => props.windows.length > 1 && Boolean(props.label)) const items = computed(() => props.windows.map((window) => { const presentation = quotaWindowPresentation(window, '4px') + const view = resolveAccountUsageWindowPresentation({ + window, + variant: 'detail', + showLocalValue: true, + now: now.value.getTime(), + }) return { key: window.key, + local: view.mode === 'local', + localLabel: view.local.label, + requestDisplay: view.local.requestDisplay, + requestBars: view.local.requestBars, + timelineTitle: view.local.timelineTitle, + durationDisplay: view.local.durationDisplay, label: grouped.value ? window.windowLabelDisplay : window.labelDisplay, @@ -48,49 +64,80 @@ const items = computed(() => props.windows.map((window) => { :class="grouped ? 'grid-cols-2' : 'grid-cols-1'" >
-
- - {{ item.label }} - - + +
diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue b/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue index b16be2150..c9177e4ec 100644 --- a/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue +++ b/frontend/src/views/accounts/components/AccountQuotaSummaryEntry.vue @@ -6,6 +6,7 @@ import { computed } from 'vue' import BasePopover from '@/components/base/BasePopover.vue' import { useUiClock } from '@/composables/useUiClock' import AccountQuotaWindowGroup from './AccountQuotaWindowGroup.vue' +import AccountRequestTimeline from './AccountUsageWindow/AccountRequestTimeline.vue' import AccountUsageWindow from './AccountUsageWindow/index.vue' import { resolveAccountUsageWindowPresentation } from './AccountUsageWindow/presenter' @@ -37,6 +38,12 @@ const detailItems = computed(() => props.windows.map((window) => { return { key: window.key, + local: view.mode === 'local', + localLabel: view.local.label, + requestDisplay: view.local.requestDisplay, + requestBars: view.local.requestBars, + timelineTitle: view.local.timelineTitle, + durationDisplay: view.local.durationDisplay, code: quotaWindowCode(window.windowSeconds, window.role), label: window.windowLabelDisplay, usedPercent: window.usedPercent, @@ -141,45 +148,69 @@ function quotaWindowCode( {{ item.code }}

- {{ item.label }} + {{ item.local ? item.localLabel : item.label }}

- {{ item.usedPercentDisplay }} + {{ item.local ? `${item.requestDisplay} 次` : item.usedPercentDisplay }} -
+ +
-
- 窗口消耗 -
-
- {{ item.localUsageDisplay }} -
-
- 重置时间 -
-
- {{ item.resetAtDisplay }} -
+ +
diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue b/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue new file mode 100644 index 000000000..bbc734726 --- /dev/null +++ b/frontend/src/views/accounts/components/AccountUsageWindow/AccountRequestTimeline.vue @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/index.vue b/frontend/src/views/accounts/components/AccountUsageWindow/index.vue index 8b50ec436..b1e5253e9 100644 --- a/frontend/src/views/accounts/components/AccountUsageWindow/index.vue +++ b/frontend/src/views/accounts/components/AccountUsageWindow/index.vue @@ -3,6 +3,7 @@ import type { AccountQuotaWindow } from '../../constants' import type { AccountUsageWindowVariant } from './presenter' import { computed } from 'vue' import { useUiClock } from '@/composables/useUiClock' +import AccountRequestTimeline from './AccountRequestTimeline.vue' import { resolveAccountUsageWindowPresentation } from './presenter' const props = withDefaults( @@ -116,25 +117,12 @@ const view = computed(() => resolveAccountUsageWindowPresentation({ {{ view.local.requestDisplay }} - + />
diff --git a/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts b/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts index afa0f9270..fddfff107 100644 --- a/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts +++ b/frontend/src/views/accounts/components/AccountUsageWindow/presenter.ts @@ -12,6 +12,13 @@ interface AccountRequestBucket { requestCount: number } +export interface AccountRequestBar { + key: string + requestCount: number + height: string + title: string +} + interface AccountLocalUsage { requestCount?: number requestCountDisplay?: string @@ -160,6 +167,7 @@ export function resolveAccountUsageWindowPresentation( requestValueVisible: localRequestValueVisible, timelineTitle: `${localRequestLabel} ${localRequestDisplay} 次`, requestBars: requestTimeline(localUsage?.requestBuckets ?? [], input.now), + durationDisplay: rollingWindowDurationDisplay(input.window?.windowSeconds), }, } } @@ -242,6 +250,21 @@ function requestCountDisplay(localUsage: AccountLocalUsage | null) { return typeof count === 'number' ? formatInteger(count) : '0' } +function rollingWindowDurationDisplay(windowSeconds: number | null | undefined) { + if (typeof windowSeconds !== 'number' || !Number.isFinite(windowSeconds) || windowSeconds <= 0) + return '—' + + const hourSeconds = 60 * 60 + const daySeconds = 24 * hourSeconds + if (windowSeconds <= daySeconds * 2 && windowSeconds % hourSeconds === 0) + return `${formatInteger(windowSeconds / hourSeconds)} 小时` + if (windowSeconds % daySeconds === 0) + return `${formatInteger(windowSeconds / daySeconds)} 天` + if (windowSeconds % hourSeconds === 0) + return `${formatInteger(windowSeconds / hourSeconds)} 小时` + return `${formatInteger(windowSeconds)} 秒` +} + export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth: string) { const percent = clamp(window.usedPercent ?? 0, 0, 100) const tone = quotaWindowTone(window.usedPercent) @@ -268,7 +291,7 @@ function quotaWindowTone(usedPercent: number | null): QuotaWindowTone { function requestTimeline( buckets: NonNullable, now: number, -) { +): AccountRequestBar[] { const currentHour = Math.floor(now / hourMilliseconds) * hourMilliseconds const bucketCounts = new Map( buckets.map((bucket) => {