248 lines
11 KiB
TypeScript
248 lines
11 KiB
TypeScript
import { CloudServerOutlined, HddOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons'
|
||
import { useEffect, useMemo, useState } from 'react'
|
||
import { Button, Card, Col, Progress, Row, Skeleton, Space, Statistic, Typography } from 'antd'
|
||
import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
||
import { inventoryApi } from '@/lib/api'
|
||
import type { ServerState } from '@/types/system'
|
||
|
||
function formatStorageFromMb(valueMb: number) {
|
||
if (valueMb >= 1024 * 1024) {
|
||
return `${(valueMb / (1024 * 1024)).toFixed(2)} TB`
|
||
}
|
||
|
||
if (valueMb >= 1024) {
|
||
return `${(valueMb / 1024).toFixed(1)} GB`
|
||
}
|
||
|
||
return `${valueMb} MB`
|
||
}
|
||
|
||
export function ServerStatePage() {
|
||
const [server, setServer] = useState<ServerState | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
const loadServerInfo = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await inventoryApi.getServerInfo()
|
||
setServer(response.data.server)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
void loadServerInfo()
|
||
}, [])
|
||
|
||
const cpuChartData = useMemo(
|
||
() =>
|
||
server?.cpu.cpus.map((usage, index) => ({
|
||
name: `CPU ${index + 1}`,
|
||
usage: Number(usage.toFixed(1)),
|
||
})) || [],
|
||
[server],
|
||
)
|
||
|
||
const averageCpuUsage = useMemo(() => {
|
||
if (!cpuChartData.length) {
|
||
return 0
|
||
}
|
||
|
||
const total = cpuChartData.reduce((sum, item) => sum + item.usage, 0)
|
||
return Number((total / cpuChartData.length).toFixed(1))
|
||
}, [cpuChartData])
|
||
|
||
const diskSummary = useMemo(() => {
|
||
const disks = server?.disk || []
|
||
const totalMb = disks.reduce((sum, item) => sum + item.totalMb, 0)
|
||
const usedMb = disks.reduce((sum, item) => sum + item.usedMb, 0)
|
||
const usedPercent = totalMb ? Number(((usedMb / totalMb) * 100).toFixed(1)) : 0
|
||
|
||
return { totalMb, usedMb, usedPercent }
|
||
}, [server])
|
||
|
||
return (
|
||
<div className="page-stack">
|
||
<Card className="glass-panel page-panel">
|
||
<div className="section-heading">
|
||
<div>
|
||
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||
服务器状态
|
||
</Typography.Title>
|
||
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
||
页面按 CPU、内存、磁盘和运行时信息聚合展示,便于快速判断资源压力和运行状态。
|
||
</Typography.Paragraph>
|
||
</div>
|
||
<Button icon={<ReloadOutlined />} onClick={() => void loadServerInfo()} loading={loading}>
|
||
刷新状态
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
<Card className="glass-panel page-panel">
|
||
{loading ? (
|
||
<Skeleton active />
|
||
) : server ? (
|
||
<div className="page-stack">
|
||
<div className="hero-grid">
|
||
<Card bordered={false} style={{ background: 'rgba(255, 255, 255, 0.86)' }}>
|
||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||
<Space size={10}>
|
||
<span className="status-dot ready"></span>
|
||
<Typography.Text strong>运行中</Typography.Text>
|
||
</Space>
|
||
<Typography.Title level={3} style={{ margin: 0 }}>
|
||
{server.os.goos.toUpperCase()} · Go Runtime
|
||
</Typography.Title>
|
||
<Typography.Paragraph className="text-muted" style={{ margin: 0 }}>
|
||
当前运行时包含 {server.os.numGoroutine} 个 goroutine,编译器为 {server.os.compiler},
|
||
Go 版本为 {server.os.goVersion}。
|
||
</Typography.Paragraph>
|
||
<div className="server-runtime-grid">
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">逻辑 CPU</Typography.Text>
|
||
<Typography.Title level={4} style={{ margin: '8px 0 0' }}>
|
||
{server.os.numCpu}
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">CPU 平均负载</Typography.Text>
|
||
<Typography.Title level={4} style={{ margin: '8px 0 0' }}>
|
||
{averageCpuUsage}%
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">内存占用</Typography.Text>
|
||
<Typography.Title level={4} style={{ margin: '8px 0 0' }}>
|
||
{server.ram.usedPercent}%
|
||
</Typography.Title>
|
||
</div>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
<Card bordered={false} style={{ background: 'rgba(16, 37, 66, 0.04)' }}>
|
||
<div className="server-meter-grid">
|
||
<div className="server-meter-card">
|
||
<Typography.Text className="text-muted">CPU 负载</Typography.Text>
|
||
<Progress
|
||
type="dashboard"
|
||
percent={Math.round(averageCpuUsage)}
|
||
strokeColor="#d16f3f"
|
||
/>
|
||
</div>
|
||
<div className="server-meter-card">
|
||
<Typography.Text className="text-muted">内存负载</Typography.Text>
|
||
<Progress
|
||
type="dashboard"
|
||
percent={Math.round(server.ram.usedPercent)}
|
||
strokeColor="#1f9d78"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="metric-grid">
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">CPU 核心数</Typography.Text>
|
||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||
{server.cpu.cores}
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">Goroutine</Typography.Text>
|
||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||
{server.os.numGoroutine}
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">已用内存</Typography.Text>
|
||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||
{formatStorageFromMb(server.ram.usedMb)}
|
||
</Typography.Title>
|
||
</div>
|
||
<div className="metric-card">
|
||
<Typography.Text className="text-muted">磁盘占用</Typography.Text>
|
||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||
{diskSummary.usedPercent}%
|
||
</Typography.Title>
|
||
</div>
|
||
</div>
|
||
|
||
<Row gutter={[20, 20]}>
|
||
<Col xs={24} xl={15}>
|
||
<Card className="glass-panel" title="CPU 核心负载走势" extra={<ThunderboltOutlined />}>
|
||
<div style={{ width: '100%', height: 320 }}>
|
||
<ResponsiveContainer>
|
||
<BarChart data={cpuChartData}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(16, 37, 66, 0.08)" />
|
||
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
||
<YAxis tickLine={false} axisLine={false} width={44} unit="%" />
|
||
<Tooltip
|
||
formatter={(value) => [`${Number(value ?? 0).toFixed(1)}%`, '占用率']}
|
||
/>
|
||
<Bar dataKey="usage" radius={[10, 10, 0, 0]}>
|
||
{cpuChartData.map((item) => (
|
||
<Cell
|
||
key={item.name}
|
||
fill={item.usage >= 80 ? '#c34747' : item.usage >= 60 ? '#d29b2f' : '#d16f3f'}
|
||
/>
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} xl={9}>
|
||
<Card className="glass-panel" title="运行时概览" extra={<CloudServerOutlined />}>
|
||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
||
<Statistic title="操作系统" value={server.os.goos.toUpperCase()} />
|
||
<Statistic title="编译器" value={server.os.compiler} />
|
||
<Statistic title="Go 版本" value={server.os.goVersion} />
|
||
<Statistic title="总内存" value={formatStorageFromMb(server.ram.totalMb)} />
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Row gutter={[20, 20]}>
|
||
<Col xs={24} xl={8}>
|
||
<Card className="glass-panel" title="内存占用" extra={`${server.ram.usedPercent}%`}>
|
||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
||
<Progress percent={server.ram.usedPercent} strokeColor="#1f9d78" />
|
||
<Statistic title="已用内存" value={formatStorageFromMb(server.ram.usedMb)} />
|
||
<Statistic title="总内存" value={formatStorageFromMb(server.ram.totalMb)} />
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
<Col xs={24} xl={16}>
|
||
<Card className="glass-panel" title="磁盘分区" extra={<HddOutlined />}>
|
||
<div className="server-disk-grid">
|
||
{server.disk.map((diskItem) => (
|
||
<Card key={diskItem.mountPoint} size="small" className="server-disk-card">
|
||
<div className="section-heading" style={{ marginBottom: 12 }}>
|
||
<div>
|
||
<Typography.Text strong>{diskItem.mountPoint}</Typography.Text>
|
||
<Typography.Paragraph className="text-muted" style={{ margin: '4px 0 0' }}>
|
||
已用 {formatStorageFromMb(diskItem.usedMb)} / {formatStorageFromMb(diskItem.totalMb)}
|
||
</Typography.Paragraph>
|
||
</div>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||
{diskItem.usedPercent}%
|
||
</Typography.Title>
|
||
</div>
|
||
<Progress percent={diskItem.usedPercent} strokeColor="#d16f3f" />
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
) : null}
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|