From 269fbcac0e5b78dd2167c8b15267ed3a9fe7efa5 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Tue, 18 Aug 2026 14:50:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Grok=20=E7=94=A8=E9=87=8F=E6=9D=A1?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=9C=AC=E7=AB=99=2024h/7d/30d=20=E8=81=9A?= =?UTF-8?q?=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把已有的本地窗口统计接到 Free 24h 与付费 7d/30d 进度条,避免只显示官方百分比。 --- .../components/account/AccountUsageCell.vue | 16 +- .../__tests__/AccountUsageCell.spec.ts | 151 ++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 8a7820cfee..3291b9b30d 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -365,6 +365,7 @@ label="24h" :title="t('admin.accounts.usageWindow.grokFreeQuota24hHint', { limit: formatCompactNumber(grokFreeTokenBar.limit) })" :utilization="grokFreeTokenBar.utilization" + :window-stats="grokFreeQuotaUsage" :show-now-when-idle="true" color="emerald" /> @@ -378,6 +379,7 @@ label="7d" :utilization="grokWeeklyBillingBar.utilization" :resets-at="grokWeeklyBillingBar.resetsAt" + :window-stats="grokWeeklyBillingBar.windowStats" :show-now-when-idle="true" color="indigo" /> @@ -386,6 +388,7 @@ label="30d" :utilization="grokMonthlyBillingBar.utilization" :resets-at="grokMonthlyBillingBar.resetsAt" + :window-stats="grokMonthlyBillingBar.windowStats" :show-now-when-idle="true" color="indigo" /> @@ -1108,9 +1111,16 @@ const geminiUsageBars = computed(() => { interface GrokQuotaBarInfo { utilization: number resetsAt: string | null + windowStats?: WindowStats | null } const grokBilling = computed(() => usageInfo.value?.grok_billing || null) +const grokLocalUsage7d = computed(() => ( + usageInfo.value?.grok_local_usage_7d || usageInfo.value?.seven_day?.window_stats || null +)) +const grokLocalUsageMonthly = computed(() => ( + usageInfo.value?.grok_local_usage_monthly || usageInfo.value?.thirty_day?.window_stats || null +)) const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => { const billing = grokBilling.value if (billing?.period_type?.toLowerCase() !== 'weekly' || billing.usage_percent == null) { @@ -1118,7 +1128,8 @@ const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => { } return { utilization: Math.min(100, Math.max(0, billing.usage_percent)), - resetsAt: billing.period_end || null + resetsAt: billing.period_end || null, + windowStats: grokLocalUsage7d.value } }) // Monthly used/limit % from billing probe (used_percent or derived from cents). @@ -1142,7 +1153,8 @@ const grokMonthlyBillingBar = computed((): GrokQuotaBarInfo | null => { } return { utilization: Math.min(100, Math.max(0, utilization)), - resetsAt: billing.billing_period_end || billing.period_end || null + resetsAt: billing.billing_period_end || billing.period_end || null, + windowStats: grokLocalUsageMonthly.value } }) const formatGrokMoney = (value?: number | null) => { diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index dc071b6de5..3e048a5346 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -1017,6 +1017,157 @@ describe('AccountUsageCell', () => { expect(wrapper.text()).toContain('24h|100') }) + it('Grok Free 24h bar shows rolling local usage chips', async () => { + getUsage.mockResolvedValue({ + grok_free_token_limit: 1_000_000, + grok_billing: { period_type: 'weekly', usage_percent: null, plan: '' }, + grok_local_usage: { + requests: 2, + tokens: 250_000, + cost: 0, + standard_cost: 0 + }, + grok_local_usage_24h: { + requests: 12, + tokens: 750_000, + cost: 0.12, + standard_cost: 0.12, + user_cost: 0.04 + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4410, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'utilization', 'windowStats'], + template: '
{{ label }}|{{ utilization }}|{{ windowStats?.tokens }}
' + }, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + expect(wrapper.text()).toContain('24h|75|750000') + expect(wrapper.text()).not.toContain('|250000') + expect(wrapper.text()).not.toContain('7d|') + }) + + it('Grok SuperGrok and Heavy bars show period-aligned local 7d and 30d usage', async () => { + getUsage.mockResolvedValue({ + subscription_tier: 'SuperGrok Heavy', + grok_billing: { + period_type: 'weekly', + usage_percent: 37, + used_percent: 12, + monthly_limit_cents: 150_000, + period_end: '2026-07-16T03:25:00Z', + billing_period_end: '2026-08-01T00:00:00Z', + plan: 'SuperGrok Heavy' + }, + grok_local_usage: { + requests: 1, + tokens: 99, + cost: 0, + standard_cost: 0 + }, + grok_local_usage_24h: { + requests: 2, + tokens: 100, + cost: 0, + standard_cost: 0 + }, + grok_local_usage_7d: { + requests: 8, + tokens: 2_200_000, + cost: 4.42, + standard_cost: 4.42 + }, + grok_local_usage_monthly: { + requests: 20, + tokens: 8_000_000, + cost: 18.5, + standard_cost: 18.5 + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4411, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'utilization', 'windowStats'], + template: '
{{ label }}|{{ utilization }}|{{ windowStats?.tokens }}
' + }, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + expect(wrapper.text()).toContain('7d|37|2200000') + expect(wrapper.text()).toContain('30d|12|8000000') + expect(wrapper.text()).not.toContain('|99') + expect(wrapper.text()).not.toContain('|100') + expect(wrapper.text()).not.toContain('24h|') + }) + + it('Grok paid bars fall back to official seven_day and thirty_day window_stats', async () => { + getUsage.mockResolvedValue({ + subscription_tier: 'SuperGrok', + grok_billing: { + period_type: 'weekly', + usage_percent: 20, + used_percent: 8, + monthly_limit_cents: 25_000, + plan: 'SuperGrok' + }, + seven_day: { + utilization: 20, + window_stats: { + requests: 6, + tokens: 1_500_000, + cost: 3.1, + standard_cost: 3.1 + } + }, + thirty_day: { + utilization: 8, + window_stats: { + requests: 14, + tokens: 4_400_000, + cost: 9.2, + standard_cost: 9.2 + } + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4412, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'utilization', 'windowStats'], + template: '
{{ label }}|{{ utilization }}|{{ windowStats?.tokens }}
' + }, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + expect(wrapper.text()).toContain('7d|20|1500000') + expect(wrapper.text()).toContain('30d|8|4400000') + }) + it('Key 账号在 today stats loading 时显示骨架屏', async () => { const wrapper = mount(AccountUsageCell, { props: { From fd42d3722d11ae4980bddd718e94105e42b5ce4f Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Tue, 18 Aug 2026 14:55:08 +0800 Subject: [PATCH 2/2] fix: hide Grok prepaid and used/limit when they are empty Skip the prepaid chip without a positive balance, and skip used/limit when monthly limit is 0. --- .../components/account/AccountUsageCell.vue | 35 +++++--- .../__tests__/AccountUsageCell.spec.ts | 88 +++++++++++++++++++ 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 3291b9b30d..966b0a5e08 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -397,12 +397,16 @@ class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400" > {{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokPrepaidMoneyLine.prepaid }} - + {{ t('admin.accounts.usageWindow.grokUsed') }} {{ grokPrepaidMoneyLine.used }}/{{ grokPrepaidMoneyLine.limit }} @@ -1164,30 +1168,33 @@ const formatGrokMoney = (value?: number | null) => { if (value >= 10) return value.toFixed(1) return value.toFixed(2) } -// Prepaid money line for paid Grok: show when prepaid_balance is present. -// Monthly used/limit numbers are optional context; primary progress is the 30d bar. +// Prepaid chip only when there is a positive prepaid balance. +// Used/limit only when monthly limit is a positive number (0 means unlimited / unset). const grokPrepaidMoneyLine = computed(() => { const billing = grokBilling.value if (!billing) return null const prepaid = billing.prepaid_balance - // "只针对预付": only render when prepaid field exists (including $0.00). - if (prepaid == null || !Number.isFinite(prepaid)) return null + const showPrepaid = prepaid != null && Number.isFinite(prepaid) && prepaid > 0 + const limitRaw = + billing.monthly_limit != null + ? billing.monthly_limit + : billing.monthly_limit_cents != null + ? billing.monthly_limit_cents / 100 + : null + const showUsedLimit = limitRaw != null && Number.isFinite(limitRaw) && limitRaw > 0 + if (!showPrepaid && !showUsedLimit) return null const used = billing.monthly_used != null ? billing.monthly_used : billing.used_cents != null ? billing.used_cents / 100 : 0 - const limit = - billing.monthly_limit != null - ? billing.monthly_limit - : billing.monthly_limit_cents != null - ? billing.monthly_limit_cents / 100 - : 0 return { - prepaid: formatGrokMoney(prepaid), - used: formatGrokMoney(used), - limit: formatGrokMoney(limit) + showPrepaid, + showUsedLimit, + prepaid: showPrepaid ? formatGrokMoney(prepaid) : null, + used: showUsedLimit ? formatGrokMoney(used) : null, + limit: showUsedLimit ? formatGrokMoney(limitRaw) : null } }) const grokPlanLabelIsFree = (value: string) => value.includes('free') || value.includes('basic') diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index 3e048a5346..1ad6866dd7 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -1168,6 +1168,94 @@ describe('AccountUsageCell', () => { expect(wrapper.text()).toContain('30d|8|4400000') }) + it('Grok paid hides zero prepaid and hides used/limit when monthly limit is 0', async () => { + getUsage.mockResolvedValue({ + subscription_tier: 'SuperGrok', + grok_billing: { + period_type: 'weekly', + usage_percent: 20, + prepaid_balance: 0, + monthly_limit: 0, + monthly_used: 3.5, + plan: 'SuperGrok' + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4413, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: true, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokPrepaid') + expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokUsed') + expect(wrapper.text()).not.toContain('3.5/0') + }) + + it('Grok paid shows used/limit without prepaid, and prepaid without a zero monthly limit', async () => { + getUsage.mockResolvedValueOnce({ + subscription_tier: 'SuperGrok', + grok_billing: { + period_type: 'weekly', + usage_percent: 20, + monthly_limit: 25, + monthly_used: 3.5, + plan: 'SuperGrok' + } + }) + + const usedOnly = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4414, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: true, + AccountQuotaInfo: true + } + } + }) + await flushPromises() + expect(usedOnly.text()).not.toContain('admin.accounts.usageWindow.grokPrepaid') + expect(usedOnly.text()).toContain('admin.accounts.usageWindow.grokUsed') + expect(usedOnly.text()).toContain('3.50/25.0') + + getUsage.mockResolvedValueOnce({ + subscription_tier: 'SuperGrok Heavy', + grok_billing: { + period_type: 'weekly', + usage_percent: 20, + prepaid_balance: 12.5, + monthly_limit: 0, + monthly_used: 8, + plan: 'SuperGrok Heavy' + } + }) + const prepaidOnly = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 4415, platform: 'grok', type: 'oauth', extra: {} }) + }, + global: { + stubs: { + UsageProgressBar: true, + AccountQuotaInfo: true + } + } + }) + await flushPromises() + expect(prepaidOnly.text()).toContain('admin.accounts.usageWindow.grokPrepaid') + expect(prepaidOnly.text()).toContain('$12.5') + expect(prepaidOnly.text()).not.toContain('admin.accounts.usageWindow.grokUsed') + expect(prepaidOnly.text()).not.toContain('8.00/0') + }) + it('Key 账号在 today stats loading 时显示骨架屏', async () => { const wrapper = mount(AccountUsageCell, { props: {