Files
NetMesh/components/monitoring/MonitoringDetailPanel.tsx
zhaolei c64e00d835 feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
2026-10-09 10:37:48 +08:00

457 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import {
X,
Server,
Cpu,
HardDrive,
Activity,
AlertTriangle,
CheckCircle2,
XCircle,
Clock,
Loader2,
ChevronRight,
} from 'lucide-react';
// ============ 类型定义 ============
export type PanelType =
| 'deviceList' // 设备列表(按状态筛选)
| 'alertList' // 告警列表(按级别筛选)
| 'deviceDetail' // 单设备详情
| 'taskList'; // 巡检任务列表(按状态筛选)
export interface DetailPanelProps {
open: boolean;
onClose: () => void;
panelType: PanelType;
panelTitle: string;
// 设备列表
deviceList?: Array<{
id: string;
name: string;
ip?: string;
type: string;
status: 'online' | 'offline' | 'warning' | 'critical';
cpu?: number;
memory?: number;
lastUpdate?: number;
alertCount?: number;
}>;
// 告警列表
alertList?: Array<{
id: string;
deviceId: string;
deviceName: string;
level: 'critical' | 'warning' | 'info';
message: string;
time: string;
metric?: string;
metricValue?: string;
threshold?: string;
}>;
// 设备详情
deviceDetail?: {
id: string;
name: string;
ip?: string;
type: string;
status: 'online' | 'offline' | 'warning' | 'critical';
cpu?: number;
memory?: number;
disk?: Array<{ mount: string; usage: number }>;
lastUpdate?: number;
alerts?: Array<{
id: string;
level: string;
message: string;
time: string;
}>;
};
// 任务列表
taskList?: Array<{
deviceId: string;
deviceName: string;
ip: string;
status: 'running' | 'success' | 'failed' | 'pending';
lastScanAt?: number;
durationMs?: number;
errorMessage?: string;
interval?: number;
}>;
// 点击设备回调
onDeviceClick?: (deviceId: string) => void;
// 点击告警回调
onAlertClick?: (alertId: string) => void;
}
// ============ 辅助函数 ============
const statusColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
online: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '在线' },
offline: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '离线' },
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
running: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '执行中' },
success: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '成功' },
failed: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '失败' },
pending: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '待执行' },
};
const levelColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
major: { bg: 'bg-orange-500/15', text: 'text-orange-400', dot: 'bg-orange-500', label: '重要' },
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
minor: { bg: 'bg-yellow-500/15', text: 'text-yellow-400', dot: 'bg-yellow-500', label: '次要' },
info: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '提示' },
};
function formatRelativeTime(ts?: number): string {
if (!ts) return '-';
const diff = Date.now() - ts;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
return `${Math.floor(diff / 86400000)} 天前`;
}
// ============ 组件 ============
export default function MonitoringDetailPanel({
open,
onClose,
panelType,
panelTitle,
deviceList = [],
alertList = [],
deviceDetail,
taskList = [],
onDeviceClick,
}: DetailPanelProps) {
// ============ 设备列表渲染 ============
const renderDeviceList = () => (
<div className="space-y-2">
{deviceList.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
)}
{deviceList.map((d) => {
const sc = statusColors[d.status] || statusColors.online;
return (
<div
key={d.id}
className="p-3 rounded-lg border cursor-pointer transition-all hover:border-cyan-500/40 hover:bg-cyan-500/5"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
onClick={() => onDeviceClick?.(d.id)}
>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full ${sc.dot} flex-shrink-0`} />
<span className="text-sm font-medium text-foreground truncate">{d.name}</span>
</div>
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{d.ip && <span>{d.ip}</span>}
<span>{d.type}</span>
<span className={`${sc.text}`}>{sc.label}</span>
</div>
{(d.cpu !== undefined || d.memory !== undefined) && (
<div className="flex items-center gap-4 mt-2 text-xs">
{d.cpu !== undefined && (
<div className="flex items-center gap-1">
<Cpu className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">CPU</span>
<span className={d.cpu > 80 ? 'text-red-400' : d.cpu > 60 ? 'text-amber-400' : 'text-emerald-400'}>
{d.cpu}%
</span>
</div>
)}
{d.memory !== undefined && (
<div className="flex items-center gap-1">
<HardDrive className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">内存</span>
<span className={d.memory > 85 ? 'text-red-400' : d.memory > 70 ? 'text-amber-400' : 'text-emerald-400'}>
{d.memory}%
</span>
</div>
)}
</div>
)}
</div>
);
})}
</div>
);
// ============ 告警列表渲染 ============
const renderAlertList = () => (
<div className="space-y-2">
{alertList.length === 0 && (
<div className="text-center py-8 text-emerald-400 text-sm">暂无告警</div>
)}
{alertList.map((a) => {
const lc = levelColors[a.level] || levelColors.info;
return (
<div
key={a.id}
className="p-3 rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
>
<div className="flex items-start justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full ${lc.dot} flex-shrink-0 mt-0.5`} />
<span className={`text-xs font-medium ${lc.text} flex-shrink-0`}>{lc.label}</span>
</div>
<span className="text-xs text-muted-foreground flex-shrink-0">{a.time}</span>
</div>
<div className="text-sm text-foreground mb-1">{a.message}</div>
<div className="text-xs text-muted-foreground">
设备:<span className="text-cyan-400">{a.deviceName}</span>
{a.metric && a.metricValue && (
<span className="ml-3">
{a.metric}: {a.metricValue}
{a.threshold && <span className="text-muted-foreground"> (阈值 {a.threshold})</span>}
</span>
)}
</div>
</div>
);
})}
</div>
);
// ============ 设备详情渲染 ============
const renderDeviceDetail = () => {
if (!deviceDetail) return null;
const sc = statusColors[deviceDetail.status] || statusColors.online;
return (
<div className="space-y-4">
{/* 基本信息 */}
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-2">
<Server className="w-4 h-4 text-cyan-400" />
<span className="text-sm font-medium">基本信息</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div className="text-muted-foreground mb-0.5">设备名称</div>
<div className="text-foreground font-medium">{deviceDetail.name}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">设备类型</div>
<div className="text-foreground">{deviceDetail.type}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">IP 地址</div>
<div className="text-foreground font-mono">{deviceDetail.ip || '-'}</div>
</div>
<div>
<div className="text-muted-foreground mb-0.5">运行状态</div>
<div className={`${sc.text} font-medium`}>{sc.label}</div>
</div>
</div>
</div>
{/* 资源使用 */}
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-cyan-400" />
<span className="text-sm font-medium">资源使用</span>
</div>
<div className="space-y-3">
{/* CPU */}
<div>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">CPU 使用率</span>
<span className={deviceDetail.cpu && deviceDetail.cpu > 80 ? 'text-red-400' : 'text-foreground'}>
{deviceDetail.cpu ?? '-'}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${deviceDetail.cpu ?? 0}%`,
background: deviceDetail.cpu && deviceDetail.cpu > 80 ? '#f87171'
: deviceDetail.cpu && deviceDetail.cpu > 60 ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
{/* 内存 */}
<div>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">内存使用率</span>
<span className={deviceDetail.memory && deviceDetail.memory > 85 ? 'text-red-400' : 'text-foreground'}>
{deviceDetail.memory ?? '-'}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${deviceDetail.memory ?? 0}%`,
background: deviceDetail.memory && deviceDetail.memory > 85 ? '#f87171'
: deviceDetail.memory && deviceDetail.memory > 70 ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
{/* 磁盘 */}
{deviceDetail.disk && deviceDetail.disk.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-muted-foreground">磁盘使用率</div>
{deviceDetail.disk.map((d, i) => (
<div key={i}>
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground font-mono">{d.mount}</span>
<span className={d.usage > 90 ? 'text-red-400' : d.usage > 80 ? 'text-amber-400' : 'text-foreground'}>
{d.usage}%
</span>
</div>
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${d.usage}%`,
background: d.usage > 90 ? '#f87171' : d.usage > 80 ? '#fbbf24' : '#34d399',
}}
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* 更新时间 */}
<div className="text-xs text-muted-foreground flex items-center gap-1.5">
<Clock className="w-3 h-3" />
最后更新:{formatRelativeTime(deviceDetail.lastUpdate)}
</div>
{/* 告警历史 */}
{deviceDetail.alerts && deviceDetail.alerts.length > 0 && (
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-amber-400" />
<span className="text-sm font-medium">告警记录</span>
</div>
<div className="space-y-2">
{deviceDetail.alerts.map((a) => {
const lc = levelColors[a.level] || levelColors.info;
return (
<div key={a.id} className="flex items-start gap-2 text-xs">
<div className={`w-1.5 h-1.5 rounded-full ${lc.dot} flex-shrink-0 mt-1`} />
<div className="flex-1 min-w-0">
<div className="text-foreground">{a.message}</div>
<div className="text-muted-foreground mt-0.5">{a.time}</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
// ============ 任务列表渲染 ============
const renderTaskList = () => (
<div className="space-y-2">
{taskList.length === 0 && (
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
)}
{taskList.map((t) => {
const sc = statusColors[t.status] || statusColors.pending;
const durationStr = t.durationMs ? `${(t.durationMs / 1000).toFixed(1)}s` : null;
const intervalStr = t.interval ? `${t.interval}秒` : null;
return (
<div
key={t.deviceId}
className="p-3 rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 min-w-0">
{t.status === 'running' && <Loader2 className="w-3.5 h-3.5 text-sky-400 animate-spin flex-shrink-0" />}
{t.status === 'success' && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 flex-shrink-0" />}
{t.status === 'failed' && <XCircle className="w-3.5 h-3.5 text-red-400 flex-shrink-0" />}
{t.status === 'pending' && <Clock className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />}
<span className="text-sm font-medium text-foreground truncate">{t.deviceName}</span>
</div>
<span className={`text-xs font-medium ${sc.text} flex-shrink-0`}>{sc.label}</span>
</div>
<div className="text-xs text-muted-foreground font-mono mb-1">{t.ip}</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
{t.lastScanAt && <span>上次 {formatRelativeTime(t.lastScanAt)}</span>}
{durationStr && <span>耗时 {durationStr}</span>}
{intervalStr && <span>间隔 {intervalStr}</span>}
</div>
{t.errorMessage && (
<div className="mt-2 text-xs text-red-400 bg-red-500/10 p-2 rounded">
{t.errorMessage}
</div>
)}
</div>
);
})}
</div>
);
// ============ 主渲染 ============
return (
<>
{/* 遮罩 */}
<div
className={`absolute inset-0 bg-black/50 z-40 transition-opacity ${
open ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
onClick={onClose}
/>
{/* 面板 */}
<div
className={`absolute right-0 top-0 bottom-0 z-50 transition-transform duration-300 ease-out flex flex-col ${
open ? 'translate-x-0' : 'translate-x-full'
}`}
style={{
width: '420px',
background: '#0f172a',
borderLeft: '1px solid rgba(148,163,184,0.2)',
}}
>
{/* 标题栏 */}
<div
className="flex items-center justify-between px-4 py-3 flex-shrink-0"
style={{ borderBottom: '1px solid rgba(148,163,184,0.15)' }}
>
<div className="flex items-center gap-2">
<div
className="w-1 h-5 rounded-full"
style={{ background: 'linear-gradient(180deg, #06b6d4, #3b82f6)' }}
/>
<span className="text-sm font-medium text-foreground">{panelTitle}</span>
</div>
<button
onClick={onClose}
className="p-1 rounded hover:bg-slate-700/50 text-muted-foreground hover:text-foreground transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 内容区 */}
<div className="flex-1 overflow-y-auto p-3">
{panelType === 'deviceList' && renderDeviceList()}
{panelType === 'alertList' && renderAlertList()}
{panelType === 'deviceDetail' && renderDeviceDetail()}
{panelType === 'taskList' && renderTaskList()}
</div>
</div>
</>
);
}