// API 基础 URL const API_BASE_URL = '/api'; // 全局状态 let state = { currentTimeRange: '1h', // 与UI默认值保持一致 customStartTime: '', customEndTime: '', currentInterval: '10m' // 固定10分钟区间 } // WebSocket连接 let ws = null; let wsReconnectAttempts = 0; const wsMaxReconnectAttempts = 5; let wsReconnectDelay = 1000; // 图表实例 const charts = {}; // 初始化应用 function initApp() { initCustomTimeRange(); bindEvents(); initPageSwitch(); loadHomeData(); initCharts(); initWebSocket(); // 设置定时刷新 setInterval(loadMetrics, 30000); setInterval(loadServerCount, 30000); } // 初始化自定义时间范围 function initCustomTimeRange() { const now = new Date(); // 默认显示过去24小时 const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 直接使用ISO字符串,包含完整的时区信息 state.customStartTime = twentyFourHoursAgo.toISOString(); state.customEndTime = now.toISOString(); } // 页面切换 function initPageSwitch() { window.addEventListener('hashchange', switchPage); switchPage(); } function switchPage() { const hash = window.location.hash; // 隐藏所有内容 hideAllContent(); // 显示对应内容 if (hash === '#servers') { showContent('serversContent'); loadAllServers(); } else if (hash === '#devices') { showContent('devicesContent'); loadDeviceManagementList(); } else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) { showContent('serverMonitorContent'); // 提取设备ID let deviceId = ''; if (hash.startsWith('#serverMonitor/')) { deviceId = hash.split('/')[1]; } // 加载服务器信息 if (deviceId) { loadServerInfo(deviceId); } loadMetrics(); } else { showContent('homeContent'); loadHomeData(); } } function hideAllContent() { document.getElementById('homeContent').classList.add('hidden'); document.getElementById('serversContent').classList.add('hidden'); document.getElementById('serverMonitorContent').classList.add('hidden'); document.getElementById('devicesContent').classList.add('hidden'); } function showContent(contentId) { document.getElementById(contentId).classList.remove('hidden'); } // 加载首页数据 async function loadHomeData() { try { // 加载业务视图数据 await loadBusinessViewData(); // 加载告警列表数据 loadAlarmListData(); // 加载服务器数量 loadServerCount(); } catch (error) { console.error('加载首页数据失败:', error); } } // 加载业务视图数据 async function loadBusinessViewData() { try { const response = await fetch(`${API_BASE_URL}/devices/`); const data = await response.json(); let devices = data.devices || []; // 如果没有设备,使用模拟数据 if (devices.length === 0) { devices = [ { id: 'device-1', name: '服务器1', ip: '192.168.1.100' }, { id: 'device-2', name: '服务器2', ip: '192.168.1.101' } ]; } renderBusinessView(devices); } catch (error) { console.error('加载业务视图数据失败:', error); // 使用模拟数据 const mockDevices = [ { id: 'device-1', name: 'LIS', ip: '192.129.6.108', os: 'Linux', status: 'P1' }, { id: 'device-2', name: '互联网医院', ip: '192.129.6.57', os: 'Linux', status: 'P3' }, { id: 'device-3', name: 'HIS', ip: '192.129.51.21', os: 'Windows', status: 'P2' }, { id: 'device-4', name: 'OA', ip: '192.129.6.42', os: 'Linux', status: 'P2' } ]; renderBusinessView(mockDevices); } } // 渲染业务视图 function renderBusinessView(devices) { const tableBody = document.getElementById('businessViewTableBody'); if (!tableBody) return; tableBody.innerHTML = ''; devices.forEach(device => { const row = document.createElement('tr'); row.className = 'hover:bg-gray-50 transition-colors'; row.innerHTML = ` ${device.name} ${device.ip} ${device.os || '未知'} 正常 `; tableBody.appendChild(row); }); } // 加载告警列表数据 function loadAlarmListData() { // 模拟数据 const alarmData = [ { level: 'warning', message: 'CPU使用率超过阈值', device: '192.129.6.108', time: '2023-11-30 14:30:00' }, { level: 'error', message: '内存使用率超过阈值', device: '192.129.6.57', time: '2023-11-30 14:28:00' }, { level: 'warning', message: '磁盘使用率超过阈值', device: '192.129.51.21', time: '2023-11-30 14:25:00' }, { level: 'info', message: '网络流量异常', device: '192.129.6.42', time: '2023-11-30 14:20:00' } ]; renderAlarmList(alarmData); } // 渲染告警列表 function renderAlarmList(alarmData) { const alarmList = document.getElementById('alarmList'); if (!alarmList) return; alarmList.innerHTML = ''; alarmData.forEach(alarm => { const alarmItem = document.createElement('div'); alarmItem.className = `p-4 bg-gray-50 rounded-lg border-l-4 ${getAlarmBorderColor(alarm.level)}`; alarmItem.innerHTML = `

${alarm.message}

${alarm.device}

${alarm.time}
`; alarmList.appendChild(alarmItem); }); } // 获取告警样式 function getAlarmBorderColor(level) { switch(level) { case 'error': return 'border-red-500'; case 'warning': return 'border-yellow-500'; case 'info': return 'border-blue-500'; default: return 'border-gray-500'; } } function getAlarmIconColor(level) { switch(level) { case 'error': return 'text-red-500'; case 'warning': return 'text-yellow-500'; case 'info': return 'text-blue-500'; default: return 'text-gray-500'; } } // 加载服务器数量 async function loadServerCount() { try { const response = await fetch(`${API_BASE_URL}/devices/`); const data = await response.json(); const count = data.devices ? data.devices.length : 0; const serverCountElement = document.getElementById('serverCount'); if (serverCountElement) { serverCountElement.textContent = count; } } catch (error) { console.error('加载服务器数量失败:', error); const serverCountElement = document.getElementById('serverCount'); if (serverCountElement) { serverCountElement.textContent = '2'; } } } // 加载所有服务器 async function loadAllServers() { try { const response = await fetch(`${API_BASE_URL}/devices/`); const data = await response.json(); const devices = data.devices || []; renderServersGrid(devices); } catch (error) { console.error('加载服务器列表失败:', error); // 模拟数据 const mockDevices = [ { id: 'device-1', name: '服务器1', ip: '192.168.1.100' }, { id: 'device-2', name: '服务器2', ip: '192.168.1.101' } ]; renderServersGrid(mockDevices); } } // 渲染服务器网格 function renderServersGrid(devices) { const serversGrid = document.getElementById('serversGrid'); if (!serversGrid) return; serversGrid.innerHTML = ''; devices.forEach(device => { const serverCard = createServerCard(device); serversGrid.appendChild(serverCard); }); } // 创建服务器卡片 function createServerCard(device) { const card = document.createElement('div'); card.className = 'bg-white rounded-xl shadow-md p-6 card-hover border border-gray-100 cursor-pointer'; // 为卡片添加点击事件 card.addEventListener('click', () => { goToServerMonitor(device.id); }); card.innerHTML = `

${device.name || device.id}

${device.ip || 'N/A'}

在线

CPU使用率

0.0%

内存使用率

0.0%

磁盘使用率

0.0%

网络流量

0.0 MB/s

`; // 为"查看详情"按钮添加点击事件 const viewDetailBtn = card.querySelector('button'); viewDetailBtn.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡 goToServerMonitor(device.id); }); return card; } // 存储原始设备列表,用于搜索和筛选 let originalDeviceList = []; // 设备管理 async function loadDeviceManagementList() { try { const response = await fetch(`${API_BASE_URL}/devices/all`); if (!response.ok) { throw new Error('Failed to fetch devices'); } const data = await response.json(); const devices = data.devices || []; // 如果设备列表为空,使用模拟数据作为回退 if (devices.length === 0) { console.warn('No devices available, using mock data'); const mockDevices = getMockDevices(); originalDeviceList = mockDevices; renderDeviceManagementList(mockDevices); return; } // 处理设备数据,转换为所需格式 const processedDevices = devices.map(device => ({ id: device.id, name: device.name || device.id, ip: device.ip || 'N/A', status: device.status || 'unknown', created_at: device.created_at ? new Date(device.created_at * 1000).toLocaleString() : 'N/A', token: device.token || 'N/A' })); // 存储原始设备列表 originalDeviceList = processedDevices; // 应用当前筛选条件 applyDeviceFilters(); } catch (error) { console.error('加载设备管理列表失败:', error); // 使用模拟数据作为回退 const mockDevices = getMockDevices(); originalDeviceList = mockDevices; renderDeviceManagementList(mockDevices); } } // 应用设备筛选条件 function applyDeviceFilters() { const searchTerm = document.getElementById('deviceSearch').value.toLowerCase(); const statusFilter = document.getElementById('deviceStatusFilter').value; // 复制原始列表 let filteredDevices = [...originalDeviceList]; // 应用搜索筛选 if (searchTerm) { filteredDevices = filteredDevices.filter(device => device.name.toLowerCase().includes(searchTerm) || device.id.toLowerCase().includes(searchTerm) || device.ip.toLowerCase().includes(searchTerm) ); } // 应用状态筛选 if (statusFilter && statusFilter !== 'all') { filteredDevices = filteredDevices.filter(device => device.status === statusFilter); } // 渲染筛选后的设备列表 renderDeviceManagementList(filteredDevices); // 显示或隐藏无数据提示 const noDataMessage = document.getElementById('noDataMessage'); if (noDataMessage) { if (filteredDevices.length === 0) { noDataMessage.classList.remove('hidden'); } else { noDataMessage.classList.add('hidden'); } } } // 清除筛选条件 function clearDeviceFilters() { document.getElementById('deviceSearch').value = ''; document.getElementById('deviceStatusFilter').value = 'all'; applyDeviceFilters(); } // 获取模拟设备数据 function getMockDevices() { return [ { id: 'device-1', name: '服务器1', ip: '192.168.1.100', status: 'active', created_at: new Date().toLocaleString(), token: 'mock-token-1' }, { id: 'device-2', name: '服务器2', ip: '192.168.1.101', status: 'inactive', created_at: new Date().toLocaleString(), token: 'mock-token-2' }, { id: 'device-3', name: '测试服务器', ip: '192.168.1.102', status: 'offline', created_at: new Date().toLocaleString(), token: 'mock-token-3' } ]; } // 复制文本到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { // 可以添加一个提示,如"已复制"的临时消息 showToast('复制成功'); }).catch(err => { console.error('复制失败:', err); showToast('复制失败'); }); } // 显示临时提示消息 function showToast(message, type = 'success') { // 检查是否已存在toast元素 let toast = document.getElementById('toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'toast'; document.body.appendChild(toast); } // 设置toast样式根据类型 let bgColor = 'bg-gray-800'; switch(type) { case 'success': bgColor = 'bg-green-800'; break; case 'error': bgColor = 'bg-red-800'; break; case 'warning': bgColor = 'bg-yellow-800'; break; } toast.className = `${bgColor} text-white px-4 py-2 rounded opacity-0 transition-opacity duration-300 z-50 fixed bottom-4 right-4`; toast.textContent = message; toast.style.opacity = '1'; // 2秒后自动隐藏 setTimeout(() => { toast.style.opacity = '0'; }, 2000); } // 切换设备状态(激活/停用) async function toggleDeviceStatus(deviceId, newStatus) { try { // 先获取设备详情 const deviceResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`); if (!deviceResponse.ok) { throw new Error('Failed to fetch device details'); } const deviceData = await deviceResponse.json(); const device = deviceData.device; // 更新设备状态 const updateResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus, name: device.name, ip: device.ip }) }); if (!updateResponse.ok) { throw new Error(`HTTP error! status: ${updateResponse.status}`); } // 重新加载设备列表 await loadDeviceManagementList(); // 显示成功提示 const statusText = newStatus === 'active' ? '激活' : '停用'; showToast(`设备已成功${statusText}`, 'success'); } catch (error) { console.error('Failed to toggle device status:', error); const statusText = newStatus === 'active' ? '激活' : '停用'; showToast(`设备${statusText}失败`, 'error'); } } // 编辑设备 function editDevice(deviceId) { // 这里可以实现编辑设备的逻辑,比如打开模态框等 console.log('编辑设备:', deviceId); // 可以添加模态框或跳转到编辑页面的逻辑 } // 删除设备 function deleteDevice(deviceId) { if (confirm('确定要删除这个设备吗?')) { fetch(`${API_BASE_URL}/devices/${deviceId}`, { method: 'DELETE' }).then(response => { if (response.ok) { showToast('设备删除成功'); loadDeviceManagementList(); // 重新加载设备列表 } else { throw new Error('删除失败'); } }).catch(err => { console.error('删除设备失败:', err); showToast('删除失败'); }); } } function renderDeviceManagementList(devices) { const tableBody = document.getElementById('deviceManagementTableBody'); if (!tableBody) return; tableBody.innerHTML = ''; devices.forEach(device => { const row = document.createElement('tr'); row.className = 'hover:bg-gray-50 transition-colors'; // 根据设备状态获取显示样式 const getStatusStyle = (status) => { switch (status) { case 'active': return 'bg-green-100 text-green-800'; case 'inactive': return 'bg-yellow-100 text-yellow-800'; case 'offline': return 'bg-red-100 text-red-800'; default: return 'bg-gray-100 text-gray-800'; } }; row.innerHTML = ` ${device.name} ${device.id} ${device.ip}
${device.token}
${device.status} ${device.created_at} `; tableBody.appendChild(row); }); } // 初始化图表 function initCharts() { initAlarmTrendChart(); initDetailedCharts(); } // 初始化详细监控图表 function initDetailedCharts() { // 初始化CPU图表 const cpuCtx = document.getElementById('cpuChart'); if (cpuCtx && !charts.cpu) { charts.cpu = new Chart(cpuCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'CPU使用率 (%)', data: [], borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false } }, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: function(value) { return value + '%'; } } } }, // 启用缩放功能 interaction: { intersect: false, mode: 'index' }, // 允许显示空白区域 spanGaps: true, // 配置缩放和拖拽 plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: true, }, pinch: { enabled: true }, mode: 'x', onZoom: function({chart}) { console.log(chart.getZoomLevel()); } } } } } }); } // 初始化内存图表 const memoryCtx = document.getElementById('memoryChart'); if (memoryCtx && !charts.memory) { charts.memory = new Chart(memoryCtx, { type: 'line', data: { labels: [], datasets: [{ label: '内存使用率 (%)', data: [], borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, spanGaps: true, plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: true, }, pinch: { enabled: true }, mode: 'x', onZoom: function({chart}) { console.log(chart.getZoomLevel()); } } } }, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: function(value) { return value + '%'; } } } } } }); } // 初始化磁盘图表 const diskCtx = document.getElementById('diskChart'); if (diskCtx && !charts.disk) { charts.disk = new Chart(diskCtx, { type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, spanGaps: true, plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: true, }, pinch: { enabled: true }, mode: 'x', onZoom: function({chart}) { console.log(chart.getZoomLevel()); } } } }, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: function(value) { return value + '%'; } } } } } }); } // 初始化网络流量图表(发送总和和接收总和) const networkCtx = document.getElementById('networkChart'); if (networkCtx && !charts.network) { charts.network = new Chart(networkCtx, { type: 'line', data: { labels: [], datasets: [ { label: '发送总和 (MB)', data: [], borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 }, { label: '接收总和 (MB)', data: [], borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, spanGaps: true, plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: true, }, pinch: { enabled: true }, mode: 'x', onZoom: function({chart}) { console.log(chart.getZoomLevel()); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value + ' MB'; } } } } } }); } // 初始化网速趋势图表 const speedCtx = document.getElementById('speedChart'); if (speedCtx && !charts.speed) { charts.speed = new Chart(speedCtx, { type: 'line', data: { labels: [], datasets: [ { label: '发送速率 (MB/s)', data: [], borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 }, { label: '接收速率 (MB/s)', data: [], borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, spanGaps: true, plugins: { legend: { position: 'top' }, tooltip: { mode: 'index', intersect: false }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: true, }, pinch: { enabled: true }, mode: 'x', onZoom: function({chart}) { console.log(chart.getZoomLevel()); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return value + ' MB/s'; } } } } } }); } } // 初始化告警趋势图 function initAlarmTrendChart() { const ctx = document.getElementById('alarmTrendChart'); if (!ctx) return; charts.alarmTrend = new Chart(ctx, { type: 'line', data: { labels: ['11-24', '11-25', '11-26', '11-27', '11-28', '11-29', '11-30'], datasets: [ { label: '报警', data: [50, 60, 75, 80, 90, 100, 112], borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.1)', borderWidth: 2, fill: true, tension: 0.4 }, { label: '提醒', data: [20, 25, 30, 35, 38, 38, 38], borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderWidth: 2, fill: true, tension: 0.4 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } }, scales: { y: { beginAtZero: true } } } }); } // 格式化字节数,根据用户要求:只在MB和GB之间转换 function formatBytes(bytes, decimals = 2, isRate = false) { if (bytes === 0) { return isRate ? '0 MB/s' : '0 MB'; } const mb = 1024 * 1024; const gb = mb * 1024; const dm = decimals < 0 ? 0 : decimals; // 对于速率(bytes/s):默认显示MB/s,超过1024MB/s显示GB/s if (isRate) { if (bytes >= gb) { return parseFloat((bytes / gb).toFixed(dm)) + ' GB/s'; } else { return parseFloat((bytes / mb).toFixed(dm)) + ' MB/s'; } } // 对于流量(bytes):默认显示MB,超过1024MB显示GB else { if (bytes >= gb) { return parseFloat((bytes / gb).toFixed(dm)) + ' GB'; } else { return parseFloat((bytes / mb).toFixed(dm)) + ' MB'; } } } // 获取单个指标数据 async function fetchMetric(metricType, aggregation = 'average') { // 构建查询参数 const params = new URLSearchParams(); // 设置时间范围参数 if (state.customStartTime && state.customEndTime) { // 自定义时间范围 params.append('start_time', state.customStartTime); params.append('end_time', state.customEndTime); } else { // 默认显示24小时的数据 params.append('start_time', '-24h'); params.append('end_time', 'now()'); } // 设置聚合方式 params.append('aggregation', aggregation); // 固定时间区间为10分钟 params.append('interval', '10m'); // 发送请求 const response = await fetch(`${API_BASE_URL}/metrics/${metricType}?${params.toString()}`); if (!response.ok) { throw new Error(`Failed to fetch ${metricType} metrics`); } const data = await response.json(); return data.data; } // 格式化时间,统一格式避免图表误解 function formatTime(timeStr) { // timeStr是ISO格式的UTC时间字符串,如"2025-12-02T01:53:19Z" const date = new Date(timeStr); // 格式化年、月、日(使用本地时间) const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); // 格式化时、分、秒(使用本地时间) const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); // 总是显示完整的日期和时间,方便区分不同日期的数据 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } // 加载监控指标 async function loadMetrics() { try { // 并行加载所有指标 const [cpuData, memoryData, diskData, networkSumData] = await Promise.all([ fetchMetric('cpu'), fetchMetric('memory'), fetchMetric('disk'), fetchMetric('network') ]); // 更新状态卡片 updateStatusCards({ cpu: cpuData, memory: memoryData, disk: diskData, network: networkSumData }); // 更新图表 updateCharts(cpuData, memoryData, diskData, networkSumData); } catch (error) { console.error('Failed to load metrics:', error); // 显示友好的错误提示 showToast('加载监控数据失败,请稍后重试', 'error'); } } // WebSocket初始化 function initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/ws`; ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('WebSocket连接已打开'); wsReconnectAttempts = 0; wsReconnectDelay = 1000; }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); handleWebSocketMessage(message); } catch (error) { console.error('解析WebSocket消息失败:', error); } }; ws.onclose = (event) => { console.log(`WebSocket连接已关闭: ${event.code} - ${event.reason}`); attemptReconnect(); }; ws.onerror = (error) => { console.error('WebSocket连接错误:', error); }; } // 处理WebSocket消息 function handleWebSocketMessage(message) { if (message.type === 'metrics_update') { handleMetricsUpdate(message); } } // 加载服务器信息 async function loadServerInfo(deviceId) { try { const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`); if (!response.ok) { throw new Error('Failed to fetch server info'); } const deviceData = await response.json(); // 更新服务器信息显示 const serverInfoDisplay = document.getElementById('serverInfoDisplay'); if (serverInfoDisplay) { serverInfoDisplay.innerHTML = `

服务器名称: ${deviceData.name || deviceId} | IP地址: ${deviceData.ip || '未知'}

`; } } catch (error) { console.error('Failed to load server info:', error); // 使用模拟数据 const serverInfoDisplay = document.getElementById('serverInfoDisplay'); if (serverInfoDisplay) { serverInfoDisplay.innerHTML = `

服务器名称: 服务器 ${deviceId} | IP地址: 192.168.1.${deviceId}

`; } } } // 处理指标更新 function handleMetricsUpdate(message) { const { device_id, metrics } = message; // 直接更新,不再检查device_id updateStatusCards(metrics); // 立即刷新数据以确保图表也更新 setTimeout(() => loadMetrics(), 500); } // 更新状态卡片 function updateStatusCards(metrics) { // 更新CPU状态卡片 if (metrics.cpu) { if (Array.isArray(metrics.cpu) && metrics.cpu.length > 0) { const latestCPU = metrics.cpu[metrics.cpu.length - 1].value; const cpuElement = document.getElementById('cpuValue'); if (cpuElement) { cpuElement.textContent = `${latestCPU.toFixed(1)}%`; } } else if (typeof metrics.cpu === 'number') { const cpuElement = document.getElementById('cpuValue'); if (cpuElement) { cpuElement.textContent = `${metrics.cpu.toFixed(1)}%`; } } } // 更新内存状态卡片 if (metrics.memory) { if (Array.isArray(metrics.memory) && metrics.memory.length > 0) { const latestMemory = metrics.memory[metrics.memory.length - 1].value; const memoryElement = document.getElementById('memoryValue'); if (memoryElement) { memoryElement.textContent = `${latestMemory.toFixed(1)}%`; } } else if (typeof metrics.memory === 'number') { const memoryElement = document.getElementById('memoryValue'); if (memoryElement) { memoryElement.textContent = `${metrics.memory.toFixed(1)}%`; } } } // 更新磁盘状态卡片 if (metrics.disk) { const diskElement = document.getElementById('diskValue'); if (diskElement) { if (typeof metrics.disk === 'object' && metrics.disk !== null && !Array.isArray(metrics.disk)) { // 计算所有挂载点的平均使用率 let totalUsage = 0; let mountpointCount = 0; for (const mountpoint in metrics.disk) { const data = metrics.disk[mountpoint]; if (data && Array.isArray(data) && data.length > 0) { const latestValue = data[data.length - 1].value; totalUsage += latestValue; mountpointCount++; } else if (typeof data === 'number') { totalUsage += data; mountpointCount++; } } if (mountpointCount > 0) { const averageUsage = totalUsage / mountpointCount; diskElement.textContent = `${averageUsage.toFixed(1)}%`; } } else if (Array.isArray(metrics.disk) && metrics.disk.length > 0) { const latestDisk = metrics.disk[metrics.disk.length - 1].value; diskElement.textContent = `${latestDisk.toFixed(1)}%`; } else if (typeof metrics.disk === 'number') { diskElement.textContent = `${metrics.disk.toFixed(1)}%`; } } } // 更新网络状态卡片 if (metrics.network) { const networkValueElement = document.getElementById('networkValue'); const networkSentElement = document.getElementById('networkSent'); const networkReceivedElement = document.getElementById('networkReceived'); // 处理不同格式的网络数据 if (metrics.network.sent && metrics.network.received) { // 处理数组格式的数据 if (Array.isArray(metrics.network.sent) && metrics.network.sent.length > 0 && Array.isArray(metrics.network.received) && metrics.network.received.length > 0) { const latestSent = metrics.network.sent[metrics.network.sent.length - 1].value; const latestReceived = metrics.network.received[metrics.network.received.length - 1].value; if (networkValueElement) { // 显示较大的值 const maxValue = Math.max(latestSent, latestReceived); networkValueElement.textContent = formatBytes(maxValue, 2, true); // 显示速率 } if (networkSentElement) { networkSentElement.textContent = formatBytes(latestSent, 2, true); } if (networkReceivedElement) { networkReceivedElement.textContent = formatBytes(latestReceived, 2, true); } } // 处理数值格式的数据 else if (typeof metrics.network.sent === 'number' && typeof metrics.network.received === 'number') { if (networkValueElement) { const maxValue = Math.max(metrics.network.sent, metrics.network.received); networkValueElement.textContent = formatBytes(maxValue, 2, true); } if (networkSentElement) { networkSentElement.textContent = formatBytes(metrics.network.sent, 2, true); } if (networkReceivedElement) { networkReceivedElement.textContent = formatBytes(metrics.network.received, 2, true); } } } else if (typeof metrics.network === 'object' && metrics.network.bytes_sent !== undefined && metrics.network.bytes_received !== undefined) { // WebSocket消息格式 if (networkValueElement) { const maxValue = Math.max(metrics.network.bytes_sent, metrics.network.bytes_received); networkValueElement.textContent = formatBytes(maxValue, 2, true); } if (networkSentElement) { networkSentElement.textContent = formatBytes(metrics.network.bytes_sent, 2, true); } if (networkReceivedElement) { networkReceivedElement.textContent = formatBytes(metrics.network.bytes_received, 2, true); } } } } // 更新图表数据 function updateCharts(cpuData, memoryData, diskData, networkData) { // 数据点排序函数 const sortDataByTime = (data) => { return [...data].sort((a, b) => { return new Date(a.time) - new Date(b.time); }); }; // 计算固定份数X轴数据 const getFixedPointsData = (data) => { if (!Array.isArray(data) || data.length === 0) { return []; } // 根据时间范围计算需要的份数(每份10分钟) let expectedPoints = 6; // 默认1小时,6份 let timeRange = state.currentTimeRange || '1h'; // 如果使用了自定义时间,检查是否是24小时范围 if (state.customStartTime && state.customEndTime) { const startTime = new Date(state.customStartTime); const endTime = new Date(state.customEndTime); const durationHours = (endTime - startTime) / (1000 * 60 * 60); // 根据实际时长设置预期点数 if (durationHours <= 0.5) { timeRange = '30m'; } else if (durationHours <= 1) { timeRange = '1h'; } else if (durationHours <= 2) { timeRange = '2h'; } else if (durationHours <= 6) { timeRange = '6h'; } else if (durationHours <= 12) { timeRange = '12h'; } else { timeRange = '24h'; } } switch(timeRange) { case '30m': expectedPoints = 3; // 30分钟,3份 break; case '1h': expectedPoints = 6; // 1小时,6份 break; case '2h': expectedPoints = 12; // 2小时,12份 break; case '6h': expectedPoints = 36; // 6小时,36份 break; case '12h': expectedPoints = 72; // 12小时,72份 break; case '24h': expectedPoints = 144; // 24小时,144份 break; } // 排序数据 const sortedData = sortDataByTime(data); // 如果数据点足够,直接返回 if (sortedData.length <= expectedPoints) { return sortedData; } // 计算采样步长 const step = Math.ceil(sortedData.length / expectedPoints); const sampled = []; // 采样数据,确保得到期望的份数 for (let i = 0; i < sortedData.length; i += step) { sampled.push(sortedData[i]); if (sampled.length >= expectedPoints) { break; } } // 确保包含最后一个数据点 if (sampled.length < expectedPoints && sortedData.length > 0) { // 如果采样点不够,从末尾补充 const remaining = expectedPoints - sampled.length; for (let i = 1; i <= remaining; i++) { const index = Math.max(0, sortedData.length - i); if (!sampled.some(item => item.time === sortedData[index].time)) { sampled.push(sortedData[index]); } } } // 再次排序,确保时间顺序 return sortDataByTime(sampled); }; // 更新CPU图表 if (cpuData && Array.isArray(cpuData) && cpuData.length > 0 && charts.cpu) { const fixedData = getFixedPointsData(cpuData); charts.cpu.data.datasets[0].data = fixedData.map(item => ({ x: formatTime(item.time), y: item.value })); charts.cpu.update(); } // 更新内存图表 if (memoryData && Array.isArray(memoryData) && memoryData.length > 0 && charts.memory) { const fixedData = getFixedPointsData(memoryData); charts.memory.data.datasets[0].data = fixedData.map(item => ({ x: formatTime(item.time), y: item.value })); charts.memory.update(); } // 更新磁盘图表,支持多个挂载点 if (diskData && charts.disk) { // 定义不同的颜色,用于区分不同的挂载点 const colors = [ { border: '#f59e0b', background: 'rgba(245, 158, 11, 0.1)' }, // 黄色 { border: '#ef4444', background: 'rgba(239, 68, 68, 0.1)' }, // 红色 { border: '#10b981', background: 'rgba(16, 185, 129, 0.1)' }, // 绿色 { border: '#3b82f6', background: 'rgba(59, 130, 246, 0.1)' }, // 蓝色 { border: '#8b5cf6', background: 'rgba(139, 92, 246, 0.1)' }, // 紫色 { border: '#ec4899', background: 'rgba(236, 72, 153, 0.1)' }, // 粉色 ]; // 清空现有的数据集 charts.disk.data.datasets = []; // 为每个挂载点创建独立的数据集 let colorIndex = 0; if (typeof diskData === 'object' && diskData !== null && !Array.isArray(diskData)) { // 处理按挂载点分组的数据 for (const [mountpoint, data] of Object.entries(diskData)) { if (data && Array.isArray(data) && data.length > 0) { // 获取颜色 const color = colors[colorIndex % colors.length]; colorIndex++; // 排序数据 const sortedData = sortDataByTime(data); // 使用固定份数X轴数据计算 const fixedPointsData = getFixedPointsData(sortedData); // 创建数据集 const dataset = { label: `磁盘使用率 (${mountpoint})`, data: fixedPointsData.map(item => ({ x: formatTime(item.time), y: item.value })), borderColor: color.border, backgroundColor: color.background, borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3, }; // 添加数据集 charts.disk.data.datasets.push(dataset); } } } else if (Array.isArray(diskData) && diskData.length > 0) { // 处理单一磁盘数据 const sortedData = sortDataByTime(diskData); // 使用固定份数X轴数据计算 const fixedPointsData = getFixedPointsData(sortedData); const dataset = { label: '磁盘使用率', data: fixedPointsData.map(item => ({ x: formatTime(item.time), y: item.value })), borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', borderWidth: 2, fill: true, tension: 0.7, pointRadius: 0, pointHoverRadius: 3, }; charts.disk.data.datasets.push(dataset); } // 更新图表 charts.disk.update(); } // 更新网络流量趋势图表(发送总和和接收总和) if (networkData && networkData.sent && networkData.received && charts.network) { // 计算发送总和(时间段内的累积值) if (Array.isArray(networkData.sent) && networkData.sent.length > 0) { // 排序发送数据 const sortedSent = sortDataByTime(networkData.sent); // 计算累积发送总和(MB) let cumulativeSent = 0; const sentSumData = sortedSent.map(item => { // 转换为MB并累积 const mbValue = item.value / (1024 * 1024); cumulativeSent += mbValue; return { time: item.time, value: cumulativeSent }; }); // 使用固定份数X轴数据计算 const fixedPointsSentSum = getFixedPointsData(sentSumData); charts.network.data.datasets[0].data = fixedPointsSentSum.map(item => ({ x: formatTime(item.time), y: item.value })); } // 计算接收总和(时间段内的累积值) if (Array.isArray(networkData.received) && networkData.received.length > 0) { // 排序接收数据 const sortedReceived = sortDataByTime(networkData.received); // 计算累积接收总和(MB) let cumulativeReceived = 0; const receivedSumData = sortedReceived.map(item => { // 转换为MB并累积 const mbValue = item.value / (1024 * 1024); cumulativeReceived += mbValue; return { time: item.time, value: cumulativeReceived }; }); // 使用固定份数X轴数据计算 const fixedPointsReceivedSum = getFixedPointsData(receivedSumData); charts.network.data.datasets[1].data = fixedPointsReceivedSum.map(item => ({ x: formatTime(item.time), y: item.value })); } charts.network.update(); } // 更新网速趋势图表 if (networkData && networkData.sent && networkData.received && charts.speed) { // 更新发送流量 if (Array.isArray(networkData.sent) && networkData.sent.length > 0) { const sortedData = sortDataByTime(networkData.sent); // 使用固定份数X轴数据计算 const fixedPointsData = getFixedPointsData(sortedData); charts.speed.data.datasets[0].data = fixedPointsData.map(item => ({ x: formatTime(item.time), y: item.value / (1024 * 1024) // 转换为MB/s })); } // 更新接收流量 if (Array.isArray(networkData.received) && networkData.received.length > 0) { const sortedData = sortDataByTime(networkData.received); // 使用固定份数X轴数据计算 const fixedPointsData = getFixedPointsData(sortedData); charts.speed.data.datasets[1].data = fixedPointsData.map(item => ({ x: formatTime(item.time), y: item.value / (1024 * 1024) // 转换为MB/s })); } charts.speed.update(); } // 初始化图表(如果尚未初始化) initDetailedCharts(); } // 尝试重连WebSocket function attemptReconnect() { if (wsReconnectAttempts < wsMaxReconnectAttempts) { wsReconnectAttempts++; wsReconnectDelay *= 2; setTimeout(() => { console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts})`); initWebSocket(); }, wsReconnectDelay); } } // 打开模态框 function openModal(isEdit = false, deviceData = null) { const modal = document.getElementById('deviceModal'); const modalContent = document.getElementById('modalContent'); const modalTitle = document.getElementById('modalTitle'); const deviceId = document.getElementById('deviceId'); const deviceName = document.getElementById('deviceName'); const deviceIp = document.getElementById('deviceIp'); const deviceStatus = document.getElementById('deviceStatus'); // 重置表单 document.getElementById('deviceForm').reset(); // 设置模态框标题和数据 if (isEdit && deviceData) { modalTitle.textContent = '编辑设备'; deviceId.value = deviceData.id; deviceName.value = deviceData.name || ''; deviceIp.value = deviceData.ip || ''; deviceStatus.value = deviceData.status || 'inactive'; } else { modalTitle.textContent = '添加设备'; deviceId.value = ''; } // 显示模态框并添加动画 modal.classList.remove('hidden'); // 触发重排后再添加动画类 setTimeout(() => { modalContent.classList.remove('scale-95', 'opacity-0'); modalContent.classList.add('scale-100', 'opacity-100'); }, 10); } // 关闭模态框 function closeModal() { const modal = document.getElementById('deviceModal'); const modalContent = document.getElementById('modalContent'); // 先应用离开动画 modalContent.classList.remove('scale-100', 'opacity-100'); modalContent.classList.add('scale-95', 'opacity-0'); // 动画结束后隐藏模态框 setTimeout(() => { modal.classList.add('hidden'); }, 300); } // 编辑设备 function editDevice(deviceId) { // 直接从API获取设备详情,确保数据最新 fetch(`${API_BASE_URL}/devices/${deviceId}`) .then(response => { if (!response.ok) { throw new Error('Failed to fetch device details'); } return response.json(); }) .then(data => { // 提取设备数据(API返回的是 {"device": {...}} 格式) const deviceData = data.device; openModal(true, deviceData); }) .catch(error => { console.error('Failed to get device details:', error); showToast('获取设备详情失败', 'error'); }); } // 处理设备表单提交 async function handleDeviceFormSubmit(event) { event.preventDefault(); const deviceId = document.getElementById('deviceId').value; const deviceName = document.getElementById('deviceName').value; const deviceIp = document.getElementById('deviceIp').value; const deviceStatus = document.getElementById('deviceStatus').value; // 生成或使用现有ID let idToUse = deviceId; if (!idToUse) { // 为新设备生成ID idToUse = 'device-' + Date.now(); } // 获取当前时间戳 const timestamp = Math.floor(Date.now() / 1000); // 为新设备生成token let token = ''; if (!deviceId) { token = 'token-' + Math.random().toString(36).substring(2, 15); } // 构建完整的设备数据对象,包含所有必需字段 const deviceData = { id: idToUse, name: deviceName, ip: deviceIp, status: deviceStatus, token: token, // 新设备生成token,编辑时不提供(由后端保持原值) created_at: timestamp, // 新设备的创建时间 updated_at: timestamp // 更新时间 }; try { let url = `${API_BASE_URL}/devices/`; let method = 'POST'; // 如果是编辑操作,修改URL和方法 if (deviceId) { url = `${API_BASE_URL}/devices/${deviceId}`; method = 'PUT'; } // 发送请求 const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(deviceData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); // 显示成功提示 showToast(deviceId ? '设备更新成功' : '设备添加成功', 'success'); // 关闭模态框 closeModal(); // 重新加载设备列表 await loadDeviceManagementList(); } catch (error) { console.error('保存设备失败:', error); showToast(deviceId ? '设备更新失败' : '设备添加失败', 'error'); } } // 绑定事件 function bindEvents() { // 添加设备按钮事件 const addDeviceBtn = document.getElementById('addDeviceBtn'); if (addDeviceBtn) { addDeviceBtn.addEventListener('click', () => { openModal(false); }); } // 关闭模态框按钮 const closeModalBtn = document.getElementById('closeModalBtn'); if (closeModalBtn) { closeModalBtn.addEventListener('click', closeModal); } // 取消按钮 const cancelModalBtn = document.getElementById('cancelModalBtn'); if (cancelModalBtn) { cancelModalBtn.addEventListener('click', closeModal); } // 设备表单提交 const deviceForm = document.getElementById('deviceForm'); if (deviceForm) { deviceForm.addEventListener('submit', handleDeviceFormSubmit); } // 点击模态框背景关闭 const modal = document.getElementById('deviceModal'); if (modal) { modal.addEventListener('click', (event) => { if (event.target === modal) { closeModal(); } }); } // 设备搜索事件 const deviceSearch = document.getElementById('deviceSearch'); if (deviceSearch) { deviceSearch.addEventListener('input', applyDeviceFilters); } // 设备状态筛选事件 const deviceStatusFilter = document.getElementById('deviceStatusFilter'); if (deviceStatusFilter) { deviceStatusFilter.addEventListener('change', applyDeviceFilters); } // 清除筛选按钮事件 const clearFilterBtn = document.getElementById('clearFilterBtn'); if (clearFilterBtn) { clearFilterBtn.addEventListener('click', clearDeviceFilters); } // 自定义时间查询事件 const customTimeQuery = document.getElementById('customTimeQuery'); if (customTimeQuery) { customTimeQuery.addEventListener('click', () => { const startTimeInput = document.getElementById('customStartTime'); const endTimeInput = document.getElementById('customEndTime'); if (startTimeInput && endTimeInput) { // 获取本地时间 const localStartTime = new Date(startTimeInput.value); const localEndTime = new Date(endTimeInput.value); // 转换为ISO字符串(UTC时间) state.customStartTime = localStartTime.toISOString(); state.customEndTime = localEndTime.toISOString(); // 清空当前时间范围状态,只使用自定义时间 state.currentTimeRange = ''; // 重新加载数据 loadMetrics(); } }); } // 初始化自定义时间输入框 const now = new Date(); const startTimeInput = document.getElementById('customStartTime'); const endTimeInput = document.getElementById('customEndTime'); if (startTimeInput && endTimeInput) { // 默认显示过去24小时 const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 显示本地时间格式,YYYY-MM-DDTHH:MM startTimeInput.value = twentyFourHoursAgo.toISOString().slice(0, 16); endTimeInput.value = now.toISOString().slice(0, 16); } // 缩放控件事件处理 const zoomOutBtn = document.getElementById('zoomOutBtn'); const zoomInBtn = document.getElementById('zoomInBtn'); const resetZoomBtn = document.getElementById('resetZoomBtn'); const currentTimeRangeDisplay = document.getElementById('currentTimeRangeDisplay'); if (zoomOutBtn && zoomInBtn && currentTimeRangeDisplay) { // 时间范围选项列表,用于缩放 const timeRanges = ['30m', '1h', '2h', '6h', '12h', '24h']; // 更新当前时间范围显示 const updateTimeRangeDisplay = () => { switch(state.currentTimeRange) { case '30m': currentTimeRangeDisplay.textContent = '过去30分钟'; break; case '1h': currentTimeRangeDisplay.textContent = '过去1小时'; break; case '2h': currentTimeRangeDisplay.textContent = '过去2小时'; break; case '6h': currentTimeRangeDisplay.textContent = '过去6小时'; break; case '12h': currentTimeRangeDisplay.textContent = '过去12小时'; break; case '24h': currentTimeRangeDisplay.textContent = '过去24小时'; break; default: currentTimeRangeDisplay.textContent = '自定义时间范围'; } }; // 初始化显示 updateTimeRangeDisplay(); // 放大事件 zoomInBtn.addEventListener('click', () => { // 只在使用预设时间范围时生效 if (state.customStartTime && state.customEndTime) { // 使用自定义时间时,先清除自定义时间 state.customStartTime = ''; state.customEndTime = ''; state.currentTimeRange = '1h'; } else { // 查找当前时间范围在列表中的索引 const currentIndex = timeRanges.indexOf(state.currentTimeRange); if (currentIndex > 0) { // 放大:使用更小的时间范围 state.currentTimeRange = timeRanges[currentIndex - 1]; } } // 更新显示 updateTimeRangeDisplay(); // 重新加载数据 loadMetrics(); }); // 缩小事件 zoomOutBtn.addEventListener('click', () => { // 只在使用预设时间范围时生效 if (state.customStartTime && state.customEndTime) { // 使用自定义时间时,先清除自定义时间 state.customStartTime = ''; state.customEndTime = ''; state.currentTimeRange = '1h'; } else { // 查找当前时间范围在列表中的索引 const currentIndex = timeRanges.indexOf(state.currentTimeRange); if (currentIndex < timeRanges.length - 1) { // 缩小:使用更大的时间范围 state.currentTimeRange = timeRanges[currentIndex + 1]; } } // 更新显示 updateTimeRangeDisplay(); // 重新加载数据 loadMetrics(); }); } // 重置缩放按钮事件处理 if (resetZoomBtn) { resetZoomBtn.addEventListener('click', () => { // 重置所有图表的缩放 Object.values(charts).forEach(chart => { if (chart && typeof chart.resetZoom === 'function') { chart.resetZoom(); } }); }); } } // 工具函数 function showContent(contentId) { const element = document.getElementById(contentId); if (element) { element.classList.remove('hidden'); } } // 跳转到服务器监控详情页面 function goToServerMonitor(deviceId) { state.currentDeviceID = deviceId; window.location.hash = `#serverMonitor/${deviceId}`; // 加载选中设备的监控数据 setTimeout(() => { loadMetrics(); }, 100); } // 初始化图表选项卡 function initChartTabs() { const tabs = document.querySelectorAll('.chart-tab'); if (tabs.length === 0) return; tabs.forEach(tab => { tab.addEventListener('click', () => { // 移除所有选项卡的激活状态 tabs.forEach(t => { t.classList.remove('active', 'text-blue-600', 'border-blue-600'); t.classList.add('text-gray-600', 'border-transparent'); }); // 添加当前选项卡的激活状态 tab.classList.add('active', 'text-blue-600', 'border-blue-600'); tab.classList.remove('text-gray-600', 'border-transparent'); // 隐藏所有图表容器 const tabId = tab.dataset.tab; const chartContainers = document.querySelectorAll('.chart-container'); chartContainers.forEach(container => { container.classList.add('hidden'); }); // 显示当前选中的图表容器 const activeContainer = document.getElementById(`${tabId}ChartContainer`); if (activeContainer) { activeContainer.classList.remove('hidden'); } }); }); } // 页面加载完成后初始化 window.addEventListener('DOMContentLoaded', () => { initApp(); // 初始化图表选项卡 initChartTabs(); // 添加路由监听,处理hash变化 window.addEventListener('hashchange', handleHashChange); // 初始检查hash handleHashChange(); }); // 处理hash变化 function handleHashChange() { const hash = window.location.hash; if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) { // 延迟一下,确保DOM已经渲染完成 setTimeout(() => { loadMetrics(); }, 300); } }