refactor: 优化代码结构,添加中文注释,完善 README 文档
- Hooks/组件添加 useMemo 优化,减少不必要的重计算 - 简化 TokenUsageDashboard 的 Suspense 嵌套层级 - 完善 README: 技术栈、构建产物位置、架构说明
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
/**
|
||||
* TokenUsageDashboard 组件
|
||||
*
|
||||
* 主仪表盘,展示 4 个账户的 claude-opus-4-5-thinking 配额使用情况
|
||||
* 使用 OpenBridge TopBar + AlertTopbarElement
|
||||
* 主仪表盘,展示多个账户的 claude-opus-4-5-thinking 配额使用情况。
|
||||
* 使用 OpenBridge 设计系统的 TopBar 和 Alert 组件。
|
||||
*
|
||||
* 特性:
|
||||
* - 多账户配额可视化 (最多显示 4 个)
|
||||
* - 实时告警通知 (低于 20% 警告,低于 5% 紧急)
|
||||
* - 支持日间/夜间主题切换
|
||||
* - OpenBridge 组件懒加载以避免 SSR 问题
|
||||
*/
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day.js'
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-night.js'
|
||||
import { AlertType } from '@oicl/openbridge-webcomponents/dist/types'
|
||||
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
|
||||
import { HealthRing } from '@/components/HealthRing'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { type ObcTheme, useTheme } from '@/hooks/useTheme'
|
||||
import type { ModelUsage } from '@/orpc/contracts/usage'
|
||||
|
||||
// 懒加载 OpenBridge 组件以避免 SSR 问题
|
||||
// ============================================================================
|
||||
// 懒加载 OpenBridge 组件(避免 SSR 问题)
|
||||
// ============================================================================
|
||||
|
||||
const ObcTopBar = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/top-bar/top-bar'
|
||||
@@ -49,17 +58,19 @@ const ObcNavigationItem = lazy(() =>
|
||||
).then((mod) => ({ default: mod.ObcNavigationItem })),
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义
|
||||
// ============================================================================
|
||||
|
||||
export interface TokenUsageDashboardProps {
|
||||
/** 从 API 获取的使用量数据 */
|
||||
data: {
|
||||
opusModels: ModelUsage[]
|
||||
fetchedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
/** 告警阈值 */
|
||||
const ALERT_THRESHOLD = 0.2 // 20% 警戒线
|
||||
const CRITICAL_THRESHOLD = 0.05 // 5% 紧急阈值
|
||||
|
||||
/** 告警信息类型 */
|
||||
interface AlertInfo {
|
||||
account: string
|
||||
model: string
|
||||
@@ -68,18 +79,37 @@ interface AlertInfo {
|
||||
type: AlertType
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 常量配置
|
||||
// ============================================================================
|
||||
|
||||
/** 告警阈值配置 */
|
||||
const ALERT_THRESHOLD = 0.2 // 20% - 警告阈值
|
||||
const CRITICAL_THRESHOLD = 0.05 // 5% - 紧急阈值
|
||||
|
||||
/** 最大显示的账户数 */
|
||||
const MAX_DISPLAY_ACCOUNTS = 4
|
||||
|
||||
/** 已知的账户前缀列表 */
|
||||
const KNOWN_PREFIXES = [
|
||||
'antigravity-',
|
||||
'anthropic-',
|
||||
'claude-',
|
||||
'openai-',
|
||||
] as const
|
||||
|
||||
// ============================================================================
|
||||
// 工具函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 从账户名中提取用户名部分
|
||||
* 例如: "antigravity-2220328339_qq" -> "2220328339_qq"
|
||||
*
|
||||
* @param account - 完整账户名 (如 "antigravity-2220328339_qq")
|
||||
* @returns 提取后的用户名 (如 "2220328339_qq")
|
||||
*/
|
||||
const extractUsername = (account: string): string => {
|
||||
// 移除 "antigravity-" 前缀
|
||||
if (account.startsWith('antigravity-')) {
|
||||
return account.slice('antigravity-'.length)
|
||||
}
|
||||
// 移除其他常见前缀
|
||||
const prefixes = ['anthropic-', 'claude-', 'openai-']
|
||||
for (const prefix of prefixes) {
|
||||
for (const prefix of KNOWN_PREFIXES) {
|
||||
if (account.startsWith(prefix)) {
|
||||
return account.slice(prefix.length)
|
||||
}
|
||||
@@ -89,11 +119,17 @@ const extractUsername = (account: string): string => {
|
||||
|
||||
/**
|
||||
* 计算告警列表
|
||||
*
|
||||
* 根据配额剩余比例生成告警列表,并按严重程度排序。
|
||||
*
|
||||
* @param models - 模型使用量列表
|
||||
* @returns 排序后的告警列表 (Alarm 优先)
|
||||
*/
|
||||
const getAlerts = (models: ModelUsage[]): AlertInfo[] => {
|
||||
const computeAlerts = (models: ModelUsage[]): AlertInfo[] => {
|
||||
const alerts: AlertInfo[] = []
|
||||
|
||||
for (const model of models) {
|
||||
// 低于 5% 为紧急告警
|
||||
if (model.remainingFraction < CRITICAL_THRESHOLD) {
|
||||
alerts.push({
|
||||
account: model.account,
|
||||
@@ -102,7 +138,9 @@ const getAlerts = (models: ModelUsage[]): AlertInfo[] => {
|
||||
remainingFraction: model.remainingFraction,
|
||||
type: AlertType.Alarm,
|
||||
})
|
||||
} else if (model.remainingFraction < ALERT_THRESHOLD) {
|
||||
}
|
||||
// 低于 20% 为警告
|
||||
else if (model.remainingFraction < ALERT_THRESHOLD) {
|
||||
alerts.push({
|
||||
account: model.account,
|
||||
model: model.model,
|
||||
@@ -113,16 +151,20 @@ const getAlerts = (models: ModelUsage[]): AlertInfo[] => {
|
||||
}
|
||||
}
|
||||
|
||||
// 按严重程度排序(Alarm 优先)
|
||||
// 按严重程度排序: Alarm > Warning,相同级别按剩余配额升序
|
||||
return alerts.sort((a, b) => {
|
||||
if (a.type === AlertType.Alarm && b.type !== AlertType.Alarm) return -1
|
||||
if (a.type !== AlertType.Alarm && b.type === AlertType.Alarm) return 1
|
||||
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
|
||||
@@ -130,42 +172,62 @@ const getHighestAlertType = (alerts: AlertInfo[]): AlertType => {
|
||||
return AlertType.Caution
|
||||
}
|
||||
|
||||
/**
|
||||
* TopBar 占位符(SSR 时显示)
|
||||
*/
|
||||
// ============================================================================
|
||||
// 子组件
|
||||
// ============================================================================
|
||||
|
||||
/** TopBar 加载占位符 (SSR 时显示) */
|
||||
const TopBarFallback = () => (
|
||||
<div className="h-14 bg-[var(--container-background-color)] border-b border-[var(--divider-color)]" />
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 主组件
|
||||
// ============================================================================
|
||||
|
||||
export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
||||
const { opusModels } = data
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
// UI 状态
|
||||
const [alertMuted, setAlertMuted] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
// 计算告警
|
||||
const alerts = useMemo(() => getAlerts(opusModels), [opusModels])
|
||||
const alertType = getHighestAlertType(alerts)
|
||||
// 计算告警信息 (使用 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)
|
||||
}, [])
|
||||
|
||||
// 处理主题切换
|
||||
/** 关闭侧边菜单 */
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuOpen(false)
|
||||
}, [])
|
||||
|
||||
/** 切换主题并关闭菜单 */
|
||||
const handleThemeChange = useCallback(
|
||||
(newTheme: 'day' | 'night') => {
|
||||
(newTheme: ObcTheme) => {
|
||||
setTheme(newTheme)
|
||||
setMenuOpen(false)
|
||||
},
|
||||
[setTheme],
|
||||
)
|
||||
|
||||
// ========== 渲染 ==========
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-[var(--container-background-color)] text-[var(--on-container-color)]">
|
||||
{/* 顶部导航栏 */}
|
||||
@@ -179,39 +241,33 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
||||
}
|
||||
menuButtonActivated={menuOpen}
|
||||
>
|
||||
{/* 右侧: 告警面板 */}
|
||||
{/* 右侧告警面板 */}
|
||||
<div slot="alerts">
|
||||
<Suspense fallback={null}>
|
||||
<ObcAlertTopbarElement
|
||||
nAlerts={alerts.length}
|
||||
alertType={alertType}
|
||||
alertMuted={alertMuted}
|
||||
showAck={false}
|
||||
onMuteclick={handleMuteClick as unknown as EventListener}
|
||||
>
|
||||
{alerts.length > 0 && alerts[0] && (
|
||||
<Suspense fallback={null}>
|
||||
<ObcNotificationMessageItem time="">
|
||||
<span slot="icon">
|
||||
<Suspense fallback={null}>
|
||||
<ObcAlertIcon
|
||||
name={
|
||||
alerts[0].type === AlertType.Alarm
|
||||
? 'alarm-unack'
|
||||
: 'warning-unack'
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</span>
|
||||
<span slot="message">
|
||||
{extractUsername(alerts[0].account)}: 剩余{' '}
|
||||
{Math.round(alerts[0].remainingFraction * 100)}%
|
||||
</span>
|
||||
</ObcNotificationMessageItem>
|
||||
</Suspense>
|
||||
)}
|
||||
</ObcAlertTopbarElement>
|
||||
</Suspense>
|
||||
<ObcAlertTopbarElement
|
||||
nAlerts={alerts.length}
|
||||
alertType={alertType}
|
||||
alertMuted={alertMuted}
|
||||
showAck={false}
|
||||
onMuteclick={handleMuteClick as unknown as EventListener}
|
||||
>
|
||||
{topAlert && (
|
||||
<ObcNotificationMessageItem time="">
|
||||
<span slot="icon">
|
||||
<ObcAlertIcon
|
||||
name={
|
||||
topAlert.type === AlertType.Alarm
|
||||
? 'alarm-unack'
|
||||
: 'warning-unack'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<span slot="message">
|
||||
{extractUsername(topAlert.account)}: 剩余{' '}
|
||||
{Math.round(topAlert.remainingFraction * 100)}%
|
||||
</span>
|
||||
</ObcNotificationMessageItem>
|
||||
)}
|
||||
</ObcAlertTopbarElement>
|
||||
</div>
|
||||
</ObcTopBar>
|
||||
</Suspense>
|
||||
@@ -223,36 +279,35 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
||||
<Suspense fallback={null}>
|
||||
<ObcNavigationMenu>
|
||||
<div slot="main">
|
||||
<Suspense fallback={null}>
|
||||
<ObcNavigationItem
|
||||
label="白天模式"
|
||||
checked={theme === 'day'}
|
||||
onClick={() => handleThemeChange('day')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-day></obi-palette-day>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<ObcNavigationItem
|
||||
label="夜间模式"
|
||||
checked={theme === 'night'}
|
||||
onClick={() => handleThemeChange('night')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-night></obi-palette-night>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
</Suspense>
|
||||
{/* 白天模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="白天模式"
|
||||
checked={theme === 'day'}
|
||||
onClick={() => handleThemeChange('day')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-day></obi-palette-day>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
|
||||
{/* 夜间模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="夜间模式"
|
||||
checked={theme === 'night'}
|
||||
onClick={() => handleThemeChange('night')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-night></obi-palette-night>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
</div>
|
||||
</ObcNavigationMenu>
|
||||
</Suspense>
|
||||
@@ -265,16 +320,15 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
||||
type="button"
|
||||
aria-label="关闭菜单"
|
||||
className="fixed inset-0 z-30 bg-black/20 cursor-default"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
onKeyDown={(e) => e.key === 'Escape' && setMenuOpen(false)}
|
||||
onClick={closeMenu}
|
||||
onKeyDown={(e) => e.key === 'Escape' && closeMenu()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 主内容区 */}
|
||||
{/* 主内容区 - 配额圆环展示 */}
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-8">
|
||||
{/* 4 个圆环横向排列 */}
|
||||
<div className="flex flex-wrap justify-center gap-10 lg:gap-16">
|
||||
{opusModels.slice(0, 4).map((model) => (
|
||||
{opusModels.slice(0, MAX_DISPLAY_ACCOUNTS).map((model) => (
|
||||
<HealthRing
|
||||
key={`${model.account}-${model.model}`}
|
||||
account={extractUsername(model.account)}
|
||||
|
||||
Reference in New Issue
Block a user