Files
monitor/backend/static_backup/static/js/app.js
2025-12-02 23:20:10 +08:00

1611 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API 基础 URL
const API_BASE_URL = '/api';
// 当前时间范围
let 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;
// 图表实例对象
const charts = {};
// 格式化字节数根据用户要求只在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';
}
}
}
// 格式化时间,根据时间范围动态调整格式
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 initApp() {
// 初始化页面切换
initPageSwitch();
// 绑定事件
bindEvents();
// 加载设备列表
loadDevices();
// 初始化WebSocket连接
initWebSocket();
// 初始化图表
initCharts();
// 初始化首页图表
initHomeCharts();
// 加载首页数据
loadHomeData();
// 加载服务器数量
loadServerCount();
// 设置定时刷新每30秒
setInterval(loadMetrics, 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;
}
// 加载所有服务器列表
loadAllServers();
} catch (error) {
console.error('Failed to load server count:', error);
// 使用模拟数据
const serverCountElement = document.getElementById('serverCount');
if (serverCountElement) {
serverCountElement.textContent = '2';
}
// 加载模拟服务器列表
loadMockServers();
}
}
// 初始化页面切换
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('serversContent').classList.add('hidden');
document.getElementById('serverMonitorContent').classList.add('hidden');
document.getElementById('devicesContent').classList.add('hidden');
// 根据hash值显示对应的内容区域
if (hash === '#servers') {
// 显示所有服务器列表
document.getElementById('serversContent').classList.remove('hidden');
// 加载所有服务器列表
loadAllServers();
} else if (hash === '#serverMonitor') {
// 显示服务器监控界面
document.getElementById('serverMonitorContent').classList.remove('hidden');
// 加载设备列表
loadDevices();
// 加载监控数据
loadMetrics();
} 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 loadHomeData() {
// 加载业务视图数据
loadBusinessViewData();
// 加载告警列表数据
loadAlarmListData();
}
// 加载业务视图数据
function loadBusinessViewData() {
// 模拟数据
const businessData = [
{
name: 'LIS',
ip: '192.129.6.108',
os: 'Linux',
status: 'P1',
cpuAlarm: 5,
deviceId: 'device-1'
},
{
name: '互联网医院(苹果站)',
ip: '192.129.6.57',
os: 'Linux',
status: 'P3',
cpuAlarm: 3,
deviceId: 'device-2'
},
{
name: 'HIS',
ip: '192.129.51.21',
os: 'Windows',
status: 'P2',
cpuAlarm: 3,
deviceId: 'device-3'
},
{
name: 'OA',
ip: '192.129.6.42',
os: 'Linux',
status: 'P2',
cpuAlarm: 2,
deviceId: 'device-4'
},
{
name: 'HR数据库',
ip: '192.129.17.11',
os: 'Linux',
status: 'P1',
cpuAlarm: 2,
deviceId: 'device-5'
},
{
name: '测试环境',
ip: '192.129.7.199',
os: 'Linux',
status: 'P4',
cpuAlarm: 2,
deviceId: 'device-6'
},
{
name: '眼科专科',
ip: '192.129.5.70',
os: 'Linux',
status: 'P3',
cpuAlarm: 1,
deviceId: 'device-7'
},
{
name: '两腺科',
ip: '192.129.5.40',
os: 'Linux',
status: 'P3',
cpuAlarm: 1,
deviceId: 'device-8'
},
];
// 更新业务视图表格
const tableBody = document.getElementById('businessViewTableBody');
if (tableBody) {
tableBody.innerHTML = '';
businessData.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600" onclick="goToServerMonitor('${item.deviceId}')">${item.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.ip}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.os}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.status}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="flex items-center gap-1">
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="w-32 bg-gray-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: ${Math.min(item.cpuAlarm * 20, 100)}%"></div>
</div>
</td>
`;
tableBody.appendChild(row);
});
}
}
// 加载告警列表数据
function loadAlarmListData() {
// 模拟数据
const alarmData = [
{
level: 'warning',
message: 'CPU使用率超过阈值',
device: '192.129.6.108',
time: '2023-11-30 14:30:00',
},
{
level: 'error',
message: '内存使用率超过阈值',
device: '192.129.6.57',
time: '2023-11-30 14:28:00',
},
{
level: 'warning',
message: '磁盘使用率超过阈值',
device: '192.129.51.21',
time: '2023-11-30 14:25:00',
},
{
level: 'info',
message: '网络流量异常',
device: '192.129.6.42',
time: '2023-11-30 14:20:00',
},
];
// 更新告警列表
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 = `
<div class="flex justify-between items-start">
<div class="flex items-center gap-2">
<i class="fa fa-exclamation-circle ${iconColor}"></i>
<div>
<p class="text-sm font-medium text-gray-900">${item.message}</p>
<p class="text-xs text-gray-500">${item.device}</p>
</div>
</div>
<div class="text-xs text-gray-500">${item.time}</div>
</div>
`;
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();
});
}
// 初始化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();
}
}
// 从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');
if (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');
// 使用模拟数据
const mockDevices = [
{ id: 'default', name: '默认设备' },
{ id: 'device-1', name: '服务器1' },
{ id: 'device-2', name: '服务器2' }
];
mockDevices.forEach(device => {
const option = document.createElement('option');
option.value = device.id;
option.textContent = device.name;
if (device.id === currentDeviceID) {
option.selected = true;
}
deviceSelect.appendChild(option);
});
}
// 如果没有选中的设备即currentDeviceID不存在于设备列表中则自动选择第一个设备
if (!deviceSelect.value) {
currentDeviceID = devices[0]?.id || 'default';
deviceSelect.value = currentDeviceID;
}
}
} catch (error) {
console.error('Failed to load devices:', error);
// 使用模拟数据
const deviceSelect = document.getElementById('deviceSelect');
if (deviceSelect) {
deviceSelect.innerHTML = '';
const mockDevices = [
{ id: 'default', name: '默认设备' },
{ id: 'device-1', name: '服务器1' },
{ id: 'device-2', name: '服务器2' }
];
mockDevices.forEach(device => {
const option = document.createElement('option');
option.value = device.id;
option.textContent = device.name;
if (device.id === currentDeviceID) {
option.selected = true;
}
deviceSelect.appendChild(option);
});
}
}
}
// 加载所有监控指标
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);
// 使用模拟数据
loadMockMetrics();
}
}
// 获取单个指标数据
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 图表
const cpuCtx = document.getElementById('cpuChart');
if (cpuCtx) {
charts.cpu = new Chart(cpuCtx, {
...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 + '%';
},
},
},
},
},
});
}
// 内存 图表
const memoryCtx = document.getElementById('memoryChart');
if (memoryCtx) {
charts.memory = new Chart(memoryCtx, {
...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 + '%';
},
},
},
},
},
});
}
// 磁盘 图表,支持多个挂载点
const diskCtx = document.getElementById('diskChart');
if (diskCtx) {
charts.disk = new Chart(diskCtx, {
...chartConfig,
data: {
datasets: [],
},
options: {
...chartConfig.options,
scales: {
...chartConfig.options.scales,
y: {
...chartConfig.options.scales.y,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
},
},
},
},
},
});
}
// 网络 图表
const networkCtx = document.getElementById('networkChart');
if (networkCtx) {
charts.network = new Chart(networkCtx, {
...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 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();
}
}
// 加载所有设备状态概览
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 = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${displayName}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.device_id}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.status.cpu ? device.status.cpu.toFixed(1) : 'N/A'}%</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.status.memory ? device.status.memory.toFixed(1) : 'N/A'}%</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.status.disk ? device.status.disk.toFixed(1) : 'N/A'}%</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.status.network_sent ? (device.status.network_sent / (1024 * 1024)).toFixed(2) : 'N/A'} MB/s</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.status.network_received ? (device.status.network_received / (1024 * 1024)).toFixed(2) : 'N/A'} MB/s</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
在线
</span>
</td>
`;
devicesTableBody.appendChild(row);
});
}
} catch (error) {
console.error('Failed to load devices status:', error);
}
}
// 加载所有服务器列表
async function loadAllServers() {
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 serversGrid = document.getElementById('serversGrid');
if (serversGrid) {
serversGrid.innerHTML = '';
devices.forEach(device => {
const serverCard = createServerCard(device);
serversGrid.appendChild(serverCard);
});
}
} catch (error) {
console.error('Failed to load servers:', error);
// 使用模拟数据
loadMockServers();
}
}
// 创建服务器卡片
function createServerCard(device) {
const card = document.createElement('div');
card.className = 'status-card bg-white rounded-xl shadow-lg p-6 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 cursor-pointer';
card.onclick = () => goToServerMonitor(device.id);
card.innerHTML = `
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-gray-900">${device.name || device.id}</h3>
<p class="text-sm text-gray-500">${device.ip || 'N/A'}</p>
</div>
<div class="px-3 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
在线
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<p class="text-xs text-gray-500">CPU使用率</p>
<p class="text-lg font-semibold text-gray-900">0.0%</p>
</div>
<div>
<p class="text-xs text-gray-500">内存使用率</p>
<p class="text-lg font-semibold text-gray-900">0.0%</p>
</div>
<div>
<p class="text-xs text-gray-500">磁盘使用率</p>
<p class="text-lg font-semibold text-gray-900">0.0%</p>
</div>
<div>
<p class="text-xs text-gray-500">网络流量</p>
<p class="text-lg font-semibold text-gray-900">0.0 MB/s</p>
</div>
</div>
<div class="flex justify-end">
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
查看详情
</button>
</div>
`;
return card;
}
// 加载模拟服务器数据
function loadMockServers() {
const mockServers = [
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100' },
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101' }
];
// 更新服务器网格
const serversGrid = document.getElementById('serversGrid');
if (serversGrid) {
serversGrid.innerHTML = '';
mockServers.forEach(device => {
const serverCard = createServerCard(device);
serversGrid.appendChild(serverCard);
});
}
}
// 加载模拟监控数据
function loadMockMetrics() {
// 生成模拟数据
const now = new Date();
const cpuData = [];
const memoryData = [];
const diskData = {};
const networkSumData = { sent: [], received: [] };
// 生成过去24小时的数据每小时一个点
for (let i = 23; i >= 0; i--) {
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
const timeStr = time.toISOString();
// CPU数据
cpuData.push({ time: timeStr, value: Math.random() * 100 });
// 内存数据
memoryData.push({ time: timeStr, value: Math.random() * 100 });
// 磁盘数据
diskData['/'] = diskData['/'] || [];
diskData['/'].push({ time: timeStr, value: Math.random() * 100 });
diskData['/boot'] = diskData['/boot'] || [];
diskData['/boot'].push({ time: timeStr, value: Math.random() * 100 });
diskData['/data'] = diskData['/data'] || [];
diskData['/data'].push({ time: timeStr, value: Math.random() * 100 });
diskData['/var'] = diskData['/var'] || [];
diskData['/var'].push({ time: timeStr, value: Math.random() * 100 });
// 网络数据
networkSumData.sent.push({ time: timeStr, value: Math.random() * 1024 * 1024 * 1024 }); // 随机值,单位为字节
networkSumData.received.push({ time: timeStr, value: Math.random() * 1024 * 1024 * 1024 }); // 随机值,单位为字节
}
// 更新状态卡片
updateStatusCards(cpuData, memoryData, diskData, networkSumData, { sent: [], received: [] });
// 更新图表
updateCharts(cpuData, memoryData, diskData, networkSumData, { sent: [], received: [] });
}
// 跳转到服务器监控界面
function goToServerMonitor(deviceId) {
currentDeviceID = deviceId;
window.location.hash = '#serverMonitor';
}
// 加载设备管理列表
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 = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.id}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-sm text-gray-500 truncate max-w-[150px]">${device.token}</span>
<button class="ml-2 text-blue-600 hover:text-blue-900 copy-token-btn" data-token="${device.token}" title="复制token">
<i class="fa fa-copy"></i>
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColorClass(device.status)}">
${getStatusText(device.status)}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${createdAt}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button class="text-blue-600 hover:text-blue-900 mr-3 edit-device-btn" data-id="${device.id}">编辑</button>
<button class="text-red-600 hover:text-red-900 delete-device-btn" data-id="${device.id}">删除</button>
</td>
`;
deviceManagementTableBody.appendChild(row);
});
// 绑定设备操作事件包括复制token
bindDeviceActionEvents();
}
} catch (error) {
console.error('Failed to load device management list:', error);
// 使用模拟数据
loadMockDeviceManagementList();
}
}
// 加载模拟设备管理数据
function loadMockDeviceManagementList() {
const mockDevices = [
{
id: 'device-1',
name: '服务器1',
ip: '192.168.1.100',
token: 'token-1',
status: 'active',
created_at: Math.floor(Date.now() / 1000) - 86400
},
{
id: 'device-2',
name: '服务器2',
ip: '192.168.1.101',
token: 'token-2',
status: 'active',
created_at: Math.floor(Date.now() / 1000) - 172800
}
];
// 更新设备管理表格
const deviceManagementTableBody = document.getElementById('deviceManagementTableBody');
if (deviceManagementTableBody) {
deviceManagementTableBody.innerHTML = '';
mockDevices.forEach(device => {
const row = document.createElement('tr');
// 格式化创建时间
const createdAt = new Date(device.created_at * 1000).toLocaleString();
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.id}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-sm text-gray-500 truncate max-w-[150px]">${device.token}</span>
<button class="ml-2 text-blue-600 hover:text-blue-900 copy-token-btn" data-token="${device.token}" title="复制token">
<i class="fa fa-copy"></i>
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColorClass(device.status)}">
${getStatusText(device.status)}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${createdAt}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button class="text-blue-600 hover:text-blue-900 mr-3 edit-device-btn" data-id="${device.id}">编辑</button>
<button class="text-red-600 hover:text-red-900 delete-device-btn" data-id="${device.id}">删除</button>
</td>
`;
deviceManagementTableBody.appendChild(row);
});
// 绑定设备操作事件包括复制token
bindDeviceActionEvents();
}
}
// 根据状态获取颜色类
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(e) {
e.stopPropagation();
const deviceId = this.getAttribute('data-id');
editDevice(deviceId);
});
});
// 删除按钮事件
document.querySelectorAll('.delete-device-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const deviceId = this.getAttribute('data-id');
deleteDevice(deviceId);
});
});
// 复制token按钮事件
document.querySelectorAll('.copy-token-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const token = this.getAttribute('data-token');
navigator.clipboard.writeText(token).then(() => {
// 显示复制成功提示
const originalIcon = this.innerHTML;
this.innerHTML = '<i class="fa fa-check"></i>';
setTimeout(() => {
this.innerHTML = originalIcon;
}, 1000);
}).catch(err => {
console.error('Failed to copy token:', err);
});
});
});
}
// 编辑设备
async function editDevice(deviceId) {
// 这里可以实现编辑设备的逻辑
console.log('Edit device:', deviceId);
}
// 删除设备
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);
}
}
}
// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', initApp);
// 初始化设备管理功能
function initDeviceManagement() {
// 添加设备按钮事件
document.getElementById('addDeviceBtn')?.addEventListener('click', function() {
// 这里可以实现添加设备的逻辑
console.log('Add device');
});
// 状态过滤事件
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 && 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();