import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { EChartsOption } from 'echarts'; import { Activity, AlertTriangle, CheckCircle2, Clock3, Cpu, Database, HardDrive, MemoryStick, Network, RefreshCw, Server, Settings2, ShieldAlert, } from 'lucide-react'; import { adminApi, type InfrastructureAlert, type InfrastructureAlertSettings, type InfrastructureAlertThresholds, type InfrastructureMetricPoint, type InfrastructureMonitoringOverview, type InfrastructureMonitoringRange, } from '@/api/adminApi'; import { Breadcrumb, Button, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui'; import { Chart } from '@/components/ui/Chart'; import './AdminSystemMonitoringPage.css'; const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [ { value: '1h', label: '近1小时' }, { value: '24h', label: '近24小时' }, { value: '7d', label: '近7天' }, ]; const STATUS_COPY = { healthy: { label: '运行正常', tone: 'success' as const }, warning: { label: '需要关注', tone: 'warning' as const }, critical: { label: '严重告警', tone: 'danger' as const }, unknown: { label: '状态未知', tone: 'neutral' as const }, }; function formatPercent(value: number | null) { return value === null ? '—' : `${value.toFixed(1)}%`; } function formatBytes(value: number | null) { if (value === null) return '—'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let amount = value; let index = 0; while (amount >= 1024 && index < units.length - 1) { amount /= 1024; index += 1; } return `${amount.toFixed(index >= 3 ? 1 : 0)} ${units[index]}`; } function formatRate(value: number | null) { return value === null ? '—' : `${formatBytes(value)}/s`; } function totalNetworkRate(receive: number | null | undefined, transmit: number | null | undefined) { if (receive === null || receive === undefined || transmit === null || transmit === undefined) return null; return receive + transmit; } function formatUptime(value: number | null) { if (value === null) return '—'; const days = Math.floor(value / 86400); const hours = Math.floor((value % 86400) / 3600); return days > 0 ? `${days}天 ${hours}小时` : `${hours}小时`; } function formatServiceMetric(value: number | null, unit: 'percent' | 'seconds' | 'count' | 'per_second' | 'bytes') { if (value === null) return '—'; if (unit === 'percent') return `${value.toFixed(2)}%`; if (unit === 'seconds') return value < 1 ? `${Math.round(value * 1000)} ms` : `${value.toFixed(1)} s`; if (unit === 'per_second') return `${value.toFixed(value < 10 ? 2 : 1)}/s`; if (unit === 'bytes') return formatBytes(value); return Math.round(value).toLocaleString('zh-CN'); } function formatTime(value: string | null) { if (!value) return '暂无采样'; return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }).format(new Date(value)); } function formatDuration(startedAt: string) { const milliseconds = Date.now() - Date.parse(startedAt); if (!Number.isFinite(milliseconds) || milliseconds < 0) return '—'; const minutes = Math.floor(milliseconds / 60_000); if (minutes < 60) return `${Math.max(minutes, 1)}分钟`; const hours = Math.floor(minutes / 60); return hours < 24 ? `${hours}小时 ${minutes % 60}分钟` : `${Math.floor(hours / 24)}天 ${hours % 24}小时`; } function timeLabels(points: InfrastructureMetricPoint[], range: InfrastructureMonitoringRange) { return points.map((point) => new Intl.DateTimeFormat('zh-CN', range === '7d' ? { month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false } : { hour: '2-digit', minute: '2-digit', hour12: false }) .format(new Date(point.timestamp))); } function makeTrendOption(params: { range: InfrastructureMonitoringRange; series: Array<{ name: string; points: InfrastructureMetricPoint[]; color: string }>; suffix: string; maximum?: number; }): EChartsOption { const timestamps = [...new Set(params.series.flatMap((series) => series.points.map((point) => point.timestamp)))].sort(); return { animationDuration: 280, color: params.series.map((item) => item.color), grid: { left: 8, right: 16, top: 34, bottom: 4, containLabel: true }, legend: params.series.length > 1 ? { type: 'scroll', top: 0, left: 0, right: 0, textStyle: { color: '#6b7280', fontSize: 12 } } : undefined, tooltip: { trigger: 'axis', valueFormatter: (value) => `${Number(value).toFixed(1)}${params.suffix}`, }, xAxis: { type: 'category', boundaryGap: false, data: timeLabels(timestamps.map((timestamp) => ({ timestamp, value: 0 })), params.range), axisLine: { lineStyle: { color: '#e5e7eb' } }, axisTick: { show: false }, axisLabel: { color: '#9ca3af', hideOverlap: true, margin: 12 }, }, yAxis: { type: 'value', min: 0, max: params.maximum, axisLabel: { color: '#9ca3af', formatter: `{value}${params.suffix}` }, splitLine: { lineStyle: { color: '#eef0f3' } }, }, series: params.series.map((item) => { const values = new Map(item.points.map((point) => [point.timestamp, point.value])); return { name: item.name, data: timestamps.map((timestamp) => values.get(timestamp) ?? null), type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2.5 }, areaStyle: { opacity: 0.07 }, }; }), }; } function severityTag(severity: InfrastructureAlert['severity']) { if (severity === 'critical') return 严重; if (severity === 'warning') return 警告; return 提示; } function makeAlertColumns(onMarkRead: (alert: InfrastructureAlert) => void, readingFingerprint: string): Array> { return [ { key: 'severity', title: '级别', width: '82px', render: (record) => severityTag(record.severity) }, { key: 'alert', title: '告警', width: '280px', render: (record) => (
{record.name}{record.summary}
), }, { key: 'service', title: '服务 / 实例', width: '190px', render: (record) => record.service || record.instance || '主机资源' }, { key: 'value', title: '当前值 / 阈值', width: '150px', render: (record) => `${record.currentValue || '—'} / ${record.threshold || '—'}` }, { key: 'startedAt', title: '开始时间', width: '150px', render: (record) => formatTime(record.startedAt) }, { key: 'duration', title: '持续时间', width: '120px', render: (record) => formatDuration(record.startedAt) }, { key: 'actions', title: '操作', width: '112px', render: (record) => record.acknowledged ? 已读 : , }, ]; } export function AdminSystemMonitoringPage() { const [range, setRange] = useState('24h'); const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [settings, setSettings] = useState(null); const [draftThresholds, setDraftThresholds] = useState({}); const [showSettings, setShowSettings] = useState(false); const [settingsError, setSettingsError] = useState(''); const [savingSettings, setSavingSettings] = useState(false); const [readingFingerprint, setReadingFingerprint] = useState(''); const [readError, setReadError] = useState(''); const requestSequence = useRef(0); const pendingRequests = useRef(0); const loadData = useCallback(async (supersede = false) => { if (!supersede && pendingRequests.current > 0) return; pendingRequests.current += 1; const sequence = ++requestSequence.current; setLoading(true); try { const result = await adminApi.getInfrastructureMonitoringOverview(range); if (sequence !== requestSequence.current) return; setOverview(result); setError(result.available ? '' : result.error || '监控数据当前不可用'); } catch (reason) { if (sequence !== requestSequence.current) return; setOverview(null); setError(reason instanceof Error ? reason.message : '监控数据加载失败'); } finally { if (sequence === requestSequence.current) setLoading(false); pendingRequests.current -= 1; } }, [range]); const loadSettings = useCallback(async () => { try { const result = await adminApi.getInfrastructureAlertThresholds(); setSettings(result); setDraftThresholds(result.thresholds); setSettingsError(''); } catch (reason) { setSettingsError(reason instanceof Error ? reason.message : '告警阈值加载失败'); } }, []); const saveSettings = useCallback(async () => { if (!settings) return; setSavingSettings(true); setSettingsError(''); try { const result = await adminApi.updateInfrastructureAlertThresholds({ configVersion: settings.configVersion, thresholds: draftThresholds }); setSettings(result); setDraftThresholds(result.thresholds); setShowSettings(false); window.dispatchEvent(new Event('cmpp-infrastructure-alert-count-refresh')); await loadData(true); } catch (reason) { setSettingsError(reason instanceof Error ? reason.message : '告警阈值保存失败'); } finally { setSavingSettings(false); } }, [draftThresholds, loadData, settings]); const markAlertRead = useCallback(async (alert: InfrastructureAlert) => { setReadingFingerprint(alert.fingerprint); setReadError(''); try { const result = await adminApi.markInfrastructureAlertRead(alert.fingerprint, alert.startedAt); setOverview((current) => current ? { ...current, alerts: current.alerts.map((item) => item.fingerprint === result.fingerprint && Date.parse(item.startedAt) === Date.parse(result.activeAt) ? { ...item, acknowledged: true, acknowledgedAt: result.acknowledgedAt } : item), } : current); window.dispatchEvent(new Event('cmpp-infrastructure-alert-count-refresh')); } catch (reason) { setReadError(reason instanceof Error ? reason.message : '活动告警标记已读失败'); } finally { setReadingFingerprint(''); } }, []); useEffect(() => { void loadData(true); void loadSettings(); const intervalId = window.setInterval(() => { if (document.visibilityState === 'visible') void loadData(); }, 30_000); const handleVisibility = () => { if (document.visibilityState === 'visible') void loadData(); }; document.addEventListener('visibilitychange', handleVisibility); return () => { requestSequence.current += 1; window.clearInterval(intervalId); document.removeEventListener('visibilitychange', handleVisibility); }; }, [loadData, loadSettings]); const status = STATUS_COPY[overview?.summary.overallStatus ?? 'unknown']; const cpuOption = useMemo(() => makeTrendOption({ range, maximum: 100, suffix: '%', series: [{ name: 'CPU', points: overview?.trends.cpuUsagePercent ?? [], color: '#2563eb' }], }), [overview?.trends.cpuUsagePercent, range]); const memoryOption = useMemo(() => makeTrendOption({ range, maximum: 100, suffix: '%', series: [{ name: '内存', points: overview?.trends.memoryUsagePercent ?? [], color: '#7c3aed' }], }), [overview?.trends.memoryUsagePercent, range]); const diskOption = useMemo(() => makeTrendOption({ range, maximum: 100, suffix: '%', series: (overview?.disks ?? []).map((disk, index) => ({ name: `${disk.mountpoint} · ${disk.device} · ${disk.instance}`, points: disk.trend, color: ['#d97706', '#2563eb', '#0f766e', '#7c3aed', '#dc2626', '#0891b2'][index % 6], })), }), [overview?.disks, range]); const networkOption = useMemo(() => makeTrendOption({ range, suffix: ' B/s', series: [ { name: '接收', points: overview?.trends.networkReceiveBytesPerSecond ?? [], color: '#0f766e' }, { name: '发送', points: overview?.trends.networkTransmitBytesPerSecond ?? [], color: '#2563eb' }, ], }), [overview?.trends.networkReceiveBytesPerSecond, overview?.trends.networkTransmitBytesPerSecond, range]); const metrics = overview?.metrics; const serviceHealthy = overview?.summary.serviceHealthy ?? 0; const serviceTotal = overview?.summary.serviceTotal ?? 6; const alertColumns = useMemo(() => makeAlertColumns((alert) => { void markAlertRead(alert); }, readingFingerprint), [markAlertRead, readingFingerprint]); return (

服务器资源、核心服务与活动告警,数据由 Prometheus 采集与计算

{status.label}
{RANGE_OPTIONS.map((option) => ( ))}
{error ? (
监控数据不可用{error}。页面不会展示历史缓存值。
) : null}
{overview?.summary.overallStatus === 'healthy' ? : }
平台基础设施 {status.label} 最新采样 {formatTime(overview?.lastSampleAt ?? null)}
核心服务{serviceHealthy}/{serviceTotal}正常运行
活动告警{overview?.summary.activeAlerts ?? 0}{overview?.summary.criticalAlerts ?? 0} 严重 · {overview?.summary.warningAlerts ?? 0} 警告
系统负载{metrics?.load1 === null || metrics?.load1 === undefined ? '—' : metrics.load1.toFixed(2)}最近1分钟
持续运行{formatUptime(metrics?.uptimeSeconds ?? null)}主机启动后
CPU 使用率{formatPercent(metrics?.cpuUsagePercent ?? null)}5分钟平均
内存使用率{formatPercent(metrics?.memoryUsagePercent ?? null)}{formatBytes(metrics?.memoryAvailableBytes ?? null)} 可用 / {formatBytes(metrics?.memoryTotalBytes ?? null)}
{(overview?.disks ?? []).map((disk) =>
{disk.mountpoint === '/' ? '系统盘' : '磁盘'} {disk.mountpoint}{formatPercent(disk.usagePercent)}{disk.device} · {disk.filesystem}{formatBytes(disk.availableBytes)} 可用 / {formatBytes(disk.totalBytes)}
)} {!overview?.disks?.length ?
磁盘暂无数据
: null}
网络吞吐{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}接收 {formatRate(metrics?.networkReceiveBytesPerSecond ?? null)} · 发送 {formatRate(metrics?.networkTransmitBytesPerSecond ?? null)}
CPU 趋势
{formatPercent(metrics?.cpuUsagePercent ?? null)}
{overview?.trends.cpuUsagePercent.length ? : }
内存趋势
{formatPercent(metrics?.memoryUsagePercent ?? null)}
{overview?.trends.memoryUsagePercent.length ? : }
全部磁盘趋势
{overview?.disks?.length ?? 0} 个挂载点
{overview?.disks?.some((disk) => disk.trend.length) ? : }
网络趋势
{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}
{overview?.trends.networkReceiveBytesPerSecond.length ? : }
服务关键指标
固定低基数聚合,不含手机号、短信ID或SQL文本
{(overview?.serviceMetrics ?? []).map((group) => (
{group.name}{group.available ? '已采集' : '待采集'}
{group.metrics.length ? group.metrics.map((metric) =>
{metric.label}{formatServiceMetric(metric.value, metric.unit)}
) :
已监控服务可用性,待原生容量指标接入
}
))}
活动告警{overview?.summary.activeAlerts ?? 0}
刷新于 {formatTime(overview?.collectedAt ?? null)}
{readError ?
标记已读失败{readError}
: null} } onClose={() => setShowSettings(false)} open={showSettings} title="Prometheus 告警阈值设置">
仅允许修改固定指标的数值阈值。保存时先由 promtool 校验,再原子替换规则并热加载 Prometheus。
{settings?.definitions.map((definition) => (
{definition.label}单位:{definition.unit}
setDraftThresholds((current) => ({ ...current, [definition.key]: { ...current[definition.key], warning: Number(event.target.value) } }))} step={definition.step} type="number" value={draftThresholds[definition.key]?.warning ?? ''} /> setDraftThresholds((current) => ({ ...current, [definition.key]: { ...current[definition.key], critical: Number(event.target.value) } }))} step={definition.step} type="number" value={draftThresholds[definition.key]?.critical ?? ''} />
))} {settings?.applyStatus === 'failed' ?
上次应用失败{settings.lastError}
: null} {settingsError ?
阈值配置不可用{settingsError}
: null}
); } function EmptyChart() { return
暂无真实趋势指标
; }