+139
-9
@@ -1,16 +1,146 @@
|
||||
import { ErrorBlock, NavBar } from 'antd-mobile'
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { NavBar, Card, Empty, DotLoading } from 'antd-mobile'
|
||||
import { api } from '../api/client.js'
|
||||
import SwipeTabs from '../components/SwipeTabs.jsx'
|
||||
|
||||
// 监控页: 两个横向分类
|
||||
// - 主机监控: 实时数据来自 komari (服务端实时代理, 不落库), 点击节点看 24h 时序
|
||||
// - 服务监控: 暂留空占位
|
||||
function fmtTime(s) {
|
||||
if (!s) return ''
|
||||
const d = new Date(s)
|
||||
if (isNaN(d.getTime())) return s
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||
}
|
||||
|
||||
function fmtBytes(b) {
|
||||
if (!b) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let i = 0
|
||||
let v = Number(b)
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
||||
return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[i]
|
||||
}
|
||||
|
||||
function fmtUptime(sec) {
|
||||
const s = Number(sec) || 0
|
||||
const d = Math.floor(s / 86400)
|
||||
const h = Math.floor((s % 86400) / 3600)
|
||||
return d > 0 ? d + '天' + h + '小时' : h + '小时'
|
||||
}
|
||||
|
||||
function pct(used, total) {
|
||||
return total > 0 ? Math.min(100, (used / total) * 100) : 0
|
||||
}
|
||||
|
||||
// 监控: 暂留占位
|
||||
// 使用 antd-mobile ErrorBlock 仿原生空状态页
|
||||
export default function MonitorPage() {
|
||||
const [active, setActive] = useState('servers')
|
||||
const tabs = [
|
||||
{ key: 'servers', label: '主机监控', render: () => <ServerView /> },
|
||||
{ key: 'services', label: '服务监控', render: () => <ServiceView /> },
|
||||
]
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<NavBar back={null}>监控</NavBar>
|
||||
<ErrorBlock
|
||||
status="empty"
|
||||
title="监控"
|
||||
description="后续接入: 指标阈值告警 / 时序趋势"
|
||||
/>
|
||||
<div className="monitor-content">
|
||||
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 主机监控: komari 实时节点列表 (30s 轮询, 静默刷新)
|
||||
function ServerView() {
|
||||
const [items, setItems] = useState(null) // null = 加载中
|
||||
const [err, setErr] = useState('')
|
||||
const navigate = useNavigate()
|
||||
|
||||
const load = useCallback((silent) => {
|
||||
if (!silent) { setErr(''); setItems(null) }
|
||||
api('/servers')
|
||||
.then((data) => { setErr(''); setItems(data.items || []) })
|
||||
.catch((e) => { if (!silent) { setErr(e.message); setItems([]) } })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load(false)
|
||||
const timer = setInterval(() => load(true), 30000)
|
||||
return () => clearInterval(timer)
|
||||
}, [load])
|
||||
|
||||
return (
|
||||
<div className="server-list">
|
||||
{items === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||
{err && (
|
||||
<div className="error-banner">
|
||||
{err}
|
||||
<span className="retry-link" onClick={load}>重试</span>
|
||||
</div>
|
||||
)}
|
||||
{items !== null && !err && items.length === 0 && <Empty description="暂无主机" />}
|
||||
{items !== null && items.length > 0 && (
|
||||
<>
|
||||
<div className="monitor-tip">实时数据 · 每 30 秒自动刷新</div>
|
||||
<div className="section-list">
|
||||
{items.map((n) => (
|
||||
<Card key={n.uuid} className="server-card" onClick={() => navigate('/monitor/server/' + n.uuid)}>
|
||||
<div className="server-head">
|
||||
<span className="server-name">
|
||||
<span className={'status-dot ' + (n.status === 'online' ? 'on' : 'off')} />
|
||||
{n.name}
|
||||
</span>
|
||||
<span className="server-update">更新于 {fmtTime(n.recordedAt)}</span>
|
||||
</div>
|
||||
<div className="server-metrics">
|
||||
<div className="metric">
|
||||
<div className="metric-label">CPU</div>
|
||||
<Meter percent={n.cpuUsage} />
|
||||
<div className="metric-value">{n.cpuUsage.toFixed ? n.cpuUsage.toFixed(1) : n.cpuUsage}%</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-label">内存</div>
|
||||
<Meter percent={pct(n.ramUsed, n.memTotal)} />
|
||||
<div className="metric-value">{fmtBytes(n.ramUsed)} / {fmtBytes(n.memTotal)}</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-label">磁盘</div>
|
||||
<Meter percent={pct(n.diskUsed, n.diskTotal)} />
|
||||
<div className="metric-value">{fmtBytes(n.diskUsed)} / {fmtBytes(n.diskTotal)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="server-foot">
|
||||
<span>负载 {n.load1} / {n.load5} / {n.load15}</span>
|
||||
<span>{n.cpuCores} 核</span>
|
||||
<span>进程 {n.process}</span>
|
||||
<span>连接 {n.connections}</span>
|
||||
<span>运行 {fmtUptime(n.uptime)}</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 进度条 (超 80% 变红)
|
||||
function Meter({ percent }) {
|
||||
const p = Math.max(0, Math.min(100, Number(percent) || 0))
|
||||
return (
|
||||
<div className="meter">
|
||||
<div className={'meter-fill' + (p >= 80 ? ' warn' : '')} style={{ width: p + '%' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 服务监控: 暂留空
|
||||
function ServiceView() {
|
||||
return (
|
||||
<div className="monitor-empty">
|
||||
<Empty description="服务监控待接入" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user