iboard/src/pages/MonitorPage.jsx
cheney 6bacda05f8
All checks were successful
Docker Build and Push / build-image (push) Successful in 3m2s
fix bugs
2026-08-06 17:43:21 +08:00

151 lines
5.4 KiB
JavaScript

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
}
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>
<div className="monitor-content">
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
</div>
</div>
)
}
// 主机监控列表模块级缓存: 切走再进入时先展示上次数据, 拉取成功后再静默更新
let cachedServers = null
// 主机监控: komari 实时节点列表 (30s 轮询, 静默刷新)
function ServerView() {
const [items, setItems] = useState(cachedServers) // 有缓存直接先展示, 无缓存为 null 进入加载态
const [err, setErr] = useState('')
const navigate = useNavigate()
const load = useCallback((silent) => {
if (!silent && cachedServers === null) setItems(null) // 仅无缓存时显示加载中
if (!silent) setErr('')
api('/servers')
.then((data) => { cachedServers = data.items || []; setErr(''); setItems(data.items || []) })
.catch((e) => { if (!silent) { setErr(e.message); if (cachedServers === null) 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(false)}>重试</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>
)
}