/** * TokenUsageDashboard 组件 * * 主仪表盘,展示多个账户的 claude-opus-4-5-thinking 配额使用情况。 * 使用自定义组件替代 OpenBridge 组件,基于 shadcn/ui 实现。 * * 特性: * - 多账户配额可视化 (根据 API 返回的账户数量动态显示) * - 实时告警通知 (低于 20% 警告,低于 5% 紧急) * - 支持四种主题切换 (day/bright/dusk/night) */ import { useCallback, useMemo, useState } from 'react' import { HealthRing } from '@/components/HealthRing' import { AlertBadge, AlertNotification, AlertType, } from '@/components/ui/AlertBadge' import { ThemeSidebar } from '@/components/ui/ThemeSidebar' import { TopBar } from '@/components/ui/TopBar' import { useTheme } from '@/hooks/useTheme' import type { ModelUsage } from '@/orpc/contracts/usage' // ============================================================================ // 类型定义 // ============================================================================ export interface TokenUsageDashboardProps { /** 从 API 获取的使用量数据 */ data: { opusModels: ModelUsage[] fetchedAt: string } } /** 告警信息类型 */ interface AlertInfo { account: string model: string displayName?: string remainingFraction: number type: AlertType } // ============================================================================ // 常量配置 // ============================================================================ /** 告警阈值配置 */ const ALERT_THRESHOLD = 0.2 // 20% - 警告阈值 const CRITICAL_THRESHOLD = 0.05 // 5% - 紧急阈值 /** 已知的账户前缀列表 */ const KNOWN_PREFIXES = [ 'antigravity-', 'anthropic-', 'claude-', 'openai-', ] as const // ============================================================================ // 工具函数 // ============================================================================ /** * 从账户名中提取用户名部分 * * @param account - 完整账户名 (如 "antigravity-2220328339_qq") * @returns 提取后的用户名 (如 "2220328339_qq") */ const extractUsername = (account: string): string => { for (const prefix of KNOWN_PREFIXES) { if (account.startsWith(prefix)) { return account.slice(prefix.length) } } return account } /** * 计算告警列表 * * 根据配额剩余比例生成告警列表,并按严重程度排序。 * * @param models - 模型使用量列表 * @returns 排序后的告警列表 (Alarm 优先) */ const computeAlerts = (models: ModelUsage[]): AlertInfo[] => { const alerts: AlertInfo[] = [] for (const model of models) { // 低于 5% 为紧急告警 if (model.remainingFraction < CRITICAL_THRESHOLD) { alerts.push({ account: model.account, model: model.model, displayName: model.displayName, remainingFraction: model.remainingFraction, type: AlertType.Alarm, }) } // 低于 20% 为警告 else if (model.remainingFraction < ALERT_THRESHOLD) { alerts.push({ account: model.account, model: model.model, displayName: model.displayName, remainingFraction: model.remainingFraction, type: AlertType.Warning, }) } } // 按严重程度排序: Alarm > Warning,相同级别按剩余配额升序 return alerts.sort((a, b) => { if (a.type !== b.type) { return a.type === AlertType.Alarm ? -1 : 1 } return a.remainingFraction - b.remainingFraction }) } /** * 获取最高级别的告警类型 * * @param alerts - 告警列表 * @returns 最高级别的告警类型 */ const getHighestAlertType = (alerts: AlertInfo[]): AlertType => { if (alerts.some((a) => a.type === AlertType.Alarm)) return AlertType.Alarm if (alerts.some((a) => a.type === AlertType.Warning)) return AlertType.Warning return AlertType.Caution } // ============================================================================ // 主组件 // ============================================================================ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => { const { opusModels } = data const { theme, setTheme } = useTheme() // UI 状态 const [alertMuted, setAlertMuted] = useState(false) const [menuOpen, setMenuOpen] = useState(false) // 计算告警信息 (使用 useMemo 缓存) const alerts = useMemo(() => computeAlerts(opusModels), [opusModels]) const alertType = useMemo(() => getHighestAlertType(alerts), [alerts]) // 获取最重要的告警 (用于顶栏显示) const topAlert = alerts[0] // ========== 事件处理器 ========== /** 切换告警静音状态 */ const handleMuteClick = useCallback(() => { setAlertMuted((prev) => !prev) }, []) /** 切换菜单开关状态 */ const handleMenuButtonClick = useCallback(() => { setMenuOpen((prev) => !prev) }, []) // ========== 渲染 ========== return (
{/* 顶部导航栏 */} {topAlert && ( )} } /> {/* 主题切换侧边栏 */} {/* 主内容区 - 配额圆环展示 */}
{opusModels.map((model) => ( ))}
) }