🏁 Final commit: Project Token Usage Viewer completed
This commit is contained in:
128
src/components/HealthRing.tsx
Normal file
128
src/components/HealthRing.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user