// API 基础 URL
const API_BASE_URL = '/api';
// 当前时间范围
let currentTimeRange = '24h';
// 自定义时间范围
let customStartTime = '';
let customEndTime = '';
// 当前选中的设备ID
let currentDeviceID = 'default';
// 当前时间区间(默认10秒)
let currentInterval = '10s';
// WebSocket连接
let ws = null;
// WebSocket重连次数
let wsReconnectAttempts = 0;
// WebSocket最大重连次数
const wsMaxReconnectAttempts = 5;
// WebSocket重连延迟(毫秒)
let wsReconnectDelay = 1000;
// 格式化字节数,根据用户要求:只在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';
}
}
}
// 图表实例对象
const charts = {};
// 初始化应用
function initApp() {
// 初始化页面切换
initPageSwitch();
// 绑定事件
bindEvents();
// 加载设备列表
loadDevices();
// 初始化WebSocket连接
initWebSocket();
// 初始化图表
initCharts();
// 初始化首页图表
initHomeCharts();
// 初始化服务器详情页面图表
initServerCharts();
// 加载首页数据
loadHomeData();
// 加载所有设备状态
loadAllDevicesStatus();
// 加载服务器数量
loadServerCount();
// 设置定时刷新(每30秒)
setInterval(loadMetrics, 30000);
setInterval(loadAllDevicesStatus, 30000);
setInterval(loadServerCount, 30000);
}
// 加载服务器数量
async function loadServerCount() {
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
if (!response.ok) {
throw new Error('Failed to fetch devices');
}
const data = await response.json();
const devices = data.devices;
// 更新服务器卡片上的服务器数量
const serverCountElement = document.getElementById('serverCount');
if (serverCountElement) {
serverCountElement.textContent = devices.length;
}
} catch (error) {
console.error('Failed to load server count:', error);
}
}
// 初始化页面切换
function initPageSwitch() {
// 页面加载时初始化页面显示
switchPage();
// 监听hashchange事件,当URL的hash值变化时触发页面切换
window.addEventListener('hashchange', switchPage);
}
// 页面切换函数
function switchPage() {
// 获取当前URL的hash值
const hash = window.location.hash;
// 隐藏所有内容区域
document.getElementById('homeContent').classList.add('hidden');
document.getElementById('dashboardContent').classList.add('hidden');
document.getElementById('serversContent').classList.add('hidden');
document.getElementById('devicesContent').classList.add('hidden');
document.getElementById('dashboardActions').classList.add('hidden');
// 根据hash值显示对应的内容区域
if (hash === '#dashboard' || hash === '#servers') {
// 显示仪表盘内容,包含所有监控服务器的列表
document.getElementById('dashboardContent').classList.remove('hidden');
document.getElementById('dashboardActions').classList.remove('hidden');
// 加载所有设备状态
loadAllDevicesStatus();
// 加载设备列表
loadDevices();
} else if (hash === '#devices') {
// 显示设备管理页面
document.getElementById('devicesContent').classList.remove('hidden');
// 加载设备管理列表
loadDeviceManagementList();
} else {
// 显示首页内容
document.getElementById('homeContent').classList.remove('hidden');
}
}
// 初始化首页图表
function initHomeCharts() {
// 近一周告警/跟进走势图表
const alarmTrendCtx = document.getElementById('alarmTrendChart');
if (alarmTrendCtx) {
new Chart(alarmTrendCtx, {
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,
},
{
label: '登记',
data: [80, 85, 90, 95, 100, 100, 100],
borderColor: '#eab308',
backgroundColor: 'rgba(234, 179, 8, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
grid: {
display: false,
},
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
},
},
},
});
}
}
// 初始化服务器详情页面图表
function initServerCharts() {
// 服务器品牌分布饼图
const serverBrandCtx = document.getElementById('serverBrandChart');
if (serverBrandCtx) {
new Chart(serverBrandCtx, {
type: 'pie',
data: {
labels: ['戴尔', '惠普', '联想', '华为'],
datasets: [
{
data: [24, 18, 12, 30],
backgroundColor: [
'#3b82f6', // 蓝色
'#10b981', // 绿色
'#f59e0b', // 黄色
'#ef4444', // 红色
],
borderWidth: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
},
tooltip: {
callbacks: {
label: function(context) {
const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
const percentage = ((context.parsed / total) * 100).toFixed(1);
return `${context.label}: ${context.parsed} 台 (${percentage}%)`;
},
},
},
},
},
});
}
// 近一周告警趋势线图
const serverAlarmTrendCtx = document.getElementById('serverAlarmTrendChart');
if (serverAlarmTrendCtx) {
new Chart(serverAlarmTrendCtx, {
type: 'line',
data: {
labels: ['11-24', '11-25', '11-26', '11-27', '11-28', '11-29', '11-30'],
datasets: [
{
label: 'CPU告警',
data: [12, 15, 18, 14, 20, 22, 25],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
},
{
label: '内存告警',
data: [8, 10, 12, 9, 14, 16, 18],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
},
{
label: '磁盘告警',
data: [5, 7, 9, 6, 11, 13, 15],
borderColor: '#eab308',
backgroundColor: 'rgba(234, 179, 8, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
},
scales: {
x: {
grid: {
display: false,
},
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
},
},
},
});
}
}
// 加载首页数据
function loadHomeData() {
// 加载业务视图数据
loadBusinessViewData();
// 加载告警列表数据
loadAlarmListData();
}
// 加载业务视图数据
function loadBusinessViewData() {
// 模拟数据
const businessData = [
{
name: 'LIS',
ip: '192.129.6.108',
os: 'Linux',
status: 'P1',
cpuAlarm: 5,
},
{
name: '互联网医院(苹果站)',
ip: '192.129.6.57',
os: 'Linux',
status: 'P3',
cpuAlarm: 3,
},
{
name: 'HIS',
ip: '192.129.51.21',
os: 'Windows',
status: 'P2',
cpuAlarm: 3,
},
{
name: 'OA',
ip: '192.129.6.42',
os: 'Linux',
status: 'P2',
cpuAlarm: 2,
},
{
name: 'HR数据库',
ip: '192.129.17.11',
os: 'Linux',
status: 'P1',
cpuAlarm: 2,
},
{
name: '测试环境',
ip: '192.129.7.199',
os: 'Linux',
status: 'P4',
cpuAlarm: 2,
},
{
name: '眼科专科',
ip: '192.129.5.70',
os: 'Linux',
status: 'P3',
cpuAlarm: 1,
},
{
name: '两腺科',
ip: '192.129.5.40',
os: 'Linux',
status: 'P3',
cpuAlarm: 1,
},
];
// 更新业务视图表格
const tableBody = document.getElementById('businessViewTableBody');
if (tableBody) {
tableBody.innerHTML = '';
businessData.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
${item.name} |
${item.ip} |
${item.os} |
${item.status} |
|
|
`;
tableBody.appendChild(row);
});
}
}
// 加载告警列表数据
function loadAlarmListData() {
// 模拟数据
const alarmData = [
{
level: 'warning',
message: 'CPU使用率超过阈值',
device: '192.129.6.108',
time: '2023-11-30 14:30:00',
},
{
level: 'error',
message: '内存使用率超过阈值',
device: '192.129.6.57',
time: '2023-11-30 14:28:00',
},
{
level: 'warning',
message: '磁盘使用率超过阈值',
device: '192.129.51.21',
time: '2023-11-30 14:25:00',
},
{
level: 'info',
message: '网络流量异常',
device: '192.129.6.42',
time: '2023-11-30 14:20:00',
},
];
// 更新告警列表
const alarmList = document.getElementById('alarmList');
if (alarmList) {
alarmList.innerHTML = '';
alarmData.forEach(item => {
const alarmItem = document.createElement('div');
alarmItem.className = 'p-3 bg-gray-50 rounded-md border-l-4 ';
// 根据告警级别设置不同的边框颜色
let borderColor = '';
let iconColor = '';
let levelText = '';
switch (item.level) {
case 'error':
borderColor = 'border-red-500';
iconColor = 'text-red-500';
levelText = '错误';
break;
case 'warning':
borderColor = 'border-yellow-500';
iconColor = 'text-yellow-500';
levelText = '警告';
break;
case 'info':
borderColor = 'border-blue-500';
iconColor = 'text-blue-500';
levelText = '信息';
break;
}
alarmItem.className += borderColor;
alarmItem.innerHTML = `
${item.message}
${item.device}
${item.time}
`;
alarmList.appendChild(alarmItem);
});
}
}
// 绑定事件
function bindEvents() {
// 设备选择事件
document.getElementById('deviceSelect').addEventListener('change', function() {
currentDeviceID = this.value;
loadMetrics();
});
// 时间设置选择器事件
document.getElementById('timeSettings').addEventListener('change', function() {
// 获取选择的值
let selectedValue = this.value;
// 转换时间范围,将1d、3d、5d转换为对应的小时数
switch (selectedValue) {
case '1d':
currentTimeRange = '24h';
break;
case '3d':
currentTimeRange = '72h';
break;
case '5d':
currentTimeRange = '120h';
break;
default:
currentTimeRange = selectedValue;
}
// 根据时间范围自动调整时间区间
if (currentTimeRange === '5s') {
// 5秒时间范围,使用5秒时间区间
currentInterval = '5s';
} else if (currentTimeRange === '10s' || currentTimeRange === '15s' || currentTimeRange === '30s') {
// 10秒到30秒时间范围,使用10秒时间区间
currentInterval = '10s';
} else if (currentTimeRange === '1h' || currentTimeRange === '3h' || currentTimeRange === '5h') {
// 中等时间范围,使用中等时间区间
currentInterval = '1m';
} else if (currentTimeRange === '24h' || currentTimeRange === '72h' || currentTimeRange === '120h') {
// 长时间范围,使用较大的时间区间
currentInterval = '1h';
} else {
// 自定义时间范围,默认使用1小时区间
currentInterval = '1h';
}
// 隐藏自定义时间范围选择器
document.getElementById('customTimeRange').classList.add('hidden');
// 加载数据
loadMetrics();
});
// 刷新按钮事件
document.getElementById('refreshBtn').addEventListener('click', function() {
loadMetrics();
loadAllDevicesStatus();
});
}
// 初始化WebSocket连接
function initWebSocket() {
// 动态生成WebSocket URL,确保协议一致性
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
console.log(`正在连接WebSocket: ${wsUrl}`);
// 创建WebSocket连接
ws = new WebSocket(wsUrl);
// 连接打开事件
ws.onopen = function() {
console.log('WebSocket连接已打开');
// 重置重连计数和延迟
wsReconnectAttempts = 0;
wsReconnectDelay = 1000;
};
// 接收消息事件
ws.onmessage = function(event) {
try {
// 解析消息
const message = JSON.parse(event.data);
// 处理不同类型的消息
switch(message.type) {
case 'metrics_update':
handleMetricsUpdate(message);
break;
default:
console.log('未知消息类型:', message.type);
}
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
// 连接关闭事件
ws.onclose = function(event) {
console.log(`WebSocket连接已关闭: ${event.code} - ${event.reason}`);
// 尝试重连
if (wsReconnectAttempts < wsMaxReconnectAttempts) {
wsReconnectAttempts++;
// 指数退避重连
wsReconnectDelay *= 2;
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts}),${wsReconnectDelay}ms后重试`);
setTimeout(initWebSocket, wsReconnectDelay);
} else {
console.error('WebSocket重连失败,已达到最大重连次数');
}
};
// 连接错误事件
ws.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
}
// 处理指标更新消息
function handleMetricsUpdate(message) {
const { device_id, metrics } = message;
// 如果是当前选中的设备,更新状态卡片和图表
if (device_id === currentDeviceID) {
// 更新状态卡片
updateStatusCardsFromWebSocket(metrics);
// 重新加载指标数据,更新图表
loadMetrics();
}
// 无论是否选中,都更新设备概览
loadAllDevicesStatus();
}
// 从WebSocket更新状态卡片
function updateStatusCardsFromWebSocket(metrics) {
// 更新CPU状态卡片
if (metrics.cpu !== undefined) {
document.getElementById('cpuValue').textContent = `${metrics.cpu.toFixed(1)}%`;
}
// 更新内存状态卡片
if (metrics.memory !== undefined) {
document.getElementById('memoryValue').textContent = `${metrics.memory.toFixed(1)}%`;
}
// 更新磁盘状态卡片
if (metrics.disk !== undefined) {
// 如果是按挂载点分组的对象
if (typeof metrics.disk === 'object' && metrics.disk !== null && !Array.isArray(metrics.disk)) {
// 计算所有挂载点的平均使用率
let totalUsage = 0;
let mountpointCount = 0;
for (const mountpoint in metrics.disk) {
const usage = metrics.disk[mountpoint];
totalUsage += usage;
mountpointCount++;
}
if (mountpointCount > 0) {
const averageUsage = totalUsage / mountpointCount;
document.getElementById('diskValue').textContent = `${averageUsage.toFixed(1)}%`;
}
} else {
// 兼容旧格式,直接使用数值
document.getElementById('diskValue').textContent = `${metrics.disk.toFixed(1)}%`;
}
}
// 更新网络状态卡片
if (metrics.network) {
const sentMB = (metrics.network.bytes_sent / (1024 * 1024)).toFixed(2);
const receivedMB = (metrics.network.bytes_received / (1024 * 1024)).toFixed(2);
document.getElementById('networkSent').textContent = sentMB;
document.getElementById('networkReceived').textContent = receivedMB;
document.getElementById('networkValue').textContent = `${Math.max(parseFloat(sentMB), parseFloat(receivedMB)).toFixed(2)} MB/s`;
}
}
// 加载设备列表
async function loadDevices() {
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
if (!response.ok) {
throw new Error('Failed to fetch devices');
}
const data = await response.json();
const devices = data.devices;
// 更新设备选择器
const deviceSelect = document.getElementById('deviceSelect');
deviceSelect.innerHTML = '';
// 添加设备选项
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.id;
option.textContent = device.name || device.id;
if (device.id === currentDeviceID) {
option.selected = true;
}
deviceSelect.appendChild(option);
});
// 如果设备列表为空,显示提示信息
if (devices.length === 0) {
console.warn('No devices available');
// 可以添加显示提示信息的逻辑
return;
}
// 如果没有选中的设备(即currentDeviceID不存在于设备列表中),则自动选择第一个设备
if (!deviceSelect.value) {
currentDeviceID = devices[0].id;
deviceSelect.value = currentDeviceID;
}
// 加载数据
loadMetrics();
} catch (error) {
console.error('Failed to load devices:', error);
// 显示友好的错误提示
alert('加载设备列表失败,请稍后重试');
}
}
// 加载所有监控指标
async function loadMetrics() {
try {
// 检查currentDeviceID是否有效
if (!currentDeviceID || currentDeviceID === 'default') {
console.warn('No valid device selected, skipping metrics load');
return;
}
// 并行加载所有指标
const [cpuData, memoryData, diskData, networkSumData, networkRateData] = await Promise.all([
fetchMetric('cpu'),
fetchMetric('memory'),
fetchMetric('disk'),
fetchMetric('network', 'sum'), // 获取总流量数据
fetchMetric('network', 'average') // 获取速率数据
]);
// 更新状态卡片(同时使用总流量和速率数据)
updateStatusCards(cpuData, memoryData, diskData, networkSumData, networkRateData);
// 更新图表
updateCharts(cpuData, memoryData, diskData, networkSumData, networkRateData);
} catch (error) {
console.error('Failed to load metrics:', error);
// 显示友好的错误提示
alert('加载监控数据失败,请稍后重试');
}
}
// 获取单个指标数据
async function fetchMetric(metricType, aggregation = 'average') {
// 构建查询参数
const params = new URLSearchParams();
// 设置设备ID
params.append('device_id', currentDeviceID);
// 设置时间范围参数
if (currentTimeRange === 'custom') {
if (customStartTime && customEndTime) {
params.append('start_time', customStartTime);
params.append('end_time', customEndTime);
}
} else {
params.append('start_time', `-${currentTimeRange}`);
params.append('end_time', 'now()');
}
// 设置聚合方式
params.append('aggregation', aggregation);
// 设置时间区间
params.append('interval', currentInterval);
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/${metricType}?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${metricType} metrics`);
}
const data = await response.json();
return data.data;
}
// 更新状态卡片
function updateStatusCards(cpuData, memoryData, diskData, networkSumData, networkRateData) {
// 更新CPU状态卡片
if (cpuData && cpuData.length > 0) {
const latestCPU = cpuData[cpuData.length - 1].value;
document.getElementById('cpuValue').textContent = `${latestCPU.toFixed(1)}%`;
}
// 更新内存状态卡片
if (memoryData && memoryData.length > 0) {
const latestMemory = memoryData[memoryData.length - 1].value;
document.getElementById('memoryValue').textContent = `${latestMemory.toFixed(1)}%`;
}
// 更新磁盘状态卡片
if (diskData && typeof diskData === 'object') {
// 计算所有挂载点的平均使用率
let totalUsage = 0;
let mountpointCount = 0;
// 如果是按挂载点分组的map
if (diskData.constructor === Object && !Array.isArray(diskData)) {
for (const mountpoint in diskData) {
const data = diskData[mountpoint];
if (data && data.length > 0) {
const latestValue = data[data.length - 1].value;
totalUsage += latestValue;
mountpointCount++;
}
}
} else if (Array.isArray(diskData) && diskData.length > 0) {
// 兼容旧格式,直接使用第一个值
const latestDisk = diskData[diskData.length - 1].value;
document.getElementById('diskValue').textContent = `${latestDisk.toFixed(1)}%`;
return;
}
// 计算平均值
if (mountpointCount > 0) {
const averageUsage = totalUsage / mountpointCount;
document.getElementById('diskValue').textContent = `${averageUsage.toFixed(1)}%`;
}
}
// 更新网络状态卡片
if (networkSumData && networkSumData.sent && networkSumData.received &&
networkRateData && networkRateData.sent && networkRateData.received) {
// 获取最新的总流量数据
const latestSentSum = networkSumData.sent[networkSumData.sent.length - 1].value;
const latestReceivedSum = networkSumData.received[networkSumData.received.length - 1].value;
// 计算当前时间段内的平均速率
const calculateAverageRate = (data) => {
if (data.length === 0) return 0;
let sum = 0;
for (const item of data) {
sum += item.y;
}
return sum / data.length;
};
const avgSentRate = calculateAverageRate(networkRateData.sent);
const avgReceivedRate = calculateAverageRate(networkRateData.received);
// 格式化总流量和速率
const sentSumFormatted = formatBytes(latestSentSum, 2, false);
const receivedSumFormatted = formatBytes(latestReceivedSum, 2, false);
const sentRateFormatted = formatBytes(avgSentRate * 1024 * 1024, 2, true);
const receivedRateFormatted = formatBytes(avgReceivedRate * 1024 * 1024, 2, true);
// 大字区域显示总流量(显示较大的那个值)
const maxSum = Math.max(latestSentSum, latestReceivedSum);
document.getElementById('networkValue').textContent = formatBytes(maxSum, 2, false);
// 小字区域显示当前时间段内的平均速率
document.getElementById('networkSent').textContent = sentRateFormatted;
document.getElementById('networkReceived').textContent = receivedRateFormatted;
}
}
// 初始化图表
function initCharts() {
// 图表配置
const chartConfig = {
type: 'line',
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
// 根据数据集标签判断是流量还是速率
const isRate = label.includes('速率');
// 注意:context.parsed.y 已经是转换为MB的值,所以需要转换回bytes
label += formatBytes(context.parsed.y * 1024 * 1024, 2, isRate);
}
return label;
}
}
},
},
scales: {
x: {
type: 'category',
grid: {
display: false,
},
ticks: {
maxRotation: 0, // 水平显示标签,减少占用空间
minRotation: 0,
maxTicksLimit: 8, // 减少最大刻度数量,避免过于密集
autoSkip: true, // 自动跳过标签
autoSkipPadding: 10, // 标签之间的最小间距
callback: function(value, index, values) {
// 进一步优化,只显示部分标签
// 对于长时间范围,只显示每n个标签中的一个
if (currentTimeRange === '24h' || currentTimeRange === '72h' || currentTimeRange === '120h') {
// 对于天级时间范围,每4个标签显示一个
return index % 4 === 0 ? value : '';
} else if (currentTimeRange === '1h' || currentTimeRange === '3h' || currentTimeRange === '5h') {
// 对于小时级时间范围,每2个标签显示一个
return index % 2 === 0 ? value : '';
}
// 短时间范围,显示所有标签
return value;
}
},
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)',
},
ticks: {
callback: function(value) {
// 根据数据集标签判断是流量还是速率
const dataset = this.chart.data.datasets[0];
const isRate = dataset.label.includes('速率');
// 注意:value 已经是转换为MB的值,所以需要转换回bytes
return formatBytes(value * 1024 * 1024, 2, isRate);
},
maxTicksLimit: 8, // 限制y轴刻度数量
}
},
},
animation: {
duration: 750,
},
},
};
// CPU 图表
charts.cpu = new Chart(
document.getElementById('cpuChart'),
{
...chartConfig,
data: {
datasets: [{
label: 'CPU 使用率',
data: [],
borderColor: '#3b82f6', // 蓝色
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}],
},
options: {
...chartConfig.options,
scales: {
...chartConfig.options.scales,
y: {
...chartConfig.options.scales.y,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
},
},
},
},
},
}
);
// 内存 图表
charts.memory = new Chart(
document.getElementById('memoryChart'),
{
...chartConfig,
data: {
datasets: [{
label: '内存使用率',
data: [],
borderColor: '#10b981', // 绿色
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}],
},
options: {
...chartConfig.options,
scales: {
...chartConfig.options.scales,
y: {
...chartConfig.options.scales.y,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
},
},
},
},
},
}
);
// 磁盘 图表,支持多个挂载点
charts.disk = new Chart(
document.getElementById('diskChart'),
{
...chartConfig,
data: {
datasets: [],
},
options: {
...chartConfig.options,
scales: {
...chartConfig.options.scales,
y: {
...chartConfig.options.scales.y,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
},
},
},
},
},
}
);
// 网络 图表
charts.network = new Chart(
document.getElementById('networkChart'),
{
...chartConfig,
data: {
datasets: [{
label: '发送流量',
data: [],
borderColor: '#8b5cf6', // 紫色
backgroundColor: 'rgba(139, 92, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}, {
label: '接收流量',
data: [],
borderColor: '#ec4899', // 粉色
backgroundColor: 'rgba(236, 72, 153, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}],
},
}
);
// 网络速率图表
charts.networkRate = new Chart(
document.getElementById('networkRateChart'),
{
...chartConfig,
data: {
datasets: [{
label: '发送速率',
data: [],
borderColor: '#8b5cf6', // 紫色
backgroundColor: 'rgba(139, 92, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}, {
label: '接收速率',
data: [],
borderColor: '#ec4899', // 粉色
backgroundColor: 'rgba(236, 72, 153, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
}],
},
}
);
}
// 格式化时间,根据时间范围动态调整格式
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');
// 根据当前时间范围动态调整时间格式
if (currentTimeRange === '5s' || currentTimeRange === '10s' || currentTimeRange === '15s' || currentTimeRange === '30s') {
// 短时间范围(秒级),显示完整时间格式
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} else if (currentTimeRange === '1h' || currentTimeRange === '3h' || currentTimeRange === '5h') {
// 中等时间范围(小时级),显示到分钟,不显示秒
return `${year}-${month}-${day} ${hours}:${minutes}`;
} else {
// 长时间范围(天级),显示到小时,不显示分钟和秒
return `${year}-${month}-${day} ${hours}:00`;
}
}
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkSumData, networkRateData) {
// 数据点排序函数
const sortDataByTime = (data) => {
return [...data].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
};
// 更新CPU图表
if (cpuData && cpuData.length > 0) {
const sortedData = sortDataByTime(cpuData);
charts.cpu.data.datasets[0].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.cpu.update();
}
// 更新内存图表
if (memoryData && memoryData.length > 0) {
const sortedData = sortDataByTime(memoryData);
charts.memory.data.datasets[0].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.memory.update();
}
// 更新磁盘图表,支持多个挂载点
if (diskData && typeof diskData === 'object') {
// 定义不同的颜色,用于区分不同的挂载点
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;
for (const [mountpoint, data] of Object.entries(diskData)) {
if (data && data.length > 0) {
// 获取颜色
const color = colors[colorIndex % colors.length];
colorIndex++;
// 排序数据
const sortedData = sortDataByTime(data);
// 创建数据集
const dataset = {
label: `磁盘使用率 (${mountpoint})`,
data: sortedData.map(item => ({
x: formatTime(item.time),
y: item.value
})),
borderColor: color.border,
backgroundColor: color.background,
borderWidth: 2,
fill: true,
tension: 0.4,
};
// 添加数据集
charts.disk.data.datasets.push(dataset);
}
}
// 更新图表
charts.disk.update();
}
// 更新网络流量趋势图表(总流量)
if (networkSumData && networkSumData.sent && networkSumData.received) {
if (networkSumData.sent.length > 0) {
const sortedData = sortDataByTime(networkSumData.sent);
charts.network.data.datasets[0].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB,图表会自动格式化
}));
}
if (networkSumData.received.length > 0) {
const sortedData = sortDataByTime(networkSumData.received);
charts.network.data.datasets[1].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB,图表会自动格式化
}));
}
charts.network.update();
}
// 更新网络速率图表
if (networkRateData && networkRateData.sent && networkRateData.received) {
if (networkRateData.sent.length > 0) {
const sortedData = sortDataByTime(networkRateData.sent);
charts.networkRate.data.datasets[0].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s,图表会自动格式化
}));
}
if (networkRateData.received.length > 0) {
const sortedData = sortDataByTime(networkRateData.received);
charts.networkRate.data.datasets[1].data = sortedData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s,图表会自动格式化
}));
}
charts.networkRate.update();
}
}
// 更新图表时间轴配置 - 已废弃,使用time轴后不再需要
function updateChartTimeRange(timeRange) {
// 对于time scale,Chart.js会自动处理时间轴配置
// 此函数保留用于向后兼容
}
// 加载所有设备状态概览
async function loadAllDevicesStatus() {
try {
const response = await fetch(`${API_BASE_URL}/devices/status`);
if (!response.ok) {
throw new Error('Failed to fetch devices status');
}
const data = await response.json();
const devices = data.devices;
// 更新设备概览表格
const devicesTableBody = document.getElementById('devicesTableBody');
if (devicesTableBody) {
devicesTableBody.innerHTML = '';
devices.forEach(device => {
const row = document.createElement('tr');
// 优先显示设备名称,如果没有则显示设备IP地址
const displayName = device.name || device.ip || device.device_id;
row.innerHTML = `
${displayName} |
${device.device_id} |
${device.status.cpu ? device.status.cpu.toFixed(1) : 'N/A'}% |
${device.status.memory ? device.status.memory.toFixed(1) : 'N/A'}% |
${device.status.disk ? device.status.disk.toFixed(1) : 'N/A'}% |
${device.status.network_sent ? (device.status.network_sent / (1024 * 1024)).toFixed(2) : 'N/A'} MB/s |
${device.status.network_received ? (device.status.network_received / (1024 * 1024)).toFixed(2) : 'N/A'} MB/s |
在线
|
`;
devicesTableBody.appendChild(row);
});
}
} catch (error) {
console.error('Failed to load devices status:', error);
}
}
// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', initApp);
// 设备管理相关功能
// 加载设备管理列表
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;
// 更新设备管理表格
const deviceManagementTableBody = document.getElementById('deviceManagementTableBody');
if (deviceManagementTableBody) {
deviceManagementTableBody.innerHTML = '';
devices.forEach(device => {
const row = document.createElement('tr');
// 格式化创建时间
const createdAt = new Date(device.created_at * 1000).toLocaleString();
row.innerHTML = `
${device.name} |
${device.id} |
${device.ip || 'N/A'} |
|
${getStatusText(device.status)}
|
${createdAt} |
|
`;
deviceManagementTableBody.appendChild(row);
});
// 绑定设备操作事件(包括复制token)
bindDeviceActionEvents();
}
} catch (error) {
console.error('Failed to load device management list:', error);
}
}
// 根据状态获取颜色类
function getStatusColorClass(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';
}
}
// 根据状态获取文本
function getStatusText(status) {
switch (status) {
case 'active':
return '活跃';
case 'inactive':
return '非活跃';
case 'offline':
return '离线';
default:
return status;
}
}
// 绑定设备操作事件
function bindDeviceActionEvents() {
// 编辑按钮事件
document.querySelectorAll('.edit-device-btn').forEach(btn => {
btn.addEventListener('click', function() {
const deviceId = this.getAttribute('data-id');
editDevice(deviceId);
});
});
// 删除按钮事件
document.querySelectorAll('.delete-device-btn').forEach(btn => {
btn.addEventListener('click', function() {
const deviceId = this.getAttribute('data-id');
deleteDevice(deviceId);
});
});
// 复制token按钮事件
document.querySelectorAll('.copy-token-btn').forEach(btn => {
btn.addEventListener('click', function() {
const token = this.getAttribute('data-token');
navigator.clipboard.writeText(token).then(() => {
// 显示复制成功提示
const originalIcon = this.innerHTML;
this.innerHTML = '';
setTimeout(() => {
this.innerHTML = originalIcon;
}, 1000);
}).catch(err => {
console.error('Failed to copy token:', err);
});
});
});
}
// 编辑设备
async function editDevice(deviceId) {
try {
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!response.ok) {
throw new Error('Failed to fetch device');
}
const data = await response.json();
const device = data.device;
// 填充表单
document.getElementById('deviceId').value = device.id;
document.getElementById('deviceName').value = device.name;
document.getElementById('deviceIdInput').value = device.id;
document.getElementById('deviceIp').value = device.ip || '';
document.getElementById('deviceStatus').value = device.status;
// 显示模态框
document.getElementById('modalTitle').textContent = '编辑设备';
document.getElementById('deviceModal').classList.remove('hidden');
} catch (error) {
console.error('Failed to edit device:', error);
alert('编辑设备失败: ' + error.message);
}
}
// 删除设备
async function deleteDevice(deviceId) {
if (confirm('确定要删除该设备吗?')) {
try {
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete device');
}
// 重新加载设备列表
loadDeviceManagementList();
} catch (error) {
console.error('Failed to delete device:', error);
alert('删除设备失败: ' + error.message);
}
}
}
// 初始化设备管理功能
function initDeviceManagement() {
// 添加设备按钮事件
document.getElementById('addDeviceBtn').addEventListener('click', function() {
// 重置表单
document.getElementById('deviceForm').reset();
document.getElementById('deviceId').value = '';
document.getElementById('deviceStatus').value = 'inactive';
// 显示模态框
document.getElementById('modalTitle').textContent = '添加设备';
document.getElementById('deviceModal').classList.remove('hidden');
});
// 取消按钮事件
document.getElementById('cancelBtn').addEventListener('click', function() {
// 隐藏模态框
document.getElementById('deviceModal').classList.add('hidden');
});
// 表单提交事件
document.getElementById('deviceForm').addEventListener('submit', async function(e) {
e.preventDefault();
const deviceId = document.getElementById('deviceId').value;
const name = document.getElementById('deviceName').value;
const id = document.getElementById('deviceIdInput').value;
const ip = document.getElementById('deviceIp').value;
const status = document.getElementById('deviceStatus').value;
const deviceData = {
name,
id,
ip,
status
};
try {
let response;
if (deviceId) {
// 更新设备
response = await fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(deviceData)
});
} else {
// 添加设备
response = await fetch(`${API_BASE_URL}/devices/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(deviceData)
});
}
if (!response.ok) {
throw new Error('Failed to save device');
}
// 隐藏模态框
document.getElementById('deviceModal').classList.add('hidden');
// 重新加载设备列表
loadDeviceManagementList();
} catch (error) {
console.error('Failed to save device:', error);
alert('保存设备失败: ' + error.message);
}
});
// 状态过滤事件
document.getElementById('statusFilter').addEventListener('change', function() {
filterDevices();
});
// 搜索设备事件
document.getElementById('searchDevice').addEventListener('input', function() {
filterDevices();
});
}
// 过滤设备
function filterDevices() {
const statusFilter = document.getElementById('statusFilter').value;
const searchTerm = document.getElementById('searchDevice').value.toLowerCase();
const rows = document.querySelectorAll('#deviceManagementTableBody tr');
rows.forEach(row => {
// 状态列的索引从4变成了5,因为添加了认证令牌列
const status = row.querySelector('td:nth-child(5) span').textContent.toLowerCase();
const name = row.querySelector('td:nth-child(1)').textContent.toLowerCase();
const id = row.querySelector('td:nth-child(2)').textContent.toLowerCase();
let showRow = true;
// 状态过滤
if (statusFilter !== 'all') {
const statusText = getStatusText(statusFilter);
if (status !== statusText.toLowerCase()) {
showRow = false;
}
}
// 搜索过滤
if (searchTerm) {
if (!name.includes(searchTerm) && !id.includes(searchTerm)) {
showRow = false;
}
}
// 显示或隐藏行
if (showRow) {
row.classList.remove('hidden');
} else {
row.classList.add('hidden');
}
});
}
// 初始化设备管理功能
initDeviceManagement();