Files
monitor/backend/static/js/app.js
2025-12-03 12:11:20 +08:00

2357 lines
84 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API 基础 URL
const API_BASE_URL = '/api';
// 全局状态
let state = {
currentTimeRange: '1h', // 与UI默认值保持一致
customStartTime: '',
customEndTime: '',
currentInterval: '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);
}
}