Files
openbridge-token-usage-viewer/src/components/TokenUsageDashboard.tsx
MAO Dongyang d22a0f8d69 feat: 替换 OpenBridge 组件并实现航海规范主题系统
- 使用 shadcn/ui 重新实现 TopBar、ThemeSidebar、AlertBadge 组件
- 解决 @oicl/openbridge-webcomponents ESM 模块解析问题
- 添加 OpenBridge 四种主题 CSS 变量 (day/bright/dusk/night)
- Night 主题使用暗黄色文字保护夜视能力
- 更新 API 端点适配新的按模型分组数据结构
2026-01-26 21:17:56 +08:00

220 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (
<div className="min-h-screen flex flex-col bg-[var(--container-background-color,#f7f7f7)] text-[var(--element-active-color,#3d3d3d)]">
{/* 顶部导航栏 */}
<TopBar
appTitle="Token Usage Viewer"
menuButtonActivated={menuOpen}
onMenuButtonClick={handleMenuButtonClick}
rightSlot={
<>
<AlertBadge
count={alerts.length}
alertType={alertType}
muted={alertMuted}
onMuteClick={handleMuteClick}
/>
{topAlert && (
<AlertNotification
account={extractUsername(topAlert.account)}
remainingPercent={Math.round(topAlert.remainingFraction * 100)}
alertType={topAlert.type}
/>
)}
</>
}
/>
{/* 主题切换侧边栏 */}
<ThemeSidebar
open={menuOpen}
onOpenChange={setMenuOpen}
currentTheme={theme}
onThemeChange={setTheme}
/>
{/* 主内容区 - 配额圆环展示 */}
<main className="flex-1 flex flex-col items-center justify-center p-8">
<div className="flex flex-wrap justify-center gap-10 lg:gap-16">
{opusModels.map((model) => (
<HealthRing
key={`${model.account}-${model.model}`}
account={extractUsername(model.account)}
displayName="Opus 4.5"
remainingFraction={model.remainingFraction}
resetTime={model.resetTime}
size={180}
/>
))}
</div>
</main>
</div>
)
}