// 全局变量 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 = `
暂无设备
'; 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 = `IP: ${device.ip}
主机名: ${device.hostname || 'N/A'}
类型: ${device.type}
系统: ${device.os_version || 'N/A'}
运行时间: ${device.uptime || 'N/A'}
状态: ${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'}
本地接口: ${neighbor.local_interface}
远程接口: ${neighbor.remote_interface}
协议: ${neighbor.protocol}
加载中...
'; 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 = `加载失败
'; 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 = ''; } }); } })();