/**
* 运行监控 - 3D 拓扑大屏
*
* 移植自 ITYUNWEI TopologyView.vue
* Three.js 场景 + ECharts 图表 + NetMesh 真实数据
*/
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { createPortal } from 'react-dom';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import { Line2 } from 'three/examples/jsm/lines/Line2.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
import * as echarts from 'echarts';
import { STORAGE_KEY_MONITORING_DASHBOARD_TITLE } from '../../infrastructure/config/storageKeys';
import {
Network,
Clock,
Maximize2,
Minimize2,
RefreshCw,
Server,
Activity,
TrendingUp,
AlertTriangle,
CheckCircle2,
ListTodo,
XCircle,
Loader2,
Bell,
Expand,
Box,
Link2,
CircleDot,
AlertOctagon,
WifiOff,
PieChart,
ArrowUpRight,
Cpu,
HardDrive,
BellRing,
Volume2,
VolumeX,
Info,
Monitor,
Router,
Shield,
Database,
} from 'lucide-react';
import { useMonitoringData } from './useMonitoringData';
import MonitoringDetailPanel, { type PanelType } from './MonitoringDetailPanel';
import { createDeviceModel, type DeviceType, type DeviceStatus } from './deviceModels';
import type { Host } from '../../domain/models';
interface Props {
hosts: Host[];
keys?: any[];
}
// ===== 全局单例:WebGLRenderer / CSS2DRenderer =====
// 浏览器 WebGL context 上限 8-16,短时间 mount/unmount 多次会丢 oldest。
// 这里挂到 window 上做真正的全局单例 —— HMR 模块替换不会重置 window 变量。
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _W = window as any;
function disposeMatTextures(mat: any) {
if (!mat) return;
// 1. 遍历材质直接属性(map / normalMap / emissiveMap 等)
for (const key of Object.keys(mat)) {
const v = mat[key];
if (v && v.isTexture) v.dispose?.();
}
// 2. ShaderMaterial 的贴图在 uniforms 里(ditu.png 在 uniforms.uMap.value)
if (mat.uniforms) {
for (const uKey of Object.keys(mat.uniforms)) {
const u = mat.uniforms[uKey];
if (u?.value?.isTexture) u.value.dispose?.();
}
}
mat.dispose?.();
}
function getGlobalWebGLRenderer(): THREE.WebGLRenderer {
if (!_W.__netMeshWebGLRenderer) {
_W.__netMeshWebGLRenderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance',
});
const r: THREE.WebGLRenderer = _W.__netMeshWebGLRenderer;
r.setPixelRatio(Math.min(window.devicePixelRatio, 2));
r.setClearColor(0x000000, 0);
r.localClippingEnabled = true;
r.domElement.style.cursor = 'grab';
r.domElement.style.pointerEvents = 'auto';
r.domElement.style.display = 'block';
r.domElement.style.width = '100%';
r.domElement.style.height = '100%';
}
return _W.__netMeshWebGLRenderer as THREE.WebGLRenderer;
}
function getGlobalCSS2DRenderer(): CSS2DRenderer {
if (!_W.__netMeshCSS2DRenderer) {
_W.__netMeshCSS2DRenderer = new CSS2DRenderer();
const c: CSS2DRenderer = _W.__netMeshCSS2DRenderer;
c.domElement.style.position = 'absolute';
c.domElement.style.top = '0';
c.domElement.style.left = '0';
c.domElement.style.width = '100%';
c.domElement.style.height = '100%';
c.domElement.style.pointerEvents = 'none';
c.domElement.style.zIndex = '10';
}
return _W.__netMeshCSS2DRenderer as CSS2DRenderer;
}
// 统一的卡片标题条 —— 发光图标块 + 标题 + 右侧 >>> 装饰箭头
const CardHeader: React.FC<{
icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
title: string;
iconColor?: string;
iconBg?: string;
extra?: React.ReactNode;
}> = ({ icon: Icon, title, iconColor = '#67e8f9', iconBg = 'rgba(14,165,233,0.25)', extra }) => (
{title}
{extra &&
{extra}
}
›
›
›
);
// 设备类型 → 图标组件
const deviceTypeIconMap: Record> = {
server: Server,
switch: Network,
router: Router,
firewall: Shield,
storage: Database,
workstation: Monitor,
h3c: Network,
huawei: Network,
};
const getDeviceIcon = (type?: string) => deviceTypeIconMap[type || ''] || Server;
// 设备类型映射
const typeLabel: Record = {
server: '服务器',
switch: '交换机',
router: '路由器',
firewall: '防火墙',
storage: '存储',
workstation: '工作站',
h3c: '交换机',
huawei: '交换机',
};
// 状态颜色
const statusColors: Record = {
normal: 0x86efac, // 浅绿色 tailwind green-300
warning: 0xffe600,
critical: 0xff4d4f,
offline: 0xcbd5e1,
};
const statusHex: Record = {
normal: '#86EFAC',
warning: '#FFE600',
critical: '#FF4D4F',
offline: '#6b7280',
};
/** 标签样式唯一入口:根据设备状态返回渐变背景 + 边框色,兜底灰色 */
function getLabelStyle(status: string): { background: string; border: string; glow: string; text: string } {
// normal:浅米色 #E4E4E1 — 干净清爽,区别于设备模型荧光绿
if (status === 'normal') {
return {
background: '#5fc06fff',
border: '1px solid #B8B4AE',
glow: '#E4E4E133',
text: '#ffffffff',
};
}
// offline:浅灰底 + 红字,清晰标识又不致黑
if (status === 'offline') {
return {
background: 'linear-gradient(135deg, #cbd5e1ee, #b8c3d3dd)',
border: '1px solid #94a3b8cc',
glow: '#ff4d4f33',
text: '#ff4d4f',
};
}
// warning / critical 保持荧光色渐变 + 白字
const hex = statusHex[status] ?? statusHex.offline;
return {
background: `linear-gradient(135deg, ${hex}ee, ${hex}88)`,
border: `1px solid ${hex}cc`,
glow: `${hex}55`,
text: '#ffffff',
};
}
// 链路颜色(不同链路类型不同色系,在浅卫星地图上清晰可辨)
const edgeColorMap: Record = {
wan: 0x2196f3, // 蓝 — WAN 骨干
uplink: 0x2196f3, // 蓝 — 上行
l3: 0x4caf50, // 深绿 — L3 路由
l2: 0x00bcd4, // 深青 — L2 交换
storage: 0xffa500, // 橙 — 存储
access: 0x9c27b0, // 深紫 — 接入点
vpn: 0x2196f3, // 蓝 — VPN
wireless: 0x66bb6a, // 浅绿 — 无线
default: 0x2196f3,
};
// 设备安全边界常量
const MIN_Y = -0.5;
const MAX_Y = 20;
// 创建离线连线的"断开"叉叉标记(两条交叉细线 + 红色发光材质)
function createBreakMark(): THREE.Group {
const group = new THREE.Group();
const size = 0.45;
const mat = new THREE.LineBasicMaterial({
color: 0xff4d4f,
depthTest: false,
transparent: true,
opacity: 1.0,
});
// 两条对角线
const pts1 = [new THREE.Vector3(-size, size, 0), new THREE.Vector3(size, -size, 0)];
const pts2 = [new THREE.Vector3(-size, -size, 0), new THREE.Vector3(size, size, 0)];
const geo1 = new THREE.BufferGeometry().setFromPoints(pts1);
const geo2 = new THREE.BufferGeometry().setFromPoints(pts2);
const line1 = new THREE.Line(geo1, mat);
const line2 = new THREE.Line(geo2, mat);
group.add(line1);
group.add(line2);
// 外圈小圆圈
const ringSize = size * 1.8;
const ringGeo = new THREE.RingGeometry(ringSize - 0.12, ringSize, 32);
// 用 LineLoop 更像"空心"圈 —— 先转成 BufferGeometry 上的点
const ringPts: THREE.Vector3[] = [];
const segs = 36;
for (let i = 0; i <= segs; i++) {
const a = (i / segs) * Math.PI * 2;
ringPts.push(new THREE.Vector3(Math.cos(a) * ringSize, Math.sin(a) * ringSize, 0));
}
const ringLine = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(ringPts),
mat
);
group.add(ringLine);
group.renderOrder = 10;
return group;
}
// 对每条边生成稳定 hash 的弧线中间控制点,让汇聚连线沿不同方向散开
function computeArcMidpoint(a: THREE.Vector3, b: THREE.Vector3, hashKey: string): { mid: THREE.Vector3 } {
// 简单 hash → 0..1
let h = 0;
for (let i = 0; i < hashKey.length; i++) h = (h * 31 + hashKey.charCodeAt(i)) >>> 0;
const t = (h & 0xffff) / 0xffff; // 0..1
const angle = t * Math.PI * 2; // 0..2π 随机方向
// 连线中点
const mx = (a.x + b.x) / 2;
const mz = (a.z + b.z) / 2;
const my = (a.y + b.y) / 2;
// 偏移:沿 xz 平面随机方向,幅值按连线长度
const dx = b.x - a.x;
const dz = b.z - a.z;
const len = Math.sqrt(dx * dx + dz * dz) || 1;
// 偏移量 = min(连线长度 * 0.25, 7),避免过弯
const amp = Math.min(len * 0.25, 7);
const ox = Math.cos(angle) * amp;
const oz = Math.sin(angle) * amp;
// 中间点也稍抬高一点,让弧线"拱"起来,不和地图贴平
const oy = my + 1.5;
return { mid: new THREE.Vector3(mx + ox, oy, mz + oz) };
}
// 相对时间格式化
function formatRelativeTime(timestamp: number): string {
if (!timestamp) return '未知';
const diff = Date.now() - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 30) return '刚刚';
if (seconds < 60) return `${seconds}秒前`;
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
const date = new Date(timestamp);
return `${date.getMonth() + 1}-${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 扩展 Host 的设备类型信息
function classifyHostType(host: Host): string {
// 优先用直接的 type 字段(remote API 会返回 switch/server/firewall/router)
const directType = (host as any).type;
if (directType && ['server', 'switch', 'router', 'firewall', 'storage', 'workstation'].includes(directType)) {
return directType;
}
const distro = (host.distro || (host as any).catalog || (host as any).catalog_id || '').toLowerCase();
const name = (host.name || '').toLowerCase();
const label = (host.label || '').toLowerCase();
const combined = `${distro} ${name} ${label}`;
if (/h3c|huawei|vrp|comware|cisco|switch/.test(combined)) return 'switch';
if (/router|路由/.test(combined)) return 'router';
if (/firewall|防火墙|fw/.test(combined)) return 'firewall';
if (/storage|存储|nas|san/.test(combined)) return 'storage';
if (/workstation|pc|桌面/.test(combined)) return 'workstation';
return 'server';
}
function classifyHostStatus(host: Host): string {
// 简化:有 SSH 连接记录就当 normal
if (!host) return 'offline';
return 'normal';
}
// 自动生成链路
interface Link {
id: string;
source: string;
target: string;
type: string;
}
function autoGenerateLinks(
devices: { id: string; type: string; name: string }[]
): Link[] {
const links: Link[] = [];
const coreSwitches = devices.filter(
(d) => d.type === 'switch' && /核心|core/i.test(d.name)
);
const accessSwitches = devices.filter(
(d) => d.type === 'switch' && !/核心|core/i.test(d.name)
);
const firewalls = devices.filter((d) => d.type === 'firewall');
const routers = devices.filter((d) => d.type === 'router');
const servers = devices.filter((d) => d.type === 'server');
let linkId = 0;
const push = (source: string, target: string, type: string) => {
links.push({ id: `link-${linkId++}`, source, target, type });
};
// 防火墙 → 路由/核心交换机
firewalls.forEach((f) => {
routers.forEach((r) => push(f.id, r.id, 'wan'));
if (coreSwitches.length === 0) {
accessSwitches.slice(0, 1).forEach((a) => push(f.id, a.id, 'wan'));
}
});
// 路由 → 核心交换机
routers.forEach((r) => {
coreSwitches.forEach((s) => push(r.id, s.id, 'l3'));
});
// 核心交换机 → 接入交换机
coreSwitches.forEach((c) => {
accessSwitches.forEach((a) => push(c.id, a.id, 'l2'));
});
// 接入交换机 → 服务器(每个交换机连 1-3 台服务器)
const serversPerSwitch = Math.max(1, Math.min(3, Math.ceil(servers.length / Math.max(accessSwitches.length, 1))));
accessSwitches.forEach((a, si) => {
for (let i = 0; i < serversPerSwitch; i++) {
const idx = (si * serversPerSwitch + i) % Math.max(servers.length, 1);
if (servers[idx]) push(a.id, servers[idx].id, 'access');
}
});
// 默认不生成连线,只使用用户手动添加的 customLinks
return [];
}
// 设备 3D 模型工厂(使用精细模型)
function createDeviceMesh(type: string, status: string): THREE.Group {
// 类型映射
const typeMap: Record = {
server: 'server',
switch: 'switch',
router: 'router',
firewall: 'firewall',
storage: 'storage',
workstation: 'workstation',
h3c: 'switch',
huawei: 'switch',
};
const deviceType = typeMap[type] || 'server';
const deviceStatus: DeviceStatus = (status as DeviceStatus) || 'normal';
const group = createDeviceModel(deviceType, deviceStatus);
// 关键设备加发光点 + 光环
const color = statusColors[status] ?? 0x22c55e;
if (status === 'critical' || status === 'warning') {
const halo = new THREE.PointLight(color, status === 'critical' ? 2.5 : 1.5, 14);
halo.position.set(0, 0, 0);
group.add(halo);
}
// 所有设备加状态光环(增强大屏可见性)
const ring = new THREE.Mesh(
new THREE.RingGeometry(0.35, 0.55, 24),
new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: status === 'offline' ? 0.15 : 0.35,
side: THREE.DoubleSide,
})
);
ring.rotation.x = -Math.PI / 2;
ring.position.y = -0.02;
ring.renderOrder = 3;
group.add(ring);
// 双面材质兜底
group.traverse((obj) => {
const mesh = obj as THREE.Mesh;
if (mesh.isMesh) {
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
mats.forEach((m) => {
if (m) (m as THREE.Material).side = THREE.DoubleSide;
});
}
});
return group;
}
export function MonitoringTopologyView({ hosts, keys }: Props) {
// 使用真实数据 hook
const monitoring = useMonitoringData(hosts, keys);
const containerRef = useRef(null);
const chartTypeRef = useRef(null);
const chartTrendRef = useRef(null);
const [selectedDevice, setSelectedDevice] = useState(null);
const [loading, setLoading] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isTopologyOnly, setIsTopologyOnly] = useState(false); // 3D拓扑大屏模式
const [isPanelsHidden, setIsPanelsHidden] = useState(false); // 面板正在滑出/已滑出(过渡动画用)
const [currentTime, setCurrentTime] = useState(new Date());
const [legend, setLegend] = useState(true);
const [showConnectionLegend, setShowConnectionLegend] = useState(true);
const [monitoringTitle, setMonitoringTitle] = useState(() => {
// 先给默认值,useEffect 里远程异步加载(时序更安全)
if (typeof window !== "undefined") {
const saved = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
if (saved) return saved;
}
return '云启Net智能巡检监测系统';
});
// 远程环境:异步加载标题 / 位置 / 连接线(useState init 阶段 token 可能还没注入)
useEffect(() => {
if (typeof window === "undefined") return;
const token = (window as any).__REMOTE_TOKEN__;
if (!token) return; // 不是远程环境或 token 未就绪
const fetchJSON = (url: string): Promise =>
fetch(url).then(r => r.json()).catch(() => null);
// 1. 标题
fetchJSON(`/api/dashboard-title?token=${encodeURIComponent(token)}`)
.then(json => { if (json?.ok && json.title) setMonitoringTitle(json.title); });
// 2. 拓扑位置
fetchJSON(`/api/topology-positions?token=${encodeURIComponent(token)}`)
.then(json => {
if (json?.ok && json.positions && Object.keys(json.positions).length > 0) {
setSavedPositions(json.positions);
}
});
// 3. 连接线
fetchJSON(`/api/topology-links?token=${encodeURIComponent(token)}`)
.then(json => { if (json?.ok && Array.isArray(json.links)) setCustomLinks(json.links); });
}, []);
// Electron 环境:监听 localStorage 变化 + mount 时同步 hosts/title 给主进程
useEffect(() => {
const nc = (window as any).netcatty;
console.log('[MonitoringTopologyView sync] nc exists:', !!nc, 'nc.inspection exists:', !!nc?.inspection, 'hosts.length:', hosts?.length);
if (!nc?.inspection) {
console.log('[MonitoringTopologyView sync] skip — not Electron env');
return;
}
// mount 时一次性同步 vault hosts + 标题 + 拓扑位置 + 拓扑连线
if (hosts?.length > 0) {
console.log('[MonitoringTopologyView sync] saving vault hosts:', hosts.length);
nc.inspection.saveVaultHosts(hosts).then((r: any) => console.log('saveVaultHosts result:', r)).catch((e: any) => console.warn('saveVaultHosts error:', e));
}
const savedTitle = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
console.log('[MonitoringTopologyView sync] savedTitle:', savedTitle);
if (savedTitle) nc.inspection.saveDashboardTitle(savedTitle).catch(() => {});
const savedPos = localStorage.getItem('monitoring-topology-positions');
if (savedPos) nc.inspection.saveTopologyPositions(JSON.parse(savedPos)).catch(() => {});
const savedLinks = localStorage.getItem('monitoring-topology-links');
console.log('[MonitoringTopologyView sync] savedLinks exists:', !!savedLinks);
if (savedLinks) {
try { nc.inspection.saveTopologyLinks(JSON.parse(savedLinks)).catch(() => {}); } catch {}
}
if (!savedLinks && nc.inspection.getTopologyLinks) {
nc.inspection.getTopologyLinks().then((linksFromMain: any) => {
if (Array.isArray(linksFromMain) && linksFromMain.length > 0) {
console.info('[MonitoringTopologyView] restore links from main:', linksFromMain.length);
localStorage.setItem('monitoring-topology-links', JSON.stringify(linksFromMain));
try { setCustomLinks(linksFromMain); } catch {}
}
}).catch(() => {});
}
const syncToMain = (value: string) => {
if (value) nc.inspection.saveDashboardTitle(value).catch(() => {});
};
const handler = (e: StorageEvent) => {
if (e.key === STORAGE_KEY_MONITORING_DASHBOARD_TITLE && e.newValue !== null) {
setMonitoringTitle(e.newValue || '云启Net智能巡检监测系统');
syncToMain(e.newValue);
}
};
const localHandler = () => {
const v = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
setMonitoringTitle(v || '云启Net智能巡检监测系统');
syncToMain(v || '');
};
window.addEventListener('storage', handler);
window.addEventListener('monitoringTitleChanged' as any, localHandler);
// mount 时同步一次当前标题
const initVal = localStorage.getItem(STORAGE_KEY_MONITORING_DASHBOARD_TITLE);
if (initVal) syncToMain(initVal);
return () => {
window.removeEventListener('storage', handler);
window.removeEventListener('monitoringTitleChanged' as any, localHandler);
};
}, []);
// Three.js 全局引用
const sceneRef = useRef(null);
const cameraRef = useRef(null);
const rendererRef = useRef(null);
const composerRef = useRef(null);
const labelRendererRef = useRef(null);
const controlsRef = useRef(null);
const nodesRef = useRef([]);
const edgesRef = useRef([]);
const linkBreakRef = useRef([]); // 离线连线的"断开"标记叉叉
const labelsRef = useRef([]);
const deviceMapRef = useRef