3856 lines
144 KiB
JavaScript
3856 lines
144 KiB
JavaScript
// 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);
|
||
}
|
||
}
|