2357 lines
84 KiB
JavaScript
2357 lines
84 KiB
JavaScript
// API 基础 URL
|
||
const API_BASE_URL = '/api';
|
||
|
||
// 全局状态
|
||
let state = {
|
||
currentTimeRange: '1h', // 与UI默认值保持一致
|
||
customStartTime: '',
|
||
customEndTime: '',
|
||
currentInterval: '10m', // 固定10分钟区间
|
||
historyMetrics: {}, // 存储历史指标数据
|
||
autoRefreshEnabled: false // 自动刷新开关状态,默认关闭
|
||
}
|
||
|
||
// WebSocket连接
|
||
let ws = null;
|
||
let wsReconnectAttempts = 0;
|
||
const wsMaxReconnectAttempts = 5;
|
||
let wsReconnectDelay = 1000;
|
||
|
||
// 图表实例
|
||
const charts = {};
|
||
|
||
// 初始化应用
|
||
function initApp() {
|
||
initCustomTimeRange();
|
||
bindEvents();
|
||
initPageSwitch();
|
||
loadHomeData();
|
||
initCharts();
|
||
initWebSocket();
|
||
|
||
// 设置定时刷新
|
||
setInterval(loadMetrics, 30000);
|
||
setInterval(loadServerCount, 30000);
|
||
|
||
// 初始化网卡列表
|
||
loadNetworkInterfaces();
|
||
}
|
||
|
||
// 初始化自定义时间范围
|
||
function initCustomTimeRange() {
|
||
const now = new Date();
|
||
// 默认显示过去24小时
|
||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||
|
||
// 直接使用ISO字符串,包含完整的时区信息
|
||
state.customStartTime = twentyFourHoursAgo.toISOString();
|
||
state.customEndTime = now.toISOString();
|
||
}
|
||
|
||
// 页面切换
|
||
function initPageSwitch() {
|
||
window.addEventListener('hashchange', switchPage);
|
||
switchPage();
|
||
}
|
||
|
||
function switchPage() {
|
||
const hash = window.location.hash;
|
||
|
||
// 隐藏所有内容
|
||
hideAllContent();
|
||
|
||
// 显示对应内容
|
||
if (hash === '#servers') {
|
||
showContent('serversContent');
|
||
loadAllServers();
|
||
} else if (hash === '#devices') {
|
||
showContent('devicesContent');
|
||
loadDeviceManagementList();
|
||
} else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
|
||
showContent('serverMonitorContent');
|
||
|
||
// 提取设备ID
|
||
let deviceId = '';
|
||
if (hash.startsWith('#serverMonitor/')) {
|
||
deviceId = hash.split('/')[1];
|
||
}
|
||
|
||
// 加载服务器信息
|
||
if (deviceId) {
|
||
loadServerInfo(deviceId);
|
||
}
|
||
|
||
loadMetrics();
|
||
} else {
|
||
showContent('homeContent');
|
||
loadHomeData();
|
||
}
|
||
}
|
||
|
||
function hideAllContent() {
|
||
document.getElementById('homeContent').classList.add('hidden');
|
||
document.getElementById('serversContent').classList.add('hidden');
|
||
document.getElementById('serverMonitorContent').classList.add('hidden');
|
||
document.getElementById('devicesContent').classList.add('hidden');
|
||
}
|
||
|
||
function showContent(contentId) {
|
||
document.getElementById(contentId).classList.remove('hidden');
|
||
}
|
||
|
||
// 加载首页数据
|
||
async function loadHomeData() {
|
||
try {
|
||
// 加载业务视图数据
|
||
await loadBusinessViewData();
|
||
// 加载告警列表数据
|
||
loadAlarmListData();
|
||
// 加载服务器数量
|
||
loadServerCount();
|
||
} catch (error) {
|
||
console.error('加载首页数据失败:', error);
|
||
}
|
||
}
|
||
|
||
// 加载业务视图数据
|
||
async function loadBusinessViewData() {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/devices/`);
|
||
const data = await response.json();
|
||
let devices = data.devices || [];
|
||
|
||
// 如果没有设备,使用模拟数据
|
||
if (devices.length === 0) {
|
||
devices = [
|
||
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100' },
|
||
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101' }
|
||
];
|
||
}
|
||
|
||
renderBusinessView(devices);
|
||
} catch (error) {
|
||
console.error('加载业务视图数据失败:', error);
|
||
// 使用模拟数据
|
||
const mockDevices = [
|
||
{ id: 'device-1', name: 'LIS', ip: '192.129.6.108', os: 'Linux', status: 'P1' },
|
||
{ id: 'device-2', name: '互联网医院', ip: '192.129.6.57', os: 'Linux', status: 'P3' },
|
||
{ id: 'device-3', name: 'HIS', ip: '192.129.51.21', os: 'Windows', status: 'P2' },
|
||
{ id: 'device-4', name: 'OA', ip: '192.129.6.42', os: 'Linux', status: 'P2' }
|
||
];
|
||
renderBusinessView(mockDevices);
|
||
}
|
||
}
|
||
|
||
// 渲染业务视图
|
||
function renderBusinessView(devices) {
|
||
const tableBody = document.getElementById('businessViewTableBody');
|
||
if (!tableBody) return;
|
||
|
||
tableBody.innerHTML = '';
|
||
|
||
devices.forEach(device => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'hover:bg-gray-50 transition-colors';
|
||
row.innerHTML = `
|
||
<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>
|
||
`;
|
||
tableBody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// 加载告警列表数据
|
||
function loadAlarmListData() {
|
||
// 模拟数据
|
||
const alarmData = [
|
||
{ level: 'warning', message: 'CPU使用率超过阈值', device: '192.129.6.108', time: '2023-11-30 14:30:00' },
|
||
{ level: 'error', message: '内存使用率超过阈值', device: '192.129.6.57', time: '2023-11-30 14:28:00' },
|
||
{ level: 'warning', message: '磁盘使用率超过阈值', device: '192.129.51.21', time: '2023-11-30 14:25:00' },
|
||
{ level: 'info', message: '网络流量异常', device: '192.129.6.42', time: '2023-11-30 14:20:00' }
|
||
];
|
||
|
||
renderAlarmList(alarmData);
|
||
}
|
||
|
||
// 渲染告警列表
|
||
function renderAlarmList(alarmData) {
|
||
const alarmList = document.getElementById('alarmList');
|
||
if (!alarmList) return;
|
||
|
||
alarmList.innerHTML = '';
|
||
|
||
alarmData.forEach(alarm => {
|
||
const alarmItem = document.createElement('div');
|
||
alarmItem.className = `p-4 bg-gray-50 rounded-lg border-l-4 ${getAlarmBorderColor(alarm.level)}`;
|
||
|
||
alarmItem.innerHTML = `
|
||
<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 {
|
||
const response = await fetch(`${API_BASE_URL}/devices/`);
|
||
const data = await response.json();
|
||
const devices = data.devices || [];
|
||
|
||
renderServersGrid(devices);
|
||
} catch (error) {
|
||
console.error('加载服务器列表失败:', error);
|
||
// 模拟数据
|
||
const mockDevices = [
|
||
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100' },
|
||
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101' }
|
||
];
|
||
renderServersGrid(mockDevices);
|
||
}
|
||
}
|
||
|
||
// 渲染服务器网格
|
||
function renderServersGrid(devices) {
|
||
const serversGrid = document.getElementById('serversGrid');
|
||
if (!serversGrid) return;
|
||
|
||
serversGrid.innerHTML = '';
|
||
|
||
devices.forEach(device => {
|
||
const serverCard = createServerCard(device);
|
||
serversGrid.appendChild(serverCard);
|
||
});
|
||
}
|
||
|
||
// 创建服务器卡片
|
||
function createServerCard(device) {
|
||
const card = document.createElement('div');
|
||
card.className = 'bg-white rounded-xl shadow-md p-6 card-hover border border-gray-100 cursor-pointer';
|
||
|
||
// 为卡片添加点击事件
|
||
card.addEventListener('click', () => {
|
||
goToServerMonitor(device.id);
|
||
});
|
||
|
||
card.innerHTML = `
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<h3 class="text-xl font-bold text-gray-900">${device.name || device.id}</h3>
|
||
<p class="text-sm text-gray-500">${device.ip || 'N/A'}</p>
|
||
</div>
|
||
<div class="px-3 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
|
||
在线
|
||
</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">0.0%</p>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500">内存使用率</p>
|
||
<p class="text-lg font-semibold text-gray-900">0.0%</p>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500">磁盘使用率</p>
|
||
<p class="text-lg font-semibold text-gray-900">0.0%</p>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500">网络流量</p>
|
||
<p class="text-lg font-semibold text-gray-900">0.0 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(); // 阻止事件冒泡
|
||
goToServerMonitor(device.id);
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// 存储原始设备列表,用于搜索和筛选
|
||
let originalDeviceList = [];
|
||
|
||
// 设备管理
|
||
async function loadDeviceManagementList() {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/devices/all`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch devices');
|
||
}
|
||
|
||
const data = await response.json();
|
||
const devices = data.devices || [];
|
||
|
||
// 如果设备列表为空,使用模拟数据作为回退
|
||
if (devices.length === 0) {
|
||
console.warn('No devices available, using mock data');
|
||
const mockDevices = getMockDevices();
|
||
originalDeviceList = mockDevices;
|
||
renderDeviceManagementList(mockDevices);
|
||
return;
|
||
}
|
||
|
||
// 处理设备数据,转换为所需格式
|
||
const processedDevices = devices.map(device => ({
|
||
id: device.id,
|
||
name: device.name || device.id,
|
||
ip: device.ip || 'N/A',
|
||
status: device.status || 'unknown',
|
||
created_at: device.created_at ? new Date(device.created_at * 1000).toLocaleString() : 'N/A',
|
||
token: device.token || 'N/A'
|
||
}));
|
||
|
||
// 存储原始设备列表
|
||
originalDeviceList = processedDevices;
|
||
|
||
// 应用当前筛选条件
|
||
applyDeviceFilters();
|
||
} catch (error) {
|
||
console.error('加载设备管理列表失败:', error);
|
||
// 使用模拟数据作为回退
|
||
const mockDevices = getMockDevices();
|
||
originalDeviceList = mockDevices;
|
||
renderDeviceManagementList(mockDevices);
|
||
}
|
||
}
|
||
|
||
// 应用设备筛选条件
|
||
function applyDeviceFilters() {
|
||
const searchTerm = document.getElementById('deviceSearch').value.toLowerCase();
|
||
const statusFilter = document.getElementById('deviceStatusFilter').value;
|
||
|
||
// 复制原始列表
|
||
let filteredDevices = [...originalDeviceList];
|
||
|
||
// 应用搜索筛选
|
||
if (searchTerm) {
|
||
filteredDevices = filteredDevices.filter(device =>
|
||
device.name.toLowerCase().includes(searchTerm) ||
|
||
device.id.toLowerCase().includes(searchTerm) ||
|
||
device.ip.toLowerCase().includes(searchTerm)
|
||
);
|
||
}
|
||
|
||
// 应用状态筛选
|
||
if (statusFilter && statusFilter !== 'all') {
|
||
filteredDevices = filteredDevices.filter(device => device.status === statusFilter);
|
||
}
|
||
|
||
// 渲染筛选后的设备列表
|
||
renderDeviceManagementList(filteredDevices);
|
||
|
||
// 显示或隐藏无数据提示
|
||
const noDataMessage = document.getElementById('noDataMessage');
|
||
if (noDataMessage) {
|
||
if (filteredDevices.length === 0) {
|
||
noDataMessage.classList.remove('hidden');
|
||
} else {
|
||
noDataMessage.classList.add('hidden');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清除筛选条件
|
||
function clearDeviceFilters() {
|
||
document.getElementById('deviceSearch').value = '';
|
||
document.getElementById('deviceStatusFilter').value = 'all';
|
||
applyDeviceFilters();
|
||
}
|
||
|
||
// 获取模拟设备数据
|
||
function getMockDevices() {
|
||
return [
|
||
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100', status: 'active', created_at: new Date().toLocaleString(), token: 'mock-token-1' },
|
||
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101', status: 'inactive', created_at: new Date().toLocaleString(), token: 'mock-token-2' },
|
||
{ id: 'device-3', name: '测试服务器', ip: '192.168.1.102', status: 'offline', created_at: new Date().toLocaleString(), token: 'mock-token-3' }
|
||
];
|
||
}
|
||
|
||
// 复制文本到剪贴板
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
// 可以添加一个提示,如"已复制"的临时消息
|
||
showToast('复制成功');
|
||
}).catch(err => {
|
||
console.error('复制失败:', err);
|
||
showToast('复制失败');
|
||
});
|
||
}
|
||
|
||
// 显示临时提示消息
|
||
function showToast(message, type = 'success') {
|
||
// 检查是否已存在toast元素
|
||
let toast = document.getElementById('toast');
|
||
if (!toast) {
|
||
toast = document.createElement('div');
|
||
toast.id = 'toast';
|
||
document.body.appendChild(toast);
|
||
}
|
||
|
||
// 设置toast样式根据类型
|
||
let bgColor = 'bg-gray-800';
|
||
switch(type) {
|
||
case 'success':
|
||
bgColor = 'bg-green-800';
|
||
break;
|
||
case 'error':
|
||
bgColor = 'bg-red-800';
|
||
break;
|
||
case 'warning':
|
||
bgColor = 'bg-yellow-800';
|
||
break;
|
||
}
|
||
|
||
toast.className = `${bgColor} text-white px-4 py-2 rounded opacity-0 transition-opacity duration-300 z-50 fixed bottom-4 right-4`;
|
||
toast.textContent = message;
|
||
toast.style.opacity = '1';
|
||
|
||
// 2秒后自动隐藏
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
}, 2000);
|
||
}
|
||
|
||
// 切换设备状态(激活/停用)
|
||
async function toggleDeviceStatus(deviceId, newStatus) {
|
||
try {
|
||
// 先获取设备详情
|
||
const deviceResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
|
||
if (!deviceResponse.ok) {
|
||
throw new Error('Failed to fetch device details');
|
||
}
|
||
const deviceData = await deviceResponse.json();
|
||
const device = deviceData.device;
|
||
|
||
// 更新设备状态
|
||
const updateResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
status: newStatus,
|
||
name: device.name,
|
||
ip: device.ip
|
||
})
|
||
});
|
||
|
||
if (!updateResponse.ok) {
|
||
throw new Error(`HTTP error! status: ${updateResponse.status}`);
|
||
}
|
||
|
||
// 重新加载设备列表
|
||
await loadDeviceManagementList();
|
||
|
||
// 显示成功提示
|
||
const statusText = newStatus === 'active' ? '激活' : '停用';
|
||
showToast(`设备已成功${statusText}`, 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Failed to toggle device status:', error);
|
||
const statusText = newStatus === 'active' ? '激活' : '停用';
|
||
showToast(`设备${statusText}失败`, 'error');
|
||
}
|
||
}
|
||
|
||
// 编辑设备
|
||
function editDevice(deviceId) {
|
||
// 这里可以实现编辑设备的逻辑,比如打开模态框等
|
||
console.log('编辑设备:', deviceId);
|
||
// 可以添加模态框或跳转到编辑页面的逻辑
|
||
}
|
||
|
||
// 删除设备
|
||
function deleteDevice(deviceId) {
|
||
if (confirm('确定要删除这个设备吗?')) {
|
||
fetch(`${API_BASE_URL}/devices/${deviceId}`, {
|
||
method: 'DELETE'
|
||
}).then(response => {
|
||
if (response.ok) {
|
||
showToast('设备删除成功');
|
||
loadDeviceManagementList(); // 重新加载设备列表
|
||
} else {
|
||
throw new Error('删除失败');
|
||
}
|
||
}).catch(err => {
|
||
console.error('删除设备失败:', err);
|
||
showToast('删除失败');
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderDeviceManagementList(devices) {
|
||
const tableBody = document.getElementById('deviceManagementTableBody');
|
||
if (!tableBody) return;
|
||
|
||
tableBody.innerHTML = '';
|
||
|
||
devices.forEach(device => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'hover:bg-gray-50 transition-colors';
|
||
|
||
// 根据设备状态获取显示样式
|
||
const getStatusStyle = (status) => {
|
||
switch (status) {
|
||
case 'active':
|
||
return 'bg-green-100 text-green-800';
|
||
case 'inactive':
|
||
return 'bg-yellow-100 text-yellow-800';
|
||
case 'offline':
|
||
return 'bg-red-100 text-red-800';
|
||
default:
|
||
return 'bg-gray-100 text-gray-800';
|
||
}
|
||
};
|
||
|
||
row.innerHTML = `
|
||
<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="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
|
||
},
|
||
zoom: {
|
||
pan: {
|
||
enabled: true,
|
||
mode: 'x'
|
||
},
|
||
zoom: {
|
||
wheel: {
|
||
enabled: true,
|
||
},
|
||
pinch: {
|
||
enabled: true
|
||
},
|
||
mode: 'x',
|
||
onZoom: function({chart}) {
|
||
console.log(chart.getZoomLevel());
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
max: 100,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return value + '%';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化磁盘图表
|
||
const diskCtx = document.getElementById('diskChart');
|
||
if (diskCtx && !charts.disk) {
|
||
charts.disk = new Chart(diskCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: []
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
},
|
||
spanGaps: true,
|
||
plugins: {
|
||
legend: {
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
zoom: {
|
||
pan: {
|
||
enabled: true,
|
||
mode: 'x'
|
||
},
|
||
zoom: {
|
||
wheel: {
|
||
enabled: true,
|
||
},
|
||
pinch: {
|
||
enabled: true
|
||
},
|
||
mode: 'x',
|
||
onZoom: function({chart}) {
|
||
console.log(chart.getZoomLevel());
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
max: 100,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return value + '%';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化网络流量图表(发送总和和接收总和)
|
||
const networkCtx = document.getElementById('networkChart');
|
||
if (networkCtx && !charts.network) {
|
||
charts.network = new Chart(networkCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [
|
||
{
|
||
label: '发送总和 (MB)',
|
||
data: [],
|
||
borderColor: '#f59e0b',
|
||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.7,
|
||
pointRadius: 0,
|
||
pointHoverRadius: 3
|
||
},
|
||
{
|
||
label: '接收总和 (MB)',
|
||
data: [],
|
||
borderColor: '#10b981',
|
||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.7,
|
||
pointRadius: 0,
|
||
pointHoverRadius: 3
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
},
|
||
spanGaps: true,
|
||
plugins: {
|
||
legend: {
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
zoom: {
|
||
pan: {
|
||
enabled: true,
|
||
mode: 'x'
|
||
},
|
||
zoom: {
|
||
wheel: {
|
||
enabled: true,
|
||
},
|
||
pinch: {
|
||
enabled: true
|
||
},
|
||
mode: 'x',
|
||
onZoom: function({chart}) {
|
||
console.log(chart.getZoomLevel());
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return value + ' MB';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化网速趋势图表
|
||
const speedCtx = document.getElementById('speedChart');
|
||
if (speedCtx && !charts.speed) {
|
||
charts.speed = new Chart(speedCtx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [
|
||
{
|
||
label: '发送速率 (MB/s)',
|
||
data: [],
|
||
borderColor: '#f59e0b',
|
||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.7,
|
||
pointRadius: 0,
|
||
pointHoverRadius: 3
|
||
},
|
||
{
|
||
label: '接收速率 (MB/s)',
|
||
data: [],
|
||
borderColor: '#10b981',
|
||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.7,
|
||
pointRadius: 0,
|
||
pointHoverRadius: 3
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
},
|
||
spanGaps: true,
|
||
plugins: {
|
||
legend: {
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
zoom: {
|
||
pan: {
|
||
enabled: true,
|
||
mode: 'x'
|
||
},
|
||
zoom: {
|
||
wheel: {
|
||
enabled: true,
|
||
},
|
||
pinch: {
|
||
enabled: true
|
||
},
|
||
mode: 'x',
|
||
onZoom: function({chart}) {
|
||
console.log(chart.getZoomLevel());
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return value + ' MB/s';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 初始化告警趋势图
|
||
function initAlarmTrendChart() {
|
||
const ctx = document.getElementById('alarmTrendChart');
|
||
if (!ctx) return;
|
||
|
||
charts.alarmTrend = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: ['11-24', '11-25', '11-26', '11-27', '11-28', '11-29', '11-30'],
|
||
datasets: [
|
||
{
|
||
label: '报警',
|
||
data: [50, 60, 75, 80, 90, 100, 112],
|
||
borderColor: '#ef4444',
|
||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.4
|
||
},
|
||
{
|
||
label: '提醒',
|
||
data: [20, 25, 30, 35, 38, 38, 38],
|
||
borderColor: '#f59e0b',
|
||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.4
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
position: 'top'
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 格式化字节数,根据用户要求:只在MB和GB之间转换
|
||
function formatBytes(bytes, decimals = 2, isRate = false) {
|
||
if (bytes === 0) {
|
||
return isRate ? '0 MB/s' : '0 MB';
|
||
}
|
||
|
||
const mb = 1024 * 1024;
|
||
const gb = mb * 1024;
|
||
const dm = decimals < 0 ? 0 : decimals;
|
||
|
||
// 对于速率(bytes/s):默认显示MB/s,超过1024MB/s显示GB/s
|
||
if (isRate) {
|
||
if (bytes >= gb) {
|
||
return parseFloat((bytes / gb).toFixed(dm)) + ' GB/s';
|
||
} else {
|
||
return parseFloat((bytes / mb).toFixed(dm)) + ' MB/s';
|
||
}
|
||
}
|
||
// 对于流量(bytes):默认显示MB,超过1024MB显示GB
|
||
else {
|
||
if (bytes >= gb) {
|
||
return parseFloat((bytes / gb).toFixed(dm)) + ' GB';
|
||
} else {
|
||
return parseFloat((bytes / mb).toFixed(dm)) + ' MB';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取单个指标数据
|
||
async function fetchMetric(metricType, aggregation = 'average') {
|
||
// 构建查询参数
|
||
const params = new URLSearchParams();
|
||
|
||
// 设置时间范围参数
|
||
if (state.customStartTime && state.customEndTime) {
|
||
// 自定义时间范围
|
||
params.append('start_time', state.customStartTime);
|
||
params.append('end_time', state.customEndTime);
|
||
} else {
|
||
// 默认显示24小时的数据
|
||
params.append('start_time', '-24h');
|
||
params.append('end_time', 'now()');
|
||
}
|
||
|
||
// 设置聚合方式
|
||
params.append('aggregation', aggregation);
|
||
|
||
// 固定时间区间为10分钟
|
||
params.append('interval', '10m');
|
||
|
||
// 发送请求
|
||
const response = await fetch(`${API_BASE_URL}/metrics/${metricType}?${params.toString()}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to fetch ${metricType} metrics`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
return data.data;
|
||
}
|
||
|
||
// 格式化时间,统一格式避免图表误解
|
||
function formatTime(timeStr) {
|
||
// timeStr是ISO格式的UTC时间字符串,如"2025-12-02T01:53:19Z"
|
||
const date = new Date(timeStr);
|
||
|
||
// 格式化年、月、日(使用本地时间)
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
|
||
// 格式化时、分、秒(使用本地时间)
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
|
||
// 总是显示完整的日期和时间,方便区分不同日期的数据
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
}
|
||
|
||
|
||
|
||
// 加载监控指标
|
||
async function loadMetrics() {
|
||
try {
|
||
// 并行加载所有指标
|
||
const [cpuData, memoryData, diskData, networkSumData] = await Promise.all([
|
||
fetchMetric('cpu'),
|
||
fetchMetric('memory'),
|
||
fetchMetric('disk'),
|
||
fetchMetric('network')
|
||
]);
|
||
|
||
// 更新状态卡片
|
||
updateStatusCards({ cpu: cpuData, memory: memoryData, disk: diskData, network: networkSumData });
|
||
|
||
// 更新图表
|
||
updateCharts(cpuData, memoryData, diskData, networkSumData);
|
||
} catch (error) {
|
||
console.error('Failed to load metrics:', error);
|
||
// 显示友好的错误提示
|
||
showToast('加载监控数据失败,请稍后重试', 'error');
|
||
}
|
||
}
|
||
|
||
// WebSocket初始化
|
||
function initWebSocket() {
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
|
||
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = () => {
|
||
console.log('WebSocket连接已打开');
|
||
wsReconnectAttempts = 0;
|
||
wsReconnectDelay = 1000;
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
try {
|
||
const message = JSON.parse(event.data);
|
||
handleWebSocketMessage(message);
|
||
} catch (error) {
|
||
console.error('解析WebSocket消息失败:', error);
|
||
}
|
||
};
|
||
|
||
ws.onclose = (event) => {
|
||
console.log(`WebSocket连接已关闭: ${event.code} - ${event.reason}`);
|
||
attemptReconnect();
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('WebSocket连接错误:', error);
|
||
};
|
||
}
|
||
|
||
// 处理WebSocket消息
|
||
function handleWebSocketMessage(message) {
|
||
if (message.type === 'metrics_update') {
|
||
handleMetricsUpdate(message);
|
||
}
|
||
}
|
||
|
||
// 加载服务器信息
|
||
async function loadServerInfo(deviceId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch server info');
|
||
}
|
||
const 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>`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load server info:', error);
|
||
// 使用模拟数据
|
||
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
|
||
if (serverInfoDisplay) {
|
||
serverInfoDisplay.innerHTML = `<p>服务器名称: <strong>服务器 ${deviceId}</strong> | IP地址: <strong>192.168.1.${deviceId}</strong></p>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理指标更新
|
||
function handleMetricsUpdate(message) {
|
||
const { device_id, metrics } = message;
|
||
// 直接更新统计卡片,始终实时更新
|
||
updateStatusCards(metrics);
|
||
// 根据自动刷新开关状态决定是否刷新图表
|
||
if (state.autoRefreshEnabled) {
|
||
// 立即刷新数据以确保图表也更新
|
||
setTimeout(() => loadMetrics(), 500);
|
||
}
|
||
}
|
||
|
||
// 更新状态卡片
|
||
function updateStatusCards(metrics) {
|
||
// 更新历史指标数据
|
||
updateHistoryMetrics(metrics);
|
||
|
||
// 使用历史数据或当前数据
|
||
const displayMetrics = {
|
||
cpu: metrics.cpu || state.historyMetrics.cpu,
|
||
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) {
|
||
cpuUsage = displayMetrics.cpu.usage;
|
||
cpuGhz = displayMetrics.cpu.frequency || 0;
|
||
cpuLoad = displayMetrics.cpu.load || 0;
|
||
}
|
||
|
||
// 更新显示
|
||
const cpuElement = document.getElementById('cpuValue');
|
||
const cpuDetailsElement = document.getElementById('cpuDetails');
|
||
if (cpuElement) {
|
||
cpuElement.textContent = `${cpuUsage.toFixed(1)}%`;
|
||
// 设置红色显示如果达到顶峰
|
||
cpuElement.className = cpuUsage > 90 ? 'text-red-500' : '';
|
||
}
|
||
if (cpuDetailsElement) {
|
||
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') {
|
||
memoryUsage = displayMetrics.memory.usage || 0;
|
||
memoryUsed = displayMetrics.memory.used || 0;
|
||
memoryTotal = displayMetrics.memory.total || 0;
|
||
}
|
||
|
||
// 更新显示
|
||
const memoryElement = document.getElementById('memoryValue');
|
||
const memoryDetailsElement = document.getElementById('memoryDetails');
|
||
if (memoryElement) {
|
||
memoryElement.textContent = `${memoryUsage.toFixed(1)}%`;
|
||
// 设置红色显示如果达到顶峰
|
||
memoryElement.className = memoryUsage > 90 ? 'text-red-500' : '';
|
||
}
|
||
if (memoryDetailsElement) {
|
||
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)) {
|
||
// 按挂载点分组的数据
|
||
for (const mountpoint in displayMetrics.disk) {
|
||
// 跳过排除的挂载点
|
||
if (excludedMountpoints.includes(mountpoint)) {
|
||
continue;
|
||
}
|
||
|
||
const data = displayMetrics.disk[mountpoint];
|
||
if (data && typeof data === 'object' && 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) {
|
||
usagePercent = (totalUsed / totalSize) * 100;
|
||
}
|
||
|
||
// 更新显示
|
||
const diskElement = document.getElementById('diskValue');
|
||
const diskDetailsElement = document.getElementById('diskDetails');
|
||
if (diskElement) {
|
||
diskElement.textContent = `${usagePercent.toFixed(1)}%`;
|
||
// 设置红色显示如果达到顶峰
|
||
diskElement.className = usagePercent > 90 ? 'text-red-500' : '';
|
||
}
|
||
if (diskDetailsElement) {
|
||
diskDetailsElement.textContent = `${formatBytes(totalUsed)} / ${formatBytes(totalSize)}`;
|
||
}
|
||
}
|
||
|
||
// 更新网络状态卡片
|
||
if (displayMetrics.network) {
|
||
let sentRate = 0;
|
||
let receivedRate = 0;
|
||
let sentTotal = 0;
|
||
let receivedTotal = 0;
|
||
|
||
// 解析网络数据
|
||
if (displayMetrics.network.sent && displayMetrics.network.received) {
|
||
// 处理数组格式的数据
|
||
if (Array.isArray(displayMetrics.network.sent) && displayMetrics.network.sent.length > 0 &&
|
||
Array.isArray(displayMetrics.network.received) && displayMetrics.network.received.length > 0) {
|
||
|
||
sentRate = displayMetrics.network.sent[displayMetrics.network.sent.length - 1].value;
|
||
receivedRate = displayMetrics.network.received[displayMetrics.network.received.length - 1].value;
|
||
// 计算总量(假设数组中的值是累积的)
|
||
sentTotal = displayMetrics.network.sent.reduce((sum, item) => sum + item.value, 0);
|
||
receivedTotal = displayMetrics.network.received.reduce((sum, item) => sum + item.value, 0);
|
||
}
|
||
// 处理数值格式的数据
|
||
else if (typeof displayMetrics.network.sent === 'number' && typeof displayMetrics.network.received === 'number') {
|
||
sentRate = displayMetrics.network.sent;
|
||
receivedRate = displayMetrics.network.received;
|
||
sentTotal = displayMetrics.network.sent_total || sentRate;
|
||
receivedTotal = displayMetrics.network.received_total || receivedRate;
|
||
}
|
||
} else if (typeof displayMetrics.network === 'object' &&
|
||
displayMetrics.network.bytes_sent !== undefined &&
|
||
displayMetrics.network.bytes_received !== undefined) {
|
||
// WebSocket消息格式 - 总流量
|
||
sentRate = displayMetrics.network.bytes_sent;
|
||
receivedRate = displayMetrics.network.bytes_received;
|
||
sentTotal = displayMetrics.network.bytes_sent_total || sentRate;
|
||
receivedTotal = displayMetrics.network.bytes_received_total || receivedRate;
|
||
} else if (typeof displayMetrics.network === 'object' && !Array.isArray(displayMetrics.network)) {
|
||
// 按网卡分组的数据,计算总流量
|
||
for (const iface in displayMetrics.network) {
|
||
const ifaceMetrics = displayMetrics.network[iface];
|
||
if (ifaceMetrics.bytes_sent !== undefined && ifaceMetrics.bytes_received !== undefined) {
|
||
sentRate += ifaceMetrics.bytes_sent;
|
||
receivedRate += ifaceMetrics.bytes_received;
|
||
sentTotal += ifaceMetrics.bytes_sent_total || ifaceMetrics.bytes_sent;
|
||
receivedTotal += ifaceMetrics.bytes_received_total || ifaceMetrics.bytes_received;
|
||
} else if (ifaceMetrics.BytesSent !== undefined && ifaceMetrics.BytesReceived !== undefined) {
|
||
sentRate += ifaceMetrics.BytesSent;
|
||
receivedRate += ifaceMetrics.BytesReceived;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算比率
|
||
let ratioText = '0.0';
|
||
if (sentRate > 0) {
|
||
ratioText = (receivedRate / sentRate).toFixed(1);
|
||
}
|
||
|
||
// 更新显示
|
||
const networkValueElement = document.getElementById('networkValue');
|
||
const networkDetailsElement = document.getElementById('networkDetails');
|
||
if (networkValueElement) {
|
||
networkValueElement.innerHTML = `${ratioText} <span style="color: green;">↓</span> / <span style="color: red;">↑</span>`;
|
||
}
|
||
if (networkDetailsElement) {
|
||
networkDetailsElement.innerHTML =
|
||
`接收: <span style="color: green;">${formatBytes(receivedRate, 2, true)}</span> | ` +
|
||
`发送: <span style="color: red;">${formatBytes(sentRate, 2, true)}</span><br>` +
|
||
`总量: 接收 ${formatBytes(receivedTotal)} | 发送 ${formatBytes(sentTotal)}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新历史指标数据
|
||
function updateHistoryMetrics(metrics) {
|
||
// 只更新有有效数据的指标
|
||
if (metrics.cpu) {
|
||
state.historyMetrics.cpu = metrics.cpu;
|
||
}
|
||
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 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';
|
||
|
||
// 更新网络流量趋势图表(发送总和和接收总和)
|
||
if (networkData && charts.network) {
|
||
let selectedNetworkData = networkData;
|
||
|
||
// 如果是按网卡分组的数据,选择当前选中的网卡数据
|
||
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
|
||
selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
|
||
}
|
||
|
||
if (selectedNetworkData.sent && selectedNetworkData.received) {
|
||
// 计算发送总和(时间段内的累积值)
|
||
if (Array.isArray(selectedNetworkData.sent) && selectedNetworkData.sent.length > 0) {
|
||
// 排序发送数据
|
||
const sortedSent = sortDataByTime(selectedNetworkData.sent);
|
||
|
||
// 计算累积发送总和(MB)
|
||
let cumulativeSent = 0;
|
||
const sentSumData = sortedSent.map(item => {
|
||
// 转换为MB并累积
|
||
const mbValue = item.value / (1024 * 1024);
|
||
cumulativeSent += mbValue;
|
||
return {
|
||
time: item.time,
|
||
value: cumulativeSent
|
||
};
|
||
});
|
||
|
||
// 使用固定份数X轴数据计算
|
||
const fixedPointsSentSum = getFixedPointsData(sentSumData);
|
||
charts.network.data.datasets[0].data = fixedPointsSentSum.map(item => ({
|
||
x: formatTime(item.time),
|
||
y: item.value
|
||
}));
|
||
}
|
||
|
||
// 计算接收总和(时间段内的累积值)
|
||
if (Array.isArray(selectedNetworkData.received) && selectedNetworkData.received.length > 0) {
|
||
// 排序接收数据
|
||
const sortedReceived = sortDataByTime(selectedNetworkData.received);
|
||
|
||
// 计算累积接收总和(MB)
|
||
let cumulativeReceived = 0;
|
||
const receivedSumData = sortedReceived.map(item => {
|
||
// 转换为MB并累积
|
||
const mbValue = item.value / (1024 * 1024);
|
||
cumulativeReceived += mbValue;
|
||
return {
|
||
time: item.time,
|
||
value: cumulativeReceived
|
||
};
|
||
});
|
||
|
||
// 使用固定份数X轴数据计算
|
||
const fixedPointsReceivedSum = getFixedPointsData(receivedSumData);
|
||
charts.network.data.datasets[1].data = fixedPointsReceivedSum.map(item => ({
|
||
x: formatTime(item.time),
|
||
y: item.value
|
||
}));
|
||
}
|
||
|
||
charts.network.update();
|
||
}
|
||
}
|
||
|
||
// 更新网速趋势图表
|
||
if (networkData && charts.speed) {
|
||
let selectedNetworkData = networkData;
|
||
|
||
// 如果是按网卡分组的数据,选择当前选中的网卡数据
|
||
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
|
||
selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
|
||
}
|
||
|
||
if (selectedNetworkData.sent && selectedNetworkData.received) {
|
||
// 更新发送流量
|
||
if (Array.isArray(selectedNetworkData.sent) && selectedNetworkData.sent.length > 0) {
|
||
const sortedData = sortDataByTime(selectedNetworkData.sent);
|
||
// 使用固定份数X轴数据计算
|
||
const fixedPointsData = getFixedPointsData(sortedData);
|
||
charts.speed.data.datasets[0].data = fixedPointsData.map(item => ({
|
||
x: formatTime(item.time),
|
||
y: item.value / (1024 * 1024) // 转换为MB/s
|
||
}));
|
||
}
|
||
|
||
// 更新接收流量
|
||
if (Array.isArray(selectedNetworkData.received) && selectedNetworkData.received.length > 0) {
|
||
const sortedData = sortDataByTime(selectedNetworkData.received);
|
||
// 使用固定份数X轴数据计算
|
||
const fixedPointsData = getFixedPointsData(sortedData);
|
||
charts.speed.data.datasets[1].data = fixedPointsData.map(item => ({
|
||
x: formatTime(item.time),
|
||
y: item.value / (1024 * 1024) // 转换为MB/s
|
||
}));
|
||
}
|
||
|
||
charts.speed.update();
|
||
}
|
||
}
|
||
|
||
// 更新网卡选择下拉框
|
||
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 mockInterfaces = ['eth0', 'eth1', 'lo'];
|
||
mockInterfaces.forEach(iface => {
|
||
const option = document.createElement('option');
|
||
option.value = iface;
|
||
option.textContent = iface;
|
||
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 (wsReconnectAttempts < wsMaxReconnectAttempts) {
|
||
wsReconnectAttempts++;
|
||
wsReconnectDelay *= 2;
|
||
setTimeout(() => {
|
||
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts})`);
|
||
initWebSocket();
|
||
}, wsReconnectDelay);
|
||
}
|
||
}
|
||
|
||
// 打开模态框
|
||
function openModal(isEdit = false, deviceData = null) {
|
||
const modal = document.getElementById('deviceModal');
|
||
const modalContent = document.getElementById('modalContent');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const deviceId = document.getElementById('deviceId');
|
||
const deviceName = document.getElementById('deviceName');
|
||
const deviceIp = document.getElementById('deviceIp');
|
||
const deviceStatus = document.getElementById('deviceStatus');
|
||
|
||
// 重置表单
|
||
document.getElementById('deviceForm').reset();
|
||
|
||
// 设置模态框标题和数据
|
||
if (isEdit && deviceData) {
|
||
modalTitle.textContent = '编辑设备';
|
||
deviceId.value = deviceData.id;
|
||
deviceName.value = deviceData.name || '';
|
||
deviceIp.value = deviceData.ip || '';
|
||
deviceStatus.value = deviceData.status || 'inactive';
|
||
} else {
|
||
modalTitle.textContent = '添加设备';
|
||
deviceId.value = '';
|
||
}
|
||
|
||
// 显示模态框并添加动画
|
||
modal.classList.remove('hidden');
|
||
// 触发重排后再添加动画类
|
||
setTimeout(() => {
|
||
modalContent.classList.remove('scale-95', 'opacity-0');
|
||
modalContent.classList.add('scale-100', 'opacity-100');
|
||
}, 10);
|
||
}
|
||
|
||
// 关闭模态框
|
||
function closeModal() {
|
||
const modal = document.getElementById('deviceModal');
|
||
const modalContent = document.getElementById('modalContent');
|
||
|
||
// 先应用离开动画
|
||
modalContent.classList.remove('scale-100', 'opacity-100');
|
||
modalContent.classList.add('scale-95', 'opacity-0');
|
||
|
||
// 动画结束后隐藏模态框
|
||
setTimeout(() => {
|
||
modal.classList.add('hidden');
|
||
}, 300);
|
||
}
|
||
|
||
// 编辑设备
|
||
function editDevice(deviceId) {
|
||
// 直接从API获取设备详情,确保数据最新
|
||
fetch(`${API_BASE_URL}/devices/${deviceId}`)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch device details');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
// 提取设备数据(API返回的是 {"device": {...}} 格式)
|
||
const deviceData = data.device;
|
||
openModal(true, deviceData);
|
||
})
|
||
.catch(error => {
|
||
console.error('Failed to get device details:', error);
|
||
showToast('获取设备详情失败', 'error');
|
||
});
|
||
}
|
||
|
||
// 处理设备表单提交
|
||
async function handleDeviceFormSubmit(event) {
|
||
event.preventDefault();
|
||
|
||
const deviceId = document.getElementById('deviceId').value;
|
||
const deviceName = document.getElementById('deviceName').value;
|
||
const deviceIp = document.getElementById('deviceIp').value;
|
||
const deviceStatus = document.getElementById('deviceStatus').value;
|
||
|
||
// 生成或使用现有ID
|
||
let idToUse = deviceId;
|
||
if (!idToUse) {
|
||
// 为新设备生成ID
|
||
idToUse = 'device-' + Date.now();
|
||
}
|
||
|
||
// 获取当前时间戳
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
|
||
// 为新设备生成token
|
||
let token = '';
|
||
if (!deviceId) {
|
||
token = 'token-' + Math.random().toString(36).substring(2, 15);
|
||
}
|
||
|
||
// 构建完整的设备数据对象,包含所有必需字段
|
||
const deviceData = {
|
||
id: idToUse,
|
||
name: deviceName,
|
||
ip: deviceIp,
|
||
status: deviceStatus,
|
||
token: token, // 新设备生成token,编辑时不提供(由后端保持原值)
|
||
created_at: timestamp, // 新设备的创建时间
|
||
updated_at: timestamp // 更新时间
|
||
};
|
||
|
||
try {
|
||
let url = `${API_BASE_URL}/devices/`;
|
||
let method = 'POST';
|
||
|
||
// 如果是编辑操作,修改URL和方法
|
||
if (deviceId) {
|
||
url = `${API_BASE_URL}/devices/${deviceId}`;
|
||
method = 'PUT';
|
||
}
|
||
|
||
// 发送请求
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(deviceData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// 显示成功提示
|
||
showToast(deviceId ? '设备更新成功' : '设备添加成功', 'success');
|
||
|
||
// 关闭模态框
|
||
closeModal();
|
||
|
||
// 重新加载设备列表
|
||
await loadDeviceManagementList();
|
||
|
||
} catch (error) {
|
||
console.error('保存设备失败:', error);
|
||
showToast(deviceId ? '设备更新失败' : '设备添加失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 绑定事件
|
||
function bindEvents() {
|
||
// 添加设备按钮事件
|
||
const addDeviceBtn = document.getElementById('addDeviceBtn');
|
||
if (addDeviceBtn) {
|
||
addDeviceBtn.addEventListener('click', () => {
|
||
openModal(false);
|
||
});
|
||
}
|
||
|
||
// 关闭模态框按钮
|
||
const closeModalBtn = document.getElementById('closeModalBtn');
|
||
if (closeModalBtn) {
|
||
closeModalBtn.addEventListener('click', closeModal);
|
||
}
|
||
|
||
// 取消按钮
|
||
const cancelModalBtn = document.getElementById('cancelModalBtn');
|
||
if (cancelModalBtn) {
|
||
cancelModalBtn.addEventListener('click', closeModal);
|
||
}
|
||
|
||
// 设备表单提交
|
||
const deviceForm = document.getElementById('deviceForm');
|
||
if (deviceForm) {
|
||
deviceForm.addEventListener('submit', handleDeviceFormSubmit);
|
||
}
|
||
|
||
// 点击模态框背景关闭
|
||
const modal = document.getElementById('deviceModal');
|
||
if (modal) {
|
||
modal.addEventListener('click', (event) => {
|
||
if (event.target === modal) {
|
||
closeModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 设备搜索事件
|
||
const deviceSearch = document.getElementById('deviceSearch');
|
||
if (deviceSearch) {
|
||
deviceSearch.addEventListener('input', applyDeviceFilters);
|
||
}
|
||
|
||
// 设备状态筛选事件
|
||
const deviceStatusFilter = document.getElementById('deviceStatusFilter');
|
||
if (deviceStatusFilter) {
|
||
deviceStatusFilter.addEventListener('change', applyDeviceFilters);
|
||
}
|
||
|
||
// 清除筛选按钮事件
|
||
const clearFilterBtn = document.getElementById('clearFilterBtn');
|
||
if (clearFilterBtn) {
|
||
clearFilterBtn.addEventListener('click', clearDeviceFilters);
|
||
}
|
||
|
||
// 自定义时间查询事件
|
||
const customTimeQuery = document.getElementById('customTimeQuery');
|
||
if (customTimeQuery) {
|
||
customTimeQuery.addEventListener('click', () => {
|
||
const startTimeInput = document.getElementById('customStartTime');
|
||
const endTimeInput = document.getElementById('customEndTime');
|
||
|
||
if (startTimeInput && endTimeInput) {
|
||
// 获取本地时间
|
||
const localStartTime = new Date(startTimeInput.value);
|
||
const localEndTime = new Date(endTimeInput.value);
|
||
|
||
// 转换为ISO字符串(UTC时间)
|
||
state.customStartTime = localStartTime.toISOString();
|
||
state.customEndTime = localEndTime.toISOString();
|
||
// 清空当前时间范围状态,只使用自定义时间
|
||
state.currentTimeRange = '';
|
||
// 重新加载数据
|
||
loadMetrics();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化自定义时间输入框
|
||
const now = new Date();
|
||
const startTimeInput = document.getElementById('customStartTime');
|
||
const endTimeInput = document.getElementById('customEndTime');
|
||
if (startTimeInput && endTimeInput) {
|
||
// 默认显示过去24小时
|
||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||
|
||
// 显示本地时间格式,YYYY-MM-DDTHH:MM
|
||
startTimeInput.value = twentyFourHoursAgo.toISOString().slice(0, 16);
|
||
endTimeInput.value = now.toISOString().slice(0, 16);
|
||
}
|
||
|
||
// 缩放控件事件处理
|
||
const zoomOutBtn = document.getElementById('zoomOutBtn');
|
||
const zoomInBtn = document.getElementById('zoomInBtn');
|
||
const resetZoomBtn = document.getElementById('resetZoomBtn');
|
||
const currentTimeRangeDisplay = document.getElementById('currentTimeRangeDisplay');
|
||
|
||
if (zoomOutBtn && zoomInBtn && currentTimeRangeDisplay) {
|
||
// 时间范围选项列表,用于缩放
|
||
const timeRanges = ['30m', '1h', '2h', '6h', '12h', '24h'];
|
||
}
|
||
|
||
// 网卡选择和刷新按钮事件
|
||
const 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 = () => {
|
||
switch(state.currentTimeRange) {
|
||
case '30m':
|
||
currentTimeRangeDisplay.textContent = '过去30分钟';
|
||
break;
|
||
case '1h':
|
||
currentTimeRangeDisplay.textContent = '过去1小时';
|
||
break;
|
||
case '2h':
|
||
currentTimeRangeDisplay.textContent = '过去2小时';
|
||
break;
|
||
case '6h':
|
||
currentTimeRangeDisplay.textContent = '过去6小时';
|
||
break;
|
||
case '12h':
|
||
currentTimeRangeDisplay.textContent = '过去12小时';
|
||
break;
|
||
case '24h':
|
||
currentTimeRangeDisplay.textContent = '过去24小时';
|
||
break;
|
||
default:
|
||
currentTimeRangeDisplay.textContent = '自定义时间范围';
|
||
}
|
||
};
|
||
|
||
// 初始化显示
|
||
updateTimeRangeDisplay();
|
||
|
||
// 放大事件
|
||
zoomInBtn.addEventListener('click', () => {
|
||
// 只在使用预设时间范围时生效
|
||
if (state.customStartTime && state.customEndTime) {
|
||
// 使用自定义时间时,先清除自定义时间
|
||
state.customStartTime = '';
|
||
state.customEndTime = '';
|
||
state.currentTimeRange = '1h';
|
||
} else {
|
||
// 查找当前时间范围在列表中的索引
|
||
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
|
||
if (currentIndex > 0) {
|
||
// 放大:使用更小的时间范围
|
||
state.currentTimeRange = timeRanges[currentIndex - 1];
|
||
}
|
||
}
|
||
|
||
// 更新显示
|
||
updateTimeRangeDisplay();
|
||
// 重新加载数据
|
||
loadMetrics();
|
||
});
|
||
|
||
// 缩小事件
|
||
zoomOutBtn.addEventListener('click', () => {
|
||
// 只在使用预设时间范围时生效
|
||
if (state.customStartTime && state.customEndTime) {
|
||
// 使用自定义时间时,先清除自定义时间
|
||
state.customStartTime = '';
|
||
state.customEndTime = '';
|
||
state.currentTimeRange = '1h';
|
||
} else {
|
||
// 查找当前时间范围在列表中的索引
|
||
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
|
||
if (currentIndex < timeRanges.length - 1) {
|
||
// 缩小:使用更大的时间范围
|
||
state.currentTimeRange = timeRanges[currentIndex + 1];
|
||
}
|
||
}
|
||
|
||
// 更新显示
|
||
updateTimeRangeDisplay();
|
||
// 重新加载数据
|
||
loadMetrics();
|
||
});
|
||
|
||
|
||
// 重置缩放按钮事件处理
|
||
if (resetZoomBtn) {
|
||
resetZoomBtn.addEventListener('click', () => {
|
||
// 重置所有图表的缩放
|
||
Object.values(charts).forEach(chart => {
|
||
if (chart && typeof chart.resetZoom === 'function') {
|
||
chart.resetZoom();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// 工具函数
|
||
function showContent(contentId) {
|
||
const element = document.getElementById(contentId);
|
||
if (element) {
|
||
element.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
// 跳转到服务器监控详情页面
|
||
function goToServerMonitor(deviceId) {
|
||
state.currentDeviceID = deviceId;
|
||
window.location.hash = `#serverMonitor/${deviceId}`;
|
||
// 加载选中设备的监控数据
|
||
setTimeout(() => {
|
||
loadMetrics();
|
||
}, 100);
|
||
}
|
||
|
||
|
||
// 初始化图表选项卡
|
||
function initChartTabs() {
|
||
const tabs = document.querySelectorAll('.chart-tab');
|
||
if (tabs.length === 0) return;
|
||
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
// 移除所有选项卡的激活状态
|
||
tabs.forEach(t => {
|
||
t.classList.remove('active', 'text-blue-600', 'border-blue-600');
|
||
t.classList.add('text-gray-600', 'border-transparent');
|
||
});
|
||
|
||
// 添加当前选项卡的激活状态
|
||
tab.classList.add('active', 'text-blue-600', 'border-blue-600');
|
||
tab.classList.remove('text-gray-600', 'border-transparent');
|
||
|
||
// 隐藏所有图表容器
|
||
const tabId = tab.dataset.tab;
|
||
const chartContainers = document.querySelectorAll('.chart-container');
|
||
chartContainers.forEach(container => {
|
||
container.classList.add('hidden');
|
||
});
|
||
|
||
// 显示当前选中的图表容器
|
||
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
|
||
if (activeContainer) {
|
||
activeContainer.classList.remove('hidden');
|
||
}
|
||
|
||
// 显示/隐藏网卡选择下拉框
|
||
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 && interfaceContainer) {
|
||
const tabId = activeTab.dataset.tab;
|
||
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();
|
||
});
|
||
|
||
// 处理hash变化
|
||
function handleHashChange() {
|
||
const hash = window.location.hash;
|
||
if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
|
||
// 延迟一下,确保DOM已经渲染完成
|
||
setTimeout(() => {
|
||
loadMetrics();
|
||
}, 300);
|
||
}
|
||
}
|