🏁 Final commit: Project Token Usage Viewer completed

This commit is contained in:
2026-01-21 14:21:43 +08:00
parent b967deb4b1
commit a77fcdd3dc
24 changed files with 1087 additions and 651 deletions

View File

@@ -0,0 +1,128 @@
/**
* HealthRing 组件
*
* Apple 健康风格的圆环进度指示器
* 中心显示倒计时
*/
import { useCountdown } from '@/hooks/useCountdown'
export interface HealthRingProps {
/** 账户名称 */
account: string
/** 模型名称(可选) */
model?: string
/** 模型显示名称 */
displayName?: string
/** 剩余配额百分比 (0-1) */
remainingFraction: number
/** 配额重置时间 (ISO 8601) */
resetTime?: string
/** 圆环尺寸 */
size?: number
}
/**
* 根据剩余配额获取颜色
*/
const getRingColor = (fraction: number): string => {
if (fraction < 0.05) return '#FF3B30' // 红色 - 紧急
if (fraction < 0.2) return '#FF9500' // 橙色 - 警告
if (fraction < 0.5) return '#FFCC00' // 黄色 - 注意
return '#34C759' // 绿色 - 正常
}
/**
* 根据剩余配额获取背景色(较暗)
*/
const getRingBgColor = (fraction: number): string => {
if (fraction < 0.05) return 'rgba(255, 59, 48, 0.2)'
if (fraction < 0.2) return 'rgba(255, 149, 0, 0.2)'
if (fraction < 0.5) return 'rgba(255, 204, 0, 0.2)'
return 'rgba(52, 199, 89, 0.2)'
}
export const HealthRing = ({
account,
model,
displayName,
remainingFraction,
resetTime,
size = 160,
}: HealthRingProps) => {
const countdown = useCountdown(resetTime)
const percentage = Math.round(remainingFraction * 100)
// SVG 参数
const strokeWidth = size * 0.1
const radius = (size - strokeWidth) / 2
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference * (1 - remainingFraction)
const ringColor = getRingColor(remainingFraction)
const ringBgColor = getRingBgColor(remainingFraction)
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(--on-container-color)] opacity-70">
{countdown.formatted}
</span>
</div>
</div>
{/* 标签 */}
<div className="text-center">
<div
className="text-sm font-medium truncate max-w-[140px] text-[var(--on-container-color)]"
title={displayName || model}
>
{displayName || 'Claude Opus 4.5'}
</div>
<div
className="text-xs truncate max-w-[140px] text-[var(--on-container-color)] opacity-50"
title={account}
>
{account}
</div>
</div>
</div>
)
}