2025-12-22 18:23:14 +08:00

295 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 资源监控组件
*
* 显示系统资源使用情况,包括:
* - CPU 使用率(带历史图表)
* - 内存使用率(带历史图表)
* - 磁盘使用率(带历史图表)
*/
'use client'
import React from 'react'
import { Cpu, HardDrive, MemoryStick, TrendingUp, TrendingDown, Minus } from 'lucide-react'
import Card from '@/components/ui/Card'
import { Title3, Body, Caption1 } from '@/components/ui/Typography'
import ProgressBar from '@/components/ui/ProgressBar'
export interface ResourceData {
current: number // 当前使用百分比
history: number[] // 历史数据点(最近N个)
total?: number // 总量(MB)
used?: number // 已用(MB)
free?: number // 空闲(MB)
}
export interface ResourceMonitorProps {
cpu?: ResourceData
memory?: ResourceData
disk?: ResourceData
cpuCores?: number
className?: string
showHistory?: boolean
historyPoints?: number
}
export default function ResourceMonitor({
cpu,
memory,
disk,
cpuCores,
className = '',
showHistory = true,
historyPoints = 20,
}: ResourceMonitorProps) {
return (
<div className={`space-y-4 ${className}`}>
{/* CPU 监控 */}
{cpu && (
<ResourceCard
icon={<Cpu className="w-5 h-5" />}
title="CPU 使用率"
subtitle={cpuCores ? `${cpuCores} 核心` : undefined}
data={cpu}
showHistory={showHistory}
historyPoints={historyPoints}
unit="%"
warningThreshold={70}
criticalThreshold={90}
/>
)}
{/* 内存监控 */}
{memory && (
<ResourceCard
icon={<MemoryStick className="w-5 h-5" />}
title="内存使用率"
subtitle={memory.total ? `${formatMB(memory.used || 0)} / ${formatMB(memory.total)}` : undefined}
data={memory}
showHistory={showHistory}
historyPoints={historyPoints}
unit="%"
warningThreshold={75}
criticalThreshold={90}
/>
)}
{/* 磁盘监控 */}
{disk && (
<ResourceCard
icon={<HardDrive className="w-5 h-5" />}
title="磁盘使用率"
subtitle={disk.total ? `${formatMB(disk.used || 0)} / ${formatMB(disk.total)}` : undefined}
data={disk}
showHistory={showHistory}
historyPoints={historyPoints}
unit="%"
warningThreshold={80}
criticalThreshold={95}
/>
)}
</div>
)
}
interface ResourceCardProps {
icon: React.ReactNode
title: string
subtitle?: string
data: ResourceData
showHistory: boolean
historyPoints: number
unit: string
warningThreshold: number
criticalThreshold: number
}
function ResourceCard({
icon,
title,
subtitle,
data,
showHistory,
historyPoints,
unit,
warningThreshold,
criticalThreshold,
}: ResourceCardProps) {
const getStatusType = (value: number) => {
if (value >= criticalThreshold) return 'error'
if (value >= warningThreshold) return 'warning'
return 'success'
}
const getTrend = () => {
if (data.history.length < 2) return 'stable'
const recent = data.history.slice(-5)
const avg = recent.reduce((a, b) => a + b, 0) / recent.length
const diff = data.current - avg
if (diff > 5) return 'up'
if (diff < -5) return 'down'
return 'stable'
}
const trend = getTrend()
return (
<Card variant="subtle" className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div style={{ color: 'var(--color-label-secondary)' }}>{icon}</div>
<div>
<Title3>{title}</Title3>
{subtitle && (
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>
{subtitle}
</Caption1>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Body
className="font-semibold"
style={{
color: getStatusType(data.current) === 'error'
? 'var(--color-red)'
: getStatusType(data.current) === 'warning'
? 'var(--color-orange)'
: 'var(--color-green)'
}}
>
{data.current.toFixed(1)}{unit}
</Body>
{trend === 'up' && <TrendingUp className="w-4 h-4" style={{ color: 'var(--color-red)' }} />}
{trend === 'down' && <TrendingDown className="w-4 h-4" style={{ color: 'var(--color-green)' }} />}
{trend === 'stable' && <Minus className="w-4 h-4" style={{ color: 'var(--color-label-tertiary)' }} />}
</div>
</div>
{/* 进度条 */}
<ProgressBar
value={data.current}
type={getStatusType(data.current) as 'success' | 'warning' | 'error'}
animated
className="mb-2"
/>
{/* 历史图表 */}
{showHistory && data.history.length > 0 && (
<div className="mt-3">
<MiniChart
data={data.history.slice(-historyPoints)}
warningThreshold={warningThreshold}
criticalThreshold={criticalThreshold}
/>
</div>
)}
</Card>
)
}
interface MiniChartProps {
data: number[]
warningThreshold: number
criticalThreshold: number
}
function MiniChart({ data, warningThreshold, criticalThreshold }: MiniChartProps) {
const height = 40
const width = 200
const padding = 2
if (data.length === 0) return null
const max = 100
const min = 0
const range = max - min
const points = data.map((value, index) => {
const x = padding + (index / (data.length - 1 || 1)) * (width - padding * 2)
const y = height - padding - ((value - min) / range) * (height - padding * 2)
return `${x},${y}`
}).join(' ')
const getColor = (value: number) => {
if (value >= criticalThreshold) return 'var(--color-red)'
if (value >= warningThreshold) return 'var(--color-orange)'
return 'var(--color-green)'
}
// 创建渐变区域
const areaPoints = `${padding},${height - padding} ${points} ${width - padding},${height - padding}`
return (
<div className="relative">
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
{/* 警告线 */}
<line
x1={padding}
y1={height - padding - (warningThreshold / 100) * (height - padding * 2)}
x2={width - padding}
y2={height - padding - (warningThreshold / 100) * (height - padding * 2)}
stroke="var(--color-orange)"
strokeWidth="1"
strokeDasharray="4,4"
opacity="0.3"
/>
{/* 危险线 */}
<line
x1={padding}
y1={height - padding - (criticalThreshold / 100) * (height - padding * 2)}
x2={width - padding}
y2={height - padding - (criticalThreshold / 100) * (height - padding * 2)}
stroke="var(--color-red)"
strokeWidth="1"
strokeDasharray="4,4"
opacity="0.3"
/>
{/* 填充区域 */}
<polygon
points={areaPoints}
fill={getColor(data[data.length - 1])}
opacity="0.1"
/>
{/* 折线 */}
<polyline
points={points}
fill="none"
stroke={getColor(data[data.length - 1])}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* 当前值点 */}
{data.length > 0 && (
<circle
cx={width - padding}
cy={height - padding - ((data[data.length - 1] - min) / range) * (height - padding * 2)}
r="3"
fill={getColor(data[data.length - 1])}
/>
)}
</svg>
<div className="flex justify-between mt-1">
<Caption1 style={{ color: 'var(--color-label-quaternary)' }}>
{data.length > 0 ? `最低 ${Math.min(...data).toFixed(0)}%` : ''}
</Caption1>
<Caption1 style={{ color: 'var(--color-label-quaternary)' }}>
{data.length > 0 ? `最高 ${Math.max(...data).toFixed(0)}%` : ''}
</Caption1>
</div>
</div>
)
}
function formatMB(mb: number): string {
if (mb >= 1024) {
return `${(mb / 1024).toFixed(1)} GB`
}
return `${mb.toFixed(0)} MB`
}