Files
monitor/backend/static/js/app.js
2025-12-06 21:12:56 +08:00

3856 lines
144 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API 基础 URL
const API_BASE_URL = '/api';
// 全局状态
let state = {
currentTimeRange: '1h', // 与UI默认值保持一致
customStartTime: '',
customEndTime: '',
currentInterval: '3m', // 固定10分钟区间
currentDeviceID: '', // 当前选中的服务器ID
historyMetrics: {}, // 存储历史指标数据
autoRefreshEnabled: false, // 自动刷新开关状态,默认关闭
lastMetricsUpdate: null, // 记录上次指标更新时间
lastChartUpdate: 0, // 记录上次图表更新时间
chartUpdateThrottle: 1000, // 图表更新节流时间(毫秒)
pendingMetricsUpdate: null, // 待处理的指标更新
pendingChartUpdate: null, // 待处理的图表更新
isUpdatingDOM: false, // 标记是否正在更新DOM
// 自动刷新相关配置
autoRefreshInterval: 30000, // 默认自动刷新间隔(毫秒)
minAutoRefreshInterval: 5000, // 最小自动刷新间隔(毫秒)
maxAutoRefreshInterval: 60000, // 最大自动刷新间隔(毫秒)
autoRefreshTimer: null, // 自动刷新定时器
lastAutoRefreshTime: 0 // 上次自动刷新时间
}
// WebSocket连接
let ws = null;
let wsReconnectAttempts = 0;
const wsMaxReconnectAttempts = 10; // 增加最大重连次数
let wsReconnectDelay = 1000;
const wsMaxReconnectDelay = 30000; // 添加最大重连延迟30秒
let wsReconnectTimeout = null; // 重连定时器
// 图表实例
const charts = {};
// 初始化应用
function initApp() {
initCustomTimeRange();
bindEvents();
initPageSwitch();
loadHomeData();
initCharts();
initWebSocket();
// 初始化自动刷新机制
setupAutoRefresh();
// 设置服务器数量定时刷新
setInterval(loadServerCount, 30000);
// 初始化网卡列表
loadNetworkInterfaces();
}
// 设置自动刷新机制
function setupAutoRefresh() {
// 清除现有的定时器
if (state.autoRefreshTimer) {
clearInterval(state.autoRefreshTimer);
state.autoRefreshTimer = null;
}
// 如果启用了自动刷新,设置新的定时器
if (state.autoRefreshEnabled) {
// 立即执行一次,然后开始定时执行
loadStatusCards();
// 设置定时器
state.autoRefreshTimer = setInterval(() => {
loadStatusCards();
state.lastAutoRefreshTime = Date.now();
// 动态调整刷新间隔
updateAutoRefreshInterval();
}, state.autoRefreshInterval);
}
}
// 动态调整自动刷新间隔
function updateAutoRefreshInterval() {
// 这里可以根据实际情况调整刷新间隔
// 例如,根据系统负载、网络延迟或数据变化频率来动态调整
// 简单实现:如果最近数据变化频繁,缩短刷新间隔;否则延长刷新间隔
const now = Date.now();
if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) < state.autoRefreshInterval / 2) {
// 数据变化频繁,缩短刷新间隔,但不低于最小值
state.autoRefreshInterval = Math.max(
state.autoRefreshInterval - 2000,
state.minAutoRefreshInterval
);
} else if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) > state.autoRefreshInterval * 2) {
// 数据变化缓慢,延长刷新间隔,但不高于最大值
state.autoRefreshInterval = Math.min(
state.autoRefreshInterval + 5000,
state.maxAutoRefreshInterval
);
}
// 更新定时器
setupAutoRefresh();
}
// 初始化自定义时间范围
function initCustomTimeRange() {
const now = new Date();
// 默认显示过去1小时
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000);
// 直接使用ISO字符串包含完整的时区信息
state.customStartTime = oneHourAgo.toISOString();
state.customEndTime = now.toISOString();
// 更新日期选择器输入框的值
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 将ISO字符串转换为datetime-local格式YYYY-MM-DDTHH:MM
startTimeInput.value = oneHourAgo.toISOString().slice(0, 16);
endTimeInput.value = now.toISOString().slice(0, 16);
}
}
// 页面切换
function initPageSwitch() {
window.addEventListener('hashchange', switchPage);
switchPage();
}
function switchPage() {
const hash = window.location.hash;
// 隐藏所有内容
hideAllContent();
// 显示对应内容
if (hash === '#servers') {
showContent('serversContent');
loadAllServers();
// 清除当前设备ID避免在服务器列表页显示特定服务器的数据
state.currentDeviceID = '';
} else if (hash === '#devices') {
showContent('devicesContent');
loadDeviceManagementList();
// 清除当前设备ID
state.currentDeviceID = '';
} else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
showContent('serverMonitorContent');
// 提取设备ID
let deviceId = '';
if (hash.startsWith('#serverMonitor/')) {
deviceId = hash.split('/')[1];
// 检查deviceId是否为'undefined'字符串
if (deviceId === 'undefined') {
deviceId = '';
}
}
// 直接设置当前设备ID确保loadMetrics能使用正确的设备ID
state.currentDeviceID = deviceId;
// 加载服务器信息
if (deviceId) {
loadServerInfo(deviceId).then(() => {
// 确保服务器信息加载完成后再加载指标数据
loadMetrics();
});
} else {
// 如果没有设备ID显示错误信息
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `<p class="text-red-500">无效的服务器ID</p>`;
}
}
} else {
showContent('homeContent');
loadHomeData();
// 清除当前设备ID
state.currentDeviceID = '';
}
}
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/`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const devices = data.devices || [];
renderBusinessView(devices);
} catch (error) {
console.error('加载业务视图数据失败:', error);
// 只显示错误信息,不使用模拟数据
const tableBody = document.getElementById('businessViewTableBody');
if (tableBody) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载业务视图数据失败</td>
</tr>
`;
}
}
}
// 渲染业务视图
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 cursor-pointer';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.os || '未知'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
正常
</span>
</td>
`;
// 为表格行添加点击事件
row.addEventListener('click', () => {
goToServerMonitor(device.id);
});
tableBody.appendChild(row);
});
}
// 加载告警列表数据
async function loadAlarmListData() {
try {
const response = await fetch(`${API_BASE_URL}/alarms`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const alarmData = data.alarms || [];
renderAlarmList(alarmData);
} catch (error) {
console.error('加载告警列表失败:', error);
// 只显示错误信息,不使用模拟数据
const alarmList = document.getElementById('alarmList');
if (alarmList) {
alarmList.innerHTML = `
<div class="flex items-center justify-center p-4 text-red-500">
加载告警列表失败
</div>
`;
}
}
}
// 渲染告警列表
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 = `
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<i class="fa fa-exclamation-circle ${getAlarmIconColor(alarm.level)}"></i>
<div>
<p class="text-sm font-medium text-gray-900">${alarm.message}</p>
<p class="text-xs text-gray-500">${alarm.device}</p>
</div>
</div>
<div class="text-xs text-gray-500">${alarm.time}</div>
</div>
`;
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 {
// 调用设备状态API获取详细信息
const response = await fetch(`${API_BASE_URL}/devices/status`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const devices = data.devices || [];
renderServersGrid(devices);
} catch (error) {
console.error('加载服务器列表失败:', error);
// 只显示错误信息,不使用模拟数据
const serversGrid = document.getElementById('serversGrid');
if (serversGrid) {
serversGrid.innerHTML = `
<div class="col-span-full bg-white rounded-xl shadow-md p-6 text-center text-red-500">
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
<p>加载服务器列表失败</p>
</div>
`;
}
}
}
// 渲染服务器网格
function renderServersGrid(devices) {
const serversGrid = document.getElementById('serversGrid');
if (!serversGrid) return;
serversGrid.innerHTML = '';
devices.forEach(device => {
const serverCard = createServerCard(device);
serversGrid.appendChild(serverCard);
});
}
// 跳转到服务器监控界面
function goToServerMonitor(deviceId) {
// 确保deviceId有效
if (!deviceId) {
console.error('Invalid device ID');
return;
}
// 设置URL hash
window.location.hash = `#serverMonitor/${deviceId}`;
// 手动调用switchPage来立即更新页面
switchPage();
}
// 创建服务器卡片
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', () => {
// 确保使用正确的设备ID字段
const deviceId = device.device_id || device.id || device.ID;
goToServerMonitor(deviceId);
});
// 获取设备状态数据
const status = device.status || {};
const cpuUsage = status.cpu !== undefined ? status.cpu.toFixed(1) : '0.0';
const memoryUsage = status.memory !== undefined ? status.memory.toFixed(1) : '0.0';
const diskUsage = status.disk !== undefined ? status.disk.toFixed(1) : '0.0';
const networkTraffic = status.network !== undefined ? status.network.toFixed(1) : '0.0';
card.innerHTML = `
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-gray-900">${device.name || device.device_id || device.id}</h3>
<p class="text-sm text-gray-500">${device.ip || 'N/A'}</p>
</div>
<div class="flex items-center">
<span class="inline-block w-2 h-2 bg-green-500 rounded-full mr-1"></span>
<span class="text-green-600 text-sm font-medium">在线</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<p class="text-xs text-gray-500">CPU使用率</p>
<p class="text-lg font-semibold text-gray-900">${cpuUsage}%</p>
<p class="text-xs text-gray-500">磁盘使用率</p>
<p class="text-lg font-semibold text-gray-900">${diskUsage}%</p>
</div>
<div>
<p class="text-xs text-gray-500">内存使用率</p>
<p class="text-lg font-semibold text-gray-900">${memoryUsage}%</p>
<p class="text-xs text-gray-500">网络流量</p>
<p class="text-lg font-semibold text-gray-900">${networkTraffic} MB/s</p>
</div>
</div>
<div class="flex justify-end">
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
查看详情
</button>
</div>
`;
// 为"查看详情"按钮添加点击事件
const viewDetailBtn = card.querySelector('button');
viewDetailBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
// 确保使用正确的设备ID字段
const deviceId = device.device_id || device.id || device.ID;
goToServerMonitor(deviceId);
});
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');
originalDeviceList = [];
renderDeviceManagementList([]);
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 deviceListBody = document.getElementById('deviceListBody');
if (deviceListBody) {
deviceListBody.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-red-500">加载设备列表失败</td>
</tr>
`;
}
}
}
// 应用设备筛选条件
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 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 = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.id}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-sm text-gray-500 truncate max-w-[150px]">${device.token}</span>
<button class="ml-2 text-blue-500 hover:text-blue-700 text-sm" onclick="copyToClipboard('${device.token}')">
复制
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusStyle(device.status)}">
${device.status}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.created_at}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button class="text-blue-600 hover:text-blue-800 mr-3" onclick="goToServerMonitor('${device.id}')">查看详情</button>
<button class="text-blue-600 hover:text-blue-800 mr-3" onclick="editDevice('${device.id}')">编辑</button>
<button class="text-green-600 hover:text-green-800 mr-3 ${device.status === 'active' ? 'hidden' : ''}" onclick="toggleDeviceStatus('${device.id}', 'active')">激活</button>
<button class="text-yellow-600 hover:text-yellow-800 mr-3 ${device.status !== 'active' ? 'hidden' : ''}" onclick="toggleDeviceStatus('${device.id}', 'inactive')">停用</button>
<button class="text-red-600 hover:text-red-800" onclick="deleteDevice('${device.id}')">删除</button>
</td>
`;
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,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value}%`,
`实际值: ${value}%`
];
}
}
},
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,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value}%`,
`实际值: ${value}%`
];
}
}
},
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,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value} MB`,
`实际值: ${value} MB`
];
}
}
},
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,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value} MB/s`,
`实际值: ${value} MB/s`
];
}
}
},
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();
// 设置设备ID参数如果存在
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
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();
// 确保返回的数据是数组格式
if (metricType === 'disk') {
// 磁盘数据可能是对象格式,需要特殊处理
return data.data || {};
} else if (metricType === 'network') {
// 网络数据可能是对象格式,需要特殊处理
return data.data || {};
} else {
// 其他数据应该是数组格式
return Array.isArray(data.data) ? 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 loadStatusCards() {
try {
// 从设备状态API获取数据用于状态卡片显示
let deviceStatusData = null;
if (state.currentDeviceID) {
try {
const response = await fetch(`${API_BASE_URL}/devices/status?device_id=${state.currentDeviceID}`);
if (response.ok) {
deviceStatusData = await response.json();
}
} catch (error) {
console.error('Failed to fetch device status:', error);
}
}
// 准备状态卡片数据
let formattedMetricsForCards = {};
if (deviceStatusData && deviceStatusData.id) {
// 使用设备状态API返回的数据
formattedMetricsForCards = {
cpu: deviceStatusData.status.cpu || 0,
cpu_hz: deviceStatusData.status.cpu_hz || 0,
memory: deviceStatusData.status.memory || 0,
disk: {
'/': {
used_percent: deviceStatusData.status.disk || 0,
total: state.historyMetrics.disk && state.historyMetrics.disk['/'] && state.historyMetrics.disk['/'].total || 0
}
},
network: {
bytes_sent: deviceStatusData.status.network_sent || 0,
bytes_received: deviceStatusData.status.network_received || 0,
tx_bytes: deviceStatusData.status.network_tx_bytes || 0,
rx_bytes: deviceStatusData.status.network_rx_bytes || 0
}
};
}
// 更新状态卡片
updateStatusCards(formattedMetricsForCards);
} catch (error) {
console.error('Failed to load status cards:', error);
}
}
// 加载监控指标,只负责更新图表
async function loadMetrics() {
try {
// 并行加载所有指标
const [cpuData, memoryData, diskData, networkSumData] = await Promise.all([
fetchMetric('cpu'),
fetchMetric('memory'),
fetchMetric('disk'),
fetchMetric('network')
]);
// 更新图表
updateCharts(cpuData, memoryData, diskData, networkSumData);
// 更新刷新状态指示器
updateRefreshStatus();
// 更新进程信息
loadProcessInfo();
// 更新系统日志
loadSystemLogs();
} catch (error) {
console.error('Failed to load metrics:', error);
// 显示友好的错误提示
showToast('加载监控数据失败,请稍后重试', 'error');
// 更新刷新状态指示器为错误状态
const statusIndicator = document.getElementById('refreshStatusIndicator');
const lastRefreshTime = document.getElementById('lastRefreshTime');
if (statusIndicator && lastRefreshTime) {
statusIndicator.className = 'w-2 h-2 bg-red-500 rounded-full animate-pulse';
lastRefreshTime.textContent = `上次刷新: 失败`;
}
// 即使发生错误,也要尝试初始化图表,避免页面空白
initDetailedCharts();
}
}
// 格式化磁盘数据,用于状态卡片显示
function formatDiskDataForCards(diskData) {
// 如果diskData是空对象返回空对象
if (!diskData || typeof diskData !== 'object' || Array.isArray(diskData)) {
return {};
}
const formattedDiskData = {};
// 遍历每个挂载点
for (const mountpoint in diskData) {
const mountpointData = diskData[mountpoint];
// 如果挂载点数据是数组,获取最新的数据点
if (Array.isArray(mountpointData) && mountpointData.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestData = mountpointData[mountpointData.length - 1];
formattedDiskData[mountpoint] = {
used_percent: latestData.value,
// 尝试从历史数据获取总容量
total: state.historyMetrics.disk && state.historyMetrics.disk[mountpoint] && state.historyMetrics.disk[mountpoint].total || 0
};
}
}
return formattedDiskData;
}
// 格式化网络数据,用于状态卡片显示
function formatNetworkDataForCards(networkData) {
// 初始化返回数据结构
const formattedNetworkData = {
bytes_sent: 0,
bytes_received: 0,
tx_bytes: 0,
rx_bytes: 0
};
// 如果没有数据,返回初始值
if (!networkData || typeof networkData === 'undefined') {
return formattedNetworkData;
}
// 处理数组格式数据
if (Array.isArray(networkData)) {
// 数组格式:直接处理最新的数据点
if (networkData.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestData = networkData[networkData.length - 1];
// 检查是否包含速率数据
if (latestData.sent !== undefined) {
formattedNetworkData.bytes_sent = latestData.sent;
} else if (latestData.bytes_sent !== undefined) {
formattedNetworkData.bytes_sent = latestData.bytes_sent;
}
if (latestData.received !== undefined) {
formattedNetworkData.bytes_received = latestData.received;
} else if (latestData.bytes_received !== undefined) {
formattedNetworkData.bytes_received = latestData.bytes_received;
}
// 检查是否包含总量数据
if (latestData.tx_bytes !== undefined) {
formattedNetworkData.tx_bytes = latestData.tx_bytes;
}
if (latestData.rx_bytes !== undefined) {
formattedNetworkData.rx_bytes = latestData.rx_bytes;
}
}
}
// 处理对象格式数据
else if (typeof networkData === 'object') {
// 检查是否为WebSocket直接返回的总流量格式
if (networkData.bytes_sent !== undefined && networkData.bytes_received !== undefined) {
// WebSocket消息格式 - 总流量
formattedNetworkData.bytes_sent = networkData.bytes_sent;
formattedNetworkData.bytes_received = networkData.bytes_received;
formattedNetworkData.tx_bytes = networkData.tx_bytes || 0;
formattedNetworkData.rx_bytes = networkData.rx_bytes || 0;
}
// 按网卡分组的数据
else {
// 遍历每个网卡
for (const iface in networkData) {
const ifaceData = networkData[iface];
if (typeof ifaceData === 'object') {
// 处理速率数据
if (ifaceData.sent && ifaceData.received) {
// 如果sent和received是数组获取最新的数据点
if (Array.isArray(ifaceData.sent) && ifaceData.sent.length > 0 &&
Array.isArray(ifaceData.received) && ifaceData.received.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestSent = ifaceData.sent[ifaceData.sent.length - 1].value;
const latestReceived = ifaceData.received[ifaceData.received.length - 1].value;
// 累加速率
formattedNetworkData.bytes_sent += latestSent;
formattedNetworkData.bytes_received += latestReceived;
}
// 如果sent和received是数值
else if (typeof ifaceData.sent === 'number' && typeof ifaceData.received === 'number') {
formattedNetworkData.bytes_sent += ifaceData.sent;
formattedNetworkData.bytes_received += ifaceData.received;
}
}
// 直接使用bytes_sent和bytes_received字段
else if (ifaceData.bytes_sent !== undefined && ifaceData.bytes_received !== undefined) {
formattedNetworkData.bytes_sent += ifaceData.bytes_sent;
formattedNetworkData.bytes_received += ifaceData.bytes_received;
}
// 检查是否有总量数据(多种可能的字段名)
// 1. 标准字段名tx_bytes, rx_bytes
if (ifaceData.tx_bytes !== undefined) {
formattedNetworkData.tx_bytes += ifaceData.tx_bytes;
}
if (ifaceData.rx_bytes !== undefined) {
formattedNetworkData.rx_bytes += ifaceData.rx_bytes;
}
// 2. 旧格式字段名bytes_sent_total, bytes_received_total
if (ifaceData.bytes_sent_total !== undefined) {
formattedNetworkData.tx_bytes += ifaceData.bytes_sent_total;
}
if (ifaceData.bytes_received_total !== undefined) {
formattedNetworkData.rx_bytes += ifaceData.bytes_received_total;
}
// 3. 可能的其他格式bytes_sent, bytes_received作为总量数据
// 仅当没有其他总量数据字段时使用
if (formattedNetworkData.tx_bytes === 0 && ifaceData.bytes_sent !== undefined && typeof ifaceData.bytes_sent === 'number') {
formattedNetworkData.tx_bytes += ifaceData.bytes_sent;
}
if (formattedNetworkData.rx_bytes === 0 && ifaceData.bytes_received !== undefined && typeof ifaceData.bytes_received === 'number') {
formattedNetworkData.rx_bytes += ifaceData.bytes_received;
}
}
}
}
}
return formattedNetworkData;
}
// 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 {
// 将设备ID存储到全局状态
state.currentDeviceID = deviceId;
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!response.ok) {
throw new Error('Failed to fetch server info');
}
const data = await response.json();
const deviceData = data.device;
// 更新服务器信息显示
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `<p>服务器名称: <strong>${deviceData.name || deviceId}</strong> | IP地址: <strong>${deviceData.ip || '未知'}</strong></p>`;
}
// 初始化状态卡片
loadStatusCards();
} catch (error) {
console.error('Failed to load server info:', error);
// 即使请求失败也要将设备ID存储到全局状态
state.currentDeviceID = deviceId;
// 显示错误信息,不使用模拟数据
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `<p class="text-red-500">加载服务器信息失败</p>`;
}
// 初始化状态卡片,即使服务器信息加载失败
loadStatusCards();
}
}
// 处理指标更新
function handleMetricsUpdate(message) {
const { device_id, metrics } = message;
// 只处理当前选中设备的WebSocket消息
if (state.currentDeviceID && device_id !== state.currentDeviceID) {
return;
}
// 格式化数据确保updateStatusCards函数能正确处理
const formattedMetrics = {
cpu: metrics.cpu,
cpu_hz: metrics.cpu_hz, // 添加cpu_hz字段确保CPU频率能显示
memory: metrics.memory,
disk: formatDiskDataForCards(metrics.disk),
network: formatNetworkDataForCards(metrics.network)
};
// 直接更新统计卡片,始终实时更新
updateStatusCards(formattedMetrics);
// 根据自动刷新开关状态决定是否更新图表
if (state.autoRefreshEnabled) {
// 直接使用WebSocket数据更新图表避免不必要的API请求
updateCharts(metrics.cpu, metrics.memory, metrics.disk, metrics.network);
}
}
// 更新刷新状态指示器
function updateRefreshStatus() {
// 更新最后刷新时间
const now = new Date();
const formattedTime = now.toLocaleTimeString();
// 更新状态指示器和时间显示
const statusIndicator = document.getElementById('refreshStatusIndicator');
const lastRefreshTime = document.getElementById('lastRefreshTime');
if (statusIndicator && lastRefreshTime) {
// 短暂显示绿色,表示数据已更新
statusIndicator.className = 'w-2 h-2 bg-green-500 rounded-full';
lastRefreshTime.textContent = `上次刷新: ${formattedTime}`;
// 1秒后恢复为黄色表示正常状态
setTimeout(() => {
if (statusIndicator) {
statusIndicator.className = 'w-2 h-2 bg-yellow-500 rounded-full';
}
}, 1000);
}
}
// 更新状态卡片
function updateStatusCards(metrics) {
// 更新历史指标数据
updateHistoryMetrics(metrics);
// 保存待处理的指标更新
state.pendingMetricsUpdate = metrics;
// 使用requestAnimationFrame优化DOM更新
if (!state.isUpdatingDOM) {
state.isUpdatingDOM = true;
requestAnimationFrame(() => {
_updateStatusCards(state.pendingMetricsUpdate);
state.isUpdatingDOM = false;
state.lastMetricsUpdate = Date.now();
});
}
}
// 内部DOM更新函数由requestAnimationFrame调用
function _updateStatusCards(metrics) {
// 使用历史数据或当前数据
const displayMetrics = {
cpu: metrics.cpu || state.historyMetrics.cpu,
cpu_hz: metrics.cpu_hz || state.historyMetrics.cpu_hz,
memory: metrics.memory || state.historyMetrics.memory,
disk: metrics.disk || state.historyMetrics.disk,
network: metrics.network || state.historyMetrics.network
};
// 更新CPU状态卡片
if (displayMetrics.cpu) {
let cpuUsage = 0;
let cpuGhz = 0;
let cpuLoad = 0;
// 解析CPU数据
if (Array.isArray(displayMetrics.cpu) && displayMetrics.cpu.length > 0) {
// 数组格式:获取最新的数据点
cpuUsage = displayMetrics.cpu[displayMetrics.cpu.length - 1].value;
} else if (typeof displayMetrics.cpu === 'number') {
// 数值格式
cpuUsage = displayMetrics.cpu;
} else if (typeof displayMetrics.cpu === 'object' && displayMetrics.cpu.usage) {
// 对象格式包含usage, frequency, load
cpuUsage = displayMetrics.cpu.usage;
cpuGhz = displayMetrics.cpu.frequency || 0;
cpuLoad = displayMetrics.cpu.load || 0;
}
// 从单独的cpu_hz字段获取CPU频率如果有
if (displayMetrics.cpu_hz && typeof displayMetrics.cpu_hz === 'number') {
cpuGhz = displayMetrics.cpu_hz / 1000; // 转换为GHz
}
// 更新显示
const cpuElement = document.getElementById('cpuValue');
const cpuDetailsElement = document.getElementById('cpuDetails');
if (cpuElement) {
cpuElement.textContent = `${cpuUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
cpuElement.className = `text-3xl font-bold metric-value ${cpuUsage > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (cpuDetailsElement) {
cpuDetailsElement.className = 'text-xs text-gray-500 mt-1';
cpuDetailsElement.textContent = `${cpuGhz.toFixed(2)} GHz | 负载: ${cpuLoad.toFixed(2)}`;
}
}
// 更新内存状态卡片
if (displayMetrics.memory) {
let memoryUsage = 0;
let memoryUsed = 0;
let memoryTotal = 0;
// 解析内存数据
if (Array.isArray(displayMetrics.memory) && displayMetrics.memory.length > 0) {
// 数组格式:获取最新的数据点
memoryUsage = displayMetrics.memory[displayMetrics.memory.length - 1].value;
} else if (typeof displayMetrics.memory === 'number') {
// 数值格式
memoryUsage = displayMetrics.memory;
} else if (typeof displayMetrics.memory === 'object') {
// 对象格式包含usage, used, total
memoryUsage = displayMetrics.memory.usage || 0;
memoryUsed = displayMetrics.memory.used || 0;
memoryTotal = displayMetrics.memory.total || 0;
}
// 如果只有使用率,没有使用量和总量,尝试从其他地方获取
if (memoryTotal === 0 && state.historyMetrics.memory && typeof state.historyMetrics.memory === 'object') {
memoryUsed = state.historyMetrics.memory.used || 0;
memoryTotal = state.historyMetrics.memory.total || 0;
}
// 如果内存总量仍为0尝试使用默认值或从API获取
if (memoryTotal === 0) {
// 尝试从系统获取内存总量(如果浏览器支持)
if (navigator.deviceMemory) {
memoryTotal = navigator.deviceMemory * 1024 * 1024 * 1024; // 转换为字节
} else {
// 使用默认值16GB
memoryTotal = 16 * 1024 * 1024 * 1024;
}
}
// 计算使用量(如果只有使用率)
if (memoryUsed === 0 && memoryTotal > 0) {
memoryUsed = (memoryTotal * memoryUsage) / 100;
}
// 更新显示
const memoryElement = document.getElementById('memoryValue');
const memoryDetailsElement = document.getElementById('memoryDetails');
if (memoryElement) {
memoryElement.textContent = `${memoryUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
memoryElement.className = `text-3xl font-bold metric-value ${memoryUsage > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (memoryDetailsElement) {
memoryDetailsElement.className = 'text-xs text-gray-500 mt-1';
memoryDetailsElement.textContent = `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}`;
}
}
// 更新磁盘状态卡片
if (displayMetrics.disk) {
let totalUsed = 0;
let totalSize = 0;
let usagePercent = 0;
// 过滤掉不需要的挂载点
const excludedMountpoints = ['/usr', '/boot', '/boot/efi'];
// 解析磁盘数据
if (typeof displayMetrics.disk === 'object' && displayMetrics.disk !== null && !Array.isArray(displayMetrics.disk)) {
// 按挂载点分组的数据
let hasValidData = false;
for (const mountpoint in displayMetrics.disk) {
// 跳过排除的挂载点
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = displayMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.used_percent !== undefined && data.total !== undefined) {
// 新格式包含used_percent和total字段
// 计算已使用大小total * used_percent / 100
const used = (data.total * data.used_percent) / 100;
totalUsed += used;
totalSize += data.total;
hasValidData = true;
} else if (data.used !== undefined && data.total !== undefined) {
// 旧格式包含used和total字段
totalUsed += data.used;
totalSize += data.total;
hasValidData = true;
}
}
}
// 如果没有有效数据,尝试使用历史数据
if (!hasValidData && state.historyMetrics.disk && typeof state.historyMetrics.disk === 'object' && !Array.isArray(state.historyMetrics.disk)) {
for (const mountpoint in state.historyMetrics.disk) {
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = state.historyMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.used_percent !== undefined && data.total !== undefined) {
const used = (data.total * data.used_percent) / 100;
totalUsed += used;
totalSize += data.total;
} else if (data.used !== undefined && data.total !== undefined) {
totalUsed += data.used;
totalSize += data.total;
}
}
}
}
} else if (typeof displayMetrics.disk === 'object' && displayMetrics.disk.used !== undefined && displayMetrics.disk.total !== undefined) {
// 单磁盘数据
totalUsed = displayMetrics.disk.used;
totalSize = displayMetrics.disk.total;
}
// 如果只有使用率,没有使用量和总量,尝试计算
if (totalSize === 0 && displayMetrics.disk && typeof displayMetrics.disk === 'number') {
// 如果disk是一个数字尝试从历史数据获取总量
if (state.historyMetrics.disk && typeof state.historyMetrics.disk === 'object' && !Array.isArray(state.historyMetrics.disk)) {
for (const mountpoint in state.historyMetrics.disk) {
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = state.historyMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.total !== undefined) {
totalSize += data.total;
}
}
}
// 计算使用量
if (totalSize > 0) {
totalUsed = (totalSize * displayMetrics.disk) / 100;
}
}
}
// 计算使用率
if (totalSize > 0) {
usagePercent = (totalUsed / totalSize) * 100;
} else if (typeof displayMetrics.disk === 'number') {
// 如果disk是一个数字直接使用它作为使用率
usagePercent = displayMetrics.disk;
}
// 更新显示
const diskElement = document.getElementById('diskValue');
const diskDetailsElement = document.getElementById('diskDetails');
if (diskElement) {
diskElement.textContent = `${usagePercent.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
diskElement.className = `text-3xl font-bold metric-value ${usagePercent > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (diskDetailsElement) {
diskDetailsElement.className = 'text-xs text-gray-500 mt-1';
diskDetailsElement.textContent = `${formatBytes(totalUsed)} / ${formatBytes(totalSize)}`;
}
}
// 更新网络流量状态卡片
if (displayMetrics.network) {
// 解析网络数据
const rxRate = displayMetrics.network.bytes_received || 0; // 接收速率 (bytes/s)
const txRate = displayMetrics.network.bytes_sent || 0; // 发送速率 (bytes/s)
// 计算速率比值
let ratio = 0;
let ratioSymbol = '';
let ratioText = '';
let symbolColor = '';
if (rxRate === 0 && txRate === 0) {
// 如果接收速率和发送速率都为0显示无穷符号
ratioText = '∞';
ratioSymbol = '';
symbolColor = 'text-gray-500';
} else if (txRate === 0) {
// 如果发送速率为0显示无穷符号和接收箭头
ratioText = '∞';
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else if (rxRate === 0) {
// 如果接收速率为0显示无穷符号和发送箭头
ratioText = '∞';
ratioSymbol = '↑';
symbolColor = 'text-red-500';
} else {
// 计算接收速率与发送速率的比值
ratio = rxRate / txRate;
ratioText = ratio.toFixed(2);
// 根据比值判断箭头方向和颜色
if (ratio > 10) {
// 接收速率远高于发送,显示绿色↓
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else if (ratio < 0.1) {
// 发送速率远高于接收,显示红色↑
ratioSymbol = '↑';
symbolColor = 'text-red-500';
} else if (ratio >= 0.5 && ratio <= 2) {
// 收发速率均衡,显示蓝色↔
ratioSymbol = '↔';
symbolColor = 'text-blue-500';
} else if (ratio > 1) {
// 接收速率高于发送,显示绿色↓
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else {
// 发送速率高于接收,显示红色↑
ratioSymbol = '↑';
symbolColor = 'text-red-500';
}
}
// 格式化速率为MB/s
const formatRate = (bytesPerSec) => {
return (bytesPerSec / (1024 * 1024)).toFixed(2);
};
const rxRateMB = formatRate(rxRate);
const txRateMB = formatRate(txRate);
// 更新显示
const networkElement = document.getElementById('networkValue');
const networkDetailsElement = document.getElementById('networkDetails');
if (networkElement) {
// 大数字显示比值和箭头
networkElement.innerHTML = `${ratioText} <span class="text-xl ${symbolColor}">${ratioSymbol}</span>`;
networkElement.className = 'text-3xl font-bold metric-value text-gray-900 flex items-center';
}
if (networkDetailsElement) {
// 速率显示
networkDetailsElement.className = 'text-xs text-gray-500 mt-1';
networkDetailsElement.textContent = `${rxRateMB} MB/s | ${txRateMB} MB/s`;
}
}
}
// 更新历史指标数据
function updateHistoryMetrics(metrics) {
// 只更新有有效数据的指标
if (metrics.cpu) {
state.historyMetrics.cpu = metrics.cpu;
}
if (metrics.cpu_hz) {
state.historyMetrics.cpu_hz = metrics.cpu_hz;
}
if (metrics.memory) {
state.historyMetrics.memory = metrics.memory;
}
if (metrics.disk) {
state.historyMetrics.disk = metrics.disk;
}
if (metrics.network) {
state.historyMetrics.network = metrics.network;
}
}
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkData) {
// 确保数据格式正确
const safeCpuData = Array.isArray(cpuData) ? cpuData : [];
const safeMemoryData = Array.isArray(memoryData) ? memoryData : [];
const safeDiskData = typeof diskData === 'object' && diskData !== null ? diskData : {};
const safeNetworkData = typeof networkData === 'object' && networkData !== null ? networkData : {};
// 保存待处理的图表更新数据
state.pendingChartUpdate = {
cpuData: safeCpuData,
memoryData: safeMemoryData,
diskData: safeDiskData,
networkData: safeNetworkData
};
// 使用节流机制更新图表
_updateChartsThrottled();
}
// 带节流功能的图表更新函数
function _updateChartsThrottled() {
const now = Date.now();
if (now - state.lastChartUpdate < state.chartUpdateThrottle) {
// 如果距离上次更新时间不足节流时间,延迟执行
setTimeout(_updateChartsThrottled, state.chartUpdateThrottle - (now - state.lastChartUpdate));
return;
}
// 执行实际的图表更新
_updateCharts(state.pendingChartUpdate);
state.lastChartUpdate = now;
}
// 内部图表更新函数,执行实际的图表渲染
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();
}
// 保存当前选中的网卡
state.currentInterface = state.currentInterface || 'all';
// 数据求和辅助函数:计算所有网卡在同一时间点的数据之和
const sumAllInterfacesData = (networkData, metricType) => {
if (typeof networkData !== 'object' || networkData === null) {
return [];
}
// 收集所有时间点
const allTimes = new Set();
const interfaceDatas = [];
// 遍历所有网卡
for (const iface in networkData) {
if (networkData.hasOwnProperty(iface) && typeof networkData[iface] === 'object') {
const ifaceData = networkData[iface][metricType];
if (Array.isArray(ifaceData)) {
interfaceDatas.push(ifaceData);
// 收集所有时间点
ifaceData.forEach(item => {
allTimes.add(item.time);
});
}
}
}
// 如果没有数据,返回空数组
if (interfaceDatas.length === 0) {
return [];
}
// 将时间点转换为数组并排序
const sortedTimes = Array.from(allTimes).sort((a, b) => new Date(a) - new Date(b));
// 计算每个时间点的总和
const summedData = sortedTimes.map(time => {
let sum = 0;
// 遍历所有网卡数据,累加同一时间点的值
interfaceDatas.forEach(ifaceData => {
// 查找当前时间点的数据
const dataPoint = ifaceData.find(item => item.time === time);
if (dataPoint) {
sum += dataPoint.value;
}
});
return {
time: time,
value: sum
};
});
return summedData;
};
// 计算流量差值的辅助函数
const calculateTrafficDiff = (data) => {
if (!Array.isArray(data) || data.length <= 1) {
return data;
}
// 确保数据按时间排序
const sortedData = [...data].sort((a, b) => new Date(a.time) - new Date(b.time));
// 计算每个时间点与前一个时间点的差值
const diffData = [];
for (let i = 1; i < sortedData.length; i++) {
const current = sortedData[i];
const previous = sortedData[i - 1];
// 计算差值,确保为正数
const diff = {
time: current.time,
value: Math.max(0, current.value - previous.value)
};
diffData.push(diff);
}
return diffData;
};
// 更新网络流量趋势图表(发送总和和接收总和)
if (networkData && charts.network) {
let txBytesData, rxBytesData;
// 如果是按网卡分组的数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
if (state.currentInterface === 'all') {
// 计算所有网卡的发送累积总流量
txBytesData = sumAllInterfacesData(networkData, 'tx_bytes');
// 计算所有网卡的接收累积总流量
rxBytesData = sumAllInterfacesData(networkData, 'rx_bytes');
} else {
// 选择当前选中的网卡数据
const selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
txBytesData = selectedNetworkData.tx_bytes || [];
rxBytesData = selectedNetworkData.rx_bytes || [];
}
} else {
// 直接使用数据
txBytesData = networkData.tx_bytes || [];
rxBytesData = networkData.rx_bytes || [];
}
if (txBytesData.length > 0 || rxBytesData.length > 0) {
// 使用发送累积总流量数据
if (Array.isArray(txBytesData) && txBytesData.length > 0) {
// 排序发送数据
const sortedTxBytes = sortDataByTime(txBytesData);
// 计算流量差值,只显示指定时间范围内的流量变化
const diffTxBytes = calculateTrafficDiff(sortedTxBytes);
// 转换为MB
const txBytesSumData = diffTxBytes.map(item => ({
time: item.time,
value: item.value / (1024 * 1024) // 转换为MB
}));
// 使用固定份数X轴数据计算
const fixedPointsTxBytesSum = getFixedPointsData(txBytesSumData);
charts.network.data.datasets[0].data = fixedPointsTxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
// 使用接收累积总流量数据
if (Array.isArray(rxBytesData) && rxBytesData.length > 0) {
// 排序接收数据
const sortedRxBytes = sortDataByTime(rxBytesData);
// 计算流量差值,只显示指定时间范围内的流量变化
const diffRxBytes = calculateTrafficDiff(sortedRxBytes);
// 转换为MB
const rxBytesSumData = diffRxBytes.map(item => ({
time: item.time,
value: item.value / (1024 * 1024) // 转换为MB
}));
// 使用固定份数X轴数据计算
const fixedPointsRxBytesSum = getFixedPointsData(rxBytesSumData);
charts.network.data.datasets[1].data = fixedPointsRxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
charts.network.update();
}
}
// 更新网速趋势图表
if (networkData && charts.speed) {
let sentData, receivedData;
// 如果是按网卡分组的数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
if (state.currentInterface === 'all') {
// 计算所有网卡的发送速率总和
sentData = sumAllInterfacesData(networkData, 'sent');
// 计算所有网卡的接收速率总和
receivedData = sumAllInterfacesData(networkData, 'received');
} else {
// 选择当前选中的网卡数据
const selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
sentData = selectedNetworkData.sent || [];
receivedData = selectedNetworkData.received || [];
}
} else {
// 直接使用数据
sentData = networkData.sent || [];
receivedData = networkData.received || [];
}
if (sentData.length > 0 || receivedData.length > 0) {
// 更新发送流量
if (Array.isArray(sentData) && sentData.length > 0) {
const sortedData = sortDataByTime(sentData);
// 使用固定份数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(receivedData) && receivedData.length > 0) {
const sortedData = sortDataByTime(receivedData);
// 使用固定份数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();
}
}
// 更新网卡选择下拉框
updateInterfaceDropdown(networkData);
// 初始化图表(如果尚未初始化)
initDetailedCharts();
}
// 更新网卡选择下拉框
function updateInterfaceDropdown(networkData) {
// 创建或获取网卡选择容器
let interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (!interfaceContainer) {
// 获取图表选项卡导航容器
const chartTabs = document.getElementById('chartTabs');
if (!chartTabs) {
return;
}
// 创建网卡选择容器
interfaceContainer = document.createElement('div');
interfaceContainer.id = 'interfaceSelectorContainer';
interfaceContainer.className = 'flex items-center gap-2 ml-4';
// 创建标签
const label = document.createElement('label');
label.htmlFor = 'interfaceSelector';
label.className = 'text-sm text-gray-600';
label.textContent = '网卡:';
// 创建下拉框
const select = document.createElement('select');
select.id = 'interfaceSelector';
select.className = 'px-3 py-1 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加事件监听
select.addEventListener('change', (e) => {
state.currentInterface = e.target.value;
// 重新加载指标
loadMetrics();
});
// 组装容器
interfaceContainer.appendChild(label);
interfaceContainer.appendChild(select);
// 将容器添加到图表选项卡导航中
const tabsContainer = chartTabs.querySelector('.flex.flex-wrap');
if (tabsContainer) {
tabsContainer.appendChild(interfaceContainer);
}
}
// 更新下拉框选项
const select = document.getElementById('interfaceSelector');
if (select) {
// 保存当前选中的值
const currentValue = select.value;
// 清空现有选项
select.innerHTML = '';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加所有网卡选项
// 检查是否有按网卡分组的网络数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
// 获取所有网卡名称
const interfaces = Object.keys(networkData);
// 添加所有网卡选项
interfaces.forEach(iface => {
// 只添加实际的网卡名称,不添加"all"选项
if (iface !== 'all') {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
select.appendChild(option);
}
});
} else {
// 如果没有按网卡分组的数据,显示"所有接口"选项
const option = document.createElement('option');
option.value = 'all';
option.textContent = '所有接口';
select.appendChild(option);
}
// 恢复当前选中的值
select.value = currentValue;
}
// 检查当前选中的选项卡,如果不是"网络"或"网速",隐藏网卡选择下拉框
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab && activeTab.dataset.tab !== 'network' && activeTab.dataset.tab !== 'speed') {
interfaceContainer.classList.add('hidden');
} else {
interfaceContainer.classList.remove('hidden');
}
}
// 尝试重连WebSocket
function attemptReconnect() {
// 清除可能存在的重连定时器
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
if (wsReconnectAttempts < wsMaxReconnectAttempts) {
wsReconnectAttempts++;
// 指数退避,但不超过最大延迟
wsReconnectDelay = Math.min(wsReconnectDelay * 2, wsMaxReconnectDelay);
// 添加随机抖动,防止多个客户端同时重连
const jitter = Math.random() * 1000;
const delay = wsReconnectDelay + jitter;
wsReconnectTimeout = setTimeout(() => {
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts}),延迟 ${Math.round(delay)}ms`);
initWebSocket();
}, delay);
} else {
console.error('WebSocket重连失败已达到最大重连次数');
// 30秒后重置重连状态允许再次尝试
setTimeout(() => {
wsReconnectAttempts = 0;
wsReconnectDelay = 1000;
console.log('WebSocket重连状态已重置准备再次尝试连接');
attemptReconnect();
}, 30000);
}
}
// 打开模态框
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 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 networkInterfaceSelect = document.getElementById('networkInterface');
const refreshInterfacesBtn = document.getElementById('refreshInterfacesBtn');
if (networkInterfaceSelect) {
networkInterfaceSelect.addEventListener('change', (e) => {
state.currentInterface = e.target.value;
loadMetrics(); // 切换网卡后重新加载数据
});
}
if (refreshInterfacesBtn) {
refreshInterfacesBtn.addEventListener('click', loadNetworkInterfaces);
}
// 自动刷新开关事件
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
state.autoRefreshEnabled = e.target.checked;
// 如果开启了自动刷新,立即刷新一次数据
if (state.autoRefreshEnabled) {
loadMetrics();
}
});
}
}
// 加载网卡列表
async function loadNetworkInterfaces() {
try {
// 获取设备ID
const hash = window.location.hash;
let deviceId = '';
if (hash.startsWith('#serverMonitor/')) {
deviceId = hash.split('/')[1];
}
// 从网络指标API获取网卡列表
const response = await fetch(`${API_BASE_URL}/metrics/network?device_id=${deviceId}`);
const data = await response.json();
// 从返回的数据中提取网卡列表
const interfaces = Object.keys(data.data || {});
// 更新下拉选择框
const selectElement = document.getElementById('networkInterface');
if (selectElement) {
// 清空现有选项
selectElement.innerHTML = '<option value="all">所有网卡</option>';
// 添加新选项
interfaces.forEach(iface => {
if (iface !== 'all') {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
selectElement.appendChild(option);
}
});
// 如果当前选中的网卡不存在,重置为'all'
if (!interfaces.includes(state.currentInterface) && state.currentInterface !== 'all') {
state.currentInterface = 'all';
}
// 设置当前选中的值
selectElement.value = state.currentInterface;
}
} catch (error) {
console.error('加载网卡列表失败:', error);
// 显示友好的错误提示
showToast('加载网卡列表失败,请稍后重试', 'error');
}
}
// 更新当前时间范围显示
const updateTimeRangeDisplay = () => {
let displayText = '';
// 计算实际的开始时间和结束时间
let startTime, endTime;
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间范围
startTime = new Date(state.customStartTime);
endTime = new Date(state.customEndTime);
displayText = `${formatTime(state.customStartTime)}${formatTime(state.customEndTime)}`;
} else {
// 使用预设时间范围
const now = new Date();
endTime = now;
// 根据预设时间范围计算开始时间
switch(state.currentTimeRange) {
case '30m':
startTime = new Date(now.getTime() - 30 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '1h':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '2h':
startTime = new Date(now.getTime() - 2 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '6h':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '12h':
startTime = new Date(now.getTime() - 12 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
case '24h':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())}${formatTime(endTime.toISOString())}`;
break;
default:
displayText = '自定义时间范围';
}
}
// 更新所有图表的时间范围显示
const displays = [
document.getElementById('cpuCurrentTimeRangeDisplay'),
document.getElementById('currentTimeRangeDisplay'),
document.getElementById('diskCurrentTimeRangeDisplay'),
document.getElementById('networkCurrentTimeRangeDisplay'),
document.getElementById('speedCurrentTimeRangeDisplay')
];
displays.forEach(display => {
if (display) display.textContent = displayText;
});
};
// 初始化显示
updateTimeRangeDisplay();
// 放大事件
const zoomInHandler = () => {
// 只在使用预设时间范围时生效
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();
};
// 为所有放大按钮添加事件监听器
document.getElementById('cpuZoomInBtn')?.addEventListener('click', zoomInHandler);
document.getElementById('memoryZoomInBtn')?.addEventListener('click', zoomInHandler);
document.getElementById('diskZoomInBtn')?.addEventListener('click', zoomInHandler);
document.getElementById('networkZoomInBtn')?.addEventListener('click', zoomInHandler);
document.getElementById('speedZoomInBtn')?.addEventListener('click', zoomInHandler);
// 缩小事件
const zoomOutHandler = () => {
// 只在使用预设时间范围时生效
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();
};
// 为所有缩小按钮添加事件监听器
document.getElementById('cpuZoomOutBtn')?.addEventListener('click', zoomOutHandler);
document.getElementById('memoryZoomOutBtn')?.addEventListener('click', zoomOutHandler);
document.getElementById('diskZoomOutBtn')?.addEventListener('click', zoomOutHandler);
document.getElementById('networkZoomOutBtn')?.addEventListener('click', zoomOutHandler);
document.getElementById('speedZoomOutBtn')?.addEventListener('click', zoomOutHandler);
// 重置缩放处理函数
const resetZoomHandler = () => {
// 重置时间范围
state.currentTimeRange = '1h';
state.customStartTime = '';
state.customEndTime = '';
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
// 重置所有图表的缩放
Object.values(charts).forEach(chart => {
if (chart && typeof chart.resetZoom === 'function') {
chart.resetZoom();
}
});
};
// 为所有重置按钮添加事件监听器
document.getElementById('cpuResetZoomBtn')?.addEventListener('click', resetZoomHandler);
document.getElementById('memoryResetZoomBtn')?.addEventListener('click', resetZoomHandler);
document.getElementById('diskResetZoomBtn')?.addEventListener('click', resetZoomHandler);
document.getElementById('networkResetZoomBtn')?.addEventListener('click', resetZoomHandler);
document.getElementById('speedResetZoomBtn')?.addEventListener('click', resetZoomHandler);
// 工具函数
function showContent(contentId) {
const element = document.getElementById(contentId);
if (element) {
element.classList.remove('hidden');
}
}
// 注意此函数已被更新的goToServerMonitor函数替代保留用于兼容
// 初始化图表选项卡
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');
console.log(`Shown chart container: ${tabId}ChartContainer`);
} else {
console.error(`Chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息和系统日志
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
// 加载进程信息
loadProcessInfo();
}
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
// 加载磁盘详细信息
loadDiskDetails();
}
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
// 加载系统日志
loadSystemLogs();
}
}
// 显示/隐藏网卡选择下拉框
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (interfaceContainer) {
// 只有在点击"网络"或"网速"选项卡时,显示网卡选择下拉框
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
});
});
// 初始状态:根据当前选中的选项卡显示/隐藏网卡选择下拉框和加载数据
const activeTab = document.querySelector('.chart-tab.active');
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示当前选中的图表容器
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
} else {
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息和系统日志
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
loadSystemLogs();
}
// 显示/隐藏网卡选择下拉框
if (interfaceContainer) {
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
}
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
initApp();
// 初始化图表选项卡
initChartTabs();
// 添加路由监听处理hash变化
window.addEventListener('hashchange', handleHashChange);
// 初始检查hash
handleHashChange();
// 为进程信息显示数量下拉框添加事件监听
const processPageSizeSelect = document.getElementById('processPageSize');
if (processPageSizeSelect) {
processPageSizeSelect.addEventListener('change', () => {
loadProcessInfo(1); // 重置为第一页
});
// 设置初始值
processPageSizeSelect.value = processPagination.itemsPerPage;
}
});
// 进程信息分页状态
let processPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allProcesses: [],
lastDeviceID: '', // 上次请求数据的设备ID
lastTimeRange: '', // 上次请求数据的时间范围
lastCustomStartTime: '', // 上次请求数据的自定义开始时间
lastCustomEndTime: '' // 上次请求数据的自定义结束时间
};
// 系统日志分页状态
let logPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allLogs: [],
lastDeviceID: '', // 上次请求数据的设备ID
lastTimeRange: '', // 上次请求数据的时间范围
lastCustomStartTime: '', // 上次请求数据的自定义开始时间
lastCustomEndTime: '' // 上次请求数据的自定义结束时间
};
// 加载进程信息
async function loadProcessInfo(page = 1) {
const processTableBody = document.getElementById('processTableBody');
const processPaginationContainer = document.getElementById('processPaginationContainer');
const processPageSizeSelect = document.getElementById('processPageSize');
if (!processTableBody) return;
// 获取并设置每页显示数量
if (processPageSizeSelect) {
const selectedPageSize = parseInt(processPageSizeSelect.value, 10);
if (selectedPageSize !== processPagination.itemsPerPage) {
processPagination.itemsPerPage = selectedPageSize;
// 重置为第一页
page = 1;
processPagination.currentPage = 1;
}
}
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数与fetchMetric函数保持一致
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
params.append('end_time', 'now()');
}
// 检查设备ID或时间范围是否变化
const timeRangeChanged =
processPagination.lastTimeRange !== state.currentTimeRange ||
processPagination.lastCustomStartTime !== state.customStartTime ||
processPagination.lastCustomEndTime !== state.customEndTime;
if (processPagination.lastDeviceID !== state.currentDeviceID || timeRangeChanged) {
// 设备ID或时间范围变化清空旧数据
processPagination.allProcesses = [];
processPagination.totalItems = 0;
processPagination.totalPages = 0;
processPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID/时间范围变化,获取全部数据
if (processPagination.allProcesses.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/processes?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch process info');
}
const data = await response.json();
processPagination.allProcesses = data.data || [];
processPagination.totalItems = processPagination.allProcesses.length;
processPagination.totalPages = Math.ceil(processPagination.totalItems / processPagination.itemsPerPage);
// 更新上次请求数据的设备ID和时间范围
processPagination.lastDeviceID = state.currentDeviceID;
processPagination.lastTimeRange = state.currentTimeRange;
processPagination.lastCustomStartTime = state.customStartTime;
processPagination.lastCustomEndTime = state.customEndTime;
}
// 更新当前页码
processPagination.currentPage = page;
// 清空表格
processTableBody.innerHTML = '';
if (processPagination.totalItems === 0) {
// 没有进程数据,显示提示
processTableBody.innerHTML = `
<tr>
<td colspan="8" class="px-6 py-4 text-center text-gray-500">暂无进程数据</td>
</tr>
`;
// 隐藏分页控件
if (processPaginationContainer) {
processPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (processPagination.currentPage - 1) * processPagination.itemsPerPage;
const endIndex = Math.min(startIndex + processPagination.itemsPerPage, processPagination.totalItems);
const paginatedProcesses = processPagination.allProcesses.slice(startIndex, endIndex);
// 填充表格数据
paginatedProcesses.forEach((proc, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${proc.process_name || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${proc.username || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${proc.pid || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${proc.cpu_usage ? parseFloat(proc.cpu_usage).toFixed(2) : '0.00'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${proc.memory_usage ? parseFloat(proc.memory_usage).toFixed(2) : '0.00'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 truncate max-w-xs">${proc.path || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 truncate max-w-xs">${proc.cmdline || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${proc.ports || 'N/A'}</td>
`;
processTableBody.appendChild(row);
});
// 创建分页控件
if (processPaginationContainer) {
createPaginationControls(processPaginationContainer, processPagination, loadProcessInfo);
}
} catch (error) {
console.error('Error loading process info:', error);
processTableBody.innerHTML = `
<tr>
<td colspan="8" class="px-6 py-4 text-center text-red-500">加载进程信息失败</td>
</tr>
`;
if (processPaginationContainer) {
processPaginationContainer.innerHTML = '';
}
}
}
// 磁盘信息分页状态
let diskPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allDisks: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 创建分页控件
function createPaginationControls(container, paginationState, loadFunction) {
// 清空容器
container.innerHTML = '';
// 如果只有一页,不需要分页
if (paginationState.totalPages <= 1) {
return;
}
const pagination = document.createElement('div');
pagination.className = 'flex justify-between items-center mt-4';
// 页码信息
const info = document.createElement('div');
info.className = 'text-sm text-gray-500';
info.textContent = `显示 ${(paginationState.currentPage - 1) * paginationState.itemsPerPage + 1}${Math.min(paginationState.currentPage * paginationState.itemsPerPage, paginationState.totalItems)} 条,共 ${paginationState.totalItems}`;
// 分页按钮容器
const buttons = document.createElement('div');
buttons.className = 'flex items-center space-x-2';
// 上一页按钮
const prevButton = document.createElement('button');
prevButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
prevButton.innerHTML = '<i class="fa fa-chevron-left"></i>';
prevButton.disabled = paginationState.currentPage === 1;
prevButton.addEventListener('click', () => {
if (paginationState.currentPage > 1) {
loadFunction(paginationState.currentPage - 1);
}
});
// 页码按钮
const pageButtons = [];
// 总是显示第一页
if (paginationState.currentPage > 3) {
pageButtons.push(1);
pageButtons.push('...');
}
// 显示当前页附近的页码
const startPage = Math.max(1, paginationState.currentPage - 2);
const endPage = Math.min(paginationState.totalPages, paginationState.currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
pageButtons.push(i);
}
// 总是显示最后一页
if (paginationState.currentPage < paginationState.totalPages - 2) {
pageButtons.push('...');
pageButtons.push(paginationState.totalPages);
}
// 创建页码按钮
pageButtons.forEach(page => {
if (page === '...') {
const ellipsis = document.createElement('span');
ellipsis.className = 'px-2 text-gray-500';
ellipsis.textContent = '...';
buttons.appendChild(ellipsis);
} else {
const pageButton = document.createElement('button');
pageButton.className = `px-3 py-1 rounded border ${page === paginationState.currentPage ? 'bg-blue-50 text-blue-600 border-blue-300' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
pageButton.textContent = page;
pageButton.addEventListener('click', () => {
loadFunction(page);
});
buttons.appendChild(pageButton);
}
});
// 下一页按钮
const nextButton = document.createElement('button');
nextButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === paginationState.totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
nextButton.innerHTML = '<i class="fa fa-chevron-right"></i>';
nextButton.disabled = paginationState.currentPage === paginationState.totalPages;
nextButton.addEventListener('click', () => {
if (paginationState.currentPage < paginationState.totalPages) {
loadFunction(paginationState.currentPage + 1);
}
});
// 添加到按钮容器
buttons.appendChild(prevButton);
// 跳转页面功能
const jumpContainer = document.createElement('div');
jumpContainer.className = 'flex items-center ml-4 space-x-2';
const jumpText = document.createElement('span');
jumpText.className = 'text-sm text-gray-500';
jumpText.textContent = '前往';
const jumpInput = document.createElement('input');
jumpInput.type = 'number';
jumpInput.min = 1;
jumpInput.max = paginationState.totalPages;
jumpInput.value = paginationState.currentPage;
jumpInput.className = 'w-12 px-2 py-1 text-sm border rounded text-center';
jumpInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
let page = parseInt(jumpInput.value);
if (!isNaN(page) && page >= 1 && page <= paginationState.totalPages) {
loadFunction(page);
} else {
jumpInput.value = paginationState.currentPage;
}
}
});
const totalText = document.createElement('span');
totalText.className = 'text-sm text-gray-500';
totalText.textContent = `页,共 ${paginationState.totalPages}`;
jumpContainer.appendChild(jumpText);
jumpContainer.appendChild(jumpInput);
jumpContainer.appendChild(totalText);
buttons.appendChild(jumpContainer);
buttons.appendChild(nextButton);
// 添加到分页容器
pagination.appendChild(info);
pagination.appendChild(buttons);
// 添加到页面
container.appendChild(pagination);
}
// 加载磁盘详细信息
async function loadDiskDetails(page = 1) {
const diskDetailsContent = document.getElementById('diskDetailsContent');
const diskPaginationContainer = document.getElementById('diskPaginationContainer');
if (!diskDetailsContent) return;
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 检查设备ID是否变化
if (diskPagination.lastDeviceID !== state.currentDeviceID) {
// 设备ID变化清空旧数据
diskPagination.allDisks = [];
diskPagination.totalItems = 0;
diskPagination.totalPages = 0;
diskPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID变化获取全部数据
if (diskPagination.allDisks.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/disk_details?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch disk details');
}
const data = await response.json();
diskPagination.allDisks = data.data || [];
diskPagination.totalItems = diskPagination.allDisks.length;
diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage);
// 更新上次请求数据的设备ID
diskPagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
diskPagination.currentPage = page;
// 清空内容
diskDetailsContent.innerHTML = '';
if (diskPagination.totalItems === 0) {
// 没有磁盘数据,显示提示
diskDetailsContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
<i class="fa fa-hdd-o text-2xl mb-2"></i>
<p>暂无磁盘详细信息</p>
</div>
`;
// 隐藏分页控件
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (diskPagination.currentPage - 1) * diskPagination.itemsPerPage;
const endIndex = Math.min(startIndex + diskPagination.itemsPerPage, diskPagination.totalItems);
const paginatedDisks = diskPagination.allDisks.slice(startIndex, endIndex);
// 填充磁盘详细信息卡片
paginatedDisks.forEach(disk => {
const diskCard = document.createElement('div');
diskCard.className = 'bg-white rounded-lg shadow-md p-6 border border-gray-100';
diskCard.innerHTML = `
<h4 class="text-md font-semibold text-gray-900 mb-3">${disk.device_id || 'Unknown Device'}</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">状态:</span>
<span class="text-gray-900 font-medium">${disk.status || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">类型:</span>
<span class="text-gray-900 font-medium">${disk.type || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">大小:</span>
<span class="text-gray-900 font-medium">${disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00'} GB</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">型号:</span>
<span class="text-gray-900 font-medium">${disk.model || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">接口类型:</span>
<span class="text-gray-900 font-medium">${disk.interface_type || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">描述:</span>
<span class="text-gray-900 font-medium truncate max-w-xs">${disk.description || 'Unknown'}</span>
</div>
</div>
`;
diskDetailsContent.appendChild(diskCard);
});
// 创建分页控件
if (diskPaginationContainer) {
createPaginationControls(diskPaginationContainer, diskPagination, loadDiskDetails);
}
} catch (error) {
console.error('Error loading disk details:', error);
diskDetailsContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-red-500">
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
<p>加载磁盘详细信息失败</p>
</div>
`;
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
}
}
// 格式化时间为易识别的日期样式
function formatLogTime(timeString) {
if (!timeString) return new Date().toLocaleString('zh-CN');
try {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
// 如果时间解析失败,返回原始字符串
return timeString;
}
// 格式化为2025-12-04 16:02:28
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
} catch (error) {
console.error('Error formatting log time:', error);
return timeString;
}
}
// 加载系统日志
async function loadSystemLogs(page = 1) {
console.log('loadSystemLogs function called');
const logTableBody = document.getElementById('logTableBody');
const logPaginationContainer = document.getElementById('logPaginationContainer');
if (!logTableBody) {
console.error('logTableBody element not found');
return;
}
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数与fetchMetric函数保持一致
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
params.append('end_time', 'now()');
}
// 检查设备ID或时间范围是否变化
const timeRangeChanged =
logPagination.lastTimeRange !== state.currentTimeRange ||
logPagination.lastCustomStartTime !== state.customStartTime ||
logPagination.lastCustomEndTime !== state.customEndTime;
if (logPagination.lastDeviceID !== state.currentDeviceID || timeRangeChanged) {
// 设备ID或时间范围变化清空旧数据
logPagination.allLogs = [];
logPagination.totalItems = 0;
logPagination.totalPages = 0;
logPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID/时间范围变化,获取全部数据
if (logPagination.allLogs.length === 0) {
console.log('Fetching logs from:', `${API_BASE_URL}/metrics/logs?${params.toString()}`);
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/logs?${params.toString()}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Logs response data:', data);
// 处理后端返回格式,支持两种格式:{"logs": [...]} 和 {"data": [...], "total": ...}
logPagination.allLogs = data.data || data.logs || [];
logPagination.totalItems = data.total || logPagination.allLogs.length;
logPagination.totalPages = Math.ceil(logPagination.totalItems / logPagination.itemsPerPage);
// 更新上次请求数据的设备ID和时间范围
logPagination.lastDeviceID = state.currentDeviceID;
logPagination.lastTimeRange = state.currentTimeRange;
logPagination.lastCustomStartTime = state.customStartTime;
logPagination.lastCustomEndTime = state.customEndTime;
}
// 更新当前页码
logPagination.currentPage = page;
// 清空表格
logTableBody.innerHTML = '';
if (logPagination.totalItems === 0) {
// 没有日志数据,显示提示
logTableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500">暂无系统日志</td>
</tr>
`;
// 隐藏分页控件
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (logPagination.currentPage - 1) * logPagination.itemsPerPage;
const endIndex = Math.min(startIndex + logPagination.itemsPerPage, logPagination.totalItems);
const paginatedLogs = logPagination.allLogs.slice(startIndex, endIndex);
// 填充表格数据
paginatedLogs.forEach((log, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${startIndex + index + 1}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${log.source || 'System'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatLogTime(log.time)}</td>
<td class="px-6 py-4 whitespace-normal text-sm text-gray-500">${log.message || 'No message'}</td>
`;
logTableBody.appendChild(row);
});
// 创建分页控件
if (logPaginationContainer) {
createPaginationControls(logPaginationContainer, logPagination, loadSystemLogs);
}
} catch (error) {
console.error('Error loading system logs:', error);
logTableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载系统日志失败: ${error.message}</td>
</tr>
`;
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
}
}
// 处理hash变化
function handleHashChange() {
const hash = window.location.hash;
if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
// 延迟一下确保DOM已经渲染完成
setTimeout(() => {
// 加载当前选项卡对应的数据
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示/隐藏附加信息容器
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
// 加载系统日志
loadSystemLogs();
} else {
// 加载其他监控数据
loadMetrics();
// 根据选项卡加载附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
}
}
} else {
// 如果没有找到激活的选项卡默认加载metrics
loadMetrics();
}
}, 300);
}
}