Files
CLICD/frontend/src/components/RingStats.tsx
T
2026-06-07 13:15:59 +08:00

138 lines
3.9 KiB
TypeScript

import type { ReactNode } from 'react'
import { useTheme } from '../contexts/ThemeContext'
interface RingStatProps {
value: number
max?: number
label: string
subLabel?: ReactNode
size?: number
strokeWidth?: number
}
export function RingStat({ value, max = 100, label, subLabel, size = 120, strokeWidth = 8 }: RingStatProps) {
const { theme } = useTheme()
const isDark = theme === 'dark'
const radius = (size - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const percentage = max === Infinity ? Math.max(value, 0) : Math.min(Math.max(value / max * 100, 0), 100)
const strokeDashoffset = circumference - (Math.min(percentage, 100) / 100) * circumference
const bgStroke = isDark ? '#374151' : '#f3f4f6'
const progressStroke = isDark ? '#f9fafb' : '#000000'
return (
<div className="flex flex-col items-center">
<div className="relative" style={{ width: size, height: size }}>
<svg width={size} height={size} className="transform -rotate-90">
{/* Background ring */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={bgStroke}
strokeWidth={strokeWidth}
/>
{/* Progress ring */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={progressStroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
style={{ transition: 'stroke-dashoffset 0.5s ease' }}
/>
</svg>
{/* Center value */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-bold text-black dark:text-white">{percentage.toFixed(percentage < 1 ? 2 : 1)}%</span>
</div>
</div>
<div className="mt-2 text-center">
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{label}</div>
{subLabel && <div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">{subLabel}</div>}
</div>
</div>
)
}
interface RingStatsProps {
cpuPercent: number
cpuCores: number
cpuUsed: number
ramPercent: number
ramUsed: number
ramTotal: number
swapPercent?: number
swapUsed?: number
swapTotal?: number
loadPercent: number
diskPercent: number
diskUsed: number
diskTotal: number
}
export default function RingStats({
cpuPercent,
cpuCores,
cpuUsed,
ramPercent,
ramUsed,
ramTotal,
swapPercent = 0,
swapUsed = 0,
swapTotal = 0,
loadPercent,
diskPercent,
diskUsed,
diskTotal,
}: RingStatsProps) {
const formatGB = (mb: number) => {
if (mb >= 1024) return `${(mb / 1024).toFixed(2)} GB`
return `${mb} MB`
}
const hasSwap = swapTotal > 0
return (
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-5">
<h2 className="text-sm font-semibold text-black dark:text-white mb-4">状态</h2>
<div className={`grid ${hasSwap ? 'grid-cols-5' : 'grid-cols-4'} gap-3`}>
<RingStat
value={cpuPercent}
label="CPU"
subLabel={`(${cpuUsed.toFixed(1)} / ${cpuCores} 核)`}
/>
<RingStat
value={ramPercent}
label="内存"
subLabel={`${formatGB(ramUsed)} / ${formatGB(ramTotal)}`}
/>
{hasSwap && (
<RingStat
value={swapPercent}
label="SWAP"
subLabel={`${formatGB(swapUsed)} / ${formatGB(swapTotal)}`}
/>
)}
<RingStat
value={loadPercent}
max={Infinity}
label="负载"
/>
<RingStat
value={diskPercent}
label="/"
subLabel={`${formatGB(diskUsed)} / ${formatGB(diskTotal)}`}
/>
</div>
</div>
)
}