1866 lines
73 KiB
JavaScript
1866 lines
73 KiB
JavaScript
/**
|
|
* 网络拓扑可视化模块
|
|
* 基于D3.js实现网络拓扑图的可视化和交互
|
|
*/
|
|
|
|
class TopologyVisualization {
|
|
constructor() {
|
|
// 初始化变量
|
|
this.svg = null;
|
|
this.simulation = null;
|
|
this.nodes = [];
|
|
this.links = [];
|
|
this.selectedNode = null;
|
|
this.selectedLink = null;
|
|
this.transform = d3.zoomIdentity;
|
|
this.width = 0;
|
|
this.height = 0;
|
|
|
|
// UI元素
|
|
this.elements = {};
|
|
|
|
// 厂商颜色映射
|
|
this.vendorColors = {
|
|
cisco: '#1e40af',
|
|
huawei: '#dc2626',
|
|
h3c: '#059669',
|
|
juniper: '#7c3aed',
|
|
other: '#6b7280'
|
|
};
|
|
|
|
// 初始化
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* 初始化拓扑可视化
|
|
*/
|
|
init() {
|
|
console.log('初始化网络拓扑可视化模块');
|
|
|
|
// 缓存DOM元素
|
|
this.cacheElements();
|
|
|
|
// 初始化SVG
|
|
this.initSVG();
|
|
|
|
// 绑定事件
|
|
this.bindEvents();
|
|
|
|
// 加载初始拓扑数据
|
|
this.loadTopology();
|
|
}
|
|
|
|
/**
|
|
* 缓存DOM元素
|
|
*/
|
|
cacheElements() {
|
|
this.elements = {
|
|
// 主要容器
|
|
topologyContent: document.getElementById('topology-content'),
|
|
topologyCanvas: document.getElementById('topology-canvas'),
|
|
topologySvg: document.getElementById('topology-svg'),
|
|
topologyLoading: document.getElementById('topology-loading'),
|
|
topologyEmpty: document.getElementById('topology-empty'),
|
|
|
|
// 工具栏按钮
|
|
discoverBtn: document.getElementById('discover-topology-btn'),
|
|
refreshBtn: document.getElementById('refresh-topology-btn'),
|
|
clearBtn: document.getElementById('clear-topology-btn'),
|
|
fitBtn: document.getElementById('fit-topology-btn'),
|
|
zoomInBtn: document.getElementById('zoom-in-btn'),
|
|
zoomOutBtn: document.getElementById('zoom-out-btn'),
|
|
centerBtn: document.getElementById('center-topology-btn'),
|
|
|
|
// 过滤器
|
|
vendorFilter: document.getElementById('vendor-filter'),
|
|
deviceTypeFilter: document.getElementById('device-type-filter'),
|
|
applyFilterBtn: document.getElementById('apply-filter-btn'),
|
|
|
|
// 统计元素
|
|
statNodes: document.getElementById('stat-nodes'),
|
|
statLinks: document.getElementById('stat-links'),
|
|
statProtocols: document.getElementById('stat-protocols'),
|
|
statVendors: document.getElementById('stat-vendors'),
|
|
|
|
// 详情面板
|
|
nodeDetails: document.getElementById('node-details'),
|
|
linkDetails: document.getElementById('link-details'),
|
|
|
|
// 操作按钮
|
|
connectBtn: document.getElementById('connect-to-device-btn'),
|
|
pingBtn: document.getElementById('ping-device-btn'),
|
|
discoverNeighborsBtn: document.getElementById('discover-neighbors-btn'),
|
|
propertiesBtn: document.getElementById('device-properties-btn'),
|
|
|
|
// 对话框
|
|
discoverModal: document.getElementById('topology-discover-modal'),
|
|
discoverForm: document.getElementById('topology-discover-form'),
|
|
seedDevicesList: document.getElementById('seed-devices-list'),
|
|
discoveryProgress: document.getElementById('discovery-progress'),
|
|
discoveryStatus: document.getElementById('discovery-status'),
|
|
discoveryProgressBar: document.getElementById('discovery-progress-bar'),
|
|
|
|
// 其他
|
|
gotoDevicesBtn: document.getElementById('goto-devices-btn')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 初始化SVG
|
|
*/
|
|
initSVG() {
|
|
const container = this.elements.topologyCanvas;
|
|
this.width = container.clientWidth;
|
|
this.height = container.clientHeight;
|
|
|
|
this.svg = d3.select('#topology-svg')
|
|
.attr('width', this.width)
|
|
.attr('height', this.height);
|
|
|
|
// 创建主要的图形组
|
|
this.g = this.svg.select('#topology-graph');
|
|
this.linkGroup = this.svg.select('#links-group');
|
|
this.nodeGroup = this.svg.select('#nodes-group');
|
|
this.labelGroup = this.svg.select('#labels-group');
|
|
|
|
// 获取或创建defs元素
|
|
let defs = this.svg.select('defs');
|
|
if (defs.empty()) {
|
|
defs = this.svg.append('defs');
|
|
}
|
|
|
|
// 添加渐变定义
|
|
this.createGradients(defs);
|
|
|
|
// 添加阴影滤镜
|
|
this.createShadowFilter(defs);
|
|
|
|
// 设置缩放行为
|
|
const zoom = d3.zoom()
|
|
.scaleExtent([0.1, 4])
|
|
.on('zoom', (event) => {
|
|
this.transform = event.transform;
|
|
this.g.attr('transform', this.transform);
|
|
});
|
|
|
|
this.svg.call(zoom);
|
|
|
|
// 初始化力仿真
|
|
this.simulation = d3.forceSimulation()
|
|
.force('link', d3.forceLink().id(d => d.id).distance(150))
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
|
|
.force('collision', d3.forceCollide().radius(30)); // 从25增加到30,适应更大的图标
|
|
}
|
|
|
|
/**
|
|
* 创建渐变定义
|
|
*/
|
|
createGradients(defs) {
|
|
const vendors = ['cisco', 'huawei', 'h3c', 'juniper', 'other'];
|
|
|
|
vendors.forEach(vendor => {
|
|
const gradient = defs.append('radialGradient')
|
|
.attr('id', `gradient-${vendor}`)
|
|
.attr('cx', '30%')
|
|
.attr('cy', '30%')
|
|
.attr('r', '70%');
|
|
|
|
const baseColor = this.vendorColors[vendor];
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '0%')
|
|
.attr('stop-color', this.lightenColor(baseColor, 0.3));
|
|
|
|
gradient.append('stop')
|
|
.attr('offset', '100%')
|
|
.attr('stop-color', baseColor);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 创建阴影滤镜
|
|
*/
|
|
createShadowFilter(defs) {
|
|
const filter = defs.append('filter')
|
|
.attr('id', 'drop-shadow')
|
|
.attr('x', '-50%')
|
|
.attr('y', '-50%')
|
|
.attr('width', '200%')
|
|
.attr('height', '200%');
|
|
|
|
filter.append('feDropShadow')
|
|
.attr('dx', 2)
|
|
.attr('dy', 2)
|
|
.attr('stdDeviation', 3)
|
|
.attr('flood-color', '#000')
|
|
.attr('flood-opacity', 0.3);
|
|
}
|
|
|
|
/**
|
|
* 调亮颜色
|
|
*/
|
|
lightenColor(color, percent) {
|
|
const num = parseInt(color.replace("#",""), 16);
|
|
const amt = Math.round(2.55 * percent * 100);
|
|
const R = (num >> 16) + amt;
|
|
const G = (num >> 8 & 0x00FF) + amt;
|
|
const B = (num & 0x0000FF) + amt;
|
|
return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
|
|
}
|
|
|
|
/**
|
|
* 绑定事件
|
|
*/
|
|
bindEvents() {
|
|
// 工具栏事件
|
|
this.elements.discoverBtn?.addEventListener('click', () => this.showDiscoverDialog());
|
|
this.elements.refreshBtn?.addEventListener('click', () => this.loadTopology());
|
|
this.elements.clearBtn?.addEventListener('click', () => this.clearTopology());
|
|
this.elements.fitBtn?.addEventListener('click', () => this.fitToView());
|
|
this.elements.zoomInBtn?.addEventListener('click', () => this.zoomIn());
|
|
this.elements.zoomOutBtn?.addEventListener('click', () => this.zoomOut());
|
|
this.elements.centerBtn?.addEventListener('click', () => this.centerView());
|
|
this.elements.applyFilterBtn?.addEventListener('click', () => this.applyFilters());
|
|
|
|
// 操作按钮事件
|
|
this.elements.connectBtn?.addEventListener('click', () => this.connectToDevice());
|
|
this.elements.pingBtn?.addEventListener('click', () => this.pingDevice());
|
|
this.elements.discoverNeighborsBtn?.addEventListener('click', () => this.discoverNeighbors());
|
|
this.elements.propertiesBtn?.addEventListener('click', () => this.showDeviceProperties());
|
|
|
|
// 其他事件
|
|
this.elements.gotoDevicesBtn?.addEventListener('click', () => this.gotoDevicesPage());
|
|
|
|
// 发现对话框事件
|
|
this.elements.discoverForm?.addEventListener('submit', (e) => this.handleDiscoverSubmit(e));
|
|
|
|
// 窗口大小改变事件
|
|
window.addEventListener('resize', () => this.handleResize());
|
|
|
|
// SVG点击事件(取消选择和隐藏菜单)
|
|
this.svg?.on('click', (event) => {
|
|
if (event.target === this.svg.node()) {
|
|
this.clearSelection();
|
|
this.hideContextMenu();
|
|
}
|
|
});
|
|
|
|
// 全局点击事件(隐藏上下文菜单)
|
|
document.addEventListener('click', (event) => {
|
|
// 如果点击的不是菜单内容,则隐藏菜单
|
|
const menu = document.getElementById('topology-context-menu');
|
|
if (menu && !menu.contains(event.target)) {
|
|
this.hideContextMenu();
|
|
}
|
|
});
|
|
|
|
// 全局ESC键事件(隐藏上下文菜单)
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
this.hideContextMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 加载拓扑数据
|
|
*/
|
|
async loadTopology() {
|
|
try {
|
|
this.showLoading(true);
|
|
|
|
const response = await fetch('/api/topology');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.topology) {
|
|
this.updateTopology(data.topology);
|
|
this.showEmpty(false);
|
|
} else {
|
|
this.showEmpty(true);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载拓扑数据失败:', error);
|
|
this.showEmpty(true);
|
|
} finally {
|
|
this.showLoading(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新拓扑数据
|
|
*/
|
|
updateTopology(topology) {
|
|
// 转换节点数据
|
|
this.nodes = Object.values(topology.nodes || {}).map(node => ({
|
|
id: node.device_id,
|
|
name: node.name,
|
|
ip: node.ip_address,
|
|
vendor: node.vendor || 'other',
|
|
deviceType: node.device_type || 'other',
|
|
status: node.status || 'unknown',
|
|
interfaces: node.interfaces || [],
|
|
interface_count: node.interface_count || (node.interfaces ? node.interfaces.length : 0), // 添加接口数字段
|
|
// D3需要的位置信息
|
|
x: Math.random() * this.width,
|
|
y: Math.random() * this.height
|
|
}));
|
|
|
|
// 转换链路数据
|
|
this.links = topology.links?.map(link => ({
|
|
id: `${link.source_device_id}-${link.target_device_id}`,
|
|
source: link.source_device_id,
|
|
target: link.target_device_id,
|
|
sourceInterface: link.source_interface,
|
|
targetInterface: link.target_interface,
|
|
protocol: link.protocol || 'unknown'
|
|
})) || [];
|
|
|
|
// 更新可视化
|
|
this.renderTopology();
|
|
|
|
// 更新统计信息
|
|
this.updateStatistics();
|
|
}
|
|
|
|
/**
|
|
* 渲染拓扑图
|
|
*/
|
|
renderTopology() {
|
|
// 更新仿真数据
|
|
this.simulation.nodes(this.nodes);
|
|
this.simulation.force('link').links(this.links);
|
|
|
|
// 渲染链路
|
|
this.renderLinks();
|
|
|
|
// 渲染节点
|
|
this.renderNodes();
|
|
|
|
// 渲染标签
|
|
this.renderLabels();
|
|
|
|
// 重启仿真
|
|
this.simulation.alpha(1).restart();
|
|
|
|
// 显示SVG并隐藏空状态
|
|
this.elements.topologySvg.style.display = 'block';
|
|
|
|
// 根据节点数量决定是否显示空状态
|
|
if (this.nodes.length > 0) {
|
|
this.showEmpty(false);
|
|
} else {
|
|
this.showEmpty(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 渲染链路
|
|
*/
|
|
renderLinks() {
|
|
const links = this.linkGroup.selectAll('.topology-link')
|
|
.data(this.links, d => d.id);
|
|
|
|
links.exit().remove();
|
|
|
|
const linkEnter = links.enter()
|
|
.append('line')
|
|
.attr('class', d => `topology-link ${d.protocol}`)
|
|
.attr('stroke-width', 2)
|
|
.on('click', (event, d) => this.selectLink(event, d));
|
|
|
|
const linkUpdate = linkEnter.merge(links);
|
|
|
|
// 更新链路位置(在仿真tick中处理)
|
|
this.simulation.on('tick', () => {
|
|
linkUpdate
|
|
.attr('x1', d => d.source.x)
|
|
.attr('y1', d => d.source.y)
|
|
.attr('x2', d => d.target.x)
|
|
.attr('y2', d => d.target.y);
|
|
|
|
this.updateNodePositions();
|
|
this.updateLabelPositions();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 渲染节点
|
|
*/
|
|
renderNodes() {
|
|
const nodes = this.nodeGroup.selectAll('.topology-node-group')
|
|
.data(this.nodes, d => d.id);
|
|
|
|
nodes.exit().remove();
|
|
|
|
// 创建节点组
|
|
const nodeEnter = nodes.enter()
|
|
.append('g')
|
|
.attr('class', d => `topology-node-group ${d.vendor}`)
|
|
.call(this.createDragBehavior());
|
|
|
|
// 绑定点击和双击事件
|
|
this.bindNodeEvents(nodeEnter);
|
|
|
|
// 添加主节点圆圈(修改为完全透明)
|
|
nodeEnter.append('circle')
|
|
.attr('class', 'node-main-circle')
|
|
.attr('r', 18)
|
|
.attr('stroke', 'transparent') // 边框透明
|
|
.attr('stroke-width', 0) // 边框宽度为0
|
|
.style('fill', 'transparent') // 填充透明
|
|
.style('filter', 'none'); // 移除阴影效果
|
|
|
|
// 添加设备类型SVG图标(进一步增大尺寸)
|
|
const iconGroup = nodeEnter.append('g')
|
|
.attr('class', 'node-icon-group')
|
|
.attr('transform', 'translate(-20, -20)'); // 调整位置以适应更大的图标
|
|
|
|
// 根据设备类型添加不同的SVG图标
|
|
iconGroup.each(function(d) {
|
|
const group = d3.select(this);
|
|
const iconSvg = window.topologyVisualization.createDeviceIcon(d.deviceType, true); // 传递黑色参数
|
|
group.node().innerHTML = iconSvg;
|
|
});
|
|
|
|
// 添加状态指示器
|
|
nodeEnter.append('circle')
|
|
.attr('class', 'node-status-indicator')
|
|
.attr('r', 5)
|
|
.attr('cx', 18) // 调整位置以适应更大的图标
|
|
.attr('cy', -18)
|
|
.attr('stroke', '#fff')
|
|
.attr('stroke-width', 2)
|
|
.style('fill', d => this.getStatusColor(d.status));
|
|
|
|
// 添加厂商徽章
|
|
nodeEnter.append('circle')
|
|
.attr('class', 'node-vendor-badge')
|
|
.attr('r', 6)
|
|
.attr('cx', -18) // 调整位置以适应更大的图标
|
|
.attr('cy', -18)
|
|
.attr('stroke', '#fff')
|
|
.attr('stroke-width', 2)
|
|
.style('fill', d => this.vendorColors[d.vendor] || this.vendorColors.other);
|
|
|
|
// 添加厂商徽章文字
|
|
nodeEnter.append('text')
|
|
.attr('class', 'node-vendor-text')
|
|
.attr('x', -18) // 调整位置以适应更大的图标
|
|
.attr('y', -15)
|
|
.attr('text-anchor', 'middle')
|
|
.style('font-size', '7px')
|
|
.style('font-weight', 'bold')
|
|
.style('fill', '#fff')
|
|
.style('pointer-events', 'none')
|
|
.text(d => this.getVendorBadgeText(d.vendor));
|
|
|
|
// 更新现有节点
|
|
const nodeUpdate = nodeEnter.merge(nodes);
|
|
|
|
// 更新事件绑定
|
|
this.bindNodeEvents(nodeUpdate);
|
|
|
|
// 更新主圆圈样式
|
|
nodeUpdate.select('.node-main-circle')
|
|
.attr('r', 18)
|
|
.style('fill', 'transparent');
|
|
|
|
// 更新状态指示器颜色
|
|
nodeUpdate.select('.node-status-indicator')
|
|
.style('fill', d => this.getStatusColor(d.status));
|
|
|
|
// 更新图标
|
|
nodeUpdate.select('.node-icon-group')
|
|
.each(function(d) {
|
|
const group = d3.select(this);
|
|
const iconSvg = window.topologyVisualization.createDeviceIcon(d.deviceType, true);
|
|
group.node().innerHTML = iconSvg;
|
|
});
|
|
|
|
// 添加选择效果
|
|
nodeUpdate.classed('selected', d => d === this.selectedNode);
|
|
}
|
|
|
|
/**
|
|
* 绑定节点事件(单击选择,双击菜单)
|
|
*/
|
|
bindNodeEvents(selection) {
|
|
let clickTimeout = null;
|
|
|
|
selection
|
|
.on('click', (event, d) => {
|
|
event.stopPropagation();
|
|
|
|
// 清除之前的延迟
|
|
if (clickTimeout) {
|
|
clearTimeout(clickTimeout);
|
|
clickTimeout = null;
|
|
}
|
|
|
|
// 延迟执行单击,以区分双击
|
|
clickTimeout = setTimeout(() => {
|
|
this.selectNode(event, d);
|
|
clickTimeout = null;
|
|
}, 250);
|
|
})
|
|
.on('dblclick', (event, d) => {
|
|
event.stopPropagation();
|
|
|
|
// 取消单击延迟
|
|
if (clickTimeout) {
|
|
clearTimeout(clickTimeout);
|
|
clickTimeout = null;
|
|
}
|
|
|
|
// 先选择节点
|
|
this.selectNode(event, d);
|
|
|
|
// 显示上下文菜单
|
|
this.showContextMenu(event, d);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 显示上下文菜单
|
|
*/
|
|
showContextMenu(event, node) {
|
|
// 隐藏已存在的菜单
|
|
this.hideContextMenu();
|
|
|
|
// 创建菜单容器
|
|
const menu = document.createElement('div');
|
|
menu.className = 'topology-context-menu';
|
|
menu.id = 'topology-context-menu';
|
|
|
|
// 菜单项配置
|
|
const menuItems = [
|
|
{
|
|
icon: 'fas fa-terminal',
|
|
text: '连接终端',
|
|
action: () => this.connectToDevice(),
|
|
disabled: false
|
|
},
|
|
{
|
|
icon: 'fas fa-satellite-dish',
|
|
text: 'Ping测试',
|
|
action: () => this.pingDevice(),
|
|
disabled: false
|
|
},
|
|
{
|
|
icon: 'fas fa-search',
|
|
text: '发现邻居',
|
|
action: () => this.discoverNeighbors(),
|
|
disabled: false
|
|
},
|
|
{
|
|
type: 'divider'
|
|
},
|
|
{
|
|
icon: 'fas fa-cog',
|
|
text: '设备属性',
|
|
action: () => this.showDeviceProperties(),
|
|
disabled: false
|
|
},
|
|
{
|
|
icon: 'fas fa-copy',
|
|
text: '复制IP地址',
|
|
action: () => this.copyIPAddress(node),
|
|
disabled: false
|
|
},
|
|
{
|
|
type: 'divider'
|
|
},
|
|
{
|
|
icon: 'fas fa-expand-arrows-alt',
|
|
text: '居中到此设备',
|
|
action: () => this.centerToNode(node),
|
|
disabled: false
|
|
}
|
|
];
|
|
|
|
// 创建菜单项
|
|
menuItems.forEach(item => {
|
|
if (item.type === 'divider') {
|
|
const divider = document.createElement('div');
|
|
divider.className = 'context-menu-divider';
|
|
menu.appendChild(divider);
|
|
} else {
|
|
const menuItem = document.createElement('div');
|
|
menuItem.className = `context-menu-item ${item.disabled ? 'disabled' : ''}`;
|
|
|
|
menuItem.innerHTML = `
|
|
<i class="${item.icon}"></i>
|
|
<span>${item.text}</span>
|
|
`;
|
|
|
|
if (!item.disabled) {
|
|
menuItem.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
item.action();
|
|
this.hideContextMenu();
|
|
});
|
|
}
|
|
|
|
menu.appendChild(menuItem);
|
|
}
|
|
});
|
|
|
|
// 添加到页面
|
|
document.body.appendChild(menu);
|
|
|
|
// 计算位置(确保菜单不超出视窗)
|
|
const rect = this.elements.topologyCanvas.getBoundingClientRect();
|
|
const menuRect = menu.getBoundingClientRect();
|
|
|
|
let x = event.clientX;
|
|
let y = event.clientY;
|
|
|
|
// 防止菜单超出右边界
|
|
if (x + menuRect.width > window.innerWidth) {
|
|
x = window.innerWidth - menuRect.width - 10;
|
|
}
|
|
|
|
// 防止菜单超出下边界
|
|
if (y + menuRect.height > window.innerHeight) {
|
|
y = window.innerHeight - menuRect.height - 10;
|
|
}
|
|
|
|
// 设置位置
|
|
menu.style.left = `${x}px`;
|
|
menu.style.top = `${y}px`;
|
|
|
|
// 添加动画效果
|
|
menu.style.opacity = '0';
|
|
menu.style.transform = 'scale(0.8)';
|
|
|
|
requestAnimationFrame(() => {
|
|
menu.style.opacity = '1';
|
|
menu.style.transform = 'scale(1)';
|
|
});
|
|
|
|
// 存储当前菜单节点
|
|
this.contextMenuNode = node;
|
|
}
|
|
|
|
/**
|
|
* 隐藏上下文菜单
|
|
*/
|
|
hideContextMenu() {
|
|
const existingMenu = document.getElementById('topology-context-menu');
|
|
if (existingMenu) {
|
|
existingMenu.remove();
|
|
}
|
|
this.contextMenuNode = null;
|
|
}
|
|
|
|
/**
|
|
* 复制IP地址到剪贴板
|
|
*/
|
|
async copyIPAddress(node) {
|
|
try {
|
|
await navigator.clipboard.writeText(node.ip);
|
|
this.showNotification(`IP地址已复制: ${node.ip}`, 'success');
|
|
} catch (error) {
|
|
console.error('复制失败:', error);
|
|
this.showNotification('复制失败,请手动复制', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 居中到指定节点
|
|
*/
|
|
centerToNode(node) {
|
|
if (!node) return;
|
|
|
|
const scale = this.transform.k || 1;
|
|
const translateX = this.width / 2 - node.x * scale;
|
|
const translateY = this.height / 2 - node.y * scale;
|
|
|
|
this.svg.transition()
|
|
.duration(750)
|
|
.call(d3.zoom().transform, d3.zoomIdentity.translate(translateX, translateY).scale(scale));
|
|
}
|
|
|
|
/**
|
|
* 显示通知消息
|
|
*/
|
|
showNotification(message, type = 'info') {
|
|
// 创建通知元素
|
|
const notification = document.createElement('div');
|
|
notification.className = `topology-notification ${type}`;
|
|
notification.textContent = message;
|
|
|
|
// 添加到页面
|
|
document.body.appendChild(notification);
|
|
|
|
// 设置初始样式
|
|
notification.style.position = 'fixed';
|
|
notification.style.top = '20px';
|
|
notification.style.right = '20px';
|
|
notification.style.zIndex = '10000';
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateX(100%)';
|
|
notification.style.transition = 'all 0.3s ease';
|
|
|
|
// 显示动画
|
|
requestAnimationFrame(() => {
|
|
notification.style.opacity = '1';
|
|
notification.style.transform = 'translateX(0)';
|
|
});
|
|
|
|
// 自动隐藏
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateX(100%)';
|
|
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.parentNode.removeChild(notification);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
/**
|
|
* 渲染标签
|
|
*/
|
|
renderLabels() {
|
|
const labels = this.labelGroup.selectAll('.node-label')
|
|
.data(this.nodes, d => d.id);
|
|
|
|
labels.exit().remove();
|
|
|
|
const labelEnter = labels.enter()
|
|
.append('text')
|
|
.attr('class', 'node-label')
|
|
.attr('dy', 35) // 调整标签位置,从25改回35
|
|
.text(d => d.name);
|
|
|
|
const labelUpdate = labelEnter.merge(labels);
|
|
}
|
|
|
|
/**
|
|
* 更新节点位置 - 优化版,支持拖拽时的实时更新
|
|
*/
|
|
updateNodePositions() {
|
|
this.nodeGroup.selectAll('.topology-node-group')
|
|
.attr('transform', d => {
|
|
// 优先使用固定位置(拖拽时设置的fx, fy),否则使用模拟位置
|
|
const x = d.fx !== undefined ? d.fx : d.x;
|
|
const y = d.fy !== undefined ? d.fy : d.y;
|
|
return `translate(${x},${y})`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 更新标签位置
|
|
*/
|
|
updateLabelPositions() {
|
|
this.labelGroup.selectAll('.node-label')
|
|
.attr('x', d => d.x)
|
|
.attr('y', d => d.y);
|
|
}
|
|
|
|
/**
|
|
* 创建拖拽行为 - 优化版,只移动当前设备,其他设备保持固定
|
|
*/
|
|
createDragBehavior() {
|
|
return d3.drag()
|
|
.on('start', (event, d) => {
|
|
// 固定所有其他节点
|
|
this.nodes.forEach(node => {
|
|
if (node !== d) {
|
|
node.fx = node.x;
|
|
node.fy = node.y;
|
|
}
|
|
});
|
|
|
|
// 设置当前节点为拖拽状态
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
|
|
// 提升被拖拽节点的层级
|
|
d3.select(event.sourceEvent.currentTarget.parentNode)
|
|
.raise()
|
|
.classed('dragging', true);
|
|
|
|
// 暂停力模拟或设置很低的alpha值
|
|
this.simulation.alphaTarget(0.01).restart();
|
|
})
|
|
.on('drag', (event, d) => {
|
|
// 实时更新当前节点位置
|
|
d.fx = event.x;
|
|
d.fy = event.y;
|
|
|
|
// 立即更新节点位置
|
|
const nodeGroup = d3.select(event.sourceEvent.currentTarget.parentNode);
|
|
nodeGroup.attr('transform', `translate(${d.fx},${d.fy})`);
|
|
|
|
// 立即更新相关链路
|
|
this.linkGroup.selectAll('.topology-link')
|
|
.filter(link => link.source === d || link.target === d)
|
|
.attr('x1', link => link.source.fx || link.source.x)
|
|
.attr('y1', link => link.source.fy || link.source.y)
|
|
.attr('x2', link => link.target.fx || link.target.x)
|
|
.attr('y2', link => link.target.fy || link.target.y);
|
|
|
|
// 立即更新标签位置
|
|
this.labelGroup.selectAll('.node-label')
|
|
.filter(node => node === d)
|
|
.attr('x', d.fx)
|
|
.attr('y', d.fy + 35); // 调整标签位置,从25改回35
|
|
})
|
|
.on('end', (event, d) => {
|
|
// 拖拽结束,停止力模拟
|
|
this.simulation.alphaTarget(0);
|
|
|
|
// 移除拖拽状态
|
|
d3.select(event.sourceEvent.currentTarget.parentNode)
|
|
.classed('dragging', false);
|
|
|
|
// 保持当前节点在拖拽结束位置(固定位置)
|
|
// 保持其他节点的固定位置不变
|
|
// 注意:不释放fx, fy,这样所有节点都保持在固定位置
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 选择节点
|
|
*/
|
|
selectNode(event, node) {
|
|
event.stopPropagation();
|
|
|
|
// 清除之前的选择
|
|
this.clearSelection();
|
|
|
|
// 设置新选择
|
|
this.selectedNode = node;
|
|
this.selectedLink = null;
|
|
|
|
// 更新UI
|
|
this.updateNodeSelection();
|
|
this.showNodeDetails(node);
|
|
this.updateActionButtons();
|
|
}
|
|
|
|
/**
|
|
* 选择链路
|
|
*/
|
|
selectLink(event, link) {
|
|
event.stopPropagation();
|
|
|
|
// 清除之前的选择
|
|
this.clearSelection();
|
|
|
|
// 设置新选择
|
|
this.selectedLink = link;
|
|
this.selectedNode = null;
|
|
|
|
// 更新UI
|
|
this.updateLinkSelection();
|
|
this.showLinkDetails(link);
|
|
this.updateActionButtons();
|
|
}
|
|
|
|
/**
|
|
* 清除选择
|
|
*/
|
|
clearSelection() {
|
|
this.selectedNode = null;
|
|
this.selectedLink = null;
|
|
|
|
// 更新UI
|
|
this.updateNodeSelection();
|
|
this.updateLinkSelection();
|
|
this.showDefaultDetails();
|
|
this.updateActionButtons();
|
|
}
|
|
|
|
/**
|
|
* 更新节点选择状态
|
|
*/
|
|
updateNodeSelection() {
|
|
this.nodeGroup.selectAll('.topology-node-group')
|
|
.classed('selected', d => d === this.selectedNode);
|
|
|
|
this.labelGroup.selectAll('.node-label')
|
|
.classed('selected', d => d === this.selectedNode);
|
|
}
|
|
|
|
/**
|
|
* 更新链路选择状态
|
|
*/
|
|
updateLinkSelection() {
|
|
this.linkGroup.selectAll('.topology-link')
|
|
.classed('selected', d => d === this.selectedLink);
|
|
}
|
|
|
|
/**
|
|
* 显示节点详情
|
|
*/
|
|
showNodeDetails(node) {
|
|
// 获取接口数,优先级:直接接口数据 > 连接数推测 > 显示未知
|
|
let interfaceCount;
|
|
if (node.interfaces && Array.isArray(node.interfaces) && node.interfaces.length > 0) {
|
|
// 有实际接口数据
|
|
interfaceCount = node.interfaces.length;
|
|
} else if (node.interface_count && node.interface_count > 0) {
|
|
// 有接口数字段
|
|
interfaceCount = node.interface_count;
|
|
} else {
|
|
// 尝试从连接的链路数推测
|
|
const connectedLinks = this.links.filter(link =>
|
|
(link.source.id || link.source) === node.id ||
|
|
(link.target.id || link.target) === node.id
|
|
);
|
|
if (connectedLinks.length > 0) {
|
|
interfaceCount = `${connectedLinks.length}+ (推测)`;
|
|
} else {
|
|
interfaceCount = '未知';
|
|
}
|
|
}
|
|
|
|
const detailsHtml = `
|
|
<div class="device-info">
|
|
<div class="device-name">${node.name}</div>
|
|
<div class="device-details">
|
|
<div class="detail-item">
|
|
<span class="detail-label">IP地址:</span>
|
|
<span>${node.ip}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">厂商:</span>
|
|
<span>${this.getVendorDisplayName(node.vendor)}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">类型:</span>
|
|
<span>${this.getDeviceTypeDisplayName(node.deviceType)}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">状态:</span>
|
|
<span class="device-status-indicator ${node.status}"></span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">接口数:</span>
|
|
<span>${interfaceCount}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.elements.nodeDetails.innerHTML = detailsHtml;
|
|
}
|
|
|
|
/**
|
|
* 显示链路详情
|
|
*/
|
|
showLinkDetails(link) {
|
|
const sourceNode = this.nodes.find(n => n.id === link.source.id || n.id === link.source);
|
|
const targetNode = this.nodes.find(n => n.id === link.target.id || n.id === link.target);
|
|
|
|
const detailsHtml = `
|
|
<div class="link-info">
|
|
<div class="link-endpoints">
|
|
${sourceNode?.name || 'Unknown'} ↔ ${targetNode?.name || 'Unknown'}
|
|
</div>
|
|
<div class="link-details-info">
|
|
<div class="detail-item">
|
|
<span class="detail-label">协议:</span>
|
|
<span class="protocol-badge ${link.protocol}">${link.protocol.toUpperCase()}</span>
|
|
</div>
|
|
${link.sourceInterface ? `
|
|
<div class="detail-item">
|
|
<span class="detail-label">源接口:</span>
|
|
<span>${link.sourceInterface}</span>
|
|
</div>
|
|
` : ''}
|
|
${link.targetInterface ? `
|
|
<div class="detail-item">
|
|
<span class="detail-label">目标接口:</span>
|
|
<span>${link.targetInterface}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.elements.linkDetails.innerHTML = detailsHtml;
|
|
}
|
|
|
|
/**
|
|
* 显示默认详情
|
|
*/
|
|
showDefaultDetails() {
|
|
this.elements.nodeDetails.innerHTML = `
|
|
<div class="no-selection">
|
|
<i class="fas fa-mouse-pointer"></i>
|
|
<span>点击节点查看详细信息</span>
|
|
</div>
|
|
`;
|
|
|
|
this.elements.linkDetails.innerHTML = `
|
|
<div class="no-selection">
|
|
<i class="fas fa-mouse-pointer"></i>
|
|
<span>点击链路查看连接信息</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 更新操作按钮状态
|
|
*/
|
|
updateActionButtons() {
|
|
const hasSelection = this.selectedNode !== null;
|
|
|
|
if (this.elements.connectBtn) this.elements.connectBtn.disabled = !hasSelection;
|
|
if (this.elements.pingBtn) this.elements.pingBtn.disabled = !hasSelection;
|
|
if (this.elements.discoverNeighborsBtn) this.elements.discoverNeighborsBtn.disabled = !hasSelection;
|
|
if (this.elements.propertiesBtn) this.elements.propertiesBtn.disabled = !hasSelection;
|
|
}
|
|
|
|
/**
|
|
* 更新统计信息
|
|
*/
|
|
updateStatistics() {
|
|
// 计算协议数量
|
|
const protocols = new Set(this.links.map(l => l.protocol));
|
|
const vendors = new Set(this.nodes.map(n => n.vendor));
|
|
|
|
if (this.elements.statNodes) this.elements.statNodes.textContent = this.nodes.length;
|
|
if (this.elements.statLinks) this.elements.statLinks.textContent = this.links.length;
|
|
if (this.elements.statProtocols) this.elements.statProtocols.textContent = protocols.size;
|
|
if (this.elements.statVendors) this.elements.statVendors.textContent = vendors.size;
|
|
}
|
|
|
|
/**
|
|
* 显示发现对话框
|
|
*/
|
|
async showDiscoverDialog() {
|
|
try {
|
|
// 加载可用设备
|
|
const response = await fetch('/api/devices');
|
|
const devices = await response.json();
|
|
|
|
this.populateDeviceList(devices);
|
|
this.showModal(this.elements.discoverModal);
|
|
} catch (error) {
|
|
console.error('加载设备列表失败:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 填充设备列表
|
|
*/
|
|
populateDeviceList(devices) {
|
|
const container = this.elements.seedDevicesList;
|
|
|
|
if (!devices || devices.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="no-devices">
|
|
<p>没有可用的设备。请先在设备管理页面添加设备。</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const deviceListHtml = devices.map(device => `
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" value="${device.id}" data-device-name="${device.name}">
|
|
<div class="device-checkbox-item">
|
|
<span class="device-checkbox-name">${device.name}</span>
|
|
<span class="device-checkbox-info">${device.ip_address} (${device.vendor})</span>
|
|
</div>
|
|
</label>
|
|
`).join('');
|
|
|
|
container.innerHTML = deviceListHtml;
|
|
}
|
|
|
|
/**
|
|
* 处理发现表单提交
|
|
*/
|
|
async handleDiscoverSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
// 获取选中的设备
|
|
const selectedDevices = Array.from(this.elements.seedDevicesList.querySelectorAll('input[type="checkbox"]:checked'))
|
|
.map(cb => cb.value);
|
|
|
|
if (selectedDevices.length === 0) {
|
|
alert('请至少选择一个种子设备');
|
|
return;
|
|
}
|
|
|
|
// 显示进度
|
|
this.showDiscoveryProgress(true);
|
|
|
|
try {
|
|
// 调用发现API
|
|
const response = await fetch('/api/topology/discover', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(selectedDevices)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// 发现成功,更新拓扑
|
|
this.updateTopology(result.topology);
|
|
this.hideModal(this.elements.discoverModal);
|
|
this.showDiscoveryProgress(false);
|
|
} else {
|
|
throw new Error(result.message || '拓扑发现失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('拓扑发现失败:', error);
|
|
alert('拓扑发现失败: ' + error.message);
|
|
this.showDiscoveryProgress(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 显示/隐藏发现进度
|
|
*/
|
|
showDiscoveryProgress(show) {
|
|
if (this.elements.discoveryProgress) {
|
|
this.elements.discoveryProgress.style.display = show ? 'block' : 'none';
|
|
}
|
|
|
|
if (this.elements.discoverForm) {
|
|
this.elements.discoverForm.style.display = show ? 'none' : 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清空拓扑
|
|
*/
|
|
async clearTopology() {
|
|
if (!confirm('确定要清空拓扑数据吗?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/topology', {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.nodes = [];
|
|
this.links = [];
|
|
this.clearSelection();
|
|
this.renderTopology();
|
|
this.updateStatistics();
|
|
this.showEmpty(true);
|
|
}
|
|
} catch (error) {
|
|
console.error('清空拓扑失败:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 缩放控制
|
|
*/
|
|
fitToView() {
|
|
if (this.nodes.length === 0) return;
|
|
|
|
const bounds = this.getTopologyBounds();
|
|
const fullWidth = this.width;
|
|
const fullHeight = this.height;
|
|
const width = bounds.x[1] - bounds.x[0];
|
|
const height = bounds.y[1] - bounds.y[0];
|
|
const midX = (bounds.x[0] + bounds.x[1]) / 2;
|
|
const midY = (bounds.y[0] + bounds.y[1]) / 2;
|
|
|
|
if (width === 0 || height === 0) return;
|
|
|
|
const scale = Math.min(fullWidth / width, fullHeight / height) * 0.8;
|
|
const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
|
|
|
|
this.svg.transition()
|
|
.duration(750)
|
|
.call(d3.zoom().transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
|
|
}
|
|
|
|
zoomIn() {
|
|
this.svg.transition()
|
|
.duration(300)
|
|
.call(d3.zoom().scaleBy, 1.5);
|
|
}
|
|
|
|
zoomOut() {
|
|
this.svg.transition()
|
|
.duration(300)
|
|
.call(d3.zoom().scaleBy, 1 / 1.5);
|
|
}
|
|
|
|
centerView() {
|
|
this.svg.transition()
|
|
.duration(750)
|
|
.call(d3.zoom().transform, d3.zoomIdentity);
|
|
}
|
|
|
|
/**
|
|
* 获取拓扑边界
|
|
*/
|
|
getTopologyBounds() {
|
|
const xs = this.nodes.map(d => d.x);
|
|
const ys = this.nodes.map(d => d.y);
|
|
|
|
return {
|
|
x: [Math.min(...xs) - 50, Math.max(...xs) + 50],
|
|
y: [Math.min(...ys) - 50, Math.max(...ys) + 50]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 应用过滤器
|
|
*/
|
|
applyFilters() {
|
|
const vendorFilter = this.elements.vendorFilter?.value;
|
|
const deviceTypeFilter = this.elements.deviceTypeFilter?.value;
|
|
|
|
// 应用节点过滤
|
|
this.nodeGroup.selectAll('.topology-node-group')
|
|
.style('opacity', d => {
|
|
let visible = true;
|
|
if (vendorFilter && d.vendor !== vendorFilter) visible = false;
|
|
if (deviceTypeFilter && d.deviceType !== deviceTypeFilter) visible = false;
|
|
return visible ? 1 : 0.2;
|
|
});
|
|
|
|
// 应用标签过滤
|
|
this.labelGroup.selectAll('.node-label')
|
|
.style('opacity', d => {
|
|
let visible = true;
|
|
if (vendorFilter && d.vendor !== vendorFilter) visible = false;
|
|
if (deviceTypeFilter && d.deviceType !== deviceTypeFilter) visible = false;
|
|
return visible ? 1 : 0.2;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 操作按钮功能
|
|
*/
|
|
connectToDevice() {
|
|
if (!this.selectedNode) return;
|
|
|
|
// 切换到终端页面并连接设备
|
|
this.gotoTerminalPage();
|
|
|
|
// 触发设备连接(需要集成到终端模块)
|
|
setTimeout(() => {
|
|
const event = new CustomEvent('connectDevice', {
|
|
detail: { deviceId: this.selectedNode.id }
|
|
});
|
|
document.dispatchEvent(event);
|
|
}, 100);
|
|
}
|
|
|
|
async pingDevice() {
|
|
if (!this.selectedNode) return;
|
|
|
|
alert(`Ping功能待实现: ${this.selectedNode.ip}`);
|
|
}
|
|
|
|
async discoverNeighbors() {
|
|
if (!this.selectedNode) return;
|
|
|
|
try {
|
|
// 显示加载状态
|
|
this.showLoading(true);
|
|
|
|
const response = await fetch(`/api/devices/${this.selectedNode.id}/discover-neighbors`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// 发现成功,重新加载拓扑但保持当前状态
|
|
await this.loadTopologyData();
|
|
|
|
// 显示成功消息
|
|
alert(`发现邻居成功,找到 ${result.neighbor_count} 个邻居`);
|
|
|
|
// 如果有数据,确保不显示空状态
|
|
if (this.nodes.length > 0) {
|
|
this.showEmpty(false);
|
|
}
|
|
} else {
|
|
throw new Error(result.message || '发现邻居失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('发现邻居失败:', error);
|
|
alert('发现邻居失败: ' + error.message);
|
|
} finally {
|
|
this.showLoading(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加载拓扑数据(不改变UI状态)
|
|
*/
|
|
async loadTopologyData() {
|
|
try {
|
|
const response = await fetch('/api/topology');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.topology) {
|
|
this.updateTopology(data.topology);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载拓扑数据失败:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
showDeviceProperties() {
|
|
if (!this.selectedNode) return;
|
|
|
|
// 切换到设备管理页面并高亮选中的设备
|
|
this.gotoDevicesPage();
|
|
|
|
setTimeout(() => {
|
|
const event = new CustomEvent('highlightDevice', {
|
|
detail: { deviceId: this.selectedNode.id }
|
|
});
|
|
document.dispatchEvent(event);
|
|
}, 100);
|
|
}
|
|
|
|
/**
|
|
* 页面导航
|
|
*/
|
|
gotoDevicesPage() {
|
|
const devicesNavLink = document.querySelector('.nav-link[data-page="devices"]');
|
|
if (devicesNavLink) {
|
|
devicesNavLink.click();
|
|
}
|
|
}
|
|
|
|
gotoTerminalPage() {
|
|
const terminalNavLink = document.querySelector('.nav-link[data-page="terminal"]');
|
|
if (terminalNavLink) {
|
|
terminalNavLink.click();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 工具函数
|
|
*/
|
|
getVendorDisplayName(vendor) {
|
|
const vendorNames = {
|
|
cisco: 'Cisco',
|
|
huawei: '华为',
|
|
h3c: 'H3C',
|
|
juniper: 'Juniper',
|
|
other: '其他'
|
|
};
|
|
return vendorNames[vendor] || vendor;
|
|
}
|
|
|
|
getDeviceTypeDisplayName(deviceType) {
|
|
const typeNames = {
|
|
router: '路由器',
|
|
switch: '交换机',
|
|
firewall: '防火墙',
|
|
wireless_controller: '无线控制器',
|
|
access_point: '接入点',
|
|
load_balancer: '负载均衡器',
|
|
other: '其他'
|
|
};
|
|
return typeNames[deviceType] || deviceType;
|
|
}
|
|
|
|
getStatusDisplayName(status) {
|
|
const statusNames = {
|
|
online: '在线',
|
|
offline: '离线',
|
|
unreachable: '不可达',
|
|
maintenance: '维护中',
|
|
unknown: '未知'
|
|
};
|
|
return statusNames[status] || status;
|
|
}
|
|
|
|
/**
|
|
* 获取状态颜色
|
|
*/
|
|
getStatusColor(status) {
|
|
const statusColors = {
|
|
online: '#10b981', // 绿色
|
|
offline: '#ef4444', // 红色
|
|
unreachable: '#f59e0b', // 橙色
|
|
maintenance: '#3b82f6', // 蓝色
|
|
unknown: '#6b7280' // 灰色
|
|
};
|
|
return statusColors[status] || statusColors.unknown;
|
|
}
|
|
|
|
/**
|
|
* 获取厂商徽章文字
|
|
*/
|
|
getVendorBadgeText(vendor) {
|
|
const vendorTexts = {
|
|
cisco: 'C',
|
|
huawei: '华',
|
|
h3c: 'H',
|
|
juniper: 'J',
|
|
other: '?'
|
|
};
|
|
return vendorTexts[vendor] || vendorTexts.other;
|
|
}
|
|
|
|
/**
|
|
* 创建设备类型图标 (SVG) - 升级版本,更具设备特征
|
|
*/
|
|
createDeviceIcon(deviceType, black = false) {
|
|
const iconSize = 40; // 从32进一步增大到40
|
|
const iconColor = black ? '#000000' : '#ffffff';
|
|
const shadowColor = 'rgba(0,0,0,0.15)';
|
|
|
|
const icons = {
|
|
router: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="routerGrad${black ? 'Black' : ''}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:${iconColor};stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:${black ? '#333333' : '#e5e7eb'};stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="routerShadow${black ? 'Black' : ''}">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#routerShadow${black ? 'Black' : ''})">
|
|
<!-- 路由器主机箱 -->
|
|
<rect x="2" y="8" width="20" height="8" rx="2" fill="url(#routerGrad${black ? 'Black' : ''})" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="3" y="9" width="18" height="6" rx="1" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.1)"/>
|
|
|
|
<!-- 前面板指示灯 -->
|
|
<circle cx="5" cy="12" r="1" fill="#10b981"/>
|
|
<circle cx="7.5" cy="12" r="1" fill="#f59e0b"/>
|
|
<circle cx="10" cy="12" r="1" fill="#ef4444"/>
|
|
|
|
<!-- 以太网端口 -->
|
|
<rect x="14" y="10.5" width="6" height="3" rx="0.5" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.3)"/>
|
|
<rect x="14.5" y="11" width="1" height="2" rx="0.2" fill="${iconColor}"/>
|
|
<rect x="16" y="11" width="1" height="2" rx="0.2" fill="${iconColor}"/>
|
|
<rect x="17.5" y="11" width="1" height="2" rx="0.2" fill="${iconColor}"/>
|
|
<rect x="19" y="11" width="1" height="2" rx="0.2" fill="${iconColor}"/>
|
|
|
|
<!-- WiFi天线 -->
|
|
<rect x="5" y="5" width="1.5" height="3" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="8" y="4" width="1.5" height="4" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="11" y="3" width="1.5" height="5" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="14" y="4" width="1.5" height="4" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="17" y="5" width="1.5" height="3" rx="0.5" fill="${iconColor}"/>
|
|
|
|
<!-- 天线顶部 -->
|
|
<circle cx="5.75" cy="4.5" r="0.5" fill="#fbbf24"/>
|
|
<circle cx="8.75" cy="3.5" r="0.5" fill="#fbbf24"/>
|
|
<circle cx="11.75" cy="2.5" r="0.5" fill="#fbbf24"/>
|
|
<circle cx="14.75" cy="3.5" r="0.5" fill="#fbbf24"/>
|
|
<circle cx="17.75" cy="4.5" r="0.5" fill="#fbbf24"/>
|
|
</g>
|
|
</svg>`,
|
|
|
|
switch: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="switchGrad${black ? 'Black' : ''}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:${iconColor};stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:${black ? '#333333' : '#e5e7eb'};stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="switchShadow${black ? 'Black' : ''}">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#switchShadow${black ? 'Black' : ''})">
|
|
<!-- 交换机主体 -->
|
|
<rect x="1" y="7" width="22" height="10" rx="2" fill="url(#switchGrad${black ? 'Black' : ''})" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="2" y="8" width="20" height="8" rx="1" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.1)"/>
|
|
|
|
<!-- 前面板 -->
|
|
<rect x="2.5" y="8.5" width="19" height="7" rx="0.5" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.05)"/>
|
|
|
|
<!-- 端口灯光指示器 -->
|
|
<rect x="3" y="9.5" width="17" height="1" rx="0.5" fill="rgba(16,185,129,0.3)"/>
|
|
<rect x="3" y="11" width="17" height="1" rx="0.5" fill="rgba(245,158,11,0.3)"/>
|
|
|
|
<!-- 网络端口组 1 -->
|
|
<rect x="3" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="5.2" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="7.4" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="9.6" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="11.8" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="14" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="16.2" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
<rect x="18.4" y="13" width="1.8" height="1.5" rx="0.3" fill="${iconColor}" stroke="rgba(${black ? '255,255,255' : '0,0,0'},0.2)" stroke-width="0.3"/>
|
|
|
|
<!-- 千兆上行端口 -->
|
|
<rect x="17" y="9" width="2.5" height="2" rx="0.3" fill="#3b82f6" stroke="${iconColor}" stroke-width="0.3"/>
|
|
<text x="18.25" y="10.5" text-anchor="middle" font-size="3" fill="${iconColor}" font-weight="bold">G</text>
|
|
|
|
<!-- 状态LED -->
|
|
<circle cx="4" cy="10.5" r="0.4" fill="#10b981"/>
|
|
<circle cx="6" cy="10.5" r="0.4" fill="#10b981"/>
|
|
<circle cx="8" cy="10.5" r="0.4" fill="#f59e0b"/>
|
|
<circle cx="10" cy="10.5" r="0.4" fill="#10b981"/>
|
|
<circle cx="12" cy="10.5" r="0.4" fill="#10b981"/>
|
|
</g>
|
|
</svg>`,
|
|
|
|
firewall: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="firewallGrad${black ? 'Black' : ''}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:${black ? '#000000' : '#ef4444'};stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:${black ? '#333333' : '#dc2626'};stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="firewallShadow${black ? 'Black' : ''}">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#firewallShadow${black ? 'Black' : ''})">
|
|
<!-- 盾牌外形 -->
|
|
<path d="M12 2L3 6v6c0 6.55 4.03 12.74 9 14 4.97-1.26 9-7.45 9-14V6l-9-4z" fill="url(#firewallGrad${black ? 'Black' : ''})" stroke="${iconColor}" stroke-width="0.5"/>
|
|
|
|
<!-- 内部盾牌 -->
|
|
<path d="M12 3.5L4.5 7v5.5c0 5.8 3.5 10.6 7.5 11.8 4-1.2 7.5-6 7.5-11.8V7L12 3.5z" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.1)"/>
|
|
|
|
<!-- 防火墙图标 -->
|
|
<rect x="7" y="8" width="10" height="1" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="7" y="10" width="10" height="1" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="7" y="12" width="10" height="1" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="7" y="14" width="10" height="1" rx="0.5" fill="${iconColor}"/>
|
|
|
|
<!-- 中心锁 -->
|
|
<rect x="10" y="9.5" width="4" height="3.5" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="10.5" y="10" width="3" height="2.5" rx="0.3" fill="${black ? '#000000' : '#ef4444'}"/>
|
|
<path d="M11.5 9.5v-1c0-0.8 0.7-1.5 1.5-1.5s1.5 0.7 1.5 1.5v1" stroke="${iconColor}" stroke-width="0.8" fill="none"/>
|
|
|
|
<!-- 安全指示器 -->
|
|
<circle cx="8" cy="16" r="0.8" fill="#10b981"/>
|
|
<circle cx="12" cy="16" r="0.8" fill="#f59e0b"/>
|
|
<circle cx="16" cy="16" r="0.8" fill="#ef4444"/>
|
|
</g>
|
|
</svg>`,
|
|
|
|
wireless_controller: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="wifiGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="wifiShadow">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#wifiShadow)">
|
|
<!-- 控制器主体 -->
|
|
<rect x="2" y="12" width="20" height="6" rx="2" fill="url(#wifiGrad)" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="3" y="13" width="18" height="4" rx="1" fill="rgba(0,0,0,0.1)"/>
|
|
|
|
<!-- WiFi信号波纹(大范围) -->
|
|
<path d="M12 9c-4 0-7.5 1.6-10 4.2L3.4 12C6.2 9.9 9 8.5 12 8.5s5.8 1.4 8.6 3.5L19.6 13.2C17.5 10.6 14 9 12 9z" fill="${iconColor}" opacity="0.3"/>
|
|
<path d="M12 6c-5.5 0-10.5 2.2-14 5.8L-0.6 10.4C3.3 6.4 7.5 4.5 12 4.5s8.7 1.9 12.6 5.9L23.4 11.8C19.5 8.2 14.5 6 12 6z" fill="${iconColor}" opacity="0.2"/>
|
|
|
|
<!-- 中等信号波 -->
|
|
<path d="M12 11c-2.2 0-4.2 0.9-5.7 2.4l1.4 1.4c1-1 2.4-1.6 3.8-1.6s2.8 0.6 3.8 1.6l1.4-1.4C16.2 11.9 14.2 11 12 11z" fill="${iconColor}" opacity="0.6"/>
|
|
|
|
<!-- 小信号波 -->
|
|
<circle cx="12" cy="15.5" r="1.5" fill="${iconColor}"/>
|
|
|
|
<!-- 天线 -->
|
|
<rect x="6" y="9" width="1" height="3" rx="0.5" fill="${iconColor}"/>
|
|
<rect x="17" y="9" width="1" height="3" rx="0.5" fill="${iconColor}"/>
|
|
|
|
<!-- 天线顶部 -->
|
|
<circle cx="6.5" cy="8.5" r="0.8" fill="#10b981"/>
|
|
<circle cx="17.5" cy="8.5" r="0.8" fill="#10b981"/>
|
|
|
|
<!-- 前面板LED -->
|
|
<rect x="4" y="14.5" width="2" height="1" rx="0.3" fill="#10b981"/>
|
|
<rect x="7" y="14.5" width="2" height="1" rx="0.3" fill="#f59e0b"/>
|
|
<rect x="10" y="14.5" width="2" height="1" rx="0.3" fill="#ef4444"/>
|
|
|
|
<!-- 接入点管理指示 -->
|
|
<text x="18" y="15.8" text-anchor="middle" font-size="3" fill="${iconColor}" font-weight="bold">AP</text>
|
|
</g>
|
|
</svg>`,
|
|
|
|
access_point: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<radialGradient id="apGrad" cx="50%" cy="50%" r="50%">
|
|
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:#d1d5db;stop-opacity:0.8"/>
|
|
</radialGradient>
|
|
<filter id="apShadow">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#apShadow)">
|
|
<!-- AP圆形外壳 -->
|
|
<circle cx="12" cy="15" r="7" fill="url(#apGrad)" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<circle cx="12" cy="15" r="5.5" fill="rgba(0,0,0,0.05)"/>
|
|
|
|
<!-- WiFi波纹效果 -->
|
|
<circle cx="12" cy="15" r="2" fill="none" stroke="#3b82f6" stroke-width="1.5" opacity="0.8"/>
|
|
<circle cx="12" cy="15" r="3.5" fill="none" stroke="#3b82f6" stroke-width="1" opacity="0.5"/>
|
|
<circle cx="12" cy="15" r="5" fill="none" stroke="#3b82f6" stroke-width="0.8" opacity="0.3"/>
|
|
|
|
<!-- 中心发射点 -->
|
|
<circle cx="12" cy="15" r="1.2" fill="#3b82f6"/>
|
|
<circle cx="12" cy="15" r="0.6" fill="${iconColor}"/>
|
|
|
|
<!-- 天线基座 -->
|
|
<rect x="11" y="7" width="2" height="8" rx="1" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.3"/>
|
|
|
|
<!-- 安装支架 -->
|
|
<rect x="9" y="6.5" width="6" height="2" rx="1" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.3"/>
|
|
<rect x="10" y="5.5" width="4" height="1" rx="0.5" fill="rgba(0,0,0,0.1)"/>
|
|
|
|
<!-- 状态指示灯 -->
|
|
<circle cx="12" cy="11" r="0.8" fill="#10b981"/>
|
|
|
|
<!-- AP标识 -->
|
|
<text x="12" y="19" text-anchor="middle" font-size="3" fill="#6b7280" font-weight="bold">WiFi</text>
|
|
</g>
|
|
</svg>`,
|
|
|
|
load_balancer: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="lbGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="lbShadow">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#lbShadow)">
|
|
<!-- 负载均衡器主体 -->
|
|
<rect x="2" y="9" width="20" height="6" rx="2" fill="url(#lbGrad)" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="3" y="10" width="18" height="4" rx="1" fill="rgba(0,0,0,0.1)"/>
|
|
|
|
<!-- 负载均衡核心 -->
|
|
<circle cx="12" cy="12" r="2.5" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
|
|
<circle cx="12" cy="12" r="1.5" fill="#8b5cf6"/>
|
|
|
|
<!-- 负载分配箭头 -->
|
|
<path d="M9 8l3 2 3-2" stroke="${iconColor}" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)"/>
|
|
<path d="M9 16l3-2 3 2" stroke="${iconColor}" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)"/>
|
|
|
|
<!-- 连接线 -->
|
|
<line x1="5" y1="12" x2="9.5" y2="12" stroke="${iconColor}" stroke-width="2"/>
|
|
<line x1="14.5" y1="12" x2="19" y2="12" stroke="${iconColor}" stroke-width="2"/>
|
|
|
|
<!-- 输入输出端点 -->
|
|
<circle cx="4" cy="12" r="1.5" fill="#10b981" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<circle cx="20" cy="12" r="1.5" fill="#10b981" stroke="${iconColor}" stroke-width="0.5"/>
|
|
|
|
<!-- 多个后端连接 -->
|
|
<circle cx="20" cy="9" r="0.8" fill="#3b82f6"/>
|
|
<circle cx="20" cy="12" r="0.8" fill="#10b981"/>
|
|
<circle cx="20" cy="15" r="0.8" fill="#f59e0b"/>
|
|
|
|
<!-- LB标识 -->
|
|
<text x="12" y="12.8" text-anchor="middle" font-size="3" fill="${iconColor}" font-weight="bold">LB</text>
|
|
|
|
<!-- 状态指示 -->
|
|
<circle cx="6" cy="10.5" r="0.5" fill="#10b981"/>
|
|
<circle cx="18" cy="10.5" r="0.5" fill="#10b981"/>
|
|
</g>
|
|
</svg>`,
|
|
|
|
server: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="serverGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#6b7280;stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:#4b5563;stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="serverShadow">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#serverShadow)">
|
|
<!-- 服务器机箱 -->
|
|
<rect x="4" y="3" width="16" height="18" rx="2" fill="url(#serverGrad)" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="5" y="4" width="14" height="16" rx="1" fill="rgba(0,0,0,0.1)"/>
|
|
|
|
<!-- 服务器单元 1 -->
|
|
<rect x="6" y="5" width="12" height="3" rx="0.5" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.3"/>
|
|
<circle cx="7.5" cy="6.5" r="0.5" fill="#10b981"/>
|
|
<circle cx="9" cy="6.5" r="0.5" fill="#10b981"/>
|
|
<rect x="15" y="6" width="2" height="1" rx="0.2" fill="rgba(0,0,0,0.3)"/>
|
|
|
|
<!-- 服务器单元 2 -->
|
|
<rect x="6" y="9" width="12" height="3" rx="0.5" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.3"/>
|
|
<circle cx="7.5" cy="10.5" r="0.5" fill="#10b981"/>
|
|
<circle cx="9" cy="10.5" r="0.5" fill="#f59e0b"/>
|
|
<rect x="15" y="10" width="2" height="1" rx="0.2" fill="rgba(0,0,0,0.3)"/>
|
|
|
|
<!-- 服务器单元 3 -->
|
|
<rect x="6" y="13" width="12" height="3" rx="0.5" fill="${iconColor}" stroke="rgba(0,0,0,0.2)" stroke-width="0.3"/>
|
|
<circle cx="7.5" cy="14.5" r="0.5" fill="#ef4444"/>
|
|
<circle cx="9" cy="14.5" r="0.5" fill="#6b7280"/>
|
|
<rect x="15" y="14" width="2" height="1" rx="0.2" fill="rgba(0,0,0,0.3)"/>
|
|
|
|
<!-- 底部电源/连接部分 -->
|
|
<rect x="6" y="17" width="12" height="2" rx="0.5" fill="rgba(0,0,0,0.2)"/>
|
|
<rect x="7" y="17.5" width="2" height="1" rx="0.2" fill="#10b981"/>
|
|
<rect x="10" y="17.5" width="2" height="1" rx="0.2" fill="#3b82f6"/>
|
|
<rect x="13" y="17.5" width="2" height="1" rx="0.2" fill="#f59e0b"/>
|
|
<rect x="16" y="17.5" width="1" height="1" rx="0.2" fill="#ef4444"/>
|
|
|
|
<!-- 散热孔 -->
|
|
<rect x="11" y="6" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
<rect x="12.5" y="6" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
<rect x="11" y="10" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
<rect x="12.5" y="10" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
<rect x="11" y="14" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
<rect x="12.5" y="14" width="0.5" height="1" fill="rgba(0,0,0,0.3)"/>
|
|
</g>
|
|
</svg>`,
|
|
|
|
other: `
|
|
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" style="pointer-events: none;">
|
|
<defs>
|
|
<linearGradient id="otherGrad${black ? 'Black' : ''}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:${iconColor};stop-opacity:0.9"/>
|
|
<stop offset="100%" style="stop-color:${black ? '#333333' : '#6b7280'};stop-opacity:0.8"/>
|
|
</linearGradient>
|
|
<filter id="otherShadow${black ? 'Black' : ''}">
|
|
<feDropShadow dx="1" dy="1" stdDeviation="1" flood-color="${shadowColor}"/>
|
|
</filter>
|
|
</defs>
|
|
<g filter="url(#otherShadow${black ? 'Black' : ''})">
|
|
<!-- 通用网络设备外壳 -->
|
|
<rect x="3" y="8" width="18" height="8" rx="2" fill="url(#otherGrad${black ? 'Black' : ''})" stroke="${iconColor}" stroke-width="0.5"/>
|
|
<rect x="4" y="9" width="16" height="6" rx="1" fill="rgba(${black ? '255,255,255' : '0,0,0'},0.1)"/>
|
|
|
|
<!-- 网络连接图标 -->
|
|
<circle cx="8" cy="12" r="2.5" fill="none" stroke="${iconColor}" stroke-width="1.5"/>
|
|
<circle cx="16" cy="12" r="2.5" fill="none" stroke="${iconColor}" stroke-width="1.5"/>
|
|
|
|
<!-- 连接线 -->
|
|
<line x1="10.5" y1="12" x2="13.5" y2="12" stroke="${iconColor}" stroke-width="2"/>
|
|
|
|
<!-- 数据流动指示 -->
|
|
<circle cx="11" cy="12" r="0.5" fill="#3b82f6"/>
|
|
<circle cx="12" cy="12" r="0.5" fill="#10b981"/>
|
|
<circle cx="13" cy="12" r="0.5" fill="#f59e0b"/>
|
|
|
|
<!-- 状态指示灯 -->
|
|
<circle cx="5" cy="10" r="0.8" fill="#10b981"/>
|
|
<circle cx="19" cy="10" r="0.8" fill="#f59e0b"/>
|
|
<circle cx="5" cy="14" r="0.8" fill="#3b82f6"/>
|
|
<circle cx="19" cy="14" r="0.8" fill="#ef4444"/>
|
|
|
|
<!-- 设备标识 -->
|
|
<text x="12" y="10.5" text-anchor="middle" font-size="2.5" fill="${iconColor}" font-weight="bold">NET</text>
|
|
</g>
|
|
</svg>`
|
|
};
|
|
|
|
return icons[deviceType] || icons.other;
|
|
}
|
|
|
|
/**
|
|
* 显示/隐藏状态
|
|
*/
|
|
showLoading(show) {
|
|
if (this.elements.topologyLoading) {
|
|
this.elements.topologyLoading.style.display = show ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
showEmpty(show) {
|
|
if (this.elements.topologyEmpty) {
|
|
this.elements.topologyEmpty.style.display = show ? 'block' : 'none';
|
|
}
|
|
if (this.elements.topologySvg) {
|
|
this.elements.topologySvg.style.display = show ? 'none' : 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 模态框控制
|
|
*/
|
|
showModal(modal) {
|
|
if (modal) {
|
|
modal.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
hideModal(modal) {
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 窗口大小改变处理
|
|
*/
|
|
handleResize() {
|
|
const container = this.elements.topologyCanvas;
|
|
if (container) {
|
|
this.width = container.clientWidth;
|
|
this.height = container.clientHeight;
|
|
|
|
this.svg
|
|
.attr('width', this.width)
|
|
.attr('height', this.height);
|
|
|
|
this.simulation
|
|
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
|
|
.alpha(0.3)
|
|
.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 全局变量
|
|
let topologyVisualization = null;
|
|
|
|
// 页面加载完成后初始化
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('拓扑模块JavaScript已加载');
|
|
|
|
// 将TopologyVisualization类暴露到全局作用域,供其他模块使用
|
|
window.TopologyVisualization = TopologyVisualization;
|
|
|
|
// 检查拓扑页面是否当前可见,如果是则立即初始化
|
|
const topologyContent = document.getElementById('topology-content');
|
|
if (topologyContent && topologyContent.style.display === 'block') {
|
|
console.log('拓扑页面当前可见,立即初始化');
|
|
try {
|
|
topologyVisualization = new TopologyVisualization();
|
|
window.topologyVisualization = topologyVisualization;
|
|
console.log('拓扑可视化模块初始化成功');
|
|
} catch (error) {
|
|
console.error('拓扑可视化模块初始化失败:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 导出模块(如果需要)
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = TopologyVisualization;
|
|
}
|