feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
This commit is contained in:
456
components/monitoring/MonitoringDetailPanel.tsx
Normal file
456
components/monitoring/MonitoringDetailPanel.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
2409
components/monitoring/MonitoringTopologyView.tsx
Normal file
2409
components/monitoring/MonitoringTopologyView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
637
components/monitoring/deviceModels.ts
Normal file
637
components/monitoring/deviceModels.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
/**
|
||||
* 精细 3D 设备模型工厂
|
||||
* 参考 ITYUNWEI modelFactory.ts 设计
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
|
||||
export type DeviceStatus = 'normal' | 'warning' | 'critical' | 'offline';
|
||||
export type DeviceType = 'server' | 'switch' | 'router' | 'firewall' | 'storage' | 'workstation';
|
||||
|
||||
const statusColors: Record<DeviceStatus, number> = {
|
||||
normal: 0x10b981,
|
||||
warning: 0xf59e0b,
|
||||
critical: 0xef4444,
|
||||
offline: 0x6b7280,
|
||||
};
|
||||
|
||||
// 不同设备类型的默认发光颜色
|
||||
const typeColors: Record<DeviceType, number> = {
|
||||
server: 0x60a5fa, // 蓝色 - 服务器
|
||||
switch: 0x22d3ee, // 青色 - 交换机
|
||||
router: 0xa78bfa, // 紫色 - 路由器
|
||||
firewall: 0xf87171, // 红色 - 防火墙
|
||||
storage: 0xfbbf24, // 黄色 - 存储
|
||||
workstation: 0x34d399, // 绿色 - 工作站
|
||||
};
|
||||
|
||||
/** 创建设备基础材质:金属外壳 + 状态发光 */
|
||||
function createBodyMaterial(status: DeviceStatus, type?: DeviceType): THREE.MeshStandardMaterial {
|
||||
// 根据状态和类型决定发光颜色
|
||||
let emissiveColor = statusColors[status];
|
||||
if (status === 'normal' && type) {
|
||||
emissiveColor = typeColors[type] || emissiveColor;
|
||||
}
|
||||
|
||||
const intensity = status === 'normal' ? 0.5 : status === 'warning' ? 0.7 : status === 'critical' ? 1.0 : 0.2;
|
||||
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x2c3e5a,
|
||||
metalness: 0.55,
|
||||
roughness: 0.35,
|
||||
emissive: emissiveColor,
|
||||
emissiveIntensity: intensity,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
|
||||
/** LED 指示灯材质 */
|
||||
function createLedMaterial(color: number, intensity = 0.8): THREE.MeshStandardMaterial {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
emissive: color,
|
||||
emissiveIntensity: intensity,
|
||||
metalness: 0.2,
|
||||
roughness: 0.3,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
|
||||
/** 前面板塑料材质 */
|
||||
function createPanelMaterial(): THREE.MeshStandardMaterial {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x334155,
|
||||
metalness: 0.3,
|
||||
roughness: 0.6,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
|
||||
/** 端口金属材质 */
|
||||
function createPortMaterial(): THREE.MeshStandardMaterial {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x0f172a,
|
||||
metalness: 0.9,
|
||||
roughness: 0.2,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1. 服务器 Server - 机架式服务器
|
||||
// ============================================================
|
||||
export function createServerModel(status: DeviceStatus, scale = 1.5): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const bodyMat = createBodyMaterial(status, 'server');
|
||||
const panelMat = createPanelMaterial();
|
||||
|
||||
// 主体机箱(扁平长方体)
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.6, scale * 0.5, scale * 1.0),
|
||||
bodyMat
|
||||
);
|
||||
body.userData.isBody = true;
|
||||
group.add(body);
|
||||
|
||||
// 前面板
|
||||
const frontPanel = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.62, scale * 0.46, scale * 0.04),
|
||||
panelMat
|
||||
);
|
||||
frontPanel.position.z = scale * 0.52;
|
||||
group.add(frontPanel);
|
||||
|
||||
// 前面板 - 硬盘槽位(4个)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const tray = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.3, scale * 0.35, scale * 0.02),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.5, roughness: 0.5 })
|
||||
);
|
||||
tray.position.set(-scale * 0.55 + i * scale * 0.37, 0, scale * 0.54);
|
||||
group.add(tray);
|
||||
|
||||
// 硬盘槽位 LED 灯
|
||||
const ledColor = i === 0 ? statusColors.normal : i === 1 ? statusColors.warning : 0x475569;
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.02, 8, 8),
|
||||
createLedMaterial(ledColor, i < 2 ? 1.0 : 0.5)
|
||||
);
|
||||
led.position.set(-scale * 0.55 + i * scale * 0.37, scale * 0.12, scale * 0.56);
|
||||
group.add(led);
|
||||
}
|
||||
|
||||
// 服务器状态 LED 灯(右上角)
|
||||
const powerLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.025, 10, 10),
|
||||
createLedMaterial(statusColors[status], 1.2)
|
||||
);
|
||||
powerLed.position.set(scale * 0.68, scale * 0.18, scale * 0.56);
|
||||
group.add(powerLed);
|
||||
|
||||
const statusLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.02, 10, 10),
|
||||
createLedMaterial(0x22d3ee, 1.0)
|
||||
);
|
||||
statusLed.position.set(scale * 0.72, scale * 0.18, scale * 0.56);
|
||||
group.add(statusLed);
|
||||
|
||||
// 顶部散热格栅
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const fin = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.2, 0.01, 0.03),
|
||||
new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.8, roughness: 0.3 })
|
||||
);
|
||||
fin.position.set(0, scale * 0.26, -scale * 0.3 + i * scale * 0.12);
|
||||
group.add(fin);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2. 交换机 Switch - 扁平机架式 + 端口阵列
|
||||
// ============================================================
|
||||
export function createSwitchModel(status: DeviceStatus, scale = 1.8): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const bodyMat = createBodyMaterial(status, 'switch');
|
||||
const panelMat = createPanelMaterial();
|
||||
const portMat = createPortMaterial();
|
||||
const earMat = new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.8, roughness: 0.3 });
|
||||
|
||||
// 主体机箱(非常扁平)
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.8, scale * 0.28, scale * 0.8),
|
||||
bodyMat
|
||||
);
|
||||
body.userData.isBody = true;
|
||||
group.add(body);
|
||||
|
||||
// 前面板
|
||||
const frontPanel = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.82, scale * 0.24, scale * 0.03),
|
||||
panelMat
|
||||
);
|
||||
frontPanel.position.z = scale * 0.41;
|
||||
group.add(frontPanel);
|
||||
|
||||
// 机架耳
|
||||
const leftEar = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.12, scale * 0.22, scale * 0.15),
|
||||
earMat
|
||||
);
|
||||
leftEar.position.set(-scale * 0.97, 0, 0);
|
||||
group.add(leftEar);
|
||||
const rightEar = leftEar.clone();
|
||||
rightEar.position.x = scale * 0.97;
|
||||
group.add(rightEar);
|
||||
|
||||
// 端口阵列(24口,2行 x 12列)
|
||||
const portSize = scale * 0.06;
|
||||
const portGap = scale * 0.08;
|
||||
for (let row = 0; row < 2; row++) {
|
||||
for (let col = 0; col < 12; col++) {
|
||||
const port = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(portSize, portSize, scale * 0.05),
|
||||
portMat
|
||||
);
|
||||
const startX = -scale * 0.72 + col * portGap;
|
||||
const y = row === 0 ? scale * 0.06 : -scale * 0.06;
|
||||
port.position.set(startX, y, scale * 0.44);
|
||||
group.add(port);
|
||||
|
||||
// 端口上方的小 LED(亮绿色)
|
||||
const portLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.012, 6, 6),
|
||||
createLedMaterial(0x22c55e, 0.8)
|
||||
);
|
||||
portLed.position.set(startX, y + scale * 0.05, scale * 0.47);
|
||||
group.add(portLed);
|
||||
}
|
||||
}
|
||||
|
||||
// 品牌区域
|
||||
const brandArea = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.3, scale * 0.08, 0.035),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.5, roughness: 0.5 })
|
||||
);
|
||||
brandArea.position.set(-scale * 0.82, scale * 0.12, scale * 0.44);
|
||||
group.add(brandArea);
|
||||
|
||||
// 状态 LED 灯组
|
||||
const ledPositions = [
|
||||
{ color: statusColors[status], x: scale * 0.82, y: scale * 0.08 },
|
||||
{ color: 0x22d3ee, x: scale * 0.87, y: scale * 0.08 },
|
||||
{ color: 0x22c55e, x: scale * 0.82, y: -scale * 0.08 },
|
||||
{ color: 0xf59e0b, x: scale * 0.87, y: -scale * 0.08 },
|
||||
];
|
||||
ledPositions.forEach(({ color, x, y }) => {
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.018, 8, 8),
|
||||
createLedMaterial(color, 1.0)
|
||||
);
|
||||
led.position.set(x, y, scale * 0.44);
|
||||
group.add(led);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. 路由器 Router - 1U 机架式 + 天线 + 端口阵列
|
||||
// ============================================================
|
||||
export function createRouterModel(status: DeviceStatus, scale = 1.8): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const bodyMat = createBodyMaterial(status, 'router');
|
||||
const panelMat = createPanelMaterial();
|
||||
const portMat = createPortMaterial();
|
||||
const earMat = new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.8, roughness: 0.3 });
|
||||
|
||||
// 1U 主体机箱
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.6, scale * 0.35, scale * 0.9),
|
||||
bodyMat
|
||||
);
|
||||
body.userData.isBody = true;
|
||||
group.add(body);
|
||||
|
||||
// 前面板
|
||||
const frontPanel = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.62, scale * 0.31, scale * 0.03),
|
||||
panelMat
|
||||
);
|
||||
frontPanel.position.z = scale * 0.47;
|
||||
group.add(frontPanel);
|
||||
|
||||
// 机架耳
|
||||
const leftEar = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.1, scale * 0.28, scale * 0.12),
|
||||
earMat
|
||||
);
|
||||
leftEar.position.set(-scale * 0.86, 0, 0);
|
||||
group.add(leftEar);
|
||||
const rightEar = leftEar.clone();
|
||||
rightEar.position.x = scale * 0.86;
|
||||
group.add(rightEar);
|
||||
|
||||
// 品牌区域 + 状态 LED
|
||||
const brandArea = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.22, scale * 0.07, 0.035),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.6, roughness: 0.4 })
|
||||
);
|
||||
brandArea.position.set(-scale * 0.65, scale * 0.1, scale * 0.49);
|
||||
group.add(brandArea);
|
||||
|
||||
// 状态 LED 组
|
||||
const routerLeds = [
|
||||
{ color: statusColors[status], x: -scale * 0.38 },
|
||||
{ color: 0x22d3ee, x: -scale * 0.33 },
|
||||
{ color: 0x22c55e, x: -scale * 0.28 },
|
||||
{ color: 0xf59e0b, x: -scale * 0.23 },
|
||||
];
|
||||
routerLeds.forEach(({ color, x }) => {
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.018, 8, 8),
|
||||
createLedMaterial(color, 1.0)
|
||||
);
|
||||
led.position.set(x, scale * 0.1, scale * 0.49);
|
||||
group.add(led);
|
||||
});
|
||||
|
||||
// WAN 口(光纤口样式)
|
||||
const sfpMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.7, roughness: 0.3 });
|
||||
const wanPort = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.12, scale * 0.14, scale * 0.06),
|
||||
sfpMat
|
||||
);
|
||||
wanPort.position.set(-scale * 0.05, -scale * 0.02, scale * 0.5);
|
||||
group.add(wanPort);
|
||||
const wanRing = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(scale * 0.045, 0.008, 6, 16),
|
||||
new THREE.MeshStandardMaterial({ color: 0xfbbf24, emissive: 0xfbbf24, emissiveIntensity: 0.6, metalness: 0.5, roughness: 0.3 })
|
||||
);
|
||||
wanRing.rotation.x = Math.PI / 2;
|
||||
wanRing.position.set(-scale * 0.05, scale * 0.08, scale * 0.53);
|
||||
group.add(wanRing);
|
||||
|
||||
// LAN 口阵列(4个)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const port = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.1, scale * 0.13, scale * 0.05),
|
||||
portMat
|
||||
);
|
||||
port.position.set(scale * 0.2 + i * scale * 0.13, -scale * 0.02, scale * 0.5);
|
||||
group.add(port);
|
||||
|
||||
const lanLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.012, 6, 6),
|
||||
createLedMaterial(0x22c55e, 0.9)
|
||||
);
|
||||
lanLed.position.set(scale * 0.2 + i * scale * 0.13, -scale * 0.11, scale * 0.52);
|
||||
group.add(lanLed);
|
||||
}
|
||||
|
||||
// 天线
|
||||
const antennaMat = new THREE.MeshStandardMaterial({ color: 0x64748b, metalness: 0.9, roughness: 0.2 });
|
||||
// 天线 1 - 左侧直立
|
||||
const ant1 = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.012, 0.015, scale * 0.7, 8),
|
||||
antennaMat
|
||||
);
|
||||
ant1.position.set(-scale * 0.55, scale * 0.5, -scale * 0.1);
|
||||
group.add(ant1);
|
||||
const ant1Tip = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.022, 10, 10),
|
||||
createLedMaterial(0x22d3ee, 1.2)
|
||||
);
|
||||
ant1Tip.position.set(-scale * 0.55, scale * 0.87, -scale * 0.1);
|
||||
group.add(ant1Tip);
|
||||
|
||||
// 天线 2 - 右侧略向后倾斜
|
||||
const ant2 = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.012, 0.015, scale * 0.65, 8),
|
||||
antennaMat
|
||||
);
|
||||
ant2.position.set(scale * 0.55, scale * 0.48, -scale * 0.15);
|
||||
ant2.rotation.x = 0.15;
|
||||
ant2.rotation.z = -0.1;
|
||||
group.add(ant2);
|
||||
const ant2Tip = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.022, 10, 10),
|
||||
createLedMaterial(0x22d3ee, 1.2)
|
||||
);
|
||||
ant2Tip.position.set(scale * 0.52, scale * 0.83, -scale * 0.2);
|
||||
group.add(ant2Tip);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 4. 防火墙 Firewall - 1U 机架式 + LCD 屏 + 盾形浮雕
|
||||
// ============================================================
|
||||
export function createFirewallModel(status: DeviceStatus, scale = 1.8): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const bodyMat = createBodyMaterial(status, 'firewall');
|
||||
const panelMat = createPanelMaterial();
|
||||
const portMat = createPortMaterial();
|
||||
const earMat = new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.8, roughness: 0.3 });
|
||||
|
||||
// 1U 主体机箱
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.8, scale * 0.36, scale * 0.9),
|
||||
bodyMat
|
||||
);
|
||||
body.userData.isBody = true;
|
||||
group.add(body);
|
||||
|
||||
// 前面板
|
||||
const frontPanel = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.82, scale * 0.32, scale * 0.03),
|
||||
panelMat
|
||||
);
|
||||
frontPanel.position.z = scale * 0.48;
|
||||
group.add(frontPanel);
|
||||
|
||||
// 机架耳
|
||||
const leftEar = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.1, scale * 0.29, scale * 0.12),
|
||||
earMat
|
||||
);
|
||||
leftEar.position.set(-scale * 0.97, 0, 0);
|
||||
group.add(leftEar);
|
||||
const rightEar = leftEar.clone();
|
||||
rightEar.position.x = scale * 0.97;
|
||||
group.add(rightEar);
|
||||
|
||||
// 红色安全装饰条
|
||||
const redStrip = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.55, 0.018, 0.035),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0xef4444,
|
||||
emissive: 0xef4444,
|
||||
emissiveIntensity: 0.4,
|
||||
metalness: 0.3,
|
||||
roughness: 0.5,
|
||||
})
|
||||
);
|
||||
redStrip.position.set(0, scale * 0.15, scale * 0.5);
|
||||
group.add(redStrip);
|
||||
|
||||
// LCD 状态显示屏
|
||||
const lcdFrame = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.32, scale * 0.2, scale * 0.04),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.6, roughness: 0.4 })
|
||||
);
|
||||
lcdFrame.position.set(-scale * 0.55, -scale * 0.02, scale * 0.49);
|
||||
group.add(lcdFrame);
|
||||
|
||||
// LCD 显示面(发光蓝绿色)
|
||||
const lcdScreen = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.28, scale * 0.16, 0.01),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x0d9488,
|
||||
emissive: 0x14b8a6,
|
||||
emissiveIntensity: 0.7,
|
||||
metalness: 0.2,
|
||||
roughness: 0.3,
|
||||
})
|
||||
);
|
||||
lcdScreen.position.set(-scale * 0.55, -scale * 0.02, scale * 0.52);
|
||||
group.add(lcdScreen);
|
||||
|
||||
// 盾形浮雕
|
||||
const shieldEmbossMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x6366f1,
|
||||
metalness: 0.7,
|
||||
roughness: 0.3,
|
||||
emissive: 0x6366f1,
|
||||
emissiveIntensity: 0.35,
|
||||
});
|
||||
const shieldTop = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(scale * 0.08, scale * 0.14, 4),
|
||||
shieldEmbossMat
|
||||
);
|
||||
shieldTop.rotation.y = Math.PI / 4;
|
||||
shieldTop.position.set(-scale * 0.18, -scale * 0.03, scale * 0.5);
|
||||
group.add(shieldTop);
|
||||
const shieldBody = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.12, scale * 0.14, 0.02),
|
||||
shieldEmbossMat
|
||||
);
|
||||
shieldBody.position.set(-scale * 0.18, -scale * 0.13, scale * 0.5);
|
||||
group.add(shieldBody);
|
||||
|
||||
// 盾心
|
||||
const shieldCore = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.012, 8, 8),
|
||||
createLedMaterial(0x22d3ee, 1.2)
|
||||
);
|
||||
shieldCore.position.set(-scale * 0.18, -scale * 0.07, scale * 0.52);
|
||||
group.add(shieldCore);
|
||||
|
||||
// 状态 LED 组
|
||||
const fwLeds = [
|
||||
{ color: statusColors[status], x: scale * 0.02 },
|
||||
{ color: 0xef4444, x: scale * 0.08 },
|
||||
{ color: 0xf59e0b, x: scale * 0.14 },
|
||||
{ color: 0x22c55e, x: scale * 0.2 },
|
||||
];
|
||||
fwLeds.forEach(({ color, x }) => {
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.018, 8, 8),
|
||||
createLedMaterial(color, 1.0)
|
||||
);
|
||||
led.position.set(x, scale * 0.08, scale * 0.5);
|
||||
group.add(led);
|
||||
});
|
||||
|
||||
// SFP 口
|
||||
const sfpMat = new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.7, roughness: 0.3 });
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const sfp = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.1, scale * 0.12, scale * 0.06),
|
||||
sfpMat
|
||||
);
|
||||
sfp.position.set(scale * 0.45 + i * scale * 0.15, -scale * 0.02, scale * 0.5);
|
||||
group.add(sfp);
|
||||
|
||||
const sfpLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.012, 6, 6),
|
||||
createLedMaterial(0x22d3ee, 1.0)
|
||||
);
|
||||
sfpLed.position.set(scale * 0.45 + i * scale * 0.15, -scale * 0.12, scale * 0.52);
|
||||
group.add(sfpLed);
|
||||
}
|
||||
|
||||
// 网口
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const port = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.08, scale * 0.1, scale * 0.05),
|
||||
portMat
|
||||
);
|
||||
port.position.set(scale * 0.78 + i * scale * 0.11, -scale * 0.03, scale * 0.5);
|
||||
group.add(port);
|
||||
|
||||
const portLed = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.01, 6, 6),
|
||||
createLedMaterial(0x22c55e, 0.9)
|
||||
);
|
||||
portLed.position.set(scale * 0.78 + i * scale * 0.11, -scale * 0.11, scale * 0.52);
|
||||
group.add(portLed);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 5. 存储 Storage - 磁盘阵列
|
||||
// ============================================================
|
||||
export function createStorageModel(status: DeviceStatus, scale = 1.5): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const bodyMat = createBodyMaterial(status, 'storage');
|
||||
const panelMat = createPanelMaterial();
|
||||
|
||||
// 主体大型方体
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.4, scale * 0.9, scale * 1.1),
|
||||
bodyMat
|
||||
);
|
||||
body.userData.isBody = true;
|
||||
group.add(body);
|
||||
|
||||
// 前面板
|
||||
const frontPanel = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 1.42, scale * 0.82, scale * 0.04),
|
||||
panelMat
|
||||
);
|
||||
frontPanel.position.z = scale * 0.57;
|
||||
group.add(frontPanel);
|
||||
|
||||
// 磁盘槽位(3行 x 4列)
|
||||
const driveW = scale * 0.28;
|
||||
const driveH = scale * 0.18;
|
||||
const startX = -scale * 0.55;
|
||||
const startY = scale * 0.32;
|
||||
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 4; col++) {
|
||||
const drive = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(driveW, driveH, scale * 0.025),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: row === 2 && col >= 2 ? 0x0f172a : 0x1e293b,
|
||||
metalness: 0.4,
|
||||
roughness: 0.5,
|
||||
})
|
||||
);
|
||||
drive.position.set(
|
||||
startX + col * (driveW + 0.02),
|
||||
startY - row * (driveH + 0.04),
|
||||
scale * 0.6
|
||||
);
|
||||
group.add(drive);
|
||||
|
||||
// 磁盘 LED
|
||||
const isActive = !(row === 2 && col >= 2);
|
||||
const ledColor = isActive
|
||||
? row === 0 && col === 0
|
||||
? statusColors[status]
|
||||
: 0x22c55e
|
||||
: 0x475569;
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.015, 8, 8),
|
||||
createLedMaterial(ledColor, isActive ? 0.8 : 0.3)
|
||||
);
|
||||
led.position.set(
|
||||
startX + col * (driveW + 0.02) - driveW * 0.35,
|
||||
startY - row * (driveH + 0.04),
|
||||
scale * 0.62
|
||||
);
|
||||
group.add(led);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态区域
|
||||
const statusArea = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(scale * 0.3, scale * 0.1, scale * 0.045),
|
||||
new THREE.MeshStandardMaterial({ color: 0x0f172a, metalness: 0.6, roughness: 0.4 })
|
||||
);
|
||||
statusArea.position.set(-scale * 0.5, -scale * 0.32, scale * 0.59);
|
||||
group.add(statusArea);
|
||||
|
||||
const stLeds = [
|
||||
{ color: statusColors[status], x: -scale * 0.58 },
|
||||
{ color: 0x22d3ee, x: -scale * 0.53 },
|
||||
{ color: 0x22c55e, x: -scale * 0.48 },
|
||||
];
|
||||
stLeds.forEach(({ color, x }) => {
|
||||
const led = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.02, 10, 10),
|
||||
createLedMaterial(color, 1.0)
|
||||
);
|
||||
led.position.set(x, -scale * 0.32, scale * 0.61);
|
||||
group.add(led);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 统一的设备模型创建入口
|
||||
// ============================================================
|
||||
export function createDeviceModel(type: DeviceType, status: DeviceStatus, scale?: number): THREE.Group {
|
||||
switch (type) {
|
||||
case 'server':
|
||||
return createServerModel(status, scale ?? 1.5);
|
||||
case 'switch':
|
||||
return createSwitchModel(status, scale ?? 1.8);
|
||||
case 'router':
|
||||
return createRouterModel(status, scale ?? 2.0);
|
||||
case 'firewall':
|
||||
return createFirewallModel(status, scale ?? 2.0);
|
||||
case 'storage':
|
||||
return createStorageModel(status, scale ?? 1.6);
|
||||
case 'workstation':
|
||||
// 工作站用服务器模型代替(简化版)
|
||||
return createServerModel(status, scale ?? 1.4);
|
||||
default:
|
||||
return createServerModel(status, scale ?? 1.5);
|
||||
}
|
||||
}
|
||||
569
components/monitoring/useMonitoringData.ts
Normal file
569
components/monitoring/useMonitoringData.ts
Normal file
@@ -0,0 +1,569 @@
|
||||
/**
|
||||
* useMonitoringData - 运行监控大屏数据 hook
|
||||
*
|
||||
* 数据源(全部来自 NetMesh 真实 API):
|
||||
* - hosts: props 传入(vault 里的设备列表)
|
||||
* - inspection.getLatestAll() → 每台设备最新巡检结果(含 metrics:cpu/memory/online)
|
||||
* - inspectionAlertApi.getActive() → 活动告警列表
|
||||
* - inspectionAlertApi.getStats() → 告警统计
|
||||
* - inspectionRuleApi.list() → 巡检规则(用于任务状态统计)
|
||||
*/
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import type { Host } from '../../domain/models';
|
||||
import {
|
||||
inspectionResultApi,
|
||||
inspectionAlertApi,
|
||||
inspectionAlertRuleApi,
|
||||
inspectionSchedulerApi,
|
||||
inspectionDeviceApi,
|
||||
type AlertRecord,
|
||||
type InspectionResult,
|
||||
type AlertLevel,
|
||||
type AlertStats,
|
||||
} from '../../infrastructure/inspection/inspectionApi';
|
||||
|
||||
// ===== 对外类型 =====
|
||||
|
||||
export interface MonitoringAlert {
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
level: 'critical' | 'major' | 'warning' | 'minor' | 'info';
|
||||
message: string;
|
||||
time: string;
|
||||
metric?: string;
|
||||
metricValue?: string;
|
||||
threshold?: string;
|
||||
}
|
||||
|
||||
export interface TrendPoint {
|
||||
date: string; // MM-DD
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface TaskStats {
|
||||
running: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
pending: number;
|
||||
}
|
||||
|
||||
export interface TaskDevice {
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
ip: string;
|
||||
status: 'running' | 'success' | 'failed' | 'pending';
|
||||
lastScanAt?: number;
|
||||
durationMs?: number;
|
||||
errorMessage?: string;
|
||||
interval?: number; // 巡检间隔(秒)
|
||||
}
|
||||
|
||||
interface UseMonitoringDataResult {
|
||||
// 指标缓存
|
||||
cpuMap: Record<string, number>;
|
||||
memMap: Record<string, number>;
|
||||
onlineMap: Record<string, boolean>;
|
||||
timestampMap: Record<string, number>; // 每台设备最新巡检时间戳
|
||||
|
||||
// 告警
|
||||
alerts: MonitoringAlert[];
|
||||
alarmTrend: TrendPoint[];
|
||||
alertStats: AlertStats | null;
|
||||
|
||||
// 巡检任务
|
||||
taskStats: TaskStats;
|
||||
taskDevices: TaskDevice[];
|
||||
schedulerRunning: boolean;
|
||||
|
||||
// 操作
|
||||
refresh: () => Promise<void>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
// ===== localStorage 持久化(降级用,API 不可用时显示缓存) =====
|
||||
const LS_CPU = 'netmesh:monitoring:cpuMap';
|
||||
const LS_MEM = 'netmesh:monitoring:memMap';
|
||||
const LS_ALERTS = 'netmesh:monitoring:alerts';
|
||||
const LS_TREND = 'netmesh:monitoring:trend';
|
||||
|
||||
function readLS<T>(key: string, fallback: T): T {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLS(key: string, value: unknown) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
// 将 NetMesh AlertLevel 映射到监控页面的 5 级
|
||||
function mapLevel(l: AlertLevel): MonitoringAlert['level'] {
|
||||
if (l === 'critical') return 'critical';
|
||||
if (l === 'warning') return 'warning';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
// 格式化告警时间:今天 → HH:mm,昨天 → 昨天 HH:mm,更早 → MM-DD HH:mm
|
||||
function formatAlertTime(ts: number): string {
|
||||
const d = new Date(ts);
|
||||
const now = new Date();
|
||||
const isToday = d.toDateString() === now.toDateString();
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const isYesterday = d.toDateString() === yesterday.toDateString();
|
||||
const timeStr = d.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
||||
if (isToday) return timeStr;
|
||||
if (isYesterday) return `昨天 ${timeStr}`;
|
||||
const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
return `${ds} ${timeStr}`;
|
||||
}
|
||||
|
||||
// 从巡检结果 metrics 里提取 cpu 使用率(数字百分比)
|
||||
function extractCpu(metrics: Record<string, any>): number {
|
||||
if (!metrics) return 0;
|
||||
// NetMesh 巡检标准字段
|
||||
if (typeof metrics.cpu_usage === 'number' && !isNaN(metrics.cpu_usage)) return metrics.cpu_usage;
|
||||
if (typeof metrics.cpu_usage === 'string') {
|
||||
const n = parseFloat(metrics.cpu_usage);
|
||||
if (!isNaN(n)) return n;
|
||||
}
|
||||
// 网络设备
|
||||
if (metrics.cpuUtilization !== undefined) {
|
||||
const v = Number(metrics.cpuUtilization);
|
||||
if (!isNaN(v)) return v;
|
||||
}
|
||||
if (metrics.displayCpu?.cpuUsage !== undefined) {
|
||||
const v = Number(metrics.displayCpu.cpuUsage);
|
||||
if (!isNaN(v)) return v;
|
||||
}
|
||||
// 遍历所有字段找数值型 CPU 字段
|
||||
for (const key of Object.keys(metrics)) {
|
||||
const kl = key.toLowerCase();
|
||||
if (kl.includes('cpu') && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) {
|
||||
const v = metrics[key];
|
||||
if (typeof v === 'number' && !isNaN(v)) return v;
|
||||
if (typeof v === 'string') {
|
||||
const n = parseFloat(v);
|
||||
if (!isNaN(n)) return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 从巡检结果 metrics 里提取内存使用率
|
||||
function extractMem(metrics: Record<string, any>): number {
|
||||
if (!metrics) return 0;
|
||||
// NetMesh 巡检标准字段
|
||||
if (typeof metrics.memory_usage === 'number' && !isNaN(metrics.memory_usage)) return metrics.memory_usage;
|
||||
if (typeof metrics.memory_usage === 'string') {
|
||||
const n = parseFloat(metrics.memory_usage);
|
||||
if (!isNaN(n)) return n;
|
||||
}
|
||||
// 网络设备
|
||||
if (metrics.displayMemory?.memPercent !== undefined) {
|
||||
const v = Number(metrics.displayMemory.memPercent);
|
||||
if (!isNaN(v)) return v;
|
||||
}
|
||||
if (metrics.memoryPercent !== undefined) {
|
||||
const v = Number(metrics.memoryPercent);
|
||||
if (!isNaN(v)) return v;
|
||||
}
|
||||
if (metrics.mem_percent !== undefined) {
|
||||
const v = Number(metrics.mem_percent);
|
||||
if (!isNaN(v)) return v;
|
||||
}
|
||||
// 遍历所有字段找数值型内存字段
|
||||
for (const key of Object.keys(metrics)) {
|
||||
const kl = key.toLowerCase();
|
||||
if ((kl.includes('mem') || kl.includes('memory')) && (kl.includes('usage') || kl.includes('util') || kl.includes('percent'))) {
|
||||
const v = metrics[key];
|
||||
if (typeof v === 'number' && !isNaN(v)) return v;
|
||||
if (typeof v === 'string') {
|
||||
const n = parseFloat(v);
|
||||
if (!isNaN(n)) return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果有 mem_used 和 mem_total,计算百分比
|
||||
const used = metrics.mem_used ?? metrics.memory_used ?? metrics.used_memory;
|
||||
const total = metrics.mem_total ?? metrics.memory_total ?? metrics.total_memory;
|
||||
if (typeof used === 'number' && typeof total === 'number' && total > 0) {
|
||||
return Math.min(100, Math.round((used / total) * 100));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 生成过去 N 天的空趋势骨架
|
||||
function buildTrendSkeleton(days = 14): TrendPoint[] {
|
||||
const today = new Date();
|
||||
const out: TrendPoint[] = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const ds = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
out.push({ date: ds, count: 0 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 把告警数组按天统计到趋势
|
||||
function buildTrendFromAlerts(allAlerts: AlertRecord[], days = 14): TrendPoint[] {
|
||||
const trend = buildTrendSkeleton(days);
|
||||
const map = new Map(trend.map((t) => [t.date, 0]));
|
||||
const today = new Date();
|
||||
const earliest = new Date(today);
|
||||
earliest.setDate(earliest.getDate() - days + 1);
|
||||
earliest.setHours(0, 0, 0, 0);
|
||||
|
||||
for (const a of allAlerts) {
|
||||
const t = new Date(a.triggered_at);
|
||||
if (t < earliest) continue;
|
||||
const ds = `${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
|
||||
if (map.has(ds)) {
|
||||
map.set(ds, (map.get(ds) || 0) + 1);
|
||||
}
|
||||
}
|
||||
return trend.map((t) => ({ ...t, count: map.get(t.date) || 0 }));
|
||||
}
|
||||
|
||||
// ===== Hook =====
|
||||
|
||||
// 工具函数:从 Host + keys 构造 SSH 连接配置(用于调度器执行巡检)
|
||||
function buildSshConfig(host: Host, keys: any[]): Record<string, unknown> {
|
||||
const config: Record<string, unknown> = {
|
||||
host: host.hostname,
|
||||
port: host.port || 22,
|
||||
username: host.username,
|
||||
label: host.label || host.hostname,
|
||||
};
|
||||
if (host.password) {
|
||||
config.password = host.password;
|
||||
}
|
||||
// 私钥优先用 identityId 匹配 keys 数组
|
||||
const keyId = host.identityId || host.identityFileId;
|
||||
if (keyId) {
|
||||
const key = keys.find((k) => k.id === keyId);
|
||||
if (key?.privateKeyContent) {
|
||||
config.privateKey = key.privateKeyContent;
|
||||
if (key.passphrase) config.passphrase = key.passphrase;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
export function useMonitoringData(hosts: Host[], keys: any[] = []): UseMonitoringDataResult {
|
||||
const [cpuMap, setCpuMap] = useState<Record<string, number>>(() => readLS(LS_CPU, {}));
|
||||
const [memMap, setMemMap] = useState<Record<string, number>>(() => readLS(LS_MEM, {}));
|
||||
const [onlineMap, setOnlineMap] = useState<Record<string, boolean>>({});
|
||||
const [timestampMap, setTimestampMap] = useState<Record<string, number>>({});
|
||||
const [alerts, setAlerts] = useState<MonitoringAlert[]>(() => {
|
||||
const cached = readLS<MonitoringAlert[]>(LS_ALERTS, []);
|
||||
return cached;
|
||||
});
|
||||
// 初始 alertStats 也从缓存告警计算,保证和列表初始状态一致
|
||||
const [alertStats, setAlertStats] = useState<AlertStats | null>(() => {
|
||||
const cached = readLS<MonitoringAlert[]>(LS_ALERTS, []);
|
||||
if (cached.length === 0) return null;
|
||||
let critical = 0, warning = 0, info = 0;
|
||||
for (const a of cached) {
|
||||
if (a.level === 'critical') critical++;
|
||||
else if (a.level === 'warning') warning++;
|
||||
else info++;
|
||||
}
|
||||
return {
|
||||
total: cached.length,
|
||||
active: cached.length,
|
||||
active_critical: critical,
|
||||
active_warning: warning,
|
||||
active_info: info,
|
||||
acknowledged: 0,
|
||||
resolved: 0,
|
||||
closed: 0,
|
||||
};
|
||||
});
|
||||
const [alarmTrend, setAlarmTrend] = useState<TrendPoint[]>(() => {
|
||||
const cached = readLS<TrendPoint[]>(LS_TREND, []);
|
||||
return cached.length > 0 ? cached : buildTrendSkeleton(14);
|
||||
});
|
||||
const [taskStats, setTaskStats] = useState<TaskStats>({
|
||||
running: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
pending: 0,
|
||||
});
|
||||
const [taskDevices, setTaskDevices] = useState<TaskDevice[]>([]);
|
||||
const [schedulerRunning, setSchedulerRunning] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const loadingRef = useRef(false);
|
||||
// 用 ref 存最新的 hosts/keys,避免闭包陷阱
|
||||
const hostsRef = useRef(hosts);
|
||||
const keysRef = useRef(keys);
|
||||
hostsRef.current = hosts;
|
||||
keysRef.current = keys;
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
console.log('[monitoring] refresh() 被调用');
|
||||
if (loadingRef.current) {
|
||||
console.log('[monitoring] refresh() 正在执行中,跳过');
|
||||
return;
|
||||
}
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
// ===== 1. 巡检最新结果 → CPU / 内存 / 在线状态 / 成功失败统计 =====
|
||||
let latestResults: InspectionResult[] = [];
|
||||
try {
|
||||
const results: InspectionResult[] = await inspectionResultApi.getLatestAll();
|
||||
latestResults = results;
|
||||
const newCpu: Record<string, number> = {};
|
||||
const newMem: Record<string, number> = {};
|
||||
const newOnline: Record<string, boolean> = {};
|
||||
const newTimestamps: Record<string, number> = {};
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const r of results) {
|
||||
const cpu = extractCpu(r.metrics);
|
||||
const mem = extractMem(r.metrics);
|
||||
newCpu[r.device_id] = cpu;
|
||||
newMem[r.device_id] = mem;
|
||||
newOnline[r.device_id] = r.online !== false;
|
||||
newTimestamps[r.device_id] = r.timestamp;
|
||||
if (r.status === 'success') successCount++;
|
||||
else failedCount++;
|
||||
// 调试:打印每台设备的 metrics 结构,方便排查字段
|
||||
console.log(`[monitoring] device ${r.device_id}: cpu=${cpu}, mem=${mem}, metrics keys=`, Object.keys(r.metrics || {}));
|
||||
}
|
||||
|
||||
console.log(`[monitoring] loaded ${results.length} results: cpuMap=`, Object.keys(newCpu).length, 'memMap=', Object.keys(newMem).length);
|
||||
|
||||
setCpuMap((prev) => {
|
||||
const merged = { ...prev, ...newCpu };
|
||||
writeLS(LS_CPU, merged);
|
||||
return merged;
|
||||
});
|
||||
setMemMap((prev) => {
|
||||
const merged = { ...prev, ...newMem };
|
||||
writeLS(LS_MEM, merged);
|
||||
return merged;
|
||||
});
|
||||
setOnlineMap((prev) => ({ ...prev, ...newOnline }));
|
||||
setTimestampMap((prev) => ({ ...prev, ...newTimestamps }));
|
||||
setTaskStats((prev) => ({ ...prev, success: successCount, failed: failedCount }));
|
||||
} catch (e) {
|
||||
console.log('[monitoring] getLatestAll failed, using cached:', e);
|
||||
}
|
||||
|
||||
// ===== 2. 活动告警(统一数据源:列表、概览、计数都从 active 列表里统计)=====
|
||||
try {
|
||||
const active: AlertRecord[] = await inspectionAlertApi.getActive();
|
||||
console.log('[monitoring] getActive 返回:', active.length, '条告警');
|
||||
|
||||
const mapped: MonitoringAlert[] = active.map((a) => ({
|
||||
id: a.id,
|
||||
deviceId: a.device_id,
|
||||
deviceName: a.device_name,
|
||||
level: mapLevel(a.level),
|
||||
message: a.message || a.rule_name,
|
||||
time: formatAlertTime(a.triggered_at),
|
||||
metric: a.metric,
|
||||
metricValue: a.metric_value,
|
||||
threshold: a.threshold,
|
||||
}));
|
||||
setAlerts(mapped);
|
||||
writeLS(LS_ALERTS, mapped);
|
||||
|
||||
// 统一从 active 列表统计告警计数(和列表、概览保持完全一致)
|
||||
let activeCritical = 0;
|
||||
let activeWarning = 0;
|
||||
let activeInfo = 0;
|
||||
for (const a of active) {
|
||||
if (a.level === 'critical') activeCritical++;
|
||||
else if (a.level === 'warning') activeWarning++;
|
||||
else activeInfo++;
|
||||
}
|
||||
setAlertStats({
|
||||
total: active.length, // 用活动告警总数,不是历史总告警
|
||||
active: active.length,
|
||||
active_critical: activeCritical,
|
||||
active_warning: activeWarning,
|
||||
active_info: activeInfo,
|
||||
acknowledged: 0,
|
||||
resolved: 0,
|
||||
closed: 0,
|
||||
});
|
||||
|
||||
// 趋势:拉最近 200 条历史告警算 14 天趋势
|
||||
try {
|
||||
const all = await inspectionAlertApi.list({ limit: 200 });
|
||||
const trend = buildTrendFromAlerts(all, 14);
|
||||
setAlarmTrend(trend);
|
||||
writeLS(LS_TREND, trend);
|
||||
} catch { /* 忽略趋势拉取失败 */ }
|
||||
} catch (e) {
|
||||
console.log('[monitoring] alerts failed:', e);
|
||||
}
|
||||
|
||||
// ===== 3. 调度器状态 + 设备扩展 → 任务统计 + 任务设备列表 =====
|
||||
try {
|
||||
const [schedulerStatus, enabledDevices, allExtensions] = await Promise.all([
|
||||
inspectionSchedulerApi.status(),
|
||||
inspectionDeviceApi.getEnabled(),
|
||||
inspectionDeviceApi.getAll(),
|
||||
]);
|
||||
|
||||
setSchedulerRunning(schedulerStatus.running);
|
||||
const running = schedulerStatus.active_scans || 0;
|
||||
|
||||
// 构建结果 map
|
||||
const resultMap = new Map(latestResults.map((r) => [r.device_id, r]));
|
||||
|
||||
// 构建所有设备的任务状态列表(过滤掉没有 hostname 的无效设备)
|
||||
const allExtList = Object.values(allExtensions || {});
|
||||
const taskDeviceList: TaskDevice[] = allExtList
|
||||
.map((ext) => {
|
||||
const host = hostsRef.current.find((h) => h.id === ext.host_id);
|
||||
const result = resultMap.get(ext.host_id);
|
||||
const isEnabled = ext.inspection_enabled;
|
||||
|
||||
// 没有 hostname 的设备(配置不完整),过滤掉
|
||||
if (!host?.hostname) return null;
|
||||
|
||||
let status: TaskDevice['status'];
|
||||
if (isEnabled) {
|
||||
// 启用了定时巡检
|
||||
if (result) {
|
||||
if (result.status === 'success') status = 'success';
|
||||
else status = 'failed';
|
||||
} else {
|
||||
// 启用了但还没巡检过 → 归为成功(表示已配置)
|
||||
status = 'success';
|
||||
}
|
||||
} else {
|
||||
// 未启用定时巡检 → 待执行
|
||||
status = 'pending';
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId: ext.host_id,
|
||||
deviceName: host?.label || host?.hostname || ext.host_id,
|
||||
ip: host?.hostname || '',
|
||||
status,
|
||||
lastScanAt: result?.timestamp,
|
||||
durationMs: result?.duration_ms,
|
||||
errorMessage: result?.error_message || undefined,
|
||||
interval: ext.inspection_interval || undefined,
|
||||
inspectionEnabled: isEnabled,
|
||||
};
|
||||
})
|
||||
.filter((d): d is TaskDevice => d !== null);
|
||||
|
||||
// 统计:执行中=启用巡检的设备数,成功/失败是其中的子集,待执行=未启用的
|
||||
const enabledTaskDevices = taskDeviceList.filter((d) => d.inspectionEnabled);
|
||||
const runningCount = enabledTaskDevices.length;
|
||||
const successCount = enabledTaskDevices.filter((d) => d.status === 'success').length;
|
||||
const failedCount = enabledTaskDevices.filter((d) => d.status === 'failed').length;
|
||||
const pendingCount = taskDeviceList.filter((d) => !d.inspectionEnabled).length;
|
||||
|
||||
console.log('[monitoring] 任务统计:', { running: runningCount, success: successCount, failed: failedCount, pending: pendingCount, total: taskDeviceList.length });
|
||||
console.log('[monitoring] 启用巡检设备数:', enabledTaskDevices.length, '全部扩展设备数:', allExtList.length, '巡检结果数:', latestResults.length);
|
||||
|
||||
setTaskDevices(taskDeviceList);
|
||||
setTaskStats({
|
||||
running: runningCount,
|
||||
success: successCount,
|
||||
failed: failedCount,
|
||||
pending: pendingCount,
|
||||
});
|
||||
|
||||
// 自动启动调度器
|
||||
if (enabledDevices.length === 0) {
|
||||
console.log('[monitoring] 没有启用巡检的设备');
|
||||
} else if (schedulerStatus.running) {
|
||||
console.log('[monitoring] 调度器已在运行中');
|
||||
} else {
|
||||
console.log('[monitoring] 条件满足,准备启动调度器');
|
||||
try {
|
||||
const hostConfigs: Record<string, Record<string, unknown>> = {};
|
||||
for (const dev of enabledDevices) {
|
||||
const hostname = dev.hostname;
|
||||
if (!hostname) {
|
||||
console.log('[monitoring] 设备', dev.host_id, '没有 hostname,跳过');
|
||||
continue;
|
||||
}
|
||||
const host = hostsRef.current.find((h) => h.hostname === hostname);
|
||||
if (!host) {
|
||||
console.log('[monitoring] vault 中未找到 hostname:', hostname, ',跳过');
|
||||
continue;
|
||||
}
|
||||
if (!host.username) {
|
||||
console.log('[monitoring] 设备', hostname, '缺少 username,跳过');
|
||||
continue;
|
||||
}
|
||||
hostConfigs[dev.host_id] = buildSshConfig(host, keysRef.current);
|
||||
}
|
||||
if (Object.keys(hostConfigs).length === 0) {
|
||||
console.log('[monitoring] 没有可巡检的设备(hostname 未写入或配置不完整)');
|
||||
} else {
|
||||
console.log('[monitoring] 启动调度器,匹配到', Object.keys(hostConfigs).length, '台设备');
|
||||
await inspectionSchedulerApi.start(hostConfigs);
|
||||
setSchedulerRunning(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[monitoring] 自动启动调度器失败:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[monitoring] scheduler status failed:', e);
|
||||
}
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始加载 + 定期刷新(每 30 秒)
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const id = setInterval(refresh, 30000);
|
||||
return () => clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
// hosts 变化时,把在线状态默认设为 true(没巡检数据前)
|
||||
useEffect(() => {
|
||||
setOnlineMap((prev) => {
|
||||
const next: Record<string, boolean> = { ...prev };
|
||||
let changed = false;
|
||||
for (const h of hosts) {
|
||||
if (next[h.id] === undefined) {
|
||||
next[h.id] = true; // 假设在线,等巡检结果覆盖
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [hosts]);
|
||||
|
||||
return {
|
||||
cpuMap,
|
||||
memMap,
|
||||
onlineMap,
|
||||
timestampMap,
|
||||
alerts,
|
||||
alarmTrend,
|
||||
alertStats,
|
||||
taskStats,
|
||||
taskDevices,
|
||||
schedulerRunning,
|
||||
refresh,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user