// 全局变量 let cy = null; let currentTaskId = null; let currentTopologyId = null; let currentTerminal = null; // 当前终端实例 // 初始化 document.addEventListener('DOMContentLoaded', function() { initCytoscape(); initEventListeners(); loadTopologyList(); // 加载拓扑列表 loadTopology(); loadDeviceList(); // 加载设备列表 }); // 初始化Cytoscape function initCytoscape() { cy = cytoscape({ container: document.getElementById('cy'), elements: [], style: [ { selector: 'node', style: { 'label': 'data(label)', 'background-color': function(ele) { return getNodeColor(ele.data('type')); }, 'width': 70, 'height': 70, 'border-width': 3, 'border-color': '#667eea', 'text-valign': 'bottom', 'text-halign': 'center', 'font-size': '11px', 'font-weight': 'bold', 'text-wrap': 'wrap', 'text-max-width': '80px' } }, { selector: 'edge', style: { 'width': 2, 'line-color': '#999', 'curve-style': 'bezier', 'label': 'data(protocol)' } }, { selector: 'node:selected', style: { 'border-width': 5, 'border-color': '#FF9800' } } ], layout: { name: 'cose', animate: true, animationDuration: 1000, padding: 30 } }); // 节点点击事件 cy.on('tap', 'node', function(evt) { const node = evt.target; showDeviceDetail(node.data('id')); }); } // 获取节点颜色 function getNodeColor(type) { const colors = { 'cisco': '#4CAF50', 'huawei': '#2196F3', 'h3c': '#9C27B0', 'asa': '#FF5722', 'linux': '#607D8B', 'windows': '#00BCD4' }; return colors[type] || '#999'; } // 初始化事件监听 function initEventListeners() { // 拓扑选择器 document.getElementById('topology-selector').addEventListener('change', switchTopology); // 新建拓扑按钮 document.getElementById('btn-new-topology').addEventListener('click', function() { document.getElementById('modal-new-topology').classList.add('active'); }); // 关闭新建拓扑模态框 document.querySelector('.close-new-topology').addEventListener('click', function() { document.getElementById('modal-new-topology').classList.remove('active'); }); // 关闭编辑设备模态框 document.querySelector('.close-edit-device').addEventListener('click', function() { document.getElementById('modal-edit-device').classList.remove('active'); }); // 新建拓扑表单 document.getElementById('new-topology-form').addEventListener('submit', createTopology); // 扫描按钮 document.getElementById('btn-scan').addEventListener('click', startScan); // 添加设备按钮 document.getElementById('btn-add-device').addEventListener('click', function() { document.getElementById('modal').classList.add('active'); }); // 关闭模态框 document.querySelector('.close').addEventListener('click', function() { document.getElementById('modal').classList.remove('active'); }); // 添加设备表单 document.getElementById('add-device-form').addEventListener('submit', addDevice); // 导出按钮 document.getElementById('btn-export').addEventListener('click', exportTopology); // 登出按钮 var logoutBtn = document.getElementById('btn-logout'); if (logoutBtn) { logoutBtn.addEventListener('click', function() { window.location.href = '/api/logout'; }); } // SSH终端相关 document.querySelector('.close-terminal').addEventListener('click', closeTerminal); // 终端模态框拖动功能 (function() { var dragHandle = document.querySelector('.modal-header-drag'); var modal = document.querySelector('.terminal-modal-content'); var isDragging = false; var offsetX = 0, offsetY = 0; if (dragHandle && modal) { dragHandle.addEventListener('mousedown', function(e) { if (e.target.classList.contains('close-terminal')) return; isDragging = true; var rect = modal.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; modal.style.margin = '0'; modal.style.position = 'absolute'; modal.style.left = rect.left + 'px'; modal.style.top = rect.top + 'px'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; modal.style.left = (e.clientX - offsetX) + 'px'; modal.style.top = (e.clientY - offsetY) + 'px'; }); document.addEventListener('mouseup', function() { isDragging = false; }); } })(); document.querySelector('.close-ssh-creds').addEventListener('click', function() { document.getElementById('modal-ssh-creds').classList.remove('active'); }); document.getElementById('ssh-creds-form').addEventListener('submit', connectTerminal); // 设备选择器相关 document.querySelector('.close-select-devices').addEventListener('click', function() { document.getElementById('modal-select-devices').classList.remove('active'); }); } // 开始扫描 async function startScan() { const scanRange = document.getElementById('scan-range').value; const sshPort = document.getElementById('ssh-port').value; const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!scanRange) { alert('请输入IP范围'); return; } try { const response = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scan_range: scanRange, ssh_port: parseInt(sshPort), username: username, password: password }) }); const data = await response.json(); currentTaskId = data.task_id; // 轮询进度 pollProgress(); } catch (error) { console.error('扫描失败:', error); alert('扫描失败: ' + error.message); } } // 轮询进度 async function pollProgress() { if (!currentTaskId) return; let failCount = 0; const MAX_FAILS = 5; const poll = async () => { try { const response = await fetch(`/api/scan/${currentTaskId}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const task = await response.json(); failCount = 0; // 重置失败计数 // 更新进度 document.getElementById('scan-status').textContent = task.status; document.getElementById('scan-progress').textContent = task.progress + '%'; document.getElementById('progress-fill').style.width = task.progress + '%'; // 更新设备列表(仅当有设备时) if (task.devices && task.devices.length > 0) { updateDeviceList(task.devices); } // 如果完成,更新拓扑 if (task.status === 'completed' || task.status === 'failed') { loadTopology(); loadDeviceList(); // 刷新设备列表 currentTaskId = null; return; } // 继续轮询 setTimeout(poll, 1000); } catch (error) { failCount++; console.error('获取进度失败:', error); // 超过最大失败次数则停止轮询 if (failCount >= MAX_FAILS) { document.getElementById('scan-status').textContent = 'error'; currentTaskId = null; return; } // 等待更长时间后重试 setTimeout(poll, 2000); } }; poll(); } // 更新设备列表 function updateDeviceList(devices) { const listContainer = document.getElementById('device-list'); listContainer.innerHTML = ''; devices.forEach(device => { const item = document.createElement('div'); item.className = 'device-item'; item.innerHTML = `
${device.ip}
${device.type} - ${device.hostname || 'Unknown'}
${device.scan_status}
`; item.addEventListener('click', () => showDeviceDetail(device.id)); listContainer.appendChild(item); }); } // 加载拓扑 async function loadTopology() { try { const response = await fetch('/api/topology'); const graph = await response.json(); // 清空现有元素 cy.elements().remove(); // 添加节点 graph.nodes.forEach(node => { // 显示格式: 主机名 + IP地址 let label = node.ip; // 默认显示IP if (node.hostname && node.hostname !== '') { label = `${node.hostname}\n${node.ip}`; } cy.add({ group: 'nodes', data: { id: node.id, label: label, type: node.type, ip: node.ip, hostname: node.hostname } }); }); // 添加边 graph.edges.forEach(edge => { cy.add({ group: 'edges', data: { id: edge.id, source: edge.source, target: edge.target, protocol: edge.protocol } }); }); // 重新布局 cy.layout({ name: 'cose', animate: true, animationDuration: 1000, padding: 30 }).run(); cy.fit(40); } catch (error) { console.error('加载拓扑失败:', error); } } // 加载设备列表 async function loadDeviceList() { try { const response = await fetch('/api/devices'); const devices = await response.json(); const listContainer = document.getElementById('device-list'); listContainer.innerHTML = ''; if (devices.length === 0) { listContainer.innerHTML = '

暂无设备

'; return; } devices.forEach(device => { const item = document.createElement('div'); item.className = 'device-item'; const interfaceCount = device.interfaces ? device.interfaces.length : 0; const neighborCount = device.neighbors ? device.neighbors.length : 0; item.innerHTML = `
${device.ip}
${device.type} - ${device.hostname || 'Unknown'}
接口: ${interfaceCount} | 邻居: ${neighborCount}
${device.scan_status || 'pending'}
`; item.addEventListener('click', () => showDeviceDetail(device.id || device.ip)); listContainer.appendChild(item); }); } catch (error) { console.error('加载设备列表失败:', error); } } // 显示设备详情 async function showDeviceDetail(deviceId) { try { const response = await fetch(`/api/device/${deviceId}`); if (response.status === 401) { window.location.href = '/login'; return; } if (!response.ok) { console.error('获取设备详情失败:', response.status, response.statusText); return; } const device = await response.json(); const detailPanel = document.getElementById('detail-panel'); const detailContainer = document.getElementById('device-detail'); detailContainer.innerHTML = `

基本信息

IP: ${device.ip}

主机名: ${device.hostname || 'N/A'}

类型: ${device.type}

系统: ${device.os_version || 'N/A'}

运行时间: ${device.uptime || 'N/A'}

接口信息 (${device.interfaces.length})

${device.interfaces.map(iface => `
${iface.name}

状态: ${iface.status}

IP: ${iface.ip || 'N/A'}

MAC: ${iface.mac || 'N/A'}

速度: ${iface.speed || 'N/A'}

双工: ${iface.duplex || 'N/A'}

VLAN: ${iface.vlan || 'N/A'}

`).join('')}

邻居设备 (${device.neighbors.length})

${device.neighbors.map(neighbor => `
${neighbor.remote_device}

本地接口: ${neighbor.local_interface}

远程接口: ${neighbor.remote_interface}

协议: ${neighbor.protocol}

`).join('')}
`; detailPanel.classList.add('active'); } catch (error) { console.error('获取设备详情失败:', error); } } // 添加设备 async function addDevice(event) { event.preventDefault(); const ip = document.getElementById('device-ip').value; const type = document.getElementById('device-type').value; const username = document.getElementById('device-username').value; const password = document.getElementById('device-password').value; try { const response = await fetch('/api/device', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip: ip, type: type, username: username, password: password }) }); if (response.ok) { document.getElementById('modal').classList.remove('active'); document.getElementById('add-device-form').reset(); loadTopology(); loadDeviceList(); // 刷新设备列表 alert('设备添加成功'); } else { const error = await response.json(); alert('添加失败: ' + error.message); } } catch (error) { console.error('添加设备失败:', error); alert('添加失败: ' + error.message); } } // 导出拓扑 function exportTopology() { const json = cy.json(); const dataStr = JSON.stringify(json, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(dataBlob); link.download = 'topology.json'; link.click(); } // 加载拓扑列表 async function loadTopologyList() { try { const response = await fetch('/api/topologies'); const topos = await response.json(); const selector = document.getElementById('topology-selector'); selector.innerHTML = ''; if (topos.length === 0) { selector.innerHTML = ''; return; } topos.forEach(topo => { const option = document.createElement('option'); option.value = topo.id; option.textContent = `${topo.name} (${topo.device_count || 0} 设备)`; if (topo.name.includes(' (当前)')) { option.selected = true; currentTopologyId = topo.id; } selector.appendChild(option); }); } catch (error) { console.error('加载拓扑列表失败:', error); } } // 切换拓扑 async function switchTopology(event) { const topoId = event.target.value; if (!topoId) return; try { const response = await fetch('/api/topology/switch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topology_id: topoId }) }); if (response.ok) { currentTopologyId = topoId; // 刷新拓扑和设备列表 loadTopology(); loadDeviceList(); } else { const error = await response.json(); alert('切换失败: ' + error); } } catch (error) { console.error('切换拓扑失败:', error); alert('切换失败: ' + error.message); } } // 新创建的拓扑ID(用于设备选择器) let newTopologyId = null; // 创建拓扑 async function createTopology(event) { event.preventDefault(); const name = document.getElementById('topo-name').value; try { const response = await fetch('/api/topologies', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name }) }); if (response.ok) { const topo = await response.json(); newTopologyId = topo.id; document.getElementById('modal-new-topology').classList.remove('active'); document.getElementById('new-topology-form').reset(); // 刷新拓扑列表 loadTopologyList(); loadTopology(); loadDeviceList(); // 弹出设备选择器 openDeviceSelector(); } else { const error = await response.json(); alert('创建失败: ' + error); } } catch (error) { console.error('创建拓扑失败:', error); alert('创建失败: ' + error.message); } } // ==================== 设备选择器 ==================== // 打开设备选择器 async function openDeviceSelector() { document.getElementById('modal-select-devices').classList.add('active'); document.getElementById('selected-count').textContent = '已选 0 台'; const poolList = document.getElementById('device-pool-list'); poolList.innerHTML = '

加载中...

'; try { const response = await fetch('/api/devices/all'); const devices = await response.json(); poolList.innerHTML = ''; if (devices.length === 0) { poolList.innerHTML = '

暂无设备,请先扫描添加设备

'; return; } devices.forEach(device => { const item = document.createElement('div'); item.className = 'device-pool-item'; item.innerHTML = `
${device.ip}
${device.type} - ${device.hostname || 'Unknown'}
`; // 点击整行切换选中 item.addEventListener('click', function(e) { if (e.target.tagName !== 'INPUT') { const cb = item.querySelector('input[type=checkbox]'); cb.checked = !cb.checked; updateSelectedCount(); } item.classList.toggle('selected', item.querySelector('input[type=checkbox]').checked); }); poolList.appendChild(item); }); } catch (error) { poolList.innerHTML = '

加载失败

'; console.error('加载设备池失败:', error); } } // 更新选中计数 function updateSelectedCount() { const checkboxes = document.querySelectorAll('#device-pool-list input[type=checkbox]:checked'); document.getElementById('selected-count').textContent = `已选 ${checkboxes.length} 台`; } // 全选/取消全选 function toggleSelectAllDevices(btn) { const checkboxes = document.querySelectorAll('#device-pool-list input[type=checkbox]'); const allChecked = Array.from(checkboxes).every(cb => cb.checked); checkboxes.forEach(cb => { cb.checked = !allChecked; cb.closest('.device-pool-item').classList.toggle('selected', !allChecked); }); btn.textContent = allChecked ? '全选' : '取消全选'; updateSelectedCount(); } // 确认添加设备到拓扑 async function confirmAddDevices() { const targetTopoId = newTopologyId || currentTopologyId; if (!targetTopoId) { alert('请先选择目标拓扑'); return; } const checkboxes = document.querySelectorAll('#device-pool-list input[type=checkbox]:checked'); if (checkboxes.length === 0) { alert('请至少选择一台设备'); return; } const deviceIds = Array.from(checkboxes).map(cb => cb.value); try { const response = await fetch(`/api/topology/${targetTopoId}/devices`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_ids: deviceIds }) }); if (response.ok) { const result = await response.json(); document.getElementById('modal-select-devices').classList.remove('active'); newTopologyId = null; // 刷新界面 loadTopologyList(); loadTopology(); loadDeviceList(); alert(`成功添加 ${result.added} 台设备到 ${result.topology}`); } else { alert('添加失败'); } } catch (error) { console.error('添加设备失败:', error); alert('添加失败: ' + error.message); } } // ==================== SSH终端功能 ==================== // 打开SSH终端(先弹出凭据输入框) function openSSHTerminal(ip, hostname) { document.getElementById('ssh-target-ip').value = ip; document.getElementById('ssh-target-username').value = document.getElementById('username').value || 'admin'; document.getElementById('ssh-target-password').value = document.getElementById('password').value || ''; document.getElementById('ssh-target-port').value = document.getElementById('ssh-port').value || '22'; document.getElementById('modal-ssh-creds').classList.add('active'); } // 连接SSH终端 async function connectTerminal(event) { event.preventDefault(); const ip = document.getElementById('ssh-target-ip').value; const port = document.getElementById('ssh-target-port').value || '22'; const username = document.getElementById('ssh-target-username').value; const password = document.getElementById('ssh-target-password').value; if (!username || !password) { alert('请输入用户名和密码'); return; } // 关闭凭据弹窗,打开终端弹窗 document.getElementById('modal-ssh-creds').classList.remove('active'); document.getElementById('modal-terminal').classList.add('active'); document.getElementById('terminal-device-name').textContent = ip; document.getElementById('terminal-status').textContent = '连接中...'; document.getElementById('terminal-status').className = 'terminal-status'; // 初始化 xterm.js if (currentTerminal) { currentTerminal.dispose(); } const term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Consolas, "Courier New", monospace', theme: { background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#d4d4d4' } }); const fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); const terminalContainer = document.getElementById('terminal-container'); terminalContainer.innerHTML = ''; term.open(terminalContainer); fitAddon.fit(); currentTerminal = term; // 构建WebSocket URL const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/api/terminal?ip=${encodeURIComponent(ip)}&port=${encodeURIComponent(port)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; let ws; try { ws = new WebSocket(wsUrl); } catch (e) { term.writeln('连接失败: ' + e.message); document.getElementById('terminal-status').textContent = '连接失败'; return; } ws.onopen = function() { document.getElementById('terminal-status').textContent = '已连接'; document.getElementById('terminal-status').className = 'terminal-status connected'; term.writeln('\x1b[32m--- SSH 连接已建立 ---\x1b[0m'); term.focus(); fitAddon.fit(); }; ws.onmessage = function(event) { term.write(event.data); }; ws.onclose = function() { document.getElementById('terminal-status').textContent = '已断开'; document.getElementById('terminal-status').className = 'terminal-status'; term.writeln('\r\n\x1b[31m--- 连接已断开 ---\x1b[0m'); }; ws.onerror = function(err) { document.getElementById('terminal-status').textContent = '连接错误'; document.getElementById('terminal-status').className = 'terminal-status'; term.writeln('\r\n\x1b[31m--- 连接错误 ---\x1b[0m'); }; // 终端输入转发到WebSocket term.onData(function(data) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ input: data })); } }); // 终端resize通知 term.onResize(function(size) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows })); } }); // 窗口resize时自动调整 const resizeObserver = new ResizeObserver(function() { fitAddon.fit(); }); resizeObserver.observe(terminalContainer); // 存储ws引用以便关闭 term._ws = ws; term._resizeObserver = resizeObserver; } // 关闭终端 function closeTerminal() { if (currentTerminal) { if (currentTerminal._ws) { currentTerminal._ws.close(); } if (currentTerminal._resizeObserver) { currentTerminal._resizeObserver.disconnect(); } currentTerminal.dispose(); currentTerminal = null; } document.getElementById('modal-terminal').classList.remove('active'); document.getElementById('terminal-container').innerHTML = ''; // 重置终端模态框位置 var modal = document.querySelector('.terminal-modal-content'); if (modal) { modal.style.position = ''; modal.style.left = ''; modal.style.top = ''; modal.style.margin = ''; } } // 删除设备 async function deleteDevice(deviceId) { if (!confirm('确定要删除设备 ' + deviceId + ' 吗?')) return; try { const response = await fetch(`/api/device/${deviceId}`, { method: 'DELETE' }); if (response.ok) { alert('删除成功'); document.getElementById('detail-panel').classList.remove('active'); // 重新加载拓扑和设备列表 loadTopology(); loadDeviceList(); } else { alert('删除失败'); } } catch (error) { alert('删除失败: ' + error.message); } } // 打开编辑设备模态框 function openEditDevice(id, hostname, type) { document.getElementById('edit-device-id').value = id; document.getElementById('edit-hostname').value = hostname; document.getElementById('edit-type').value = type; document.getElementById('modal-edit-device').classList.add('active'); } // 保存编辑设备 async function saveEditDevice(event) { event.preventDefault(); const id = document.getElementById('edit-device-id').value; const hostname = document.getElementById('edit-hostname').value; const type = document.getElementById('edit-type').value; try { const response = await fetch(`/api/device/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hostname, type }) }); if (response.ok) { alert('修改成功'); document.getElementById('modal-edit-device').classList.remove('active'); document.getElementById('detail-panel').classList.remove('active'); loadTopology(); loadDeviceList(); } else { const data = await response.json(); alert('修改失败: ' + (data.error || '未知错误')); } } catch (error) { alert('修改失败: ' + error.message); } } // 面板拖拽调整大小功能 (function() { // 左侧面板拖拽 var resizerLeft = document.getElementById('resizer-left'); var sidebar = document.getElementById('sidebar'); if (resizerLeft && sidebar) { var isResizingLeft = false; resizerLeft.addEventListener('mousedown', function(e) { isResizingLeft = true; resizerLeft.classList.add('resizing'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isResizingLeft) return; var newWidth = e.clientX; var minWidth = 200; var maxWidth = 600; if (newWidth >= minWidth && newWidth <= maxWidth) { sidebar.style.width = newWidth + 'px'; } }); document.addEventListener('mouseup', function() { if (isResizingLeft) { isResizingLeft = false; resizerLeft.classList.remove('resizing'); document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); } // 右侧面板拖拽 var resizerRight = document.getElementById('resizer-right'); var detailPanel = document.getElementById('detail-panel'); if (resizerRight && detailPanel) { var isResizingRight = false; resizerRight.addEventListener('mousedown', function(e) { isResizingRight = true; resizerRight.classList.add('resizing'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isResizingRight) return; var containerWidth = document.querySelector('.main-content').offsetWidth; var newWidth = containerWidth - e.clientX; var minWidth = 300; var maxWidth = 1000; if (newWidth >= minWidth && newWidth <= maxWidth) { detailPanel.style.width = newWidth + 'px'; } }); document.addEventListener('mouseup', function() { if (isResizingRight) { isResizingRight = false; resizerRight.classList.remove('resizing'); document.body.style.cursor = ''; document.body.style.userSelect = ''; } }); } })();