🎨 优化UI

This commit is contained in:
2026-04-08 12:48:09 +08:00
parent 7599146f24
commit 6a80fcc388
6 changed files with 1123 additions and 97 deletions

View File

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