🎨 优化UI
This commit is contained in:
@@ -1,70 +1,246 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Descriptions, Skeleton, Typography } from 'antd'
|
||||
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 renderValue(value: unknown) {
|
||||
if (value === null || value === undefined) {
|
||||
return '-'
|
||||
function formatStorageFromMb(valueMb: number) {
|
||||
if (valueMb >= 1024 * 1024) {
|
||||
return `${(valueMb / (1024 * 1024)).toFixed(2)} TB`
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return (
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
)
|
||||
if (valueMb >= 1024) {
|
||||
return `${(valueMb / 1024).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
return String(value)
|
||||
return `${valueMb} MB`
|
||||
}
|
||||
|
||||
export function ServerStatePage() {
|
||||
const [server, setServer] = useState<Record<string, unknown> | null>(null)
|
||||
const [server, setServer] = useState<ServerState | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false
|
||||
|
||||
inventoryApi
|
||||
.getServerInfo()
|
||||
.then((response) => {
|
||||
if (!ignore) {
|
||||
setServer(response.data.server)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!ignore) {
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
ignore = 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">
|
||||
<Typography.Title level={2} style={{ marginBottom: 8 }}>
|
||||
服务器状态
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
|
||||
该页面直接读取 `/system/getServerInfo` 的结构化结果,不预设字段,优先保证信息完整。
|
||||
</Typography.Paragraph>
|
||||
<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 />
|
||||
) : (
|
||||
<Descriptions bordered column={1} size="middle">
|
||||
{Object.entries(server || {}).map(([key, value]) => (
|
||||
<Descriptions.Item key={key} label={key}>
|
||||
{renderValue(value)}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
)}
|
||||
) : 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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user