33 *
44 * @ai -context: 数据来自服务端权威计数(GET /api/v1/license/quota),
55 * 展示今日调用次数与费用双进度条;网关不可达时静默隐藏(增强信息不阻塞)。
6+ * 配额状态由全局 useQuotaStore 持有(与标题栏胶囊共用,30s 去抖拉取),
7+ * 挂载时兜底刷新一次(30s 窗口内不重复请求);开发者白名单账户
8+ * (total_calls=-1)展示 ∞ 不限量。
69 */
10+ import { useEffect } from 'react' ;
711import { Gauge , Coins } from 'lucide-react' ;
8- import { useQuota } from '@/features/beta/hooks/useQuota' ;
12+ import { useQuotaStore } from '@/features/beta/useQuotaStore' ;
13+ import { useAuth } from '@/lib/auth/AuthContext' ;
914import { TIER_LABELS } from '@/types/beta' ;
1015import { cn } from '@/lib/utils' ;
1116
@@ -27,14 +32,29 @@ function UsageBar({ used, total, tone }: { used: number; total: number; tone: 'b
2732
2833/** AI 用量卡(设置页内嵌) */
2934export function AiQuotaCard ( ) {
30- const { quota } = useQuota ( ) ;
35+ const quota = useQuotaStore ( ( s ) => s . quota ) ;
36+ const refresh = useQuotaStore ( ( s ) => s . refresh ) ;
37+ const { getAccessToken } = useAuth ( ) ;
3138
32- // 网关不可达或未登录时不展示(增强信息)
33- if ( ! quota || quota . totalCalls <= 0 ) return null ;
39+ // 挂载时兜底刷新一次(30s 去抖窗口内与标题栏胶囊不重复请求)
40+ useEffect ( ( ) => {
41+ getAccessToken ( )
42+ . then ( ( token ) => refresh ( token ?? undefined ) )
43+ . catch ( ( ) => {
44+ // 未登录静默(本地模式不展示配额)
45+ } ) ;
46+ } , [ getAccessToken , refresh ] ) ;
3447
48+ // 网关不可达或未登录时不展示(增强信息);total_calls=0 视为数据异常;
49+ // -1(开发者无限配额)为合法值,不得在此过滤
50+ if ( ! quota || quota . totalCalls === 0 ) return null ;
51+
52+ // 开发者白名单账户:服务端返回 total_calls=-1(完全豁免限流/费用)
53+ const isUnlimited = quota . totalCalls === - 1 ;
3554 const callPct = quota . totalCalls > 0 ? quota . usedCalls / quota . totalCalls : 0 ;
3655 const costPct = quota . costLimit > 0 ? quota . usedCost / quota . costLimit : 0 ;
37- const callsNearlyDone = callPct >= 0.8 ;
56+ const callsNearlyDone = isUnlimited ? false : callPct >= 0.8 ;
57+ const callsExhausted = ! isUnlimited && quota . usedCalls >= quota . totalCalls ;
3858
3959 return (
4060 < div className = "flex flex-col gap-3 p-3 rounded-kb-md bg-bg-elevated border border-border-default" >
@@ -52,24 +72,28 @@ export function AiQuotaCard() {
5272 < div className = "flex items-center justify-between text-c1" >
5373 < span className = "text-text-tertiary" > AI 调用</ span >
5474 < span className = { cn ( 'font-medium' , callsNearlyDone ? 'text-semantic-warning' : 'text-text-secondary' ) } >
55- { quota . usedCalls } /{ quota . totalCalls } 次
75+ { isUnlimited ? '∞ 次(开发者不限量)' : callsExhausted ? '今日额度已用完' : ` ${ quota . usedCalls } /$ {quota . totalCalls } 次` }
5676 </ span >
5777 </ div >
58- < UsageBar used = { quota . usedCalls } total = { quota . totalCalls } tone = { callsNearlyDone ? 'warning' : 'brand' } />
78+ { isUnlimited ? (
79+ < p className = "text-c1 text-text-tertiary" > 开发者白名单账户,完全豁免配额限制</ p >
80+ ) : (
81+ < UsageBar used = { quota . usedCalls } total = { quota . totalCalls } tone = { callsNearlyDone ? 'warning' : 'brand' } />
82+ ) }
5983 </ div >
6084
61- { /* 费用 */ }
85+ { /* 费用(开发者豁免时同样展示 ∞) */ }
6286 < div className = "space-y-1" >
6387 < div className = "flex items-center justify-between text-c1" >
6488 < span className = "inline-flex items-center gap-1 text-text-tertiary" >
6589 < Coins className = "w-3 h-3" strokeWidth = { 1.5 } />
6690 今日费用
6791 </ span >
6892 < span className = "font-medium text-text-secondary" >
69- ¥ { quota . usedCost . toFixed ( 2 ) } /¥{ quota . costLimit . toFixed ( 1 ) }
93+ { isUnlimited ? '∞(开发者豁免)' : `¥ $ {quota . usedCost . toFixed ( 2 ) } /¥$ {quota . costLimit . toFixed ( 1 ) } ` }
7094 </ span >
7195 </ div >
72- < UsageBar used = { quota . usedCost } total = { quota . costLimit } tone = { costPct >= 0.8 ? 'warning' : 'brand' } />
96+ { ! isUnlimited && < UsageBar used = { quota . usedCost } total = { quota . costLimit } tone = { costPct >= 0.8 ? 'warning' : 'brand' } /> }
7397 </ div >
7498
7599 { /* 到期时间(订阅用户) */ }
0 commit comments