// API 基础 URL
// 自动检测当前页面的协议和域名,构建完整的API URL
const API_BASE_URL = `${window.location.protocol}//${window.location.host}/api`;
// 带超时的fetch请求封装
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
// 通用的数据验证和转换函数
function validateAndTransformData(data, expectedType, defaultValue = null) {
// 检查数据类型是否匹配
if (typeof data === expectedType) {
return data;
}
// 特殊处理数组类型
if (expectedType === 'array' && Array.isArray(data)) {
return data;
}
// 特殊处理对象类型
if (expectedType === 'object' && typeof data === 'object' && data !== null) {
return data;
}
// 类型转换逻辑
switch (expectedType) {
case 'string':
// 将非字符串类型转换为字符串,布尔值转换为默认值
return typeof data === 'boolean' ? defaultValue : String(data);
case 'number':
// 将非数字类型转换为数字,布尔值转换为0或1
const num = typeof data === 'boolean' ? (data ? 1 : 0) : Number(data);
return isNaN(num) ? defaultValue : num;
case 'boolean':
// 将非布尔值转换为布尔值
return Boolean(data);
case 'array':
// 将非数组转换为默认值
return defaultValue;
case 'object':
// 将非对象转换为默认值
return defaultValue;
default:
// 未知类型,返回默认值
return defaultValue;
}
}
// 全局状态
let state = {
currentTimeRange: '1h', // 与UI默认值保持一致
customStartTime: '',
customEndTime: '',
currentInterval: '3m', // 固定10分钟区间
currentDeviceID: '', // 当前选中的服务器ID
historyMetrics: {}, // 存储历史指标数据
autoRefreshEnabled: false, // 自动刷新开关状态,默认关闭
lastMetricsUpdate: null, // 记录上次指标更新时间
lastChartUpdate: 0, // 记录上次图表更新时间
chartUpdateThrottle: 1000, // 图表更新节流时间(毫秒)
pendingMetricsUpdate: null, // 待处理的指标更新
pendingChartUpdate: null, // 待处理的图表更新
isUpdatingDOM: false, // 标记是否正在更新DOM
// 自动刷新相关配置
autoRefreshInterval: 30000, // 默认自动刷新间隔(毫秒)
minAutoRefreshInterval: 5000, // 最小自动刷新间隔(毫秒)
maxAutoRefreshInterval: 60000, // 最大自动刷新间隔(毫秒)
autoRefreshTimer: null, // 自动刷新定时器
lastAutoRefreshTime: 0 // 上次自动刷新时间
}
// WebSocket连接
let ws = null;
let wsReconnectAttempts = 0;
const wsMaxReconnectAttempts = 10; // 增加最大重连次数
let wsReconnectDelay = 1000;
const wsMaxReconnectDelay = 30000; // 添加最大重连延迟(30秒)
let wsReconnectTimeout = null; // 重连定时器
// 图表实例
const charts = {};
// 初始化应用
function initApp() {
initCustomTimeRange();
bindEvents();
initPageSwitch();
loadHomeData();
initCharts();
initWebSocket();
// 初始化自动刷新机制
setupAutoRefresh();
// 设置服务器数量定时刷新
setInterval(loadServerCount, 30000);
// 初始化网卡列表
loadNetworkInterfaces();
}
// 设置自动刷新机制
function setupAutoRefresh() {
// 清除现有的定时器
if (state.autoRefreshTimer) {
clearInterval(state.autoRefreshTimer);
state.autoRefreshTimer = null;
}
// 如果启用了自动刷新,设置新的定时器
if (state.autoRefreshEnabled) {
// 立即执行一次,然后开始定时执行
loadStatusCards();
// 设置定时器
state.autoRefreshTimer = setInterval(() => {
loadStatusCards();
state.lastAutoRefreshTime = Date.now();
// 动态调整刷新间隔
updateAutoRefreshInterval();
}, state.autoRefreshInterval);
}
}
// 动态调整自动刷新间隔
function updateAutoRefreshInterval() {
// 这里可以根据实际情况调整刷新间隔
// 例如,根据系统负载、网络延迟或数据变化频率来动态调整
// 简单实现:如果最近数据变化频繁,缩短刷新间隔;否则延长刷新间隔
const now = Date.now();
if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) < state.autoRefreshInterval / 2) {
// 数据变化频繁,缩短刷新间隔,但不低于最小值
state.autoRefreshInterval = Math.max(
state.autoRefreshInterval - 2000,
state.minAutoRefreshInterval
);
} else if (state.lastMetricsUpdate && (now - state.lastMetricsUpdate) > state.autoRefreshInterval * 2) {
// 数据变化缓慢,延长刷新间隔,但不高于最大值
state.autoRefreshInterval = Math.min(
state.autoRefreshInterval + 5000,
state.maxAutoRefreshInterval
);
}
// 更新定时器
setupAutoRefresh();
}
// 初始化自定义时间范围
function initCustomTimeRange() {
const now = new Date();
// 默认显示过去1小时
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000);
// 直接使用ISO字符串,包含完整的时区信息
state.customStartTime = oneHourAgo.toISOString();
state.customEndTime = now.toISOString();
// 更新日期选择器输入框的值
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 将ISO字符串转换为datetime-local格式(YYYY-MM-DDTHH:MM)
startTimeInput.value = oneHourAgo.toISOString().slice(0, 16);
endTimeInput.value = now.toISOString().slice(0, 16);
}
}
// 页面切换
function initPageSwitch() {
window.addEventListener('hashchange', switchPage);
switchPage();
}
function switchPage() {
const hash = window.location.hash;
// 隐藏所有内容
hideAllContent();
// 显示对应内容
if (hash === '#servers') {
showContent('serversContent');
loadAllServers();
// 清除当前设备ID,避免在服务器列表页显示特定服务器的数据
state.currentDeviceID = '';
} else if (hash === '#devices') {
showContent('devicesContent');
loadDeviceManagementList();
// 清除当前设备ID
state.currentDeviceID = '';
} else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
showContent('serverMonitorContent');
// 提取设备ID
let deviceId = '';
if (hash.startsWith('#serverMonitor/')) {
deviceId = hash.split('/')[1];
// 检查deviceId是否为'undefined'字符串
if (deviceId === 'undefined') {
deviceId = '';
}
}
// 直接设置当前设备ID,确保loadMetrics能使用正确的设备ID
state.currentDeviceID = deviceId;
// 加载服务器信息
if (deviceId) {
loadServerInfo(deviceId).then(() => {
// 确保服务器信息加载完成后再加载指标数据
loadMetrics();
});
} else {
// 如果没有设备ID,显示错误信息
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `
无效的服务器ID
`;
}
}
} else if (hash === '#alarms') {
showContent('alarmsContent');
loadAlarmsList();
// 清除当前设备ID
state.currentDeviceID = '';
} else if (hash === '#alarm-info' || hash.startsWith('#alarm-info/')) {
showContent('alarm-infoContent');
// 清除当前设备ID
state.currentDeviceID = '';
// 提取告警ID并加载详情
let alarmId = '';
if (hash.startsWith('#alarm-info/')) {
alarmId = hash.split('/')[1];
}
loadAlarmDetails(alarmId);
} else {
showContent('homeContent');
loadHomeData();
// 清除当前设备ID
state.currentDeviceID = '';
}
}
function hideAllContent() {
document.getElementById('homeContent').classList.add('hidden');
document.getElementById('serversContent').classList.add('hidden');
document.getElementById('serverMonitorContent').classList.add('hidden');
document.getElementById('devicesContent').classList.add('hidden');
document.getElementById('alarmsContent').classList.add('hidden');
document.getElementById('alarm-infoContent').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() {
// 显示加载状态
const tableBody = document.getElementById('businessViewTableBody');
if (tableBody) {
tableBody.innerHTML = `
|
加载中...
|
`;
}
try {
const response = await fetchWithTimeout(`${API_BASE_URL}/devices/`, {}, 3000);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const devices = Array.isArray(data.devices) ? data.devices : [];
// 添加重试机制的fetch请求
const fetchWithRetry = async (url, options = {}, timeout = 5000, maxRetries = 1) => {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fetchWithTimeout(url, options, timeout);
} catch (error) {
if (i === maxRetries) {
// 最后一次重试失败,抛出错误
throw error;
}
// 重试之间的延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
}
};
// 为每个设备获取完整信息,使用并行API调用优化加载速度
const devicesWithDetails = await Promise.allSettled(devices.map(async (device) => {
let ip = '未知';
let os = '未知';
// 获取设备详情,包括IP地址(最多重试1次)
try {
const deviceDetailResponse = await fetchWithRetry(`${API_BASE_URL}/devices/${device.id}`, {}, 3000, 1);
if (deviceDetailResponse.ok) {
const deviceDetailData = await deviceDetailResponse.json();
if (deviceDetailData && deviceDetailData.device) {
ip = deviceDetailData.device.ip || '未知';
}
}
} catch (error) {
console.error(`获取设备 ${device.id} IP地址失败:`, error);
}
// 获取硬件信息,包括操作系统(最多重试1次)
try {
const hardwareResponse = await fetchWithRetry(`${API_BASE_URL}/metrics/hardware?device_id=${device.id}`, {}, 8000, 1);
if (hardwareResponse.ok) {
const hardwareData = await hardwareResponse.json();
if (hardwareData && hardwareData.hardware && hardwareData.hardware.os) {
// 确保操作系统信息始终是字符串类型
const fullname = hardwareData.hardware.os.fullname;
os = typeof fullname === 'string' ? fullname : '未知';
}
}
} catch (error) {
console.error(`获取设备 ${device.id} 操作系统失败:`, error);
}
return {
...device,
ip: ip,
os: os
};
}));
// 处理Promise.allSettled的结果,只保留成功的结果
const successfulDevices = devicesWithDetails
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
renderBusinessView(successfulDevices);
} catch (error) {
console.error('加载业务视图数据失败:', error);
// 显示错误信息
if (tableBody) {
tableBody.innerHTML = `
|
加载业务视图数据失败
${error.message}
|
`;
}
}
}
// 渲染业务视图
function renderBusinessView(devices) {
const tableBody = document.getElementById('businessViewTableBody');
if (!tableBody) return;
tableBody.innerHTML = '';
// 确保devices是数组类型
const deviceList = Array.isArray(devices) ? devices : [];
// 处理空数组情况
if (deviceList.length === 0) {
tableBody.innerHTML = `
| 暂无设备数据 |
`;
return;
}
deviceList.forEach(device => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors cursor-pointer';
row.innerHTML = `
${device.name || '未知设备'} |
${device.ip || '未知'} |
${device.os || '未知'} |
正常
|
`;
// 为表格行添加点击事件
row.addEventListener('click', () => {
if (device.id) {
goToServerMonitor(device.id);
}
});
tableBody.appendChild(row);
});
}
// 加载告警列表数据
async function loadAlarmListData() {
try {
const response = await fetch(`${API_BASE_URL}/alarms`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const alarmData = data.alarms || [];
renderAlarmList(alarmData);
} catch (error) {
console.error('加载告警列表失败:', error);
// 只显示错误信息,不使用模拟数据
const alarmList = document.getElementById('alarmList');
if (alarmList) {
alarmList.innerHTML = `
加载告警列表失败
`;
}
}
}
// 渲染告警列表
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)} cursor-pointer hover:shadow-md transition-all`;
alarmItem.innerHTML = `
${alarm.message}
${alarm.device}
${alarm.time}
`;
// 添加点击事件,跳转到告警详情页面
alarmItem.addEventListener('click', () => {
window.location.hash = `#alarm-info/${alarm.id}`;
});
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';
}
}
// 获取告警级别文本
function getAlarmLevelText(level) {
switch(level) {
case 'error': return '错误';
case 'warning': return '警告';
case 'info': return '信息';
default: return '未知';
}
}
// 获取告警级别样式类
function getAlarmLevelClass(level) {
switch(level) {
case 'error': return 'bg-red-100 text-red-800';
case 'warning': return 'bg-yellow-100 text-yellow-800';
case 'info': return 'bg-blue-100 text-blue-800';
default: return 'bg-gray-100 text-gray-800';
}
}
// 告警管理相关功能
let alarmsPagination = {
currentPage: 1,
itemsPerPage: 10,
totalItems: 0,
totalPages: 0,
allAlarms: [],
filteredAlarms: []
};
// 加载告警列表
async function loadAlarmsList(page = 1) {
try {
const response = await fetch(`${API_BASE_URL}/alarms`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const alarms = data.alarms || [];
// 更新告警总数
const alarmCount = document.getElementById('alarmCount');
if (alarmCount) {
alarmCount.textContent = alarms.length;
}
// 更新分页状态
alarmsPagination.allAlarms = alarms;
alarmsPagination.filteredAlarms = [...alarms];
alarmsPagination.totalItems = alarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = page;
// 渲染告警表格
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
(alarmsPagination.currentPage - 1) * alarmsPagination.itemsPerPage,
alarmsPagination.currentPage * alarmsPagination.itemsPerPage
));
// 创建分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
} catch (error) {
console.error('加载告警列表失败:', error);
// 由于后端尚未实现,使用模拟数据
const mockAlarms = [
{
"id": "alarm-1",
"time": new Date(Date.now() - 3600000).toISOString(),
"device": "服务器A",
"level": "error",
"message": "CPU使用率超过90%",
"status": "unacknowledged"
},
{
"id": "alarm-2",
"time": new Date(Date.now() - 7200000).toISOString(),
"device": "服务器B",
"level": "warning",
"message": "内存使用率接近80%",
"status": "unacknowledged"
},
{
"id": "alarm-3",
"time": new Date(Date.now() - 10800000).toISOString(),
"device": "服务器C",
"level": "info",
"message": "硬盘空间充足",
"status": "acknowledged"
},
{
"id": "alarm-4",
"time": new Date(Date.now() - 14400000).toISOString(),
"device": "服务器D",
"level": "error",
"message": "网络连接中断",
"status": "unacknowledged"
},
{
"id": "alarm-5",
"time": new Date(Date.now() - 18000000).toISOString(),
"device": "服务器E",
"level": "warning",
"message": "CPU温度过高",
"status": "acknowledged"
}
];
// 更新告警总数
const alarmCount = document.getElementById('alarmCount');
if (alarmCount) {
alarmCount.textContent = mockAlarms.length;
}
// 更新分页状态
alarmsPagination.allAlarms = mockAlarms;
alarmsPagination.filteredAlarms = [...mockAlarms];
alarmsPagination.totalItems = mockAlarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = page;
// 渲染告警表格
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
(alarmsPagination.currentPage - 1) * alarmsPagination.itemsPerPage,
alarmsPagination.currentPage * alarmsPagination.itemsPerPage
));
// 创建分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
// 显示提示信息
showToast('使用模拟数据显示告警列表', 'info');
}
}
// 渲染告警表格
function renderAlarmsTable(alarms) {
const alarmsTableBody = document.getElementById('alarmsTableBody');
if (!alarmsTableBody) return;
alarmsTableBody.innerHTML = '';
if (alarms.length === 0) {
alarmsTableBody.innerHTML = `
|
暂无告警数据
|
`;
return;
}
alarms.forEach(alarm => {
const alarmRow = document.createElement('tr');
alarmRow.className = 'hover:bg-gray-50 transition-colors cursor-pointer';
// 告警级别样式
const levelClass = getAlarmLevelClass(alarm.level);
alarmRow.innerHTML = `
${alarm.time} |
${alarm.device || '未知设备'} |
${getAlarmLevelText(alarm.level)}
|
${alarm.message} |
${alarm.status || '未确认'}
|
|
`;
// 添加点击事件,跳转到告警详情页面
alarmRow.addEventListener('click', (e) => {
// 如果点击的是按钮,不执行跳转
if (e.target.closest('button')) {
return;
}
window.location.hash = `#alarm-info/${alarm.id}`;
});
alarmsTableBody.appendChild(alarmRow);
});
}
// 确认告警
function acknowledgeAlarm(alarmId) {
// 由于后端尚未实现,仅显示提示信息
showToast('告警确认功能需要后端支持', 'warning');
}
// 清除告警
function clearAlarm(alarmId) {
// 由于后端尚未实现,仅显示提示信息
showToast('告警清除功能需要后端支持', 'warning');
}
// 应用告警过滤条件
function applyAlarmFilters() {
const searchKeyword = document.getElementById('alarmSearch').value.toLowerCase();
const levelFilter = document.getElementById('alarmLevelFilter').value;
const statusFilter = document.getElementById('alarmStatusFilter').value;
// 过滤告警列表
alarmsPagination.filteredAlarms = alarmsPagination.allAlarms.filter(alarm => {
const matchesKeyword = !searchKeyword || alarm.message.toLowerCase().includes(searchKeyword) ||
(alarm.device && alarm.device.toLowerCase().includes(searchKeyword));
const matchesLevel = !levelFilter || alarm.level === levelFilter;
const matchesStatus = !statusFilter || alarm.status === statusFilter;
return matchesKeyword && matchesLevel && matchesStatus;
});
// 更新分页状态
alarmsPagination.totalItems = alarmsPagination.filteredAlarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = 1;
// 重新渲染表格
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
(alarmsPagination.currentPage - 1) * alarmsPagination.itemsPerPage,
alarmsPagination.currentPage * alarmsPagination.itemsPerPage
));
// 更新分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
}
// 清除告警过滤条件
function clearAlarmFilters() {
document.getElementById('alarmSearch').value = '';
document.getElementById('alarmLevelFilter').value = '';
document.getElementById('alarmStatusFilter').value = '';
// 重置过滤后的告警列表
alarmsPagination.filteredAlarms = [...alarmsPagination.allAlarms];
// 更新分页状态
alarmsPagination.totalItems = alarmsPagination.filteredAlarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = 1;
// 重新渲染表格
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
(alarmsPagination.currentPage - 1) * alarmsPagination.itemsPerPage,
alarmsPagination.currentPage * alarmsPagination.itemsPerPage
));
// 更新分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
}
// 确认所有告警
function acknowledgeAllAlarms() {
// 由于后端尚未实现,仅显示提示信息
showToast('确认所有告警功能需要后端支持', 'warning');
}
// 清除所有告警
function clearAllAlarms() {
// 由于后端尚未实现,仅显示提示信息
showToast('清除所有告警功能需要后端支持', 'warning');
}
// 获取告警级别样式类
function getAlarmLevelClass(level) {
switch(level) {
case 'info': return 'bg-blue-100 text-blue-800';
case 'warning': return 'bg-yellow-100 text-yellow-800';
case 'error': return 'bg-red-100 text-red-800';
case 'critical': return 'bg-red-900 text-white';
default: return 'bg-gray-100 text-gray-800';
}
}
// 获取告警级别中文文本
function getAlarmLevelText(level) {
switch(level) {
case 'info': return '信息';
case 'warning': return '警告';
case 'error': return '错误';
case 'critical': return '严重';
default: return '未知';
}
}
// 确认告警
async function acknowledgeAlarm(alarmId) {
try {
// 这里应该调用API来确认告警
console.log('确认告警:', alarmId);
showToast('告警已确认', 'success');
// 重新加载告警列表
loadAlarmsList(alarmsPagination.currentPage);
} catch (error) {
console.error('确认告警失败:', error);
showToast('确认告警失败', 'error');
}
}
// 清除告警
async function clearAlarm(alarmId) {
try {
// 这里应该调用API来清除告警
console.log('清除告警:', alarmId);
showToast('告警已清除', 'success');
// 重新加载告警列表
loadAlarmsList(alarmsPagination.currentPage);
} catch (error) {
console.error('清除告警失败:', error);
showToast('清除告警失败', 'error');
}
}
// 确认所有告警
async function acknowledgeAllAlarms() {
try {
// 这里应该调用API来确认所有告警
console.log('确认所有告警');
showToast('所有告警已确认', 'success');
// 重新加载告警列表
loadAlarmsList(alarmsPagination.currentPage);
} catch (error) {
console.error('确认所有告警失败:', error);
showToast('确认所有告警失败', 'error');
}
}
// 清除所有告警
async function clearAllAlarms() {
try {
// 这里应该调用API来清除所有告警
console.log('清除所有告警');
showToast('所有告警已清除', 'success');
// 重新加载告警列表
loadAlarmsList(alarmsPagination.currentPage);
} catch (error) {
console.error('清除所有告警失败:', error);
showToast('清除所有告警失败', 'error');
}
}
// 应用告警过滤
function applyAlarmFilters() {
const searchInput = document.getElementById('alarmSearchInput');
const levelFilter = document.getElementById('alarmLevelFilter');
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
const level = levelFilter ? levelFilter.value : '';
// 过滤告警
alarmsPagination.filteredAlarms = alarmsPagination.allAlarms.filter(alarm => {
const matchesSearch = !searchTerm ||
(alarm.message && alarm.message.toLowerCase().includes(searchTerm)) ||
(alarm.device && alarm.device.toLowerCase().includes(searchTerm));
const matchesLevel = !level || alarm.level === level;
return matchesSearch && matchesLevel;
});
// 更新分页信息
alarmsPagination.totalItems = alarmsPagination.filteredAlarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = 1;
// 重新渲染
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
0, alarmsPagination.itemsPerPage
));
// 更新分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
}
// 清除告警过滤
function clearAlarmFilters() {
const searchInput = document.getElementById('alarmSearchInput');
const levelFilter = document.getElementById('alarmLevelFilter');
if (searchInput) {
searchInput.value = '';
}
if (levelFilter) {
levelFilter.value = '';
}
// 重置过滤
alarmsPagination.filteredAlarms = [...alarmsPagination.allAlarms];
alarmsPagination.totalItems = alarmsPagination.filteredAlarms.length;
alarmsPagination.totalPages = Math.ceil(alarmsPagination.totalItems / alarmsPagination.itemsPerPage);
alarmsPagination.currentPage = 1;
// 重新渲染
renderAlarmsTable(alarmsPagination.filteredAlarms.slice(
0, alarmsPagination.itemsPerPage
));
// 更新分页控件
createPaginationControls(
document.getElementById('alarmsPagination'),
alarmsPagination,
loadAlarmsList
);
}
// 加载服务器数量
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';
}
}
}
// 告警详情模拟数据
let alertDetailsData = {
"id": "ALARM-20240520-0001",
"time": "2024-05-20 14:30:45",
"device": "服务器-01",
"level": "critical",
"message": "CPU使用率超过阈值",
"description": "服务器-01的CPU使用率持续5分钟超过90%,当前使用率为95%。请立即检查系统负载和运行中的进程。",
"status": "未处理",
"handler": "",
"handleTime": "",
"handleResult": ""
};
// 加载告警详情
function loadAlarmDetails(alarmId) {
// 获取告警详情数据(这里使用模拟数据,实际项目中应从API获取)
let alarmDetails = alertDetailsData;
// 如果提供了告警ID,尝试从告警列表中找到对应的告警
if (alarmId && alarmsPagination.allAlarms.length > 0) {
const foundAlarm = alarmsPagination.allAlarms.find(a => a.id === alarmId);
if (foundAlarm) {
// 将找到的告警转换为详情格式
alarmDetails = {
id: foundAlarm.id,
time: foundAlarm.time,
device: foundAlarm.device,
level: foundAlarm.level,
message: foundAlarm.message,
description: `详细描述: ${foundAlarm.message}`,
status: foundAlarm.status,
handler: "",
handleTime: "",
handleResult: ""
};
}
}
// 渲染告警详情
document.getElementById('alarmId').textContent = alarmDetails.id || '未设置';
document.getElementById('alarmTime').textContent = alarmDetails.time || '未设置';
document.getElementById('alarmDevice').textContent = alarmDetails.device || '未设置';
document.getElementById('alarmLevel').textContent = getAlarmLevelText(alarmDetails.level) || '未设置';
document.getElementById('alarmStatus').textContent = alarmDetails.status || '未处理';
document.getElementById('alarmMessage').textContent = alarmDetails.message || '无';
document.getElementById('alarmDescription').textContent = alarmDetails.description || '无';
document.getElementById('alarmHandler').textContent = alarmDetails.handler || '未处理';
document.getElementById('alarmHandleTime').textContent = alarmDetails.handleTime || '未处理';
document.getElementById('alarmHandleResult').textContent = alarmDetails.handleResult || '未处理';
// 添加返回按钮事件
const backToAlarmListBtn = document.getElementById('backToAlarmList');
if (backToAlarmListBtn) {
// 先移除可能存在的事件监听器
backToAlarmListBtn.removeEventListener('click', backToAlarmListHandler);
// 添加新的事件监听器
backToAlarmListBtn.addEventListener('click', backToAlarmListHandler);
}
// 添加确认和清除按钮事件
const acknowledgeBtn = document.getElementById('alarmAcknowledgeBtn');
if (acknowledgeBtn) {
acknowledgeBtn.removeEventListener('click', acknowledgeAlarmHandler);
acknowledgeBtn.addEventListener('click', acknowledgeAlarmHandler.bind(null, alarmDetails.id));
}
const clearBtn = document.getElementById('alarmClearBtn');
if (clearBtn) {
clearBtn.removeEventListener('click', clearAlarmHandler);
clearBtn.addEventListener('click', clearAlarmHandler.bind(null, alarmDetails.id));
}
}
// 返回告警列表的事件处理函数
function backToAlarmListHandler() {
window.location.hash = '#alarms';
}
// 确认告警的事件处理函数
async function acknowledgeAlarmHandler(alarmId) {
try {
// 这里应该调用API来确认告警
console.log('确认告警:', alarmId);
showToast('告警已确认', 'success');
// 返回告警列表页面
window.location.hash = '#alarms';
} catch (error) {
console.error('确认告警失败:', error);
showToast('确认告警失败', 'error');
}
}
// 清除告警的事件处理函数
async function clearAlarmHandler(alarmId) {
try {
// 这里应该调用API来清除告警
console.log('清除告警:', alarmId);
showToast('告警已清除', 'success');
// 返回告警列表页面
window.location.hash = '#alarms';
} catch (error) {
console.error('清除告警失败:', error);
showToast('清除告警失败', 'error');
}
}
// 加载所有服务器
async function loadAllServers() {
try {
// 调用设备状态API获取详细信息
const response = await fetch(`${API_BASE_URL}/devices/status`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const devices = data.devices || [];
renderServersGrid(devices);
} catch (error) {
console.error('加载服务器列表失败:', error);
// 只显示错误信息,不使用模拟数据
const serversGrid = document.getElementById('serversGrid');
if (serversGrid) {
serversGrid.innerHTML = `
`;
}
}
}
// 渲染服务器网格
function renderServersGrid(devices) {
const serversGrid = document.getElementById('serversGrid');
if (!serversGrid) return;
serversGrid.innerHTML = '';
devices.forEach(device => {
const serverCard = createServerCard(device);
serversGrid.appendChild(serverCard);
});
}
// 跳转到服务器监控界面
function goToServerMonitor(deviceId) {
// 确保deviceId有效
if (!deviceId) {
console.error('Invalid device ID');
return;
}
// 设置URL hash
window.location.hash = `#serverMonitor/${deviceId}`;
// 手动调用switchPage来立即更新页面
switchPage();
}
// 创建服务器卡片
function createServerCard(device) {
const card = document.createElement('div');
card.className = 'bg-white rounded-xl shadow-md p-6 card-hover border border-gray-100 cursor-pointer';
// 为卡片添加点击事件
card.addEventListener('click', () => {
// 确保使用正确的设备ID字段
const deviceId = device.device_id || device.id || device.ID;
goToServerMonitor(deviceId);
});
// 获取设备状态数据
const status = device.status || {};
const cpuUsage = status.cpu !== undefined ? status.cpu.toFixed(1) : '0.0';
const memoryUsage = status.memory !== undefined ? status.memory.toFixed(1) : '0.0';
const diskUsage = status.disk !== undefined ? status.disk.toFixed(1) : '0.0';
const networkTraffic = status.network !== undefined ? status.network.toFixed(1) : '0.0';
card.innerHTML = `
${device.name || device.device_id || device.id}
${device.ip || 'N/A'}
在线
CPU使用率
${cpuUsage}%
磁盘使用率
${diskUsage}%
内存使用率
${memoryUsage}%
网络流量
${networkTraffic} MB/s
`;
// 为"查看详情"按钮添加点击事件
const viewDetailBtn = card.querySelector('button');
viewDetailBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
// 确保使用正确的设备ID字段
const deviceId = device.device_id || device.id || device.ID;
goToServerMonitor(deviceId);
});
return card;
}
// 存储原始设备列表,用于搜索和筛选
let originalDeviceList = [];
// 设备管理
async function loadDeviceManagementList() {
try {
const response = await fetch(`${API_BASE_URL}/devices/all`);
if (!response.ok) {
throw new Error('Failed to fetch devices');
}
const data = await response.json();
const devices = data.devices || [];
if (devices.length === 0) {
console.warn('No devices available');
originalDeviceList = [];
renderDeviceManagementList([]);
return;
}
// 处理设备数据,转换为所需格式
const processedDevices = devices.map(device => ({
id: device.id,
name: device.name || device.id,
ip: device.ip || 'N/A',
status: device.status || 'unknown',
created_at: device.created_at ? new Date(device.created_at * 1000).toLocaleString() : 'N/A',
token: device.token || 'N/A'
}));
// 存储原始设备列表
originalDeviceList = processedDevices;
// 应用当前筛选条件
applyDeviceFilters();
} catch (error) {
console.error('加载设备管理列表失败:', error);
// 只显示错误信息,不使用模拟数据
const deviceListBody = document.getElementById('deviceListBody');
if (deviceListBody) {
deviceListBody.innerHTML = `
| 加载设备列表失败 |
`;
}
}
}
// 应用设备筛选条件
function applyDeviceFilters() {
const searchTerm = document.getElementById('deviceSearch').value.toLowerCase();
const statusFilter = document.getElementById('deviceStatusFilter').value;
// 复制原始列表
let filteredDevices = [...originalDeviceList];
// 应用搜索筛选
if (searchTerm) {
filteredDevices = filteredDevices.filter(device =>
device.name.toLowerCase().includes(searchTerm) ||
device.id.toLowerCase().includes(searchTerm) ||
device.ip.toLowerCase().includes(searchTerm)
);
}
// 应用状态筛选
if (statusFilter && statusFilter !== 'all') {
filteredDevices = filteredDevices.filter(device => device.status === statusFilter);
}
// 渲染筛选后的设备列表
renderDeviceManagementList(filteredDevices);
// 显示或隐藏无数据提示
const noDataMessage = document.getElementById('noDataMessage');
if (noDataMessage) {
if (filteredDevices.length === 0) {
noDataMessage.classList.remove('hidden');
} else {
noDataMessage.classList.add('hidden');
}
}
}
// 清除筛选条件
function clearDeviceFilters() {
document.getElementById('deviceSearch').value = '';
document.getElementById('deviceStatusFilter').value = 'all';
applyDeviceFilters();
}
// 复制文本到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// 可以添加一个提示,如"已复制"的临时消息
showToast('复制成功');
}).catch(err => {
console.error('复制失败:', err);
showToast('复制失败');
});
}
// 显示临时提示消息
function showToast(message, type = 'success') {
// 检查是否已存在toast元素
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
// 设置toast样式根据类型
let bgColor = 'bg-gray-800';
switch(type) {
case 'success':
bgColor = 'bg-green-800';
break;
case 'error':
bgColor = 'bg-red-800';
break;
case 'warning':
bgColor = 'bg-yellow-800';
break;
}
toast.className = `${bgColor} text-white px-4 py-2 rounded opacity-0 transition-opacity duration-300 z-50 fixed bottom-4 right-4`;
toast.textContent = message;
toast.style.opacity = '1';
// 2秒后自动隐藏
setTimeout(() => {
toast.style.opacity = '0';
}, 2000);
}
// 切换设备状态(激活/停用)
async function toggleDeviceStatus(deviceId, newStatus) {
try {
// 先获取设备详情
const deviceResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!deviceResponse.ok) {
throw new Error('Failed to fetch device details');
}
const deviceData = await deviceResponse.json();
const device = deviceData.device;
// 更新设备状态
const updateResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: newStatus,
name: device.name,
ip: device.ip
})
});
if (!updateResponse.ok) {
throw new Error(`HTTP error! status: ${updateResponse.status}`);
}
// 重新加载设备列表
await loadDeviceManagementList();
// 显示成功提示
const statusText = newStatus === 'active' ? '激活' : '停用';
showToast(`设备已成功${statusText}`, 'success');
} catch (error) {
console.error('Failed to toggle device status:', error);
const statusText = newStatus === 'active' ? '激活' : '停用';
showToast(`设备${statusText}失败`, 'error');
}
}
// 编辑设备
function editDevice(deviceId) {
// 这里可以实现编辑设备的逻辑,比如打开模态框等
console.log('编辑设备:', deviceId);
// 可以添加模态框或跳转到编辑页面的逻辑
}
// 删除设备
function deleteDevice(deviceId) {
if (confirm('确定要删除这个设备吗?')) {
fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'DELETE'
}).then(response => {
if (response.ok) {
showToast('设备删除成功');
loadDeviceManagementList(); // 重新加载设备列表
} else {
throw new Error('删除失败');
}
}).catch(err => {
console.error('删除设备失败:', err);
showToast('删除失败');
});
}
}
function renderDeviceManagementList(devices) {
const tableBody = document.getElementById('deviceManagementTableBody');
if (!tableBody) return;
tableBody.innerHTML = '';
devices.forEach(device => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors';
// 根据设备状态获取显示样式
const getStatusStyle = (status) => {
switch (status) {
case 'active':
return 'bg-green-100 text-green-800';
case 'inactive':
return 'bg-yellow-100 text-yellow-800';
case 'offline':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
row.innerHTML = `
${device.name} |
${device.id} |
${device.ip} |
${device.token}
|
${device.status}
|
${device.created_at} |
|
`;
tableBody.appendChild(row);
});
}
// 初始化图表
function initCharts() {
initAlarmTrendChart();
initDetailedCharts();
}
// 初始化详细监控图表
function initDetailedCharts() {
// 初始化CPU图表
const cpuCtx = document.getElementById('cpuChart');
if (cpuCtx && !charts.cpu) {
charts.cpu = new Chart(cpuCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU使用率 (%)',
data: [],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
// 启用缩放功能
interaction: {
intersect: false,
mode: 'index'
},
// 允许显示空白区域
spanGaps: true,
// 配置缩放和拖拽
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
}
}
});
}
// 初始化内存图表
const memoryCtx = document.getElementById('memoryChart');
if (memoryCtx && !charts.memory) {
charts.memory = new Chart(memoryCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '内存使用率 (%)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value}%`,
`实际值: ${value}%`
];
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化磁盘图表
const diskCtx = document.getElementById('diskChart');
if (diskCtx && !charts.disk) {
charts.disk = new Chart(diskCtx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value}%`,
`实际值: ${value}%`
];
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化网络流量图表(发送总和和接收总和)
const networkCtx = document.getElementById('networkChart');
if (networkCtx && !charts.network) {
charts.network = new Chart(networkCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '发送总和 (MB)',
data: [],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
},
{
label: '接收总和 (MB)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value} MB`,
`实际值: ${value} MB`
];
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + ' MB';
}
}
}
}
}
});
}
// 初始化网速趋势图表
const speedCtx = document.getElementById('speedChart');
if (speedCtx && !charts.speed) {
charts.speed = new Chart(speedCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '发送速率 (MB/s)',
data: [],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
},
{
label: '接收速率 (MB/s)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
// 第二行显示实际值
return [
`${label}: ${value} MB/s`,
`实际值: ${value} MB/s`
];
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + ' MB/s';
}
}
}
}
}
});
}
}
// 初始化告警趋势图
function initAlarmTrendChart() {
const ctx = document.getElementById('alarmTrendChart');
if (!ctx) return;
charts.alarmTrend = new Chart(ctx, {
type: 'line',
data: {
labels: ['11-24', '11-25', '11-26', '11-27', '11-28', '11-29', '11-30'],
datasets: [
{
label: '报警',
data: [50, 60, 75, 80, 90, 100, 112],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: '提醒',
data: [20, 25, 30, 35, 38, 38, 38],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 格式化字节数,根据用户要求:只在MB和GB之间转换
function formatBytes(bytes, decimals = 2, isRate = false) {
if (bytes === 0) {
return isRate ? '0 MB/s' : '0 MB';
}
const mb = 1024 * 1024;
const gb = mb * 1024;
const dm = decimals < 0 ? 0 : decimals;
// 对于速率(bytes/s):默认显示MB/s,超过1024MB/s显示GB/s
if (isRate) {
if (bytes >= gb) {
return parseFloat((bytes / gb).toFixed(dm)) + ' GB/s';
} else {
return parseFloat((bytes / mb).toFixed(dm)) + ' MB/s';
}
}
// 对于流量(bytes):默认显示MB,超过1024MB显示GB
else {
if (bytes >= gb) {
return parseFloat((bytes / gb).toFixed(dm)) + ' GB';
} else {
return parseFloat((bytes / mb).toFixed(dm)) + ' MB';
}
}
}
// 获取单个指标数据
async function fetchMetric(metricType, aggregation = 'average') {
// 构建查询参数
const params = new URLSearchParams();
// 设置设备ID参数(如果存在)
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
params.append('end_time', 'now()');
}
// 设置聚合方式
params.append('aggregation', aggregation);
// 固定时间区间为10分钟
params.append('interval', '10m');
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/${metricType}?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${metricType} metrics`);
}
const data = await response.json();
// 确保返回的数据是数组格式
if (metricType === 'disk') {
// 磁盘数据可能是对象格式,需要特殊处理
return data.data || {};
} else if (metricType === 'network') {
// 网络数据可能是对象格式,需要特殊处理
return data.data || {};
} else {
// 其他数据应该是数组格式
return Array.isArray(data.data) ? data.data : [];
}
}
// 格式化时间,统一格式避免图表误解
function formatTime(timeStr) {
// timeStr是ISO格式的UTC时间字符串,如"2025-12-02T01:53:19Z"
const date = new Date(timeStr);
// 格式化年、月、日(使用本地时间)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
// 格式化时、分、秒(使用本地时间)
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// 总是显示完整的日期和时间,方便区分不同日期的数据
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 加载状态卡片数据,专门负责更新指标卡片
async function loadStatusCards() {
try {
// 从设备状态API获取数据,用于状态卡片显示
let deviceStatusData = null;
if (state.currentDeviceID) {
try {
const response = await fetch(`${API_BASE_URL}/devices/status?device_id=${state.currentDeviceID}`);
if (response.ok) {
deviceStatusData = await response.json();
}
} catch (error) {
console.error('Failed to fetch device status:', error);
}
}
// 准备状态卡片数据
let formattedMetricsForCards = {};
if (deviceStatusData && deviceStatusData.id) {
// 使用设备状态API返回的数据
formattedMetricsForCards = {
cpu: deviceStatusData.status.cpu || 0,
cpu_hz: deviceStatusData.status.cpu_hz || 0,
memory: deviceStatusData.status.memory || 0,
disk: {
'/': {
used_percent: deviceStatusData.status.disk || 0,
total: state.historyMetrics.disk && state.historyMetrics.disk['/'] && state.historyMetrics.disk['/'].total || 0
}
},
network: {
bytes_sent: deviceStatusData.status.network_sent || 0,
bytes_received: deviceStatusData.status.network_received || 0,
tx_bytes: deviceStatusData.status.network_tx_bytes || 0,
rx_bytes: deviceStatusData.status.network_rx_bytes || 0
}
};
}
// 更新状态卡片
updateStatusCards(formattedMetricsForCards);
} catch (error) {
console.error('Failed to load status cards:', error);
}
}
// 加载监控指标,只负责更新图表
async function loadMetrics() {
try {
// 并行加载所有指标
const [cpuData, memoryData, diskData, networkSumData] = await Promise.all([
fetchMetric('cpu'),
fetchMetric('memory'),
fetchMetric('disk'),
fetchMetric('network')
]);
// 更新图表
updateCharts(cpuData, memoryData, diskData, networkSumData);
// 更新刷新状态指示器
updateRefreshStatus();
// 更新进程信息
loadProcessInfo();
// 更新系统日志
loadSystemLogs();
} catch (error) {
console.error('Failed to load metrics:', error);
// 显示友好的错误提示
showToast('加载监控数据失败,请稍后重试', 'error');
// 更新刷新状态指示器为错误状态
const statusIndicator = document.getElementById('refreshStatusIndicator');
const lastRefreshTime = document.getElementById('lastRefreshTime');
if (statusIndicator && lastRefreshTime) {
statusIndicator.className = 'w-2 h-2 bg-red-500 rounded-full animate-pulse';
lastRefreshTime.textContent = `上次刷新: 失败`;
}
// 即使发生错误,也要尝试初始化图表,避免页面空白
initDetailedCharts();
}
}
// 格式化磁盘数据,用于状态卡片显示
function formatDiskDataForCards(diskData) {
// 如果diskData是空对象,返回空对象
if (!diskData || typeof diskData !== 'object' || Array.isArray(diskData)) {
return {};
}
const formattedDiskData = {};
// 遍历每个挂载点
for (const mountpoint in diskData) {
const mountpointData = diskData[mountpoint];
// 如果挂载点数据是数组,获取最新的数据点
if (Array.isArray(mountpointData) && mountpointData.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestData = mountpointData[mountpointData.length - 1];
formattedDiskData[mountpoint] = {
used_percent: latestData.value,
// 尝试从历史数据获取总容量
total: state.historyMetrics.disk && state.historyMetrics.disk[mountpoint] && state.historyMetrics.disk[mountpoint].total || 0
};
}
}
return formattedDiskData;
}
// 格式化网络数据,用于状态卡片显示
function formatNetworkDataForCards(networkData) {
// 初始化返回数据结构
const formattedNetworkData = {
bytes_sent: 0,
bytes_received: 0,
tx_bytes: 0,
rx_bytes: 0
};
// 如果没有数据,返回初始值
if (!networkData || typeof networkData === 'undefined') {
return formattedNetworkData;
}
// 处理数组格式数据
if (Array.isArray(networkData)) {
// 数组格式:直接处理最新的数据点
if (networkData.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestData = networkData[networkData.length - 1];
// 检查是否包含速率数据
if (latestData.sent !== undefined) {
formattedNetworkData.bytes_sent = latestData.sent;
} else if (latestData.bytes_sent !== undefined) {
formattedNetworkData.bytes_sent = latestData.bytes_sent;
}
if (latestData.received !== undefined) {
formattedNetworkData.bytes_received = latestData.received;
} else if (latestData.bytes_received !== undefined) {
formattedNetworkData.bytes_received = latestData.bytes_received;
}
// 检查是否包含总量数据
if (latestData.tx_bytes !== undefined) {
formattedNetworkData.tx_bytes = latestData.tx_bytes;
}
if (latestData.rx_bytes !== undefined) {
formattedNetworkData.rx_bytes = latestData.rx_bytes;
}
}
}
// 处理对象格式数据
else if (typeof networkData === 'object') {
// 检查是否为WebSocket直接返回的总流量格式
if (networkData.bytes_sent !== undefined && networkData.bytes_received !== undefined) {
// WebSocket消息格式 - 总流量
formattedNetworkData.bytes_sent = networkData.bytes_sent;
formattedNetworkData.bytes_received = networkData.bytes_received;
formattedNetworkData.tx_bytes = networkData.tx_bytes || 0;
formattedNetworkData.rx_bytes = networkData.rx_bytes || 0;
}
// 按网卡分组的数据
else {
// 遍历每个网卡
for (const iface in networkData) {
const ifaceData = networkData[iface];
if (typeof ifaceData === 'object') {
// 处理速率数据
if (ifaceData.sent && ifaceData.received) {
// 如果sent和received是数组,获取最新的数据点
if (Array.isArray(ifaceData.sent) && ifaceData.sent.length > 0 &&
Array.isArray(ifaceData.received) && ifaceData.received.length > 0) {
// 最新的数据点是数组的最后一个元素
const latestSent = ifaceData.sent[ifaceData.sent.length - 1].value;
const latestReceived = ifaceData.received[ifaceData.received.length - 1].value;
// 累加速率
formattedNetworkData.bytes_sent += latestSent;
formattedNetworkData.bytes_received += latestReceived;
}
// 如果sent和received是数值
else if (typeof ifaceData.sent === 'number' && typeof ifaceData.received === 'number') {
formattedNetworkData.bytes_sent += ifaceData.sent;
formattedNetworkData.bytes_received += ifaceData.received;
}
}
// 直接使用bytes_sent和bytes_received字段
else if (ifaceData.bytes_sent !== undefined && ifaceData.bytes_received !== undefined) {
formattedNetworkData.bytes_sent += ifaceData.bytes_sent;
formattedNetworkData.bytes_received += ifaceData.bytes_received;
}
// 检查是否有总量数据(多种可能的字段名)
// 1. 标准字段名:tx_bytes, rx_bytes
if (ifaceData.tx_bytes !== undefined) {
formattedNetworkData.tx_bytes += ifaceData.tx_bytes;
}
if (ifaceData.rx_bytes !== undefined) {
formattedNetworkData.rx_bytes += ifaceData.rx_bytes;
}
// 2. 旧格式字段名:bytes_sent_total, bytes_received_total
if (ifaceData.bytes_sent_total !== undefined) {
formattedNetworkData.tx_bytes += ifaceData.bytes_sent_total;
}
if (ifaceData.bytes_received_total !== undefined) {
formattedNetworkData.rx_bytes += ifaceData.bytes_received_total;
}
// 3. 可能的其他格式:bytes_sent, bytes_received作为总量数据
// 仅当没有其他总量数据字段时使用
if (formattedNetworkData.tx_bytes === 0 && ifaceData.bytes_sent !== undefined && typeof ifaceData.bytes_sent === 'number') {
formattedNetworkData.tx_bytes += ifaceData.bytes_sent;
}
if (formattedNetworkData.rx_bytes === 0 && ifaceData.bytes_received !== undefined && typeof ifaceData.bytes_received === 'number') {
formattedNetworkData.rx_bytes += ifaceData.bytes_received;
}
}
}
}
}
return formattedNetworkData;
}
// WebSocket初始化
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket连接已打开');
wsReconnectAttempts = 0;
wsReconnectDelay = 1000;
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
ws.onclose = (event) => {
console.log(`WebSocket连接已关闭: ${event.code} - ${event.reason}`);
attemptReconnect();
};
ws.onerror = (error) => {
console.error('WebSocket连接错误:', error);
};
}
// 处理WebSocket消息
function handleWebSocketMessage(message) {
if (message.type === 'metrics_update') {
handleMetricsUpdate(message);
}
}
// 加载服务器信息
async function loadServerInfo(deviceId) {
try {
// 将设备ID存储到全局状态
state.currentDeviceID = deviceId;
// 获取服务器基本信息
const deviceResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!deviceResponse.ok) {
throw new Error('Failed to fetch server info');
}
const deviceData = await deviceResponse.json();
// 获取硬件信息,特别是操作系统和CPU型号信息
const hardwareResponse = await fetch(`${API_BASE_URL}/metrics/hardware?device_id=${deviceId}`);
let osFullname = '未知';
let cpuModel = '未知';
if (hardwareResponse.ok) {
const hardwareData = await hardwareResponse.json();
if (hardwareData && hardwareData.hardware) {
// 获取操作系统信息
if (hardwareData.hardware.os) {
osFullname = hardwareData.hardware.os.fullname || '未知';
}
// 获取CPU型号信息
if (hardwareData.hardware.cpu) {
// 尝试从不同字段获取CPU型号
const cpu = hardwareData.hardware.cpu;
cpuModel = cpu.model || cpu.name || cpu.brand || '未知';
// 确保CPU型号是字符串类型
cpuModel = typeof cpuModel === 'string' ? cpuModel : '未知';
}
}
}
// 更新服务器信息显示,增加CPU型号
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `服务器名称: ${deviceData.device.name || deviceId} | IP地址: ${deviceData.device.ip || '未知'} | 操作系统: ${osFullname} | CPU型号: ${cpuModel}
`;
}
// 初始化状态卡片
loadStatusCards();
} catch (error) {
console.error('Failed to load server info:', error);
// 即使请求失败,也要将设备ID存储到全局状态
state.currentDeviceID = deviceId;
// 显示错误信息,不使用模拟数据
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `加载服务器信息失败
`;
}
// 初始化状态卡片,即使服务器信息加载失败
loadStatusCards();
}
}
// 处理指标更新
function handleMetricsUpdate(message) {
const { device_id, metrics } = message;
// 只处理当前选中设备的WebSocket消息
if (state.currentDeviceID && device_id !== state.currentDeviceID) {
return;
}
// 格式化数据,确保updateStatusCards函数能正确处理
const formattedMetrics = {
cpu: metrics.cpu,
cpu_hz: metrics.cpu_hz, // 添加cpu_hz字段,确保CPU频率能显示
memory: metrics.memory,
disk: formatDiskDataForCards(metrics.disk),
network: formatNetworkDataForCards(metrics.network)
};
// 直接更新统计卡片,始终实时更新
updateStatusCards(formattedMetrics);
// 根据自动刷新开关状态决定是否更新图表
if (state.autoRefreshEnabled) {
// 直接使用WebSocket数据更新图表,避免不必要的API请求
updateCharts(metrics.cpu, metrics.memory, metrics.disk, metrics.network);
}
}
// 更新刷新状态指示器
function updateRefreshStatus() {
// 更新最后刷新时间
const now = new Date();
const formattedTime = now.toLocaleTimeString();
// 更新状态指示器和时间显示
const statusIndicator = document.getElementById('refreshStatusIndicator');
const lastRefreshTime = document.getElementById('lastRefreshTime');
if (statusIndicator && lastRefreshTime) {
// 短暂显示绿色,表示数据已更新
statusIndicator.className = 'w-2 h-2 bg-green-500 rounded-full';
lastRefreshTime.textContent = `上次刷新: ${formattedTime}`;
// 1秒后恢复为黄色,表示正常状态
setTimeout(() => {
if (statusIndicator) {
statusIndicator.className = 'w-2 h-2 bg-yellow-500 rounded-full';
}
}, 1000);
}
}
// 更新状态卡片
function updateStatusCards(metrics) {
// 更新历史指标数据
updateHistoryMetrics(metrics);
// 保存待处理的指标更新
state.pendingMetricsUpdate = metrics;
// 使用requestAnimationFrame优化DOM更新
if (!state.isUpdatingDOM) {
state.isUpdatingDOM = true;
requestAnimationFrame(() => {
_updateStatusCards(state.pendingMetricsUpdate);
state.isUpdatingDOM = false;
state.lastMetricsUpdate = Date.now();
});
}
}
// 内部DOM更新函数,由requestAnimationFrame调用
function _updateStatusCards(metrics) {
// 使用历史数据或当前数据
const displayMetrics = {
cpu: metrics.cpu || state.historyMetrics.cpu,
cpu_hz: metrics.cpu_hz || state.historyMetrics.cpu_hz,
memory: metrics.memory || state.historyMetrics.memory,
disk: metrics.disk || state.historyMetrics.disk,
network: metrics.network || state.historyMetrics.network
};
// 更新CPU状态卡片
if (displayMetrics.cpu) {
let cpuUsage = 0;
let cpuGhz = 0;
let cpuLoad = 0;
// 解析CPU数据
if (Array.isArray(displayMetrics.cpu) && displayMetrics.cpu.length > 0) {
// 数组格式:获取最新的数据点
cpuUsage = displayMetrics.cpu[displayMetrics.cpu.length - 1].value;
} else if (typeof displayMetrics.cpu === 'number') {
// 数值格式
cpuUsage = displayMetrics.cpu;
} else if (typeof displayMetrics.cpu === 'object' && displayMetrics.cpu.usage) {
// 对象格式,包含usage, frequency, load
cpuUsage = displayMetrics.cpu.usage;
cpuGhz = displayMetrics.cpu.frequency || 0;
cpuLoad = displayMetrics.cpu.load || 0;
}
// 从单独的cpu_hz字段获取CPU频率(如果有)
if (displayMetrics.cpu_hz && typeof displayMetrics.cpu_hz === 'number') {
cpuGhz = displayMetrics.cpu_hz / 1000; // 转换为GHz
}
// 更新显示
const cpuElement = document.getElementById('cpuValue');
const cpuDetailsElement = document.getElementById('cpuDetails');
if (cpuElement) {
cpuElement.textContent = `${cpuUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
cpuElement.className = `text-3xl font-bold metric-value ${cpuUsage > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (cpuDetailsElement) {
cpuDetailsElement.className = 'text-xs text-gray-500 mt-1';
cpuDetailsElement.textContent = `${cpuGhz.toFixed(2)} GHz | 负载: ${cpuLoad.toFixed(2)}`;
}
}
// 更新内存状态卡片
if (displayMetrics.memory) {
let memoryUsage = 0;
let memoryUsed = 0;
let memoryTotal = 0;
// 解析内存数据
if (Array.isArray(displayMetrics.memory) && displayMetrics.memory.length > 0) {
// 数组格式:获取最新的数据点
memoryUsage = displayMetrics.memory[displayMetrics.memory.length - 1].value;
} else if (typeof displayMetrics.memory === 'number') {
// 数值格式
memoryUsage = displayMetrics.memory;
} else if (typeof displayMetrics.memory === 'object') {
// 对象格式,包含usage, used, total
memoryUsage = displayMetrics.memory.usage || 0;
memoryUsed = displayMetrics.memory.used || 0;
memoryTotal = displayMetrics.memory.total || 0;
}
// 如果只有使用率,没有使用量和总量,尝试从其他地方获取
if (memoryTotal === 0 && state.historyMetrics.memory && typeof state.historyMetrics.memory === 'object') {
memoryUsed = state.historyMetrics.memory.used || 0;
memoryTotal = state.historyMetrics.memory.total || 0;
}
// 如果内存总量仍为0,尝试使用默认值或从API获取
if (memoryTotal === 0) {
// 尝试从系统获取内存总量(如果浏览器支持)
if (navigator.deviceMemory) {
memoryTotal = navigator.deviceMemory * 1024 * 1024 * 1024; // 转换为字节
} else {
// 使用默认值16GB
memoryTotal = 16 * 1024 * 1024 * 1024;
}
}
// 计算使用量(如果只有使用率)
if (memoryUsed === 0 && memoryTotal > 0) {
memoryUsed = (memoryTotal * memoryUsage) / 100;
}
// 更新显示
const memoryElement = document.getElementById('memoryValue');
const memoryDetailsElement = document.getElementById('memoryDetails');
if (memoryElement) {
memoryElement.textContent = `${memoryUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
memoryElement.className = `text-3xl font-bold metric-value ${memoryUsage > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (memoryDetailsElement) {
memoryDetailsElement.className = 'text-xs text-gray-500 mt-1';
memoryDetailsElement.textContent = `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}`;
}
}
// 更新磁盘状态卡片
if (displayMetrics.disk) {
let totalUsed = 0;
let totalSize = 0;
let usagePercent = 0;
// 过滤掉不需要的挂载点
const excludedMountpoints = ['/usr', '/boot', '/boot/efi'];
// 解析磁盘数据
if (typeof displayMetrics.disk === 'object' && displayMetrics.disk !== null && !Array.isArray(displayMetrics.disk)) {
// 按挂载点分组的数据
let hasValidData = false;
for (const mountpoint in displayMetrics.disk) {
// 跳过排除的挂载点
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = displayMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.used_percent !== undefined && data.total !== undefined) {
// 新格式:包含used_percent和total字段
// 计算已使用大小(total * used_percent / 100)
const used = (data.total * data.used_percent) / 100;
totalUsed += used;
totalSize += data.total;
hasValidData = true;
} else if (data.used !== undefined && data.total !== undefined) {
// 旧格式:包含used和total字段
totalUsed += data.used;
totalSize += data.total;
hasValidData = true;
}
}
}
// 如果没有有效数据,尝试使用历史数据
if (!hasValidData && state.historyMetrics.disk && typeof state.historyMetrics.disk === 'object' && !Array.isArray(state.historyMetrics.disk)) {
for (const mountpoint in state.historyMetrics.disk) {
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = state.historyMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.used_percent !== undefined && data.total !== undefined) {
const used = (data.total * data.used_percent) / 100;
totalUsed += used;
totalSize += data.total;
} else if (data.used !== undefined && data.total !== undefined) {
totalUsed += data.used;
totalSize += data.total;
}
}
}
}
} else if (typeof displayMetrics.disk === 'object' && displayMetrics.disk.used !== undefined && displayMetrics.disk.total !== undefined) {
// 单磁盘数据
totalUsed = displayMetrics.disk.used;
totalSize = displayMetrics.disk.total;
}
// 如果只有使用率,没有使用量和总量,尝试计算
if (totalSize === 0 && displayMetrics.disk && typeof displayMetrics.disk === 'number') {
// 如果disk是一个数字,尝试从历史数据获取总量
if (state.historyMetrics.disk && typeof state.historyMetrics.disk === 'object' && !Array.isArray(state.historyMetrics.disk)) {
for (const mountpoint in state.historyMetrics.disk) {
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = state.historyMetrics.disk[mountpoint];
if (data && typeof data === 'object') {
if (data.total !== undefined) {
totalSize += data.total;
}
}
}
// 计算使用量
if (totalSize > 0) {
totalUsed = (totalSize * displayMetrics.disk) / 100;
}
}
}
// 计算使用率
if (totalSize > 0) {
usagePercent = (totalUsed / totalSize) * 100;
} else if (typeof displayMetrics.disk === 'number') {
// 如果disk是一个数字,直接使用它作为使用率
usagePercent = displayMetrics.disk;
}
// 更新显示
const diskElement = document.getElementById('diskValue');
const diskDetailsElement = document.getElementById('diskDetails');
if (diskElement) {
diskElement.textContent = `${usagePercent.toFixed(1)}%`;
// 设置红色显示如果达到顶峰,同时保留原有的样式类
diskElement.className = `text-3xl font-bold metric-value ${usagePercent > 90 ? 'text-red-500' : 'text-gray-900'}`;
}
if (diskDetailsElement) {
diskDetailsElement.className = 'text-xs text-gray-500 mt-1';
diskDetailsElement.textContent = `${formatBytes(totalUsed)} / ${formatBytes(totalSize)}`;
}
}
// 更新网络流量状态卡片
if (displayMetrics.network) {
// 解析网络数据
const rxRate = displayMetrics.network.bytes_received || 0; // 接收速率 (bytes/s)
const txRate = displayMetrics.network.bytes_sent || 0; // 发送速率 (bytes/s)
// 计算速率比值
let ratio = 0;
let ratioSymbol = '';
let ratioText = '';
let symbolColor = '';
if (rxRate === 0 && txRate === 0) {
// 如果接收速率和发送速率都为0,显示无穷符号
ratioText = '∞';
ratioSymbol = '';
symbolColor = 'text-gray-500';
} else if (txRate === 0) {
// 如果发送速率为0,显示无穷符号和接收箭头
ratioText = '∞';
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else if (rxRate === 0) {
// 如果接收速率为0,显示无穷符号和发送箭头
ratioText = '∞';
ratioSymbol = '↑';
symbolColor = 'text-red-500';
} else {
// 计算接收速率与发送速率的比值
ratio = rxRate / txRate;
ratioText = ratio.toFixed(2);
// 根据比值判断箭头方向和颜色
if (ratio > 10) {
// 接收速率远高于发送,显示绿色↓
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else if (ratio < 0.1) {
// 发送速率远高于接收,显示红色↑
ratioSymbol = '↑';
symbolColor = 'text-red-500';
} else if (ratio >= 0.5 && ratio <= 2) {
// 收发速率均衡,显示蓝色↔
ratioSymbol = '↔';
symbolColor = 'text-blue-500';
} else if (ratio > 1) {
// 接收速率高于发送,显示绿色↓
ratioSymbol = '↓';
symbolColor = 'text-green-500';
} else {
// 发送速率高于接收,显示红色↑
ratioSymbol = '↑';
symbolColor = 'text-red-500';
}
}
// 格式化速率为MB/s
const formatRate = (bytesPerSec) => {
return (bytesPerSec / (1024 * 1024)).toFixed(2);
};
const rxRateMB = formatRate(rxRate);
const txRateMB = formatRate(txRate);
// 更新显示
const networkElement = document.getElementById('networkValue');
const networkDetailsElement = document.getElementById('networkDetails');
if (networkElement) {
// 大数字显示比值和箭头
networkElement.innerHTML = `${ratioText} ${ratioSymbol}`;
networkElement.className = 'text-3xl font-bold metric-value text-gray-900 flex items-center';
}
if (networkDetailsElement) {
// 速率显示
networkDetailsElement.className = 'text-xs text-gray-500 mt-1';
networkDetailsElement.textContent = `${rxRateMB} MB/s | ${txRateMB} MB/s`;
}
}
}
// 更新历史指标数据
function updateHistoryMetrics(metrics) {
// 只更新有有效数据的指标
if (metrics.cpu) {
state.historyMetrics.cpu = metrics.cpu;
}
if (metrics.cpu_hz) {
state.historyMetrics.cpu_hz = metrics.cpu_hz;
}
if (metrics.memory) {
state.historyMetrics.memory = metrics.memory;
}
if (metrics.disk) {
state.historyMetrics.disk = metrics.disk;
}
if (metrics.network) {
state.historyMetrics.network = metrics.network;
}
}
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkData) {
// 确保数据格式正确
const safeCpuData = Array.isArray(cpuData) ? cpuData : [];
const safeMemoryData = Array.isArray(memoryData) ? memoryData : [];
const safeDiskData = typeof diskData === 'object' && diskData !== null ? diskData : {};
const safeNetworkData = typeof networkData === 'object' && networkData !== null ? networkData : {};
// 保存待处理的图表更新数据
state.pendingChartUpdate = {
cpuData: safeCpuData,
memoryData: safeMemoryData,
diskData: safeDiskData,
networkData: safeNetworkData
};
// 使用节流机制更新图表
_updateChartsThrottled();
}
// 带节流功能的图表更新函数
function _updateChartsThrottled() {
const now = Date.now();
if (now - state.lastChartUpdate < state.chartUpdateThrottle) {
// 如果距离上次更新时间不足节流时间,延迟执行
setTimeout(_updateChartsThrottled, state.chartUpdateThrottle - (now - state.lastChartUpdate));
return;
}
// 执行实际的图表更新
_updateCharts(state.pendingChartUpdate);
state.lastChartUpdate = now;
}
// 内部图表更新函数,执行实际的图表渲染
function _updateCharts({ cpuData, memoryData, diskData, networkData }) {
// 数据点排序函数
const sortDataByTime = (data) => {
return [...data].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
};
// 计算固定份数X轴数据
const getFixedPointsData = (data) => {
if (!Array.isArray(data) || data.length === 0) {
return [];
}
// 根据时间范围计算需要的份数(每份10分钟)
let expectedPoints = 6; // 默认1小时,6份
let timeRange = state.currentTimeRange || '1h';
// 如果使用了自定义时间,检查是否是24小时范围
if (state.customStartTime && state.customEndTime) {
const startTime = new Date(state.customStartTime);
const endTime = new Date(state.customEndTime);
const durationHours = (endTime - startTime) / (1000 * 60 * 60);
// 根据实际时长设置预期点数
if (durationHours <= 0.5) {
timeRange = '30m';
} else if (durationHours <= 1) {
timeRange = '1h';
} else if (durationHours <= 2) {
timeRange = '2h';
} else if (durationHours <= 6) {
timeRange = '6h';
} else if (durationHours <= 12) {
timeRange = '12h';
} else {
timeRange = '24h';
}
}
switch(timeRange) {
case '30m':
expectedPoints = 3; // 30分钟,3份
break;
case '1h':
expectedPoints = 6; // 1小时,6份
break;
case '2h':
expectedPoints = 12; // 2小时,12份
break;
case '6h':
expectedPoints = 36; // 6小时,36份
break;
case '12h':
expectedPoints = 72; // 12小时,72份
break;
case '24h':
expectedPoints = 144; // 24小时,144份
break;
}
// 排序数据
const sortedData = sortDataByTime(data);
// 如果数据点足够,直接返回
if (sortedData.length <= expectedPoints) {
return sortedData;
}
// 计算采样步长
const step = Math.ceil(sortedData.length / expectedPoints);
const sampled = [];
// 采样数据,确保得到期望的份数
for (let i = 0; i < sortedData.length; i += step) {
sampled.push(sortedData[i]);
if (sampled.length >= expectedPoints) {
break;
}
}
// 确保包含最后一个数据点
if (sampled.length < expectedPoints && sortedData.length > 0) {
// 如果采样点不够,从末尾补充
const remaining = expectedPoints - sampled.length;
for (let i = 1; i <= remaining; i++) {
const index = Math.max(0, sortedData.length - i);
if (!sampled.some(item => item.time === sortedData[index].time)) {
sampled.push(sortedData[index]);
}
}
}
// 再次排序,确保时间顺序
return sortDataByTime(sampled);
};
// 更新CPU图表
if (cpuData && Array.isArray(cpuData) && cpuData.length > 0 && charts.cpu) {
const fixedData = getFixedPointsData(cpuData);
charts.cpu.data.datasets[0].data = fixedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.cpu.update();
}
// 更新内存图表
if (memoryData && Array.isArray(memoryData) && memoryData.length > 0 && charts.memory) {
const fixedData = getFixedPointsData(memoryData);
charts.memory.data.datasets[0].data = fixedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.memory.update();
}
// 更新磁盘图表,支持多个挂载点
if (diskData && charts.disk) {
// 定义不同的颜色,用于区分不同的挂载点
const colors = [
{ border: '#f59e0b', background: 'rgba(245, 158, 11, 0.1)' }, // 黄色
{ border: '#ef4444', background: 'rgba(239, 68, 68, 0.1)' }, // 红色
{ border: '#10b981', background: 'rgba(16, 185, 129, 0.1)' }, // 绿色
{ border: '#3b82f6', background: 'rgba(59, 130, 246, 0.1)' }, // 蓝色
{ border: '#8b5cf6', background: 'rgba(139, 92, 246, 0.1)' }, // 紫色
{ border: '#ec4899', background: 'rgba(236, 72, 153, 0.1)' }, // 粉色
];
// 清空现有的数据集
charts.disk.data.datasets = [];
// 为每个挂载点创建独立的数据集
let colorIndex = 0;
if (typeof diskData === 'object' && diskData !== null && !Array.isArray(diskData)) {
// 处理按挂载点分组的数据
for (const [mountpoint, data] of Object.entries(diskData)) {
if (data && Array.isArray(data) && data.length > 0) {
// 获取颜色
const color = colors[colorIndex % colors.length];
colorIndex++;
// 排序数据
const sortedData = sortDataByTime(data);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
// 创建数据集
const dataset = {
label: `磁盘使用率 (${mountpoint})`,
data: fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value
})),
borderColor: color.border,
backgroundColor: color.background,
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3,
};
// 添加数据集
charts.disk.data.datasets.push(dataset);
}
}
} else if (Array.isArray(diskData) && diskData.length > 0) {
// 处理单一磁盘数据
const sortedData = sortDataByTime(diskData);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
const dataset = {
label: '磁盘使用率',
data: fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value
})),
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3,
};
charts.disk.data.datasets.push(dataset);
}
// 更新图表
charts.disk.update();
}
// 保存当前选中的网卡
state.currentInterface = state.currentInterface || 'all';
// 数据求和辅助函数:计算所有网卡在同一时间点的数据之和
const sumAllInterfacesData = (networkData, metricType) => {
if (typeof networkData !== 'object' || networkData === null) {
return [];
}
// 收集所有时间点
const allTimes = new Set();
const interfaceDatas = [];
// 遍历所有网卡
for (const iface in networkData) {
if (networkData.hasOwnProperty(iface) && typeof networkData[iface] === 'object') {
const ifaceData = networkData[iface][metricType];
if (Array.isArray(ifaceData)) {
interfaceDatas.push(ifaceData);
// 收集所有时间点
ifaceData.forEach(item => {
allTimes.add(item.time);
});
}
}
}
// 如果没有数据,返回空数组
if (interfaceDatas.length === 0) {
return [];
}
// 将时间点转换为数组并排序
const sortedTimes = Array.from(allTimes).sort((a, b) => new Date(a) - new Date(b));
// 计算每个时间点的总和
const summedData = sortedTimes.map(time => {
let sum = 0;
// 遍历所有网卡数据,累加同一时间点的值
interfaceDatas.forEach(ifaceData => {
// 查找当前时间点的数据
const dataPoint = ifaceData.find(item => item.time === time);
if (dataPoint) {
sum += dataPoint.value;
}
});
return {
time: time,
value: sum
};
});
return summedData;
};
// 计算流量差值的辅助函数
const calculateTrafficDiff = (data) => {
if (!Array.isArray(data) || data.length <= 1) {
return data;
}
// 确保数据按时间排序
const sortedData = [...data].sort((a, b) => new Date(a.time) - new Date(b.time));
// 计算每个时间点与前一个时间点的差值
const diffData = [];
for (let i = 1; i < sortedData.length; i++) {
const current = sortedData[i];
const previous = sortedData[i - 1];
// 计算差值,确保为正数
const diff = {
time: current.time,
value: Math.max(0, current.value - previous.value)
};
diffData.push(diff);
}
return diffData;
};
// 更新网络流量趋势图表(发送总和和接收总和)
if (networkData && charts.network) {
let txBytesData, rxBytesData;
// 如果是按网卡分组的数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
if (state.currentInterface === 'all') {
// 计算所有网卡的发送累积总流量
txBytesData = sumAllInterfacesData(networkData, 'tx_bytes');
// 计算所有网卡的接收累积总流量
rxBytesData = sumAllInterfacesData(networkData, 'rx_bytes');
} else {
// 选择当前选中的网卡数据
const selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
txBytesData = selectedNetworkData.tx_bytes || [];
rxBytesData = selectedNetworkData.rx_bytes || [];
}
} else {
// 直接使用数据
txBytesData = networkData.tx_bytes || [];
rxBytesData = networkData.rx_bytes || [];
}
if (txBytesData.length > 0 || rxBytesData.length > 0) {
// 使用发送累积总流量数据
if (Array.isArray(txBytesData) && txBytesData.length > 0) {
// 排序发送数据
const sortedTxBytes = sortDataByTime(txBytesData);
// 计算流量差值,只显示指定时间范围内的流量变化
const diffTxBytes = calculateTrafficDiff(sortedTxBytes);
// 转换为MB
const txBytesSumData = diffTxBytes.map(item => ({
time: item.time,
value: item.value / (1024 * 1024) // 转换为MB
}));
// 使用固定份数X轴数据计算
const fixedPointsTxBytesSum = getFixedPointsData(txBytesSumData);
charts.network.data.datasets[0].data = fixedPointsTxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
// 使用接收累积总流量数据
if (Array.isArray(rxBytesData) && rxBytesData.length > 0) {
// 排序接收数据
const sortedRxBytes = sortDataByTime(rxBytesData);
// 计算流量差值,只显示指定时间范围内的流量变化
const diffRxBytes = calculateTrafficDiff(sortedRxBytes);
// 转换为MB
const rxBytesSumData = diffRxBytes.map(item => ({
time: item.time,
value: item.value / (1024 * 1024) // 转换为MB
}));
// 使用固定份数X轴数据计算
const fixedPointsRxBytesSum = getFixedPointsData(rxBytesSumData);
charts.network.data.datasets[1].data = fixedPointsRxBytesSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
charts.network.update();
}
}
// 更新网速趋势图表
if (networkData && charts.speed) {
let sentData, receivedData;
// 如果是按网卡分组的数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
if (state.currentInterface === 'all') {
// 计算所有网卡的发送速率总和
sentData = sumAllInterfacesData(networkData, 'sent');
// 计算所有网卡的接收速率总和
receivedData = sumAllInterfacesData(networkData, 'received');
} else {
// 选择当前选中的网卡数据
const selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
sentData = selectedNetworkData.sent || [];
receivedData = selectedNetworkData.received || [];
}
} else {
// 直接使用数据
sentData = networkData.sent || [];
receivedData = networkData.received || [];
}
if (sentData.length > 0 || receivedData.length > 0) {
// 更新发送流量
if (Array.isArray(sentData) && sentData.length > 0) {
const sortedData = sortDataByTime(sentData);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
charts.speed.data.datasets[0].data = fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s
}));
}
// 更新接收流量
if (Array.isArray(receivedData) && receivedData.length > 0) {
const sortedData = sortDataByTime(receivedData);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
charts.speed.data.datasets[1].data = fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s
}));
}
charts.speed.update();
}
}
// 更新网卡选择下拉框
updateInterfaceDropdown(networkData);
// 初始化图表(如果尚未初始化)
initDetailedCharts();
}
// 更新网卡选择下拉框
function updateInterfaceDropdown(networkData) {
// 创建或获取网卡选择容器
let interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (!interfaceContainer) {
// 获取图表选项卡导航容器
const chartTabs = document.getElementById('chartTabs');
if (!chartTabs) {
return;
}
// 创建网卡选择容器
interfaceContainer = document.createElement('div');
interfaceContainer.id = 'interfaceSelectorContainer';
interfaceContainer.className = 'flex items-center gap-2 ml-4';
// 创建标签
const label = document.createElement('label');
label.htmlFor = 'interfaceSelector';
label.className = 'text-sm text-gray-600';
label.textContent = '网卡:';
// 创建下拉框
const select = document.createElement('select');
select.id = 'interfaceSelector';
select.className = 'px-3 py-1 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加事件监听
select.addEventListener('change', (e) => {
state.currentInterface = e.target.value;
// 重新加载指标
loadMetrics();
});
// 组装容器
interfaceContainer.appendChild(label);
interfaceContainer.appendChild(select);
// 将容器添加到图表选项卡导航中
const tabsContainer = chartTabs.querySelector('.flex.flex-wrap');
if (tabsContainer) {
tabsContainer.appendChild(interfaceContainer);
}
}
// 更新下拉框选项
const select = document.getElementById('interfaceSelector');
if (select) {
// 保存当前选中的值
const currentValue = select.value;
// 清空现有选项
select.innerHTML = '';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加所有网卡选项
// 检查是否有按网卡分组的网络数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
// 获取所有网卡名称
const interfaces = Object.keys(networkData);
// 添加所有网卡选项
interfaces.forEach(iface => {
// 只添加实际的网卡名称,不添加"all"选项
if (iface !== 'all') {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
select.appendChild(option);
}
});
} else {
// 如果没有按网卡分组的数据,显示"所有接口"选项
const option = document.createElement('option');
option.value = 'all';
option.textContent = '所有接口';
select.appendChild(option);
}
// 恢复当前选中的值
select.value = currentValue;
}
// 检查当前选中的选项卡,如果不是"网络"或"网速",隐藏网卡选择下拉框
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab && activeTab.dataset.tab !== 'network' && activeTab.dataset.tab !== 'speed') {
interfaceContainer.classList.add('hidden');
} else {
interfaceContainer.classList.remove('hidden');
}
}
// 尝试重连WebSocket
function attemptReconnect() {
// 清除可能存在的重连定时器
if (wsReconnectTimeout) {
clearTimeout(wsReconnectTimeout);
wsReconnectTimeout = null;
}
if (wsReconnectAttempts < wsMaxReconnectAttempts) {
wsReconnectAttempts++;
// 指数退避,但不超过最大延迟
wsReconnectDelay = Math.min(wsReconnectDelay * 2, wsMaxReconnectDelay);
// 添加随机抖动,防止多个客户端同时重连
const jitter = Math.random() * 1000;
const delay = wsReconnectDelay + jitter;
wsReconnectTimeout = setTimeout(() => {
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts}),延迟 ${Math.round(delay)}ms`);
initWebSocket();
}, delay);
} else {
console.error('WebSocket重连失败,已达到最大重连次数');
// 30秒后重置重连状态,允许再次尝试
setTimeout(() => {
wsReconnectAttempts = 0;
wsReconnectDelay = 1000;
console.log('WebSocket重连状态已重置,准备再次尝试连接');
attemptReconnect();
}, 30000);
}
}
// 打开模态框
function openModal(isEdit = false, deviceData = null) {
const modal = document.getElementById('deviceModal');
const modalContent = document.getElementById('modalContent');
const modalTitle = document.getElementById('modalTitle');
const deviceId = document.getElementById('deviceId');
const deviceName = document.getElementById('deviceName');
const deviceIp = document.getElementById('deviceIp');
const deviceStatus = document.getElementById('deviceStatus');
// 重置表单
document.getElementById('deviceForm').reset();
// 设置模态框标题和数据
if (isEdit && deviceData) {
modalTitle.textContent = '编辑设备';
deviceId.value = deviceData.id;
deviceName.value = deviceData.name || '';
deviceIp.value = deviceData.ip || '';
deviceStatus.value = deviceData.status || 'inactive';
} else {
modalTitle.textContent = '添加设备';
deviceId.value = '';
}
// 显示模态框并添加动画
modal.classList.remove('hidden');
// 触发重排后再添加动画类
setTimeout(() => {
modalContent.classList.remove('scale-95', 'opacity-0');
modalContent.classList.add('scale-100', 'opacity-100');
}, 10);
}
// 关闭模态框
function closeModal() {
const modal = document.getElementById('deviceModal');
const modalContent = document.getElementById('modalContent');
// 先应用离开动画
modalContent.classList.remove('scale-100', 'opacity-100');
modalContent.classList.add('scale-95', 'opacity-0');
// 动画结束后隐藏模态框
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
// 编辑设备
function editDevice(deviceId) {
// 直接从API获取设备详情,确保数据最新
fetch(`${API_BASE_URL}/devices/${deviceId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch device details');
}
return response.json();
})
.then(data => {
// 提取设备数据(API返回的是 {"device": {...}} 格式)
const deviceData = data.device;
openModal(true, deviceData);
})
.catch(error => {
console.error('Failed to get device details:', error);
showToast('获取设备详情失败', 'error');
});
}
// 处理设备表单提交
async function handleDeviceFormSubmit(event) {
event.preventDefault();
const deviceId = document.getElementById('deviceId').value;
const deviceName = document.getElementById('deviceName').value;
const deviceIp = document.getElementById('deviceIp').value;
const deviceStatus = document.getElementById('deviceStatus').value;
// 生成或使用现有ID
let idToUse = deviceId;
if (!idToUse) {
// 为新设备生成ID
idToUse = 'device-' + Date.now();
}
// 获取当前时间戳
const timestamp = Math.floor(Date.now() / 1000);
// 为新设备生成token
let token = '';
if (!deviceId) {
token = 'token-' + Math.random().toString(36).substring(2, 15);
}
// 构建完整的设备数据对象,包含所有必需字段
const deviceData = {
id: idToUse,
name: deviceName,
ip: deviceIp,
status: deviceStatus,
token: token, // 新设备生成token,编辑时不提供(由后端保持原值)
created_at: timestamp, // 新设备的创建时间
updated_at: timestamp // 更新时间
};
try {
let url = `${API_BASE_URL}/devices/`;
let method = 'POST';
// 如果是编辑操作,修改URL和方法
if (deviceId) {
url = `${API_BASE_URL}/devices/${deviceId}`;
method = 'PUT';
}
// 发送请求
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(deviceData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// 显示成功提示
showToast(deviceId ? '设备更新成功' : '设备添加成功', 'success');
// 关闭模态框
closeModal();
// 重新加载设备列表
await loadDeviceManagementList();
} catch (error) {
console.error('保存设备失败:', error);
showToast(deviceId ? '设备更新失败' : '设备添加失败', 'error');
}
}
// 绑定事件
function bindEvents() {
// 添加设备按钮事件
const addDeviceBtn = document.getElementById('addDeviceBtn');
if (addDeviceBtn) {
addDeviceBtn.addEventListener('click', () => {
openModal(false);
});
}
// 关闭模态框按钮
const closeModalBtn = document.getElementById('closeModalBtn');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeModal);
}
// 取消按钮
const cancelModalBtn = document.getElementById('cancelModalBtn');
if (cancelModalBtn) {
cancelModalBtn.addEventListener('click', closeModal);
}
// 设备表单提交
const deviceForm = document.getElementById('deviceForm');
if (deviceForm) {
deviceForm.addEventListener('submit', handleDeviceFormSubmit);
}
// 点击模态框背景关闭
const modal = document.getElementById('deviceModal');
if (modal) {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
// 设备搜索事件
const deviceSearch = document.getElementById('deviceSearch');
if (deviceSearch) {
deviceSearch.addEventListener('input', applyDeviceFilters);
}
// 设备状态筛选事件
const deviceStatusFilter = document.getElementById('deviceStatusFilter');
if (deviceStatusFilter) {
deviceStatusFilter.addEventListener('change', applyDeviceFilters);
}
// 清除筛选按钮事件
const clearFilterBtn = document.getElementById('clearFilterBtn');
if (clearFilterBtn) {
clearFilterBtn.addEventListener('click', clearDeviceFilters);
}
// 自定义时间查询事件
const customTimeQuery = document.getElementById('customTimeQuery');
if (customTimeQuery) {
customTimeQuery.addEventListener('click', () => {
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 获取本地时间
const localStartTime = new Date(startTimeInput.value);
const localEndTime = new Date(endTimeInput.value);
// 转换为ISO字符串(UTC时间)
state.customStartTime = localStartTime.toISOString();
state.customEndTime = localEndTime.toISOString();
// 清空当前时间范围状态,只使用自定义时间
state.currentTimeRange = '';
// 重新加载数据
loadMetrics();
}
});
}
// 缩放控件事件处理
const zoomOutBtn = document.getElementById('zoomOutBtn');
const zoomInBtn = document.getElementById('zoomInBtn');
const resetZoomBtn = document.getElementById('resetZoomBtn');
const currentTimeRangeDisplay = document.getElementById('currentTimeRangeDisplay');
if (zoomOutBtn && zoomInBtn && currentTimeRangeDisplay) {
// 网卡选择和刷新按钮事件
const 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 = '';
// 添加新选项
interfaces.forEach(iface => {
if (iface !== 'all') {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
selectElement.appendChild(option);
}
});
// 如果当前选中的网卡不存在,重置为'all'
if (!interfaces.includes(state.currentInterface) && state.currentInterface !== 'all') {
state.currentInterface = 'all';
}
// 设置当前选中的值
selectElement.value = state.currentInterface;
}
} catch (error) {
console.error('加载网卡列表失败:', error);
// 显示友好的错误提示
showToast('加载网卡列表失败,请稍后重试', 'error');
}
}
// 更新当前时间范围显示
const updateTimeRangeDisplay = () => {
let displayText = '';
// 计算实际的开始时间和结束时间
let startTime, endTime;
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间范围
startTime = new Date(state.customStartTime);
endTime = new Date(state.customEndTime);
displayText = `${formatTime(state.customStartTime)} 至 ${formatTime(state.customEndTime)}`;
} else {
// 使用预设时间范围
const now = new Date();
endTime = now;
// 根据预设时间范围计算开始时间
switch(state.currentTimeRange) {
case '30m':
startTime = new Date(now.getTime() - 30 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
case '1h':
startTime = new Date(now.getTime() - 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
case '2h':
startTime = new Date(now.getTime() - 2 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
case '6h':
startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
case '12h':
startTime = new Date(now.getTime() - 12 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
case '24h':
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000);
displayText = `${formatTime(startTime.toISOString())} 至 ${formatTime(endTime.toISOString())}`;
break;
default:
displayText = '自定义时间范围';
}
}
// 更新所有图表的时间范围显示
const displays = [
document.getElementById('cpuCurrentTimeRangeDisplay'),
document.getElementById('currentTimeRangeDisplay'),
document.getElementById('diskCurrentTimeRangeDisplay'),
document.getElementById('networkCurrentTimeRangeDisplay'),
document.getElementById('speedCurrentTimeRangeDisplay')
];
displays.forEach(display => {
if (display) display.textContent = displayText;
});
};
// 时间范围选项列表,用于缩放
const timeRanges = ['30m', '1h', '2h', '6h', '12h', '24h'];
// 初始化显示
updateTimeRangeDisplay();
// 放大事件
const zoomInHandler = () => {
// 只在使用预设时间范围时生效
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间时,先清除自定义时间
state.customStartTime = '';
state.customEndTime = '';
state.currentTimeRange = '1h';
} else {
// 查找当前时间范围在列表中的索引
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
if (currentIndex > 0) {
// 放大:使用更小的时间范围
state.currentTimeRange = timeRanges[currentIndex - 1];
}
}
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
};
// 为所有放大按钮添加事件监听器
const cpuZoomInBtn = document.getElementById('cpuZoomInBtn');
if (cpuZoomInBtn) {
cpuZoomInBtn.addEventListener('click', zoomInHandler);
}
const memoryZoomInBtn = document.getElementById('memoryZoomInBtn');
if (memoryZoomInBtn) {
memoryZoomInBtn.addEventListener('click', zoomInHandler);
}
const diskZoomInBtn = document.getElementById('diskZoomInBtn');
if (diskZoomInBtn) {
diskZoomInBtn.addEventListener('click', zoomInHandler);
}
const networkZoomInBtn = document.getElementById('networkZoomInBtn');
if (networkZoomInBtn) {
networkZoomInBtn.addEventListener('click', zoomInHandler);
}
const speedZoomInBtn = document.getElementById('speedZoomInBtn');
if (speedZoomInBtn) {
speedZoomInBtn.addEventListener('click', zoomInHandler);
}
// 缩小事件
const zoomOutHandler = () => {
// 只在使用预设时间范围时生效
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间时,先清除自定义时间
state.customStartTime = '';
state.customEndTime = '';
state.currentTimeRange = '1h';
} else {
// 查找当前时间范围在列表中的索引
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
if (currentIndex < timeRanges.length - 1) {
// 缩小:使用更大的时间范围
state.currentTimeRange = timeRanges[currentIndex + 1];
}
}
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
};
// 为所有缩小按钮添加事件监听器
const cpuZoomOutBtn = document.getElementById('cpuZoomOutBtn');
if (cpuZoomOutBtn) {
cpuZoomOutBtn.addEventListener('click', zoomOutHandler);
}
const memoryZoomOutBtn = document.getElementById('memoryZoomOutBtn');
if (memoryZoomOutBtn) {
memoryZoomOutBtn.addEventListener('click', zoomOutHandler);
}
const diskZoomOutBtn = document.getElementById('diskZoomOutBtn');
if (diskZoomOutBtn) {
diskZoomOutBtn.addEventListener('click', zoomOutHandler);
}
const networkZoomOutBtn = document.getElementById('networkZoomOutBtn');
if (networkZoomOutBtn) {
networkZoomOutBtn.addEventListener('click', zoomOutHandler);
}
const speedZoomOutBtn = document.getElementById('speedZoomOutBtn');
if (speedZoomOutBtn) {
speedZoomOutBtn.addEventListener('click', zoomOutHandler);
}
// 重置缩放处理函数
const resetZoomHandler = () => {
// 重置时间范围
state.currentTimeRange = '1h';
state.customStartTime = '';
state.customEndTime = '';
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
// 重置所有图表的缩放
Object.values(charts).forEach(chart => {
if (chart && typeof chart.resetZoom === 'function') {
chart.resetZoom();
}
});
};
// 为所有重置按钮添加事件监听器
const cpuResetZoomBtn = document.getElementById('cpuResetZoomBtn');
if (cpuResetZoomBtn) {
cpuResetZoomBtn.addEventListener('click', resetZoomHandler);
}
const memoryResetZoomBtn = document.getElementById('memoryResetZoomBtn');
if (memoryResetZoomBtn) {
memoryResetZoomBtn.addEventListener('click', resetZoomHandler);
}
const diskResetZoomBtn = document.getElementById('diskResetZoomBtn');
if (diskResetZoomBtn) {
diskResetZoomBtn.addEventListener('click', resetZoomHandler);
}
const networkResetZoomBtn = document.getElementById('networkResetZoomBtn');
if (networkResetZoomBtn) {
networkResetZoomBtn.addEventListener('click', resetZoomHandler);
}
const speedResetZoomBtn = document.getElementById('speedResetZoomBtn');
if (speedResetZoomBtn) {
speedResetZoomBtn.addEventListener('click', resetZoomHandler);
}
// 工具函数
function showContent(contentId) {
const element = document.getElementById(contentId);
if (element) {
element.classList.remove('hidden');
}
}
// 注意:此函数已被更新的goToServerMonitor函数替代,保留用于兼容
// 初始化图表选项卡
function initChartTabs() {
const tabs = document.querySelectorAll('.chart-tab');
if (tabs.length === 0) return;
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// 移除所有选项卡的激活状态
tabs.forEach(t => {
t.classList.remove('active', 'text-blue-600', 'border-blue-600');
t.classList.add('text-gray-600', 'border-transparent');
});
// 添加当前选项卡的激活状态
tab.classList.add('active', 'text-blue-600', 'border-blue-600');
tab.classList.remove('text-gray-600', 'border-transparent');
// 隐藏所有服务器监控页面中的图表容器
const tabId = tab.dataset.tab;
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
chartContainers.forEach(container => {
container.classList.add('hidden');
});
// 显示当前选中的图表容器
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
console.log(`Shown chart container: ${tabId}ChartContainer`);
} else {
console.error(`Chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息、系统日志和网卡详细信息
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
// 加载进程信息
loadProcessInfo();
}
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
// 加载磁盘详细信息
loadDiskDetails();
}
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
// 加载系统日志
loadSystemLogs();
}
} else if (tabId === 'network' || tabId === 'speed') {
// 显示网卡详细信息
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.remove('hidden');
// 加载网卡详细信息
loadNetworkInterfaceDetails();
}
}
// 显示/隐藏网卡选择下拉框
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (interfaceContainer) {
// 只有在点击"网络"或"网速"选项卡时,显示网卡选择下拉框
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
});
});
// 初始状态:根据当前选中的选项卡显示/隐藏网卡选择下拉框和加载数据
const activeTab = document.querySelector('.chart-tab.active');
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示当前选中的图表容器,隐藏其他服务器监控图表容器
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
chartContainers.forEach(container => {
container.classList.add('hidden');
});
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
} else {
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息、系统日志和网卡详细信息
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
loadSystemLogs();
} else if (tabId === 'network' || tabId === 'speed') {
// 显示网卡详细信息
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.remove('hidden');
}
loadNetworkInterfaceDetails();
}
// 显示/隐藏网卡选择下拉框
if (interfaceContainer) {
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
}
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
initApp();
// 初始化图表选项卡
initChartTabs();
// 添加路由监听,处理hash变化
window.addEventListener('hashchange', handleHashChange);
// 初始检查hash
handleHashChange();
// 为进程信息显示数量下拉框添加事件监听
const processPageSizeSelect = document.getElementById('processPageSize');
if (processPageSizeSelect) {
processPageSizeSelect.addEventListener('change', () => {
loadProcessInfo(1); // 重置为第一页
});
// 设置初始值
processPageSizeSelect.value = processPagination.itemsPerPage;
}
});
// 进程信息分页状态
let processPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allProcesses: [],
lastDeviceID: '', // 上次请求数据的设备ID
lastTimeRange: '', // 上次请求数据的时间范围
lastCustomStartTime: '', // 上次请求数据的自定义开始时间
lastCustomEndTime: '' // 上次请求数据的自定义结束时间
};
// 系统日志分页状态
let logPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allLogs: [],
lastDeviceID: '', // 上次请求数据的设备ID
lastTimeRange: '', // 上次请求数据的时间范围
lastCustomStartTime: '', // 上次请求数据的自定义开始时间
lastCustomEndTime: '' // 上次请求数据的自定义结束时间
};
// 加载进程信息
async function loadProcessInfo(page = 1) {
const processTableBody = document.getElementById('processTableBody');
const processPaginationContainer = document.getElementById('processPaginationContainer');
const processPageSizeSelect = document.getElementById('processPageSize');
if (!processTableBody) return;
// 获取并设置每页显示数量
if (processPageSizeSelect) {
const selectedPageSize = parseInt(processPageSizeSelect.value, 10);
if (selectedPageSize !== processPagination.itemsPerPage) {
processPagination.itemsPerPage = selectedPageSize;
// 重置为第一页
page = 1;
processPagination.currentPage = 1;
}
}
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数(与fetchMetric函数保持一致)
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
params.append('end_time', 'now()');
}
// 检查设备ID或时间范围是否变化
const timeRangeChanged =
processPagination.lastTimeRange !== state.currentTimeRange ||
processPagination.lastCustomStartTime !== state.customStartTime ||
processPagination.lastCustomEndTime !== state.customEndTime;
if (processPagination.lastDeviceID !== state.currentDeviceID || timeRangeChanged) {
// 设备ID或时间范围变化,清空旧数据
processPagination.allProcesses = [];
processPagination.totalItems = 0;
processPagination.totalPages = 0;
processPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID/时间范围变化,获取全部数据
if (processPagination.allProcesses.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/processes?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch process info');
}
const data = await response.json();
processPagination.allProcesses = data.data || [];
processPagination.totalItems = processPagination.allProcesses.length;
processPagination.totalPages = Math.ceil(processPagination.totalItems / processPagination.itemsPerPage);
// 更新上次请求数据的设备ID和时间范围
processPagination.lastDeviceID = state.currentDeviceID;
processPagination.lastTimeRange = state.currentTimeRange;
processPagination.lastCustomStartTime = state.customStartTime;
processPagination.lastCustomEndTime = state.customEndTime;
}
// 更新当前页码
processPagination.currentPage = page;
// 清空表格
processTableBody.innerHTML = '';
if (processPagination.totalItems === 0) {
// 没有进程数据,显示提示
processTableBody.innerHTML = `
| 暂无进程数据 |
`;
// 隐藏分页控件
if (processPaginationContainer) {
processPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (processPagination.currentPage - 1) * processPagination.itemsPerPage;
const endIndex = Math.min(startIndex + processPagination.itemsPerPage, processPagination.totalItems);
const paginatedProcesses = processPagination.allProcesses.slice(startIndex, endIndex);
// 填充表格数据
paginatedProcesses.forEach((proc, index) => {
const row = document.createElement('tr');
row.innerHTML = `
${proc.process_name || 'N/A'} |
${proc.username || 'N/A'} |
${proc.pid || 'N/A'} |
${proc.cpu_usage ? parseFloat(proc.cpu_usage).toFixed(2) : '0.00'} |
${proc.memory_usage ? parseFloat(proc.memory_usage).toFixed(2) : '0.00'} |
${proc.path || 'N/A'} |
${proc.cmdline || 'N/A'} |
${proc.ports || 'N/A'} |
`;
processTableBody.appendChild(row);
});
// 创建分页控件
if (processPaginationContainer) {
createPaginationControls(processPaginationContainer, processPagination, loadProcessInfo);
}
} catch (error) {
console.error('Error loading process info:', error);
processTableBody.innerHTML = `
| 加载进程信息失败 |
`;
if (processPaginationContainer) {
processPaginationContainer.innerHTML = '';
}
}
}
// 磁盘信息分页状态
let diskPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allDisks: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 网卡信息分页状态
let networkInterfacePagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allInterfaces: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 创建分页控件
function createPaginationControls(container, paginationState, loadFunction) {
// 清空容器
container.innerHTML = '';
// 如果只有一页,不需要分页
if (paginationState.totalPages <= 1) {
return;
}
const pagination = document.createElement('div');
pagination.className = 'flex justify-between items-center mt-4';
// 页码信息
const info = document.createElement('div');
info.className = 'text-sm text-gray-500';
info.textContent = `显示 ${(paginationState.currentPage - 1) * paginationState.itemsPerPage + 1} 至 ${Math.min(paginationState.currentPage * paginationState.itemsPerPage, paginationState.totalItems)} 条,共 ${paginationState.totalItems} 条`;
// 分页按钮容器
const buttons = document.createElement('div');
buttons.className = 'flex items-center space-x-2';
// 上一页按钮
const prevButton = document.createElement('button');
prevButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
prevButton.innerHTML = '';
prevButton.disabled = paginationState.currentPage === 1;
prevButton.addEventListener('click', () => {
if (paginationState.currentPage > 1) {
loadFunction(paginationState.currentPage - 1);
}
});
// 页码按钮
const pageButtons = [];
// 总是显示第一页
if (paginationState.currentPage > 3) {
pageButtons.push(1);
pageButtons.push('...');
}
// 显示当前页附近的页码
const startPage = Math.max(1, paginationState.currentPage - 2);
const endPage = Math.min(paginationState.totalPages, paginationState.currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
pageButtons.push(i);
}
// 总是显示最后一页
if (paginationState.currentPage < paginationState.totalPages - 2) {
pageButtons.push('...');
pageButtons.push(paginationState.totalPages);
}
// 创建页码按钮
pageButtons.forEach(page => {
if (page === '...') {
const ellipsis = document.createElement('span');
ellipsis.className = 'px-2 text-gray-500';
ellipsis.textContent = '...';
buttons.appendChild(ellipsis);
} else {
const pageButton = document.createElement('button');
pageButton.className = `px-3 py-1 rounded border ${page === paginationState.currentPage ? 'bg-blue-50 text-blue-600 border-blue-300' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
pageButton.textContent = page;
pageButton.addEventListener('click', () => {
loadFunction(page);
});
buttons.appendChild(pageButton);
}
});
// 下一页按钮
const nextButton = document.createElement('button');
nextButton.className = `px-3 py-1 rounded border ${paginationState.currentPage === paginationState.totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50'}`;
nextButton.innerHTML = '';
nextButton.disabled = paginationState.currentPage === paginationState.totalPages;
nextButton.addEventListener('click', () => {
if (paginationState.currentPage < paginationState.totalPages) {
loadFunction(paginationState.currentPage + 1);
}
});
// 添加到按钮容器
buttons.appendChild(prevButton);
// 跳转页面功能
const jumpContainer = document.createElement('div');
jumpContainer.className = 'flex items-center ml-4 space-x-2';
const jumpText = document.createElement('span');
jumpText.className = 'text-sm text-gray-500';
jumpText.textContent = '前往';
const jumpInput = document.createElement('input');
jumpInput.type = 'number';
jumpInput.min = 1;
jumpInput.max = paginationState.totalPages;
jumpInput.value = paginationState.currentPage;
jumpInput.className = 'w-12 px-2 py-1 text-sm border rounded text-center';
jumpInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
let page = parseInt(jumpInput.value);
if (!isNaN(page) && page >= 1 && page <= paginationState.totalPages) {
loadFunction(page);
} else {
jumpInput.value = paginationState.currentPage;
}
}
});
const totalText = document.createElement('span');
totalText.className = 'text-sm text-gray-500';
totalText.textContent = `页,共 ${paginationState.totalPages} 页`;
jumpContainer.appendChild(jumpText);
jumpContainer.appendChild(jumpInput);
jumpContainer.appendChild(totalText);
buttons.appendChild(jumpContainer);
buttons.appendChild(nextButton);
// 添加到分页容器
pagination.appendChild(info);
pagination.appendChild(buttons);
// 添加到页面
container.appendChild(pagination);
}
// 加载磁盘详细信息
async function loadDiskDetails(page = 1) {
const diskDetailsContent = document.getElementById('diskDetailsContent');
const diskPaginationContainer = document.getElementById('diskPaginationContainer');
if (!diskDetailsContent) return;
// 显示加载状态
diskDetailsContent.innerHTML = `
`;
try {
if (!state.currentDeviceID) {
diskDetailsContent.innerHTML = `
`;
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
return;
}
// 检查是否已经有缓存数据,且设备ID没有变化
const shouldReload = !diskPagination.allDisks || diskPagination.lastDeviceID !== state.currentDeviceID;
if (shouldReload) {
// 使用hardware API获取磁盘详细信息
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
if (!response.ok) {
throw new Error('Failed to fetch hardware details');
}
const data = await response.json();
let diskDetails = [];
// 提取磁盘详细信息并进行严格的数据验证
if (data.hardware && Array.isArray(data.hardware.disk_details)) {
diskDetails = data.hardware.disk_details.map(disk => {
// 确保每个磁盘信息是对象类型
if (typeof disk !== 'object' || disk === null) {
return null;
}
// 验证和转换各个字段
return {
id: typeof disk.id === 'string' ? disk.id : Math.random().toString(36).substr(2, 9),
// 确保状态是字符串类型,不是布尔值
status: typeof disk.status === 'boolean' ? 'N/A' : (disk.status || 'N/A'),
// 确保路径是字符串类型
path: typeof disk.path === 'string' ? disk.path : 'N/A',
// 确保类型是字符串类型
type: typeof disk.type === 'boolean' ? 'N/A' : (disk.type || 'N/A'),
// 确保大小是数字类型,不是布尔值
size_gb: typeof disk.size_gb === 'boolean' ? 0 : (disk.size_gb || 0),
// 确保文件系统是字符串类型
file_system: typeof disk.file_system === 'string' ? disk.file_system : 'N/A',
// 确保厂商是字符串类型
vendor: typeof disk.vendor === 'string' ? disk.vendor : 'N/A',
// 确保型号是字符串类型
model: typeof disk.model === 'string' ? disk.model : 'N/A',
// 确保接口类型是字符串类型
interface_type: typeof disk.interface_type === 'boolean' ? 'N/A' : (disk.interface_type || 'N/A')
};
}).filter(Boolean); // 过滤掉null值
}
// 更新分页信息
diskPagination.allDisks = diskDetails || [];
diskPagination.totalItems = diskDetails.length;
diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage);
diskPagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
diskPagination.currentPage = page;
// 清空内容
diskDetailsContent.innerHTML = '';
if (diskPagination.totalItems === 0) {
// 没有磁盘数据,显示提示
diskDetailsContent.innerHTML = `
`;
// 隐藏分页控件
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (diskPagination.currentPage - 1) * diskPagination.itemsPerPage;
const endIndex = Math.min(startIndex + diskPagination.itemsPerPage, diskPagination.totalItems);
const paginatedDisks = diskPagination.allDisks.slice(startIndex, endIndex);
// 创建磁盘详细信息容器
const container = document.createElement('div');
container.className = 'space-y-4';
// 创建响应式表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'overflow-x-auto';
// 创建表格
const table = document.createElement('table');
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
// 创建表头
const thead = document.createElement('thead');
thead.className = 'bg-gray-50';
thead.innerHTML = `
| 状态 |
路径 |
类型 |
大小 (GB) |
文件系统 |
厂商 |
型号 |
接口类型 |
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
tbody.className = 'divide-y divide-gray-200';
paginatedDisks.forEach(disk => {
// 跳过无效的磁盘数据
if (!disk || !disk.id) return;
// 格式化数据,空值显示为N/A
const status = disk.status || 'N/A';
const path = disk.path || 'N/A';
const type = disk.type || 'N/A';
const size = disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00';
const fileSystem = disk.file_system || 'N/A';
const vendor = disk.vendor || 'N/A';
const model = disk.model || 'N/A';
const interfaceType = disk.interface_type || 'N/A';
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
${status} |
${path} |
${type} |
${size} |
${fileSystem} |
${vendor} |
${model} |
${interfaceType} |
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
container.appendChild(tableContainer);
// 创建分页信息和控件容器
const paginationContainer = document.createElement('div');
paginationContainer.className = 'flex justify-between items-center';
// 创建每页显示数量选择
const pageSizeDiv = document.createElement('div');
pageSizeDiv.className = 'flex items-center gap-2';
pageSizeDiv.innerHTML = `
`;
// 添加每页显示数量变化事件
const pageSizeSelect = pageSizeDiv.querySelector('#diskPageSize');
pageSizeSelect.addEventListener('change', (e) => {
const newPageSize = parseInt(e.target.value, 10);
if (newPageSize !== diskPagination.itemsPerPage) {
diskPagination.itemsPerPage = newPageSize;
loadDiskDetails(1); // 重置为第一页
}
});
paginationContainer.appendChild(pageSizeDiv);
// 创建显示当前页和总条数的文本
const infoText = document.createElement('div');
infoText.className = 'text-sm text-gray-500';
infoText.textContent = `显示 ${startIndex + 1} 至 ${endIndex},共 ${diskPagination.totalItems} 条`;
paginationContainer.appendChild(infoText);
// 添加分页控件
if (diskPaginationContainer) {
createPaginationControls(diskPaginationContainer, diskPagination, loadDiskDetails);
}
container.appendChild(paginationContainer);
diskDetailsContent.appendChild(container);
} catch (error) {
console.error('Error loading disk details:', error);
diskDetailsContent.innerHTML = `
加载磁盘详细信息失败
${error.message}
`;
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
}
}
// 加载网卡详细信息
async function loadNetworkInterfaceDetails(page = 1) {
const networkInterfaceContent = document.getElementById('networkInterfaceContent');
const networkInterfacePaginationContainer = document.getElementById('networkInterfacePaginationContainer');
if (!networkInterfaceContent) return;
// 显示加载状态
networkInterfaceContent.innerHTML = `
`;
try {
if (!state.currentDeviceID) {
networkInterfaceContent.innerHTML = `
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
return;
}
// 检查是否已经有缓存数据,且设备ID没有变化
const shouldReload = !networkInterfacePagination.allInterfaces || networkInterfacePagination.lastDeviceID !== state.currentDeviceID;
if (shouldReload) {
// 使用hardware API获取网卡详细信息
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
if (!response.ok) {
throw new Error('Failed to fetch hardware details');
}
const data = await response.json();
let networkInterfaces = [];
// 提取网卡详细信息并进行严格的数据验证
if (data.hardware && Array.isArray(data.hardware.network_cards)) {
networkInterfaces = data.hardware.network_cards.map(iface => {
// 确保每个网卡信息是对象类型
if (typeof iface !== 'object' || iface === null) {
return null;
}
// 验证和转换各个字段
return {
name: typeof iface.name === 'string' ? iface.name : 'N/A',
mac: typeof iface.mac === 'string' ? iface.mac : 'N/A',
// 确保ip_addresses始终是数组类型
ip_addresses: Array.isArray(iface.ip_addresses) ? iface.ip_addresses : [],
// 确保mtu是数字或字符串,不是布尔值
mtu: typeof iface.mtu === 'boolean' ? 'N/A' : (iface.mtu || 'N/A'),
// 确保speed是数字或字符串,不是布尔值
speed: typeof iface.speed === 'boolean' ? 'N/A' : (iface.speed || 'N/A')
};
}).filter(Boolean); // 过滤掉null值
}
// 更新分页信息
networkInterfacePagination.allInterfaces = networkInterfaces || [];
networkInterfacePagination.totalItems = networkInterfaces.length;
networkInterfacePagination.totalPages = Math.ceil(networkInterfacePagination.totalItems / networkInterfacePagination.itemsPerPage);
networkInterfacePagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
networkInterfacePagination.currentPage = page;
// 清空内容
networkInterfaceContent.innerHTML = '';
if (networkInterfacePagination.totalItems === 0) {
// 没有网卡数据,显示提示
networkInterfaceContent.innerHTML = `
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (networkInterfacePagination.currentPage - 1) * networkInterfacePagination.itemsPerPage;
const endIndex = Math.min(startIndex + networkInterfacePagination.itemsPerPage, networkInterfacePagination.totalItems);
const paginatedInterfaces = networkInterfacePagination.allInterfaces.slice(startIndex, endIndex);
// 创建网卡详细信息容器
const container = document.createElement('div');
container.className = 'space-y-4';
// 创建响应式表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'overflow-x-auto';
// 创建表格
const table = document.createElement('table');
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
// 创建表头
const thead = document.createElement('thead');
thead.className = 'bg-gray-50';
thead.innerHTML = `
| 名称 |
MAC地址 |
IP地址 |
MTU |
速度 |
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
tbody.className = 'divide-y divide-gray-200';
paginatedInterfaces.forEach(iface => {
// 跳过无效的网卡数据
if (!iface) return;
// 格式化数据,空值显示为N/A
const name = iface.name || 'N/A';
const mac = iface.mac || 'N/A';
const ipAddresses = iface.ip_addresses ? iface.ip_addresses.join(', ') : 'N/A';
const mtu = iface.mtu || 'N/A';
const speed = iface.speed || 'N/A';
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
${name} |
${mac} |
${ipAddresses} |
${mtu} |
${speed} |
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
container.appendChild(tableContainer);
// 创建分页信息和控件容器
const paginationContainer = document.createElement('div');
paginationContainer.className = 'flex justify-between items-center';
// 创建每页显示数量选择
const pageSizeDiv = document.createElement('div');
pageSizeDiv.className = 'flex items-center gap-2';
pageSizeDiv.innerHTML = `
`;
// 添加每页显示数量变化事件
const pageSizeSelect = pageSizeDiv.querySelector('#networkInterfacePageSize');
pageSizeSelect.addEventListener('change', (e) => {
const newPageSize = parseInt(e.target.value, 10);
if (newPageSize !== networkInterfacePagination.itemsPerPage) {
networkInterfacePagination.itemsPerPage = newPageSize;
loadNetworkInterfaceDetails(1); // 重置为第一页
}
});
paginationContainer.appendChild(pageSizeDiv);
// 创建显示当前页和总条数的文本
const infoText = document.createElement('div');
infoText.className = 'text-sm text-gray-500';
infoText.textContent = `显示 ${startIndex + 1} 至 ${endIndex},共 ${networkInterfacePagination.totalItems} 条`;
paginationContainer.appendChild(infoText);
// 添加分页控件
if (networkInterfacePaginationContainer) {
createPaginationControls(networkInterfacePaginationContainer, networkInterfacePagination, loadNetworkInterfaceDetails);
}
container.appendChild(paginationContainer);
networkInterfaceContent.appendChild(container);
} catch (error) {
console.error('Error loading network interface details:', error);
networkInterfaceContent.innerHTML = `
加载网卡详细信息失败
${error.message}
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
}
}
// 格式化时间为易识别的日期样式
function formatLogTime(timeString) {
if (!timeString) return new Date().toLocaleString('zh-CN');
try {
const date = new Date(timeString);
if (isNaN(date.getTime())) {
// 如果时间解析失败,返回原始字符串
return timeString;
}
// 格式化为:2025-12-04 16:02:28
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
} catch (error) {
console.error('Error formatting log time:', error);
return timeString;
}
}
// 加载系统日志
async function loadSystemLogs(page = 1) {
console.log('loadSystemLogs function called');
const logTableBody = document.getElementById('logTableBody');
const logPaginationContainer = document.getElementById('logPaginationContainer');
if (!logTableBody) {
console.error('logTableBody element not found');
return;
}
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
}
// 设置时间范围参数(与fetchMetric函数保持一致)
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 使用状态中的时间范围设置
params.append('start_time', `-${state.currentTimeRange}`);
params.append('end_time', 'now()');
}
// 检查设备ID或时间范围是否变化
const timeRangeChanged =
logPagination.lastTimeRange !== state.currentTimeRange ||
logPagination.lastCustomStartTime !== state.customStartTime ||
logPagination.lastCustomEndTime !== state.customEndTime;
if (logPagination.lastDeviceID !== state.currentDeviceID || timeRangeChanged) {
// 设备ID或时间范围变化,清空旧数据
logPagination.allLogs = [];
logPagination.totalItems = 0;
logPagination.totalPages = 0;
logPagination.currentPage = 1;
}
// 如果是第一次加载或设备ID/时间范围变化,获取全部数据
if (logPagination.allLogs.length === 0) {
console.log('Fetching logs from:', `${API_BASE_URL}/metrics/logs?${params.toString()}`);
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/logs?${params.toString()}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Logs response data:', data);
// 处理后端返回格式,支持两种格式:{"logs": [...]} 和 {"data": [...], "total": ...}
logPagination.allLogs = data.data || data.logs || [];
logPagination.totalItems = data.total || logPagination.allLogs.length;
logPagination.totalPages = Math.ceil(logPagination.totalItems / logPagination.itemsPerPage);
// 更新上次请求数据的设备ID和时间范围
logPagination.lastDeviceID = state.currentDeviceID;
logPagination.lastTimeRange = state.currentTimeRange;
logPagination.lastCustomStartTime = state.customStartTime;
logPagination.lastCustomEndTime = state.customEndTime;
}
// 更新当前页码
logPagination.currentPage = page;
// 清空表格
logTableBody.innerHTML = '';
if (logPagination.totalItems === 0) {
// 没有日志数据,显示提示
logTableBody.innerHTML = `
| 暂无系统日志 |
`;
// 隐藏分页控件
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (logPagination.currentPage - 1) * logPagination.itemsPerPage;
const endIndex = Math.min(startIndex + logPagination.itemsPerPage, logPagination.totalItems);
const paginatedLogs = logPagination.allLogs.slice(startIndex, endIndex);
// 填充表格数据
paginatedLogs.forEach((log, index) => {
const row = document.createElement('tr');
row.innerHTML = `
${startIndex + index + 1} |
${log.source || 'System'} |
${formatLogTime(log.time)} |
${log.message || 'No message'} |
`;
logTableBody.appendChild(row);
});
// 创建分页控件
if (logPaginationContainer) {
createPaginationControls(logPaginationContainer, logPagination, loadSystemLogs);
}
} catch (error) {
console.error('Error loading system logs:', error);
logTableBody.innerHTML = `
| 加载系统日志失败: ${error.message} |
`;
if (logPaginationContainer) {
logPaginationContainer.innerHTML = '';
}
}
}
// 处理hash变化
function handleHashChange() {
const hash = window.location.hash;
if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
// 延迟一下,确保DOM已经渲染完成
setTimeout(() => {
// 加载当前选项卡对应的数据
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示/隐藏附加信息容器
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
// 加载系统日志
loadSystemLogs();
} else {
// 加载其他监控数据
loadMetrics();
// 根据选项卡加载附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
}
}
} else {
// 如果没有找到激活的选项卡,默认加载metrics
loadMetrics();
}
}, 300);
}
}