Files
Go-Web-Template/web-admin/src/features/server/ServerStatePage.tsx
2026-04-08 12:48:09 +08:00

248 lines
11 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.

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>
)
}