All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 7s
125 lines
5.2 KiB
TypeScript
125 lines
5.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { Card, Row, Col, Progress, Table, Tag, Typography, Button, Space, Tooltip, App } from 'antd'
|
|
import { CloudServerOutlined, ReloadOutlined, CopyOutlined, GlobalOutlined } from '@ant-design/icons'
|
|
import { getServerMetrics, type ServerInfo, type ProcessInfo } from '@/services/server-api'
|
|
|
|
const { Text, Title } = Typography
|
|
|
|
function CopyTag({ text, icon, color }: { text: string; icon?: React.ReactNode; color?: string }) {
|
|
const { message } = App.useApp()
|
|
return (
|
|
<Tag color={color || 'default'} style={{ cursor: 'pointer' }}
|
|
onClick={async () => {
|
|
try { await navigator.clipboard.writeText(text); message.success(`已复制: ${text}`) }
|
|
catch { message.error('复制失败') }
|
|
}}>
|
|
{icon}{text} <CopyOutlined style={{ fontSize: 10 }} />
|
|
</Tag>
|
|
)
|
|
}
|
|
|
|
function ServerCard({ server }: { server: ServerInfo }) {
|
|
const cpuColor = server.cpu.usagePercent > 80 ? '#ff4d4f' : server.cpu.usagePercent > 50 ? '#faad14' : '#52c41a'
|
|
const memColor = server.memory.percent > 80 ? '#ff4d4f' : server.memory.percent > 50 ? '#faad14' : '#52c41a'
|
|
|
|
return (
|
|
<Card title={<Space><CloudServerOutlined />{server.name}<Tag color="blue">{server.role}</Tag></Space>} style={{ height: '100%' }}>
|
|
{/* IP & domains — fixed height */}
|
|
<div style={{ marginBottom: 12, height: 54, overflow: 'hidden' }}>
|
|
<Space wrap size={[4, 4]}>
|
|
<CopyTag text={server.network.publicIp} icon="🌐 " color="cyan" />
|
|
<CopyTag text={server.network.privateIp} icon="🔒 " />
|
|
{server.network.domains.map(d => (
|
|
<CopyTag key={d} text={d} icon={<GlobalOutlined style={{ marginRight: 2 }} />} color="blue" />
|
|
))}
|
|
</Space>
|
|
</div>
|
|
|
|
{/* Metrics — fixed height grid */}
|
|
<div style={{ marginBottom: 10, minHeight: 72 }}>
|
|
<Row gutter={[16, 8]}>
|
|
<Col span={8}>
|
|
<Text type="secondary" style={{ fontSize: 11 }}>CPU · {server.cpu.cores}核</Text>
|
|
<Progress percent={server.cpu.usagePercent} strokeColor={cpuColor} size="small" format={p => `${p}%`} />
|
|
</Col>
|
|
<Col span={8}>
|
|
<Text type="secondary" style={{ fontSize: 11 }}>内存 {server.memory.percent}%</Text>
|
|
<Progress percent={server.memory.percent} strokeColor={memColor} size="small" />
|
|
<Text style={{ fontSize: 10 }} type="secondary">{server.memory.used}/{server.memory.total}</Text>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Text type="secondary" style={{ fontSize: 11, marginBottom: 2 }}>磁盘</Text>
|
|
{(server.disks || []).map(d => (
|
|
<div key={d.mount} style={{ marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<Text style={{ fontSize: 10, width: 32 }} type="secondary">{d.mount}</Text>
|
|
<Progress percent={d.percent} size="small" strokeColor={d.percent > 80 ? '#ff4d4f' : '#1677ff'} style={{ flex: 1, margin: 0 }} />
|
|
<Text style={{ fontSize: 10, whiteSpace: 'nowrap' }}>{d.used}</Text>
|
|
</div>
|
|
))}
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
{/* Uptime */}
|
|
<div style={{ marginBottom: 12, minHeight: 22 }}>
|
|
<Text style={{ fontSize: 14, fontWeight: 500 }}>🕐 {server.uptime}</Text>
|
|
</div>
|
|
|
|
{/* Process table — fixed height */}
|
|
<Table
|
|
dataSource={server.processes || []}
|
|
rowKey="pid" size="small" pagination={false}
|
|
|
|
style={{ marginTop: 0 }}
|
|
columns={[
|
|
{ title: '进程', dataIndex: 'name', width: 110, ellipsis: true, render: (name: string, r: ProcessInfo) => (
|
|
<Tooltip title={r.desc ? `${r.desc}\n${r.command}` : r.command}><span>{name}</span></Tooltip>
|
|
)},
|
|
{ title: '说明', dataIndex: 'desc', width: 90, ellipsis: true, render: (d: string) => <Text type="secondary" style={{ fontSize: 12 }}>{d || '-'}</Text> },
|
|
{ title: 'CPU', dataIndex: 'cpu', width: 50 },
|
|
{ title: 'MEM', dataIndex: 'mem', width: 50 },
|
|
]}
|
|
locale={{ emptyText: '暂无' }}
|
|
/>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function ServersContent() {
|
|
const qc = useQueryClient()
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
|
|
const { data } = useQuery({
|
|
queryKey: ['servers', 'metrics'],
|
|
queryFn: getServerMetrics,
|
|
staleTime: 30_000,
|
|
})
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true)
|
|
await qc.invalidateQueries({ queryKey: ['servers', 'metrics'] })
|
|
setTimeout(() => setRefreshing(false), 800)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
|
<Title level={5} style={{ margin: 0 }}><CloudServerOutlined /> 服务器运维</Title>
|
|
<Button icon={<ReloadOutlined spin={refreshing} />} onClick={handleRefresh} loading={refreshing}>刷新</Button>
|
|
</div>
|
|
<Row gutter={[16, 16]}>
|
|
{(data?.servers || []).map(s => (
|
|
<Col xs={24} lg={12} key={s.hostname}>
|
|
<ServerCard server={s} />
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function ServersPage() {
|
|
return <App><ServersContent /></App>
|
|
}
|