1
0
Ficheiros
network-topology-discovery/web/js/app.js
T
Your Name 32fadd9a7e Feat: 添加SQLite数据库持久化和设备列表显示
- 使用SQLite存储设备数据,重启后数据不丢失
- 添加 /api/devices 接口获取所有设备
- 前端显示完整的设备列表(含接口数、邻居数)
- 设备添加/扫描后自动刷新列表
- 启动时从数据库加载设备到拓扑构建器
2026-04-25 23:42:02 +08:00

393 linhas
12 KiB
JavaScript

// 全局变量
let cy = null;
let currentTaskId = null;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
initCytoscape();
initEventListeners();
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',
'target-arrow-color': '#999',
'target-arrow-shape': 'triangle',
'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('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);
}
// 开始扫描
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;
const poll = async () => {
try {
const response = await fetch(`/api/scan/${currentTaskId}`);
const task = await response.json();
// 更新进度
document.getElementById('scan-status').textContent = task.status;
document.getElementById('scan-progress').textContent = task.progress + '%';
document.getElementById('progress-fill').style.width = task.progress + '%';
// 更新设备列表
updateDeviceList(task.devices);
// 如果完成,更新拓扑
if (task.status === 'completed' || task.status === 'failed') {
loadTopology();
loadDeviceList(); // 刷新设备列表
currentTaskId = null;
return;
}
// 继续轮询
setTimeout(poll, 1000);
} catch (error) {
console.error('获取进度失败:', error);
}
};
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 = `
<div class="ip">${device.ip}</div>
<div class="type">${device.type} - ${device.hostname || 'Unknown'}</div>
<div class="status status-${device.scan_status.replace(' ', '-')}">${device.scan_status}</div>
`;
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 = '<p style="color: #999; text-align: center;">暂无设备</p>';
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 = `
<div class="ip">${device.ip}</div>
<div class="type">${device.type} - ${device.hostname || 'Unknown'}</div>
<div class="info" style="font-size: 11px; color: #999; margin-top: 5px;">
接口: ${interfaceCount} | 邻居: ${neighborCount}
</div>
<div class="status status-${(device.scan_status || 'pending').replace(' ', '-')}">${device.scan_status || 'pending'}</div>
`;
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}`);
const device = await response.json();
const detailPanel = document.getElementById('detail-panel');
const detailContainer = document.getElementById('device-detail');
detailContainer.innerHTML = `
<div class="detail-section">
<h4>基本信息</h4>
<p><strong>IP:</strong> ${device.ip}</p>
<p><strong>主机名:</strong> ${device.hostname || 'N/A'}</p>
<p><strong>类型:</strong> ${device.type}</p>
<p><strong>系统:</strong> ${device.os_version || 'N/A'}</p>
<p><strong>运行时间:</strong> ${device.uptime || 'N/A'}</p>
</div>
<div class="detail-section">
<h4>接口信息 (${device.interfaces.length})</h4>
${device.interfaces.map(iface => `
<div class="interface-item">
<div class="name">${iface.name}</div>
<div class="info">
<p>状态: <span class="status-${iface.status.replace(' ', '-')}">${iface.status}</span></p>
<p>IP: ${iface.ip || 'N/A'}</p>
<p>MAC: ${iface.mac || 'N/A'}</p>
<p>速度: ${iface.speed || 'N/A'}</p>
</div>
</div>
`).join('')}
</div>
<div class="detail-section">
<h4>邻居设备 (${device.neighbors.length})</h4>
${device.neighbors.map(neighbor => `
<div class="interface-item">
<div class="name">${neighbor.remote_device}</div>
<div class="info">
<p>本地接口: ${neighbor.local_interface}</p>
<p>远程接口: ${neighbor.remote_interface}</p>
<p>协议: ${neighbor.protocol}</p>
</div>
</div>
`).join('')}
</div>
`;
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();
}