添加服务状态和资源监控组件

This commit is contained in:
WuYongJie 2025-12-22 18:23:14 +08:00
parent 6c954f6e57
commit 8a88fcde10
3 changed files with 427 additions and 0 deletions

View File

@ -0,0 +1,294 @@
/**
* 资源监控组件
*
* 显示系统资源使用情况,包括:
* - 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`
}

View File

@ -0,0 +1,124 @@
/**
* 服务状态卡片组件
*
* 显示单个服务的状态信息,包括:
* - 服务名称和状态指示器
* - 响应时间显示
* - 错误信息显示
*/
'use client'
import React from 'react'
import { CheckCircle, XCircle, AlertTriangle, Loader2, Clock } from 'lucide-react'
import Card from '@/components/ui/Card'
import { Title3, Body, Caption1 } from '@/components/ui/Typography'
import Badge from '@/components/ui/Badge'
export interface ServiceStatus {
name: string
status: 'healthy' | 'unhealthy' | 'degraded' | 'unknown' | 'loading'
responseTime?: number // 毫秒
lastChecked?: Date
errorMessage?: string
details?: Record<string, string | number | boolean>
}
interface ServiceStatusCardProps {
service: ServiceStatus
className?: string
}
export default function ServiceStatusCard({ service, className = '' }: ServiceStatusCardProps) {
const getStatusIcon = () => {
switch (service.status) {
case 'healthy':
return <CheckCircle className="w-6 h-6" style={{ color: 'var(--color-green)' }} />
case 'unhealthy':
return <XCircle className="w-6 h-6" style={{ color: 'var(--color-red)' }} />
case 'degraded':
return <AlertTriangle className="w-6 h-6" style={{ color: 'var(--color-orange)' }} />
case 'loading':
return <Loader2 className="w-6 h-6 animate-spin" style={{ color: 'var(--color-blue)' }} />
default:
return <AlertTriangle className="w-6 h-6" style={{ color: 'var(--color-label-tertiary)' }} />
}
}
const getStatusBadge = () => {
const statusMap = {
healthy: { type: 'success' as const, label: '正常' },
unhealthy: { type: 'error' as const, label: '异常' },
degraded: { type: 'warning' as const, label: '降级' },
unknown: { type: 'info' as const, label: '未知' },
loading: { type: 'info' as const, label: '检查中' },
}
const config = statusMap[service.status]
return <Badge type={config.type} variant="subtle">{config.label}</Badge>
}
const getResponseTimeColor = () => {
if (!service.responseTime) return 'var(--color-label-tertiary)'
if (service.responseTime < 100) return 'var(--color-green)'
if (service.responseTime < 500) return 'var(--color-orange)'
return 'var(--color-red)'
}
return (
<Card variant="elevated" className={`p-4 ${className}`}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
{getStatusIcon()}
<Title3>{service.name}</Title3>
</div>
{getStatusBadge()}
</div>
{/* 响应时间 */}
{service.responseTime !== undefined && (
<div className="flex items-center gap-2 mb-2">
<Clock className="w-4 h-4" style={{ color: 'var(--color-label-tertiary)' }} />
<Body style={{ color: getResponseTimeColor() }}>
{service.responseTime}ms
</Body>
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>
响应时间
</Caption1>
</div>
)}
{/* 最后检查时间 */}
{service.lastChecked && (
<Caption1 style={{ color: 'var(--color-label-tertiary)' }} className="mb-2">
最后检查: {service.lastChecked.toLocaleTimeString()}
</Caption1>
)}
{/* 错误信息 */}
{service.errorMessage && (
<div
className="mt-3 p-2 rounded-apple-sm"
style={{ backgroundColor: 'var(--color-red-subtle)' }}
>
<Caption1 style={{ color: 'var(--color-red)' }}>
{service.errorMessage}
</Caption1>
</div>
)}
{/* 详细信息 */}
{service.details && Object.keys(service.details).length > 0 && (
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--color-separator)' }}>
{Object.entries(service.details).map(([key, value]) => (
<div key={key} className="flex justify-between items-center py-1">
<Caption1 style={{ color: 'var(--color-label-tertiary)' }}>{key}</Caption1>
<Caption1 style={{ color: 'var(--color-label-secondary)' }}>
{typeof value === 'boolean' ? (value ? '是' : '否') : String(value)}
</Caption1>
</div>
))}
</div>
)}
</Card>
)
}

View File

@ -0,0 +1,9 @@
/**
* 监控组件导出
*/
export { default as ServiceStatusCard } from './ServiceStatusCard'
export type { ServiceStatus } from './ServiceStatusCard'
export { default as ResourceMonitor } from './ResourceMonitor'
export type { ResourceData, ResourceMonitorProps } from './ResourceMonitor'