diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 81d97efb9c..5d7af74fc6 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -386,6 +386,7 @@ :label="t('admin.accounts.usageWindow.grokRequests')" :utilization="grokRequestQuotaBar.utilization" :resets-at="grokRequestQuotaBar.resetsAt" + :remaining-capacity="true" color="indigo" />
@@ -1036,9 +1038,9 @@ interface GrokQuotaBarInfo { const makeGrokQuotaBar = (quota?: { limit?: number | null; remaining?: number | null; reset_at?: string | null } | null): GrokQuotaBarInfo | null => { if (!quota || quota.limit == null || quota.remaining == null || quota.limit <= 0) return null - const used = Math.max(0, quota.limit - quota.remaining) + const remaining = Math.min(quota.limit, Math.max(0, quota.remaining)) return { - utilization: (used / quota.limit) * 100, + utilization: (remaining / quota.limit) * 100, resetsAt: quota.reset_at || null } } diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue index 6a69357318..2f8b9a88a7 100644 --- a/frontend/src/components/account/UsageProgressBar.vue +++ b/frontend/src/components/account/UsageProgressBar.vue @@ -69,6 +69,7 @@ const props = defineProps<{ color: 'indigo' | 'emerald' | 'purple' | 'amber' windowStats?: WindowStats | null showNowWhenIdle?: boolean + remainingCapacity?: boolean }>() const { t } = useI18n() @@ -109,6 +110,14 @@ const labelClass = computed(() => { // Progress bar color based on utilization const barClass = computed(() => { + if (props.remainingCapacity) { + if (props.utilization <= 20) { + return 'bg-red-500' + } else if (props.utilization <= 50) { + return 'bg-amber-500' + } + return 'bg-green-500' + } if (props.utilization >= 100) { return 'bg-red-500' } else if (props.utilization >= 80) { @@ -120,6 +129,14 @@ const barClass = computed(() => { // Text color based on utilization const textClass = computed(() => { + if (props.remainingCapacity) { + if (props.utilization <= 20) { + return 'text-red-600 dark:text-red-400' + } else if (props.utilization <= 50) { + return 'text-amber-600 dark:text-amber-400' + } + return 'text-gray-600 dark:text-gray-400' + } if (props.utilization >= 100) { return 'text-red-600 dark:text-red-400' } else if (props.utilization >= 80) { @@ -131,12 +148,16 @@ const textClass = computed(() => { // Bar width (capped at 100%) const barWidth = computed(() => { - return `${Math.min(props.utilization, 100)}%` + return `${Math.min(Math.max(props.utilization, 0), 100)}%` }) // Display percentage (cap at 999% for readability) const displayPercent = computed(() => { - const percent = Math.round(props.utilization) + const percent = Math.round( + props.remainingCapacity + ? Math.min(Math.max(props.utilization, 0), 100) + : props.utilization + ) return percent > 999 ? '>999%' : `${percent}%` }) diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index 2df3cc07e2..2abf6513ca 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -566,7 +566,7 @@ describe('AccountUsageCell', () => { expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true) }) - it('Grok OAuth 会展示本地 user billed 用量并保留超限百分比', async () => { + it('Grok OAuth 会展示本地 user billed 用量并把耗尽配额显示为 0% 剩余', async () => { getUsage.mockResolvedValue({ grok_local_usage: { requests: 4, @@ -611,13 +611,55 @@ describe('AccountUsageCell', () => { expect(wrapper.text()).toContain('1.2K') expect(wrapper.text()).toContain('A $0.12') expect(wrapper.text()).toContain('U $0.34') - expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|120|2026-07-09T16:00:00Z') + expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|0|2026-07-09T16:00:00Z') const badges = wrapper.findAll('span[title]') expect(badges.some(node => node.attributes('title') === 'usage.accountBilled')).toBe(true) expect(badges.some(node => node.attributes('title') === 'usage.userBilled')).toBe(true) }) + it('Grok OAuth 配额条按剩余容量显示 100% 满格和 25% 低量', async () => { + getUsage.mockResolvedValue({ + grok_request_quota: { + limit: 100, + remaining: 100, + reset_at: '2026-07-09T16:00:00Z' + }, + grok_token_quota: { + limit: 1000, + remaining: 250, + reset_at: '2026-07-09T16:00:00Z' + }, + grok_quota_snapshot_state: 'observed' + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ + id: 4073, + platform: 'grok', + type: 'oauth', + extra: {} + }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'utilization', 'resetsAt', 'color', 'remainingCapacity'], + template: '
{{ label }}|{{ utilization }}|{{ remainingCapacity }}
' + }, + AccountQuotaInfo: true, + GrokQuotaProbeCell: true + } + } + }) + + await flushPromises() + + expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokRequests|100|true') + expect(wrapper.text()).toContain('admin.accounts.usageWindow.grokTokens|25|true') + }) + it('Key 账号在 today stats loading 时显示骨架屏', async () => { const wrapper = mount(AccountUsageCell, { props: { diff --git a/frontend/src/components/account/__tests__/UsageProgressBar.spec.ts b/frontend/src/components/account/__tests__/UsageProgressBar.spec.ts index 6fa6575f54..af5fc5d66d 100644 --- a/frontend/src/components/account/__tests__/UsageProgressBar.spec.ts +++ b/frontend/src/components/account/__tests__/UsageProgressBar.spec.ts @@ -96,4 +96,54 @@ describe('UsageProgressBar', () => { expect(wrapper.text()).toContain('usage.resetNow') expect(wrapper.text()).not.toContain('usage.resetPending') }) + + it('剩余容量模式在 100% 时显示满格绿色', () => { + const wrapper = mount(UsageProgressBar, { + props: { + label: 'Req', + utilization: 100, + remainingCapacity: true, + color: 'indigo' + } + }) + + expect(wrapper.text()).toContain('100%') + expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 100%') + expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-green-500') + }) + + it('剩余容量模式在低量和耗尽时缩短并变红', async () => { + const wrapper = mount(UsageProgressBar, { + props: { + label: 'Req', + utilization: 15, + remainingCapacity: true, + color: 'indigo' + } + }) + + expect(wrapper.text()).toContain('15%') + expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 15%') + expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500') + + await wrapper.setProps({ utilization: 0 }) + + expect(wrapper.text()).toContain('0%') + expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 0%') + expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500') + }) + + it('默认利用率模式仍把超限显示为满格红色', () => { + const wrapper = mount(UsageProgressBar, { + props: { + label: '5h', + utilization: 120, + color: 'indigo' + } + }) + + expect(wrapper.text()).toContain('120%') + expect(wrapper.get('.h-1\\.5 > div').attributes('style')).toContain('width: 100%') + expect(wrapper.get('.h-1\\.5 > div').classes()).toContain('bg-red-500') + }) })