1.18升级

This commit is contained in:
dengqichen 2025-12-11 13:44:26 +08:00
parent 38b6e873c8
commit eebd3240b3

View File

@ -0,0 +1,570 @@
import React, { useState, useEffect } from 'react';
import ReactECharts from 'echarts-for-react';
import type { EChartsOption } from 'echarts';
import { Loader2, TrendingUp, Activity, HardDrive, Network as NetworkIcon } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent } from '@/components/ui/card';
import type { ServerResponse, ServerMonitorResponse, TimeRange, MetricType } from '../types';
import { TimeRangeLabels, MetricTypeLabels } from '../types';
import { getServerMonitorMetrics } from '../service';
import dayjs from 'dayjs';
interface ServerMonitorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
server: ServerResponse | null;
}
export const ServerMonitorDialog: React.FC<ServerMonitorDialogProps> = ({
open,
onOpenChange,
server,
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [monitorData, setMonitorData] = useState<ServerMonitorResponse | null>(null);
const [selectedTimeRange, setSelectedTimeRange] = useState<TimeRange>('LAST_1_HOUR' as TimeRange);
const [autoRefresh, setAutoRefresh] = useState(true);
// 加载监控数据
const loadMonitorData = async () => {
if (!server) return;
try {
setLoading(true);
const response = await getServerMonitorMetrics(server.id, selectedTimeRange);
setMonitorData(response);
} catch (error: any) {
console.error('加载监控数据失败:', error);
} finally {
setLoading(false);
}
};
// 初始加载和时间范围变化时加载数据
useEffect(() => {
if (open && server) {
loadMonitorData();
}
}, [open, server, selectedTimeRange]);
// 自动刷新仅最近1小时
useEffect(() => {
if (!open || !autoRefresh || selectedTimeRange !== 'LAST_1_HOUR') {
return;
}
const timer = setInterval(() => {
loadMonitorData();
}, 30000); // 30秒刷新一次
return () => clearInterval(timer);
}, [open, autoRefresh, selectedTimeRange]);
// CPU 图表配置
const getCpuChartOption = (): EChartsOption => {
if (!monitorData?.metrics.cpu || monitorData.metrics.cpu.length === 0) {
return {};
}
const cpuData = monitorData.metrics.cpu;
const statistics = monitorData.statistics.cpu;
return {
title: {
text: 'CPU 使用率',
left: 'center',
textStyle: { fontSize: 14, fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
const dataItem = cpuData[param.dataIndex];
return `${dayjs(param.name).format('MM-DD HH:mm')}<br/>
CPU: ${param.value}%<br/>
状态: ${dataItem.status === 'SUCCESS' ? '正常' : '失败'}`;
},
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: cpuData.map(item => item.time),
axisLabel: {
formatter: (value: string) => dayjs(value).format('HH:mm'),
},
},
yAxis: {
type: 'value',
name: 'CPU (%)',
max: 100,
axisLabel: {
formatter: '{value}%',
},
},
series: [
{
name: 'CPU使用率',
type: 'line',
smooth: true,
data: cpuData.map(item => item.value),
lineStyle: {
color: MetricTypeLabels.CPU.color,
width: 2,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: `${MetricTypeLabels.CPU.color}40` },
{ offset: 1, color: `${MetricTypeLabels.CPU.color}10` },
],
},
},
markLine: statistics
? {
data: [
{ yAxis: statistics.avg, name: '平均值', lineStyle: { type: 'dashed', color: '#666' } },
],
label: {
formatter: '平均: {c}%',
},
}
: undefined,
},
],
};
};
// 内存图表配置
const getMemoryChartOption = (): EChartsOption => {
if (!monitorData?.metrics.memory || monitorData.metrics.memory.length === 0) {
return {};
}
const memoryData = monitorData.metrics.memory;
const statistics = monitorData.statistics.memory;
return {
title: {
text: '内存使用率',
left: 'center',
textStyle: { fontSize: 14, fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const param = params[0];
const dataItem = memoryData[param.dataIndex];
return `${dayjs(param.name).format('MM-DD HH:mm')}<br/>
内存: ${param.value}%<br/>
已用: ${dataItem.usedGB}GB<br/>
状态: ${dataItem.status === 'SUCCESS' ? '正常' : '失败'}`;
},
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: memoryData.map(item => item.time),
axisLabel: {
formatter: (value: string) => dayjs(value).format('HH:mm'),
},
},
yAxis: {
type: 'value',
name: '内存 (%)',
max: 100,
axisLabel: {
formatter: '{value}%',
},
},
series: [
{
name: '内存使用率',
type: 'line',
smooth: true,
data: memoryData.map(item => item.usagePercent),
lineStyle: {
color: MetricTypeLabels.MEMORY.color,
width: 2,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: `${MetricTypeLabels.MEMORY.color}40` },
{ offset: 1, color: `${MetricTypeLabels.MEMORY.color}10` },
],
},
},
markLine: statistics
? {
data: [
{ yAxis: statistics.avgPercent, name: '平均值', lineStyle: { type: 'dashed', color: '#666' } },
],
label: {
formatter: '平均: {c}%',
},
}
: undefined,
},
],
};
};
// 网络流量图表配置
const getNetworkChartOption = (): EChartsOption => {
if (!monitorData?.metrics.network || monitorData.metrics.network.length === 0) {
return {};
}
const networkData = monitorData.metrics.network;
return {
title: {
text: '网络流量',
left: 'center',
textStyle: { fontSize: 14, fontWeight: 'bold' },
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const dataItem = networkData[params[0].dataIndex];
return `${dayjs(params[0].name).format('MM-DD HH:mm')}<br/>
接收: ${dataItem.rxMBps.toFixed(2)} MB/s<br/>
发送: ${dataItem.txMBps.toFixed(2)} MB/s`;
},
},
legend: {
data: ['接收', '发送'],
top: '8%',
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: networkData.map(item => item.time),
axisLabel: {
formatter: (value: string) => dayjs(value).format('HH:mm'),
},
},
yAxis: {
type: 'value',
name: '流量 (MB/s)',
axisLabel: {
formatter: '{value}',
},
},
series: [
{
name: '接收',
type: 'line',
smooth: true,
data: networkData.map(item => item.rxMBps),
lineStyle: {
color: '#3b82f6',
width: 2,
},
},
{
name: '发送',
type: 'line',
smooth: true,
data: networkData.map(item => item.txMBps),
lineStyle: {
color: '#8b5cf6',
width: 2,
},
},
],
};
};
// 磁盘使用情况卡片
const renderDiskInfo = () => {
if (!monitorData?.metrics.disk) {
return (
<div className="flex items-center justify-center h-64 text-muted-foreground">
</div>
);
}
const disk = monitorData.metrics.disk;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">使</h3>
<Badge variant="outline">
使: {disk.maxUsagePercent.toFixed(2)}% ({disk.maxUsagePartition})
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{disk.partitions.map((partition, index) => (
<Card key={index}>
<CardContent className="pt-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="font-mono font-medium">{partition.mountPoint}</span>
</div>
<Badge
variant={partition.usagePercent > 80 ? 'destructive' : 'outline'}
>
{partition.usagePercent.toFixed(2)}%
</Badge>
</div>
<div className="text-sm text-muted-foreground">
{partition.fileSystem}
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{partition.usedSizeGB}GB / {partition.totalSizeGB}GB
</span>
</div>
<div className="w-full bg-secondary rounded-full h-2">
<div
className={`h-2 rounded-full ${
partition.usagePercent > 80
? 'bg-destructive'
: partition.usagePercent > 60
? 'bg-yellow-500'
: 'bg-primary'
}`}
style={{ width: `${partition.usagePercent}%` }}
/>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
// 统计信息卡片
const renderStatistics = () => {
if (!monitorData) return null;
const stats = [];
if (monitorData.statistics.cpu) {
stats.push({
icon: <Activity className="h-5 w-5" />,
label: 'CPU',
avg: `${monitorData.statistics.cpu.avg.toFixed(2)}%`,
max: `${monitorData.statistics.cpu.max.toFixed(2)}%`,
min: `${monitorData.statistics.cpu.min.toFixed(2)}%`,
color: 'text-blue-600',
});
}
if (monitorData.statistics.memory) {
stats.push({
icon: <TrendingUp className="h-5 w-5" />,
label: '内存',
avg: `${monitorData.statistics.memory.avgPercent.toFixed(2)}%`,
max: `${monitorData.statistics.memory.maxPercent.toFixed(2)}%`,
min: `${monitorData.statistics.memory.minPercent.toFixed(2)}%`,
color: 'text-green-600',
});
}
if (monitorData.statistics.network) {
const network = monitorData.statistics.network;
const totalRxMB = (network.totalRxBytes / 1024 / 1024).toFixed(2);
const totalTxMB = (network.totalTxBytes / 1024 / 1024).toFixed(2);
stats.push({
icon: <NetworkIcon className="h-5 w-5" />,
label: '网络',
avg: `接收 ${totalRxMB}MB`,
max: `发送 ${totalTxMB}MB`,
min: '',
color: 'text-purple-600',
});
}
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{stats.map((stat, index) => (
<Card key={index}>
<CardContent className="pt-4">
<div className="flex items-center gap-3">
<div className={`${stat.color}`}>{stat.icon}</div>
<div className="flex-1">
<div className="text-sm font-medium text-muted-foreground">{stat.label}</div>
<div className="text-xs text-muted-foreground mt-1 space-y-0.5">
<div>: {stat.avg}</div>
<div>: {stat.max}</div>
{stat.min && <div>: {stat.min}</div>}
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>
- {server?.serverName} ({server?.hostIp})
</DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-y-auto">
<div className="space-y-4">
{/* 时间范围选择器 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{Object.entries(TimeRangeLabels).map(([key, value]) => (
<Button
key={key}
variant={selectedTimeRange === key ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedTimeRange(key as TimeRange)}
>
{value.label}
</Button>
))}
</div>
<div className="flex items-center gap-2">
{selectedTimeRange === 'LAST_1_HOUR' && (
<Button
variant="outline"
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? '停止自动刷新' : '开启自动刷新'}
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={loadMonitorData}
disabled={loading}
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : '刷新'}
</Button>
</div>
</div>
{loading && !monitorData ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : monitorData ? (
<>
{/* 统计信息 */}
{renderStatistics()}
{/* 图表标签页 */}
<Tabs defaultValue="cpu" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="cpu">CPU</TabsTrigger>
<TabsTrigger value="memory"></TabsTrigger>
<TabsTrigger value="network"></TabsTrigger>
<TabsTrigger value="disk"></TabsTrigger>
</TabsList>
<TabsContent value="cpu" className="space-y-4">
{monitorData.metrics.cpu && monitorData.metrics.cpu.length > 0 ? (
<ReactECharts
option={getCpuChartOption()}
style={{ height: '400px', width: '100%' }}
/>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
CPU数据
</div>
)}
</TabsContent>
<TabsContent value="memory" className="space-y-4">
{monitorData.metrics.memory && monitorData.metrics.memory.length > 0 ? (
<ReactECharts
option={getMemoryChartOption()}
style={{ height: '400px', width: '100%' }}
/>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
</div>
)}
</TabsContent>
<TabsContent value="network" className="space-y-4">
{monitorData.metrics.network && monitorData.metrics.network.length > 0 ? (
<ReactECharts
option={getNetworkChartOption()}
style={{ height: '400px', width: '100%' }}
/>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
</div>
)}
</TabsContent>
<TabsContent value="disk" className="space-y-4">
{renderDiskInfo()}
</TabsContent>
</Tabs>
</>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
</div>
)}
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
};