1620 lines
58 KiB
JavaScript
1620 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;
|
||
|
||
// 格式化字节数,根据用户要求:只在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 = `
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${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">
|
||
<svg class="w-5 h-5 ${iconColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||
</svg>
|
||
<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();
|
||
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 = `
|
||
<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);
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化应用
|
||
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 = `
|
||
<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">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||
</svg>
|
||
</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);
|
||
}
|
||
}
|
||
|
||
// 根据状态获取颜色类
|
||
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 = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
|
||
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(); |