- 使用 shadcn/ui 重新实现 TopBar、ThemeSidebar、AlertBadge 组件 - 解决 @oicl/openbridge-webcomponents ESM 模块解析问题 - 添加 OpenBridge 四种主题 CSS 变量 (day/bright/dusk/night) - Night 主题使用暗黄色文字保护夜视能力 - 更新 API 端点适配新的按模型分组数据结构
144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
/**
|
|
* HealthRing 组件
|
|
*
|
|
* Apple 健康风格的圆环进度指示器,用于可视化配额使用情况。
|
|
* 中心显示百分比和倒计时,颜色根据剩余配额自动变化。
|
|
*/
|
|
import { useMemo } from 'react'
|
|
import { useCountdown } from '@/hooks/useCountdown'
|
|
|
|
/** 组件 Props 类型定义 */
|
|
export interface HealthRingProps {
|
|
/** 账户名称 */
|
|
account: string
|
|
/** 模型名称(可选) */
|
|
model?: string
|
|
/** 模型显示名称 */
|
|
displayName?: string
|
|
/** 剩余配额百分比 (0-1) */
|
|
remainingFraction: number
|
|
/** 配额重置时间 (ISO 8601) */
|
|
resetTime?: string
|
|
/** 圆环尺寸 (像素),默认 160 */
|
|
size?: number
|
|
}
|
|
|
|
/**
|
|
* 颜色阈值配置
|
|
* 根据剩余配额百分比决定显示颜色
|
|
*/
|
|
const COLOR_THRESHOLDS = [
|
|
{ threshold: 0.05, color: '#FF3B30', bgColor: 'rgba(255, 59, 48, 0.2)' }, // 红色 - 紧急
|
|
{ threshold: 0.2, color: '#FF9500', bgColor: 'rgba(255, 149, 0, 0.2)' }, // 橙色 - 警告
|
|
{ threshold: 0.5, color: '#FFCC00', bgColor: 'rgba(255, 204, 0, 0.2)' }, // 黄色 - 注意
|
|
] as const
|
|
|
|
/** 默认颜色 (绿色 - 正常状态) */
|
|
const DEFAULT_COLOR = { color: '#34C759', bgColor: 'rgba(52, 199, 89, 0.2)' }
|
|
|
|
/**
|
|
* 根据剩余配额获取对应的颜色配置
|
|
*
|
|
* @param fraction - 剩余配额百分比 (0-1)
|
|
* @returns 前景色和背景色配置
|
|
*/
|
|
const getColorConfig = (
|
|
fraction: number,
|
|
): { color: string; bgColor: string } => {
|
|
for (const { threshold, color, bgColor } of COLOR_THRESHOLDS) {
|
|
if (fraction < threshold) return { color, bgColor }
|
|
}
|
|
return DEFAULT_COLOR
|
|
}
|
|
|
|
export const HealthRing = ({
|
|
account,
|
|
model,
|
|
displayName,
|
|
remainingFraction,
|
|
resetTime,
|
|
size = 160,
|
|
}: HealthRingProps) => {
|
|
const countdown = useCountdown(resetTime)
|
|
|
|
// 使用 useMemo 缓存 SVG 计算值,避免不必要的重新计算
|
|
const svgParams = useMemo(() => {
|
|
const strokeWidth = size * 0.1
|
|
const radius = (size - strokeWidth) / 2
|
|
const circumference = 2 * Math.PI * radius
|
|
const strokeDashoffset = circumference * (1 - remainingFraction)
|
|
|
|
return { strokeWidth, radius, circumference, strokeDashoffset }
|
|
}, [size, remainingFraction])
|
|
|
|
const percentage = Math.round(remainingFraction * 100)
|
|
const { color: ringColor, bgColor: ringBgColor } =
|
|
getColorConfig(remainingFraction)
|
|
const { strokeWidth, radius, circumference, strokeDashoffset } = svgParams
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-3">
|
|
{/* 圆环容器 */}
|
|
<div className="relative" style={{ width: size, height: size }}>
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
viewBox={`0 0 ${size} ${size}`}
|
|
className="transform -rotate-90"
|
|
role="img"
|
|
aria-label={`配额剩余 ${percentage}%`}
|
|
>
|
|
{/* 背景圆环 */}
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
fill="none"
|
|
stroke={ringBgColor}
|
|
strokeWidth={strokeWidth}
|
|
/>
|
|
{/* 进度圆环 */}
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
fill="none"
|
|
stroke={ringColor}
|
|
strokeWidth={strokeWidth}
|
|
strokeLinecap="round"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={strokeDashoffset}
|
|
className="transition-all duration-500 ease-out"
|
|
/>
|
|
</svg>
|
|
|
|
{/* 中心内容 - 百分比和倒计时 */}
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className="text-2xl font-bold" style={{ color: ringColor }}>
|
|
{percentage}%
|
|
</span>
|
|
<span className="text-sm text-[var(--element-inactive-color,#707070)]">
|
|
{countdown.formatted}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 底部标签 */}
|
|
<div className="text-center">
|
|
<div
|
|
className="text-sm font-medium truncate max-w-[140px] text-[var(--element-active-color,#3d3d3d)]"
|
|
title={displayName || model}
|
|
>
|
|
{displayName || 'Claude Opus 4.5'}
|
|
</div>
|
|
<div
|
|
className="text-xs truncate max-w-[140px] text-[var(--element-inactive-color,#707070)]"
|
|
title={account}
|
|
>
|
|
{account}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|