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')
+ })
})