1611 lines
58 KiB
JavaScript
1611 lines
58 KiB
JavaScript
// 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(); |