app.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // 全局变量
  2. let cy = null;
  3. let currentTaskId = null;
  4. // 初始化
  5. document.addEventListener('DOMContentLoaded', function() {
  6. initCytoscape();
  7. initEventListeners();
  8. loadTopology();
  9. });
  10. // 初始化Cytoscape
  11. function initCytoscape() {
  12. cy = cytoscape({
  13. container: document.getElementById('cy'),
  14. elements: [],
  15. style: [
  16. {
  17. selector: 'node',
  18. style: {
  19. 'label': 'data(label)',
  20. 'background-color': function(ele) {
  21. return getNodeColor(ele.data('type'));
  22. },
  23. 'width': 70,
  24. 'height': 70,
  25. 'border-width': 3,
  26. 'border-color': '#667eea',
  27. 'text-valign': 'bottom',
  28. 'text-halign': 'center',
  29. 'font-size': '11px',
  30. 'font-weight': 'bold',
  31. 'text-wrap': 'wrap',
  32. 'text-max-width': '80px'
  33. }
  34. },
  35. {
  36. selector: 'edge',
  37. style: {
  38. 'width': 2,
  39. 'line-color': '#999',
  40. 'target-arrow-color': '#999',
  41. 'target-arrow-shape': 'triangle',
  42. 'curve-style': 'bezier',
  43. 'label': 'data(protocol)'
  44. }
  45. },
  46. {
  47. selector: 'node:selected',
  48. style: {
  49. 'border-width': 5,
  50. 'border-color': '#FF9800'
  51. }
  52. }
  53. ],
  54. layout: {
  55. name: 'cose',
  56. animate: true,
  57. animationDuration: 1000,
  58. padding: 30
  59. }
  60. });
  61. // 节点点击事件
  62. cy.on('tap', 'node', function(evt) {
  63. const node = evt.target;
  64. showDeviceDetail(node.data('id'));
  65. });
  66. }
  67. // 获取节点颜色
  68. function getNodeColor(type) {
  69. const colors = {
  70. 'cisco': '#4CAF50',
  71. 'huawei': '#2196F3',
  72. 'h3c': '#9C27B0',
  73. 'asa': '#FF5722',
  74. 'linux': '#607D8B',
  75. 'windows': '#00BCD4'
  76. };
  77. return colors[type] || '#999';
  78. }
  79. // 初始化事件监听
  80. function initEventListeners() {
  81. // 扫描按钮
  82. document.getElementById('btn-scan').addEventListener('click', startScan);
  83. // 添加设备按钮
  84. document.getElementById('btn-add-device').addEventListener('click', function() {
  85. document.getElementById('modal').classList.add('active');
  86. });
  87. // 关闭模态框
  88. document.querySelector('.close').addEventListener('click', function() {
  89. document.getElementById('modal').classList.remove('active');
  90. });
  91. // 添加设备表单
  92. document.getElementById('add-device-form').addEventListener('submit', addDevice);
  93. // 导出按钮
  94. document.getElementById('btn-export').addEventListener('click', exportTopology);
  95. }
  96. // 开始扫描
  97. async function startScan() {
  98. const scanRange = document.getElementById('scan-range').value;
  99. const sshPort = document.getElementById('ssh-port').value;
  100. const username = document.getElementById('username').value;
  101. const password = document.getElementById('password').value;
  102. if (!scanRange) {
  103. alert('请输入IP范围');
  104. return;
  105. }
  106. try {
  107. const response = await fetch('/api/scan', {
  108. method: 'POST',
  109. headers: {
  110. 'Content-Type': 'application/json'
  111. },
  112. body: JSON.stringify({
  113. scan_range: scanRange,
  114. ssh_port: parseInt(sshPort),
  115. username: username,
  116. password: password
  117. })
  118. });
  119. const data = await response.json();
  120. currentTaskId = data.task_id;
  121. // 轮询进度
  122. pollProgress();
  123. } catch (error) {
  124. console.error('扫描失败:', error);
  125. alert('扫描失败: ' + error.message);
  126. }
  127. }
  128. // 轮询进度
  129. async function pollProgress() {
  130. if (!currentTaskId) return;
  131. const poll = async () => {
  132. try {
  133. const response = await fetch(`/api/scan/${currentTaskId}`);
  134. const task = await response.json();
  135. // 更新进度
  136. document.getElementById('scan-status').textContent = task.status;
  137. document.getElementById('scan-progress').textContent = task.progress + '%';
  138. document.getElementById('progress-fill').style.width = task.progress + '%';
  139. // 更新设备列表
  140. updateDeviceList(task.devices);
  141. // 如果完成,更新拓扑
  142. if (task.status === 'completed' || task.status === 'failed') {
  143. loadTopology();
  144. currentTaskId = null;
  145. return;
  146. }
  147. // 继续轮询
  148. setTimeout(poll, 1000);
  149. } catch (error) {
  150. console.error('获取进度失败:', error);
  151. }
  152. };
  153. poll();
  154. }
  155. // 更新设备列表
  156. function updateDeviceList(devices) {
  157. const listContainer = document.getElementById('device-list');
  158. listContainer.innerHTML = '';
  159. devices.forEach(device => {
  160. const item = document.createElement('div');
  161. item.className = 'device-item';
  162. item.innerHTML = `
  163. <div class="ip">${device.ip}</div>
  164. <div class="type">${device.type} - ${device.hostname || 'Unknown'}</div>
  165. <div class="status status-${device.scan_status.replace(' ', '-')}">${device.scan_status}</div>
  166. `;
  167. item.addEventListener('click', () => showDeviceDetail(device.id));
  168. listContainer.appendChild(item);
  169. });
  170. }
  171. // 加载拓扑
  172. async function loadTopology() {
  173. try {
  174. const response = await fetch('/api/topology');
  175. const graph = await response.json();
  176. // 清空现有元素
  177. cy.elements().remove();
  178. // 添加节点
  179. graph.nodes.forEach(node => {
  180. // 显示格式: 主机名 + IP地址
  181. let label = node.ip; // 默认显示IP
  182. if (node.hostname && node.hostname !== '') {
  183. label = `${node.hostname}\n${node.ip}`;
  184. }
  185. cy.add({
  186. group: 'nodes',
  187. data: {
  188. id: node.id,
  189. label: label,
  190. type: node.type,
  191. ip: node.ip,
  192. hostname: node.hostname
  193. }
  194. });
  195. });
  196. // 添加边
  197. graph.edges.forEach(edge => {
  198. cy.add({
  199. group: 'edges',
  200. data: {
  201. id: edge.id,
  202. source: edge.source,
  203. target: edge.target,
  204. protocol: edge.protocol
  205. }
  206. });
  207. });
  208. // 重新布局
  209. cy.layout({
  210. name: 'cose',
  211. animate: true,
  212. animationDuration: 1000,
  213. padding: 30
  214. }).run();
  215. cy.fit(40);
  216. } catch (error) {
  217. console.error('加载拓扑失败:', error);
  218. }
  219. }
  220. // 显示设备详情
  221. async function showDeviceDetail(deviceId) {
  222. try {
  223. const response = await fetch(`/api/device/${deviceId}`);
  224. const device = await response.json();
  225. const detailPanel = document.getElementById('detail-panel');
  226. const detailContainer = document.getElementById('device-detail');
  227. detailContainer.innerHTML = `
  228. <div class="detail-section">
  229. <h4>基本信息</h4>
  230. <p><strong>IP:</strong> ${device.ip}</p>
  231. <p><strong>主机名:</strong> ${device.hostname || 'N/A'}</p>
  232. <p><strong>类型:</strong> ${device.type}</p>
  233. <p><strong>系统:</strong> ${device.os_version || 'N/A'}</p>
  234. <p><strong>运行时间:</strong> ${device.uptime || 'N/A'}</p>
  235. </div>
  236. <div class="detail-section">
  237. <h4>接口信息 (${device.interfaces.length})</h4>
  238. ${device.interfaces.map(iface => `
  239. <div class="interface-item">
  240. <div class="name">${iface.name}</div>
  241. <div class="info">
  242. <p>状态: <span class="status-${iface.status.replace(' ', '-')}">${iface.status}</span></p>
  243. <p>IP: ${iface.ip || 'N/A'}</p>
  244. <p>MAC: ${iface.mac || 'N/A'}</p>
  245. <p>速度: ${iface.speed || 'N/A'}</p>
  246. </div>
  247. </div>
  248. `).join('')}
  249. </div>
  250. <div class="detail-section">
  251. <h4>邻居设备 (${device.neighbors.length})</h4>
  252. ${device.neighbors.map(neighbor => `
  253. <div class="interface-item">
  254. <div class="name">${neighbor.remote_device}</div>
  255. <div class="info">
  256. <p>本地接口: ${neighbor.local_interface}</p>
  257. <p>远程接口: ${neighbor.remote_interface}</p>
  258. <p>协议: ${neighbor.protocol}</p>
  259. </div>
  260. </div>
  261. `).join('')}
  262. </div>
  263. `;
  264. detailPanel.classList.add('active');
  265. } catch (error) {
  266. console.error('获取设备详情失败:', error);
  267. }
  268. }
  269. // 添加设备
  270. async function addDevice(event) {
  271. event.preventDefault();
  272. const ip = document.getElementById('device-ip').value;
  273. const type = document.getElementById('device-type').value;
  274. const username = document.getElementById('device-username').value;
  275. const password = document.getElementById('device-password').value;
  276. try {
  277. const response = await fetch('/api/device', {
  278. method: 'POST',
  279. headers: {
  280. 'Content-Type': 'application/json'
  281. },
  282. body: JSON.stringify({
  283. ip: ip,
  284. type: type,
  285. username: username,
  286. password: password
  287. })
  288. });
  289. if (response.ok) {
  290. document.getElementById('modal').classList.remove('active');
  291. document.getElementById('add-device-form').reset();
  292. loadTopology();
  293. alert('设备添加成功');
  294. } else {
  295. const error = await response.json();
  296. alert('添加失败: ' + error.message);
  297. }
  298. } catch (error) {
  299. console.error('添加设备失败:', error);
  300. alert('添加失败: ' + error.message);
  301. }
  302. }
  303. // 导出拓扑
  304. function exportTopology() {
  305. const json = cy.json();
  306. const dataStr = JSON.stringify(json, null, 2);
  307. const dataBlob = new Blob([dataStr], { type: 'application/json' });
  308. const link = document.createElement('a');
  309. link.href = URL.createObjectURL(dataBlob);
  310. link.download = 'topology.json';
  311. link.click();
  312. }