// API 基础 URL
const API_BASE_URL = '/api';
// 全局状态
let state = {
currentTimeRange: '1h', // 与UI默认值保持一致
customStartTime: '',
customEndTime: '',
currentInterval: '10m', // 固定10分钟区间
historyMetrics: {} // 存储历史指标数据
}
// WebSocket连接
let ws = null;
let wsReconnectAttempts = 0;
const wsMaxReconnectAttempts = 5;
let wsReconnectDelay = 1000;
// 图表实例
const charts = {};
// 初始化应用
function initApp() {
initCustomTimeRange();
bindEvents();
initPageSwitch();
loadHomeData();
initCharts();
initWebSocket();
// 设置定时刷新
setInterval(loadMetrics, 30000);
setInterval(loadServerCount, 30000);
}
// 初始化自定义时间范围
function initCustomTimeRange() {
const now = new Date();
// 默认显示过去24小时
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// 直接使用ISO字符串,包含完整的时区信息
state.customStartTime = twentyFourHoursAgo.toISOString();
state.customEndTime = now.toISOString();
}
// 页面切换
function initPageSwitch() {
window.addEventListener('hashchange', switchPage);
switchPage();
}
function switchPage() {
const hash = window.location.hash;
// 隐藏所有内容
hideAllContent();
// 显示对应内容
if (hash === '#servers') {
showContent('serversContent');
loadAllServers();
} else if (hash === '#devices') {
showContent('devicesContent');
loadDeviceManagementList();
} else if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
showContent('serverMonitorContent');
// 提取设备ID
let deviceId = '';
if (hash.startsWith('#serverMonitor/')) {
deviceId = hash.split('/')[1];
}
// 加载服务器信息
if (deviceId) {
loadServerInfo(deviceId);
}
loadMetrics();
} else {
showContent('homeContent');
loadHomeData();
}
}
function hideAllContent() {
document.getElementById('homeContent').classList.add('hidden');
document.getElementById('serversContent').classList.add('hidden');
document.getElementById('serverMonitorContent').classList.add('hidden');
document.getElementById('devicesContent').classList.add('hidden');
}
function showContent(contentId) {
document.getElementById(contentId).classList.remove('hidden');
}
// 加载首页数据
async function loadHomeData() {
try {
// 加载业务视图数据
await loadBusinessViewData();
// 加载告警列表数据
loadAlarmListData();
// 加载服务器数量
loadServerCount();
} catch (error) {
console.error('加载首页数据失败:', error);
}
}
// 加载业务视图数据
async function loadBusinessViewData() {
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
const data = await response.json();
let devices = data.devices || [];
// 如果没有设备,使用模拟数据
if (devices.length === 0) {
devices = [
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100' },
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101' }
];
}
renderBusinessView(devices);
} catch (error) {
console.error('加载业务视图数据失败:', error);
// 使用模拟数据
const mockDevices = [
{ id: 'device-1', name: 'LIS', ip: '192.129.6.108', os: 'Linux', status: 'P1' },
{ id: 'device-2', name: '互联网医院', ip: '192.129.6.57', os: 'Linux', status: 'P3' },
{ id: 'device-3', name: 'HIS', ip: '192.129.51.21', os: 'Windows', status: 'P2' },
{ id: 'device-4', name: 'OA', ip: '192.129.6.42', os: 'Linux', status: 'P2' }
];
renderBusinessView(mockDevices);
}
}
// 渲染业务视图
function renderBusinessView(devices) {
const tableBody = document.getElementById('businessViewTableBody');
if (!tableBody) return;
tableBody.innerHTML = '';
devices.forEach(device => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors';
row.innerHTML = `
${device.name} |
${device.ip} |
${device.os || '未知'} |
正常
|
`;
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' }
];
renderAlarmList(alarmData);
}
// 渲染告警列表
function renderAlarmList(alarmData) {
const alarmList = document.getElementById('alarmList');
if (!alarmList) return;
alarmList.innerHTML = '';
alarmData.forEach(alarm => {
const alarmItem = document.createElement('div');
alarmItem.className = `p-4 bg-gray-50 rounded-lg border-l-4 ${getAlarmBorderColor(alarm.level)}`;
alarmItem.innerHTML = `
${alarm.message}
${alarm.device}
${alarm.time}
`;
alarmList.appendChild(alarmItem);
});
}
// 获取告警样式
function getAlarmBorderColor(level) {
switch(level) {
case 'error': return 'border-red-500';
case 'warning': return 'border-yellow-500';
case 'info': return 'border-blue-500';
default: return 'border-gray-500';
}
}
function getAlarmIconColor(level) {
switch(level) {
case 'error': return 'text-red-500';
case 'warning': return 'text-yellow-500';
case 'info': return 'text-blue-500';
default: return 'text-gray-500';
}
}
// 加载服务器数量
async function loadServerCount() {
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
const data = await response.json();
const count = data.devices ? data.devices.length : 0;
const serverCountElement = document.getElementById('serverCount');
if (serverCountElement) {
serverCountElement.textContent = count;
}
} catch (error) {
console.error('加载服务器数量失败:', error);
const serverCountElement = document.getElementById('serverCount');
if (serverCountElement) {
serverCountElement.textContent = '2';
}
}
}
// 加载所有服务器
async function loadAllServers() {
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
const data = await response.json();
const devices = data.devices || [];
renderServersGrid(devices);
} catch (error) {
console.error('加载服务器列表失败:', error);
// 模拟数据
const mockDevices = [
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100' },
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101' }
];
renderServersGrid(mockDevices);
}
}
// 渲染服务器网格
function renderServersGrid(devices) {
const serversGrid = document.getElementById('serversGrid');
if (!serversGrid) return;
serversGrid.innerHTML = '';
devices.forEach(device => {
const serverCard = createServerCard(device);
serversGrid.appendChild(serverCard);
});
}
// 创建服务器卡片
function createServerCard(device) {
const card = document.createElement('div');
card.className = 'bg-white rounded-xl shadow-md p-6 card-hover border border-gray-100 cursor-pointer';
// 为卡片添加点击事件
card.addEventListener('click', () => {
goToServerMonitor(device.id);
});
card.innerHTML = `
${device.name || device.id}
${device.ip || 'N/A'}
在线
`;
// 为"查看详情"按钮添加点击事件
const viewDetailBtn = card.querySelector('button');
viewDetailBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
goToServerMonitor(device.id);
});
return card;
}
// 存储原始设备列表,用于搜索和筛选
let originalDeviceList = [];
// 设备管理
async function loadDeviceManagementList() {
try {
const response = await fetch(`${API_BASE_URL}/devices/all`);
if (!response.ok) {
throw new Error('Failed to fetch devices');
}
const data = await response.json();
const devices = data.devices || [];
// 如果设备列表为空,使用模拟数据作为回退
if (devices.length === 0) {
console.warn('No devices available, using mock data');
const mockDevices = getMockDevices();
originalDeviceList = mockDevices;
renderDeviceManagementList(mockDevices);
return;
}
// 处理设备数据,转换为所需格式
const processedDevices = devices.map(device => ({
id: device.id,
name: device.name || device.id,
ip: device.ip || 'N/A',
status: device.status || 'unknown',
created_at: device.created_at ? new Date(device.created_at * 1000).toLocaleString() : 'N/A',
token: device.token || 'N/A'
}));
// 存储原始设备列表
originalDeviceList = processedDevices;
// 应用当前筛选条件
applyDeviceFilters();
} catch (error) {
console.error('加载设备管理列表失败:', error);
// 使用模拟数据作为回退
const mockDevices = getMockDevices();
originalDeviceList = mockDevices;
renderDeviceManagementList(mockDevices);
}
}
// 应用设备筛选条件
function applyDeviceFilters() {
const searchTerm = document.getElementById('deviceSearch').value.toLowerCase();
const statusFilter = document.getElementById('deviceStatusFilter').value;
// 复制原始列表
let filteredDevices = [...originalDeviceList];
// 应用搜索筛选
if (searchTerm) {
filteredDevices = filteredDevices.filter(device =>
device.name.toLowerCase().includes(searchTerm) ||
device.id.toLowerCase().includes(searchTerm) ||
device.ip.toLowerCase().includes(searchTerm)
);
}
// 应用状态筛选
if (statusFilter && statusFilter !== 'all') {
filteredDevices = filteredDevices.filter(device => device.status === statusFilter);
}
// 渲染筛选后的设备列表
renderDeviceManagementList(filteredDevices);
// 显示或隐藏无数据提示
const noDataMessage = document.getElementById('noDataMessage');
if (noDataMessage) {
if (filteredDevices.length === 0) {
noDataMessage.classList.remove('hidden');
} else {
noDataMessage.classList.add('hidden');
}
}
}
// 清除筛选条件
function clearDeviceFilters() {
document.getElementById('deviceSearch').value = '';
document.getElementById('deviceStatusFilter').value = 'all';
applyDeviceFilters();
}
// 获取模拟设备数据
function getMockDevices() {
return [
{ id: 'device-1', name: '服务器1', ip: '192.168.1.100', status: 'active', created_at: new Date().toLocaleString(), token: 'mock-token-1' },
{ id: 'device-2', name: '服务器2', ip: '192.168.1.101', status: 'inactive', created_at: new Date().toLocaleString(), token: 'mock-token-2' },
{ id: 'device-3', name: '测试服务器', ip: '192.168.1.102', status: 'offline', created_at: new Date().toLocaleString(), token: 'mock-token-3' }
];
}
// 复制文本到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// 可以添加一个提示,如"已复制"的临时消息
showToast('复制成功');
}).catch(err => {
console.error('复制失败:', err);
showToast('复制失败');
});
}
// 显示临时提示消息
function showToast(message, type = 'success') {
// 检查是否已存在toast元素
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
// 设置toast样式根据类型
let bgColor = 'bg-gray-800';
switch(type) {
case 'success':
bgColor = 'bg-green-800';
break;
case 'error':
bgColor = 'bg-red-800';
break;
case 'warning':
bgColor = 'bg-yellow-800';
break;
}
toast.className = `${bgColor} text-white px-4 py-2 rounded opacity-0 transition-opacity duration-300 z-50 fixed bottom-4 right-4`;
toast.textContent = message;
toast.style.opacity = '1';
// 2秒后自动隐藏
setTimeout(() => {
toast.style.opacity = '0';
}, 2000);
}
// 切换设备状态(激活/停用)
async function toggleDeviceStatus(deviceId, newStatus) {
try {
// 先获取设备详情
const deviceResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!deviceResponse.ok) {
throw new Error('Failed to fetch device details');
}
const deviceData = await deviceResponse.json();
const device = deviceData.device;
// 更新设备状态
const updateResponse = await fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: newStatus,
name: device.name,
ip: device.ip
})
});
if (!updateResponse.ok) {
throw new Error(`HTTP error! status: ${updateResponse.status}`);
}
// 重新加载设备列表
await loadDeviceManagementList();
// 显示成功提示
const statusText = newStatus === 'active' ? '激活' : '停用';
showToast(`设备已成功${statusText}`, 'success');
} catch (error) {
console.error('Failed to toggle device status:', error);
const statusText = newStatus === 'active' ? '激活' : '停用';
showToast(`设备${statusText}失败`, 'error');
}
}
// 编辑设备
function editDevice(deviceId) {
// 这里可以实现编辑设备的逻辑,比如打开模态框等
console.log('编辑设备:', deviceId);
// 可以添加模态框或跳转到编辑页面的逻辑
}
// 删除设备
function deleteDevice(deviceId) {
if (confirm('确定要删除这个设备吗?')) {
fetch(`${API_BASE_URL}/devices/${deviceId}`, {
method: 'DELETE'
}).then(response => {
if (response.ok) {
showToast('设备删除成功');
loadDeviceManagementList(); // 重新加载设备列表
} else {
throw new Error('删除失败');
}
}).catch(err => {
console.error('删除设备失败:', err);
showToast('删除失败');
});
}
}
function renderDeviceManagementList(devices) {
const tableBody = document.getElementById('deviceManagementTableBody');
if (!tableBody) return;
tableBody.innerHTML = '';
devices.forEach(device => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors';
// 根据设备状态获取显示样式
const getStatusStyle = (status) => {
switch (status) {
case 'active':
return 'bg-green-100 text-green-800';
case 'inactive':
return 'bg-yellow-100 text-yellow-800';
case 'offline':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
row.innerHTML = `
${device.name} |
${device.id} |
${device.ip} |
${device.token}
|
${device.status}
|
${device.created_at} |
|
`;
tableBody.appendChild(row);
});
}
// 初始化图表
function initCharts() {
initAlarmTrendChart();
initDetailedCharts();
}
// 初始化详细监控图表
function initDetailedCharts() {
// 初始化CPU图表
const cpuCtx = document.getElementById('cpuChart');
if (cpuCtx && !charts.cpu) {
charts.cpu = new Chart(cpuCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU使用率 (%)',
data: [],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
// 启用缩放功能
interaction: {
intersect: false,
mode: 'index'
},
// 允许显示空白区域
spanGaps: true,
// 配置缩放和拖拽
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
}
}
});
}
// 初始化内存图表
const memoryCtx = document.getElementById('memoryChart');
if (memoryCtx && !charts.memory) {
charts.memory = new Chart(memoryCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '内存使用率 (%)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化磁盘图表
const diskCtx = document.getElementById('diskChart');
if (diskCtx && !charts.disk) {
charts.disk = new Chart(diskCtx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化网络流量图表(发送总和和接收总和)
const networkCtx = document.getElementById('networkChart');
if (networkCtx && !charts.network) {
charts.network = new Chart(networkCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '发送总和 (MB)',
data: [],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
},
{
label: '接收总和 (MB)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + ' MB';
}
}
}
}
}
});
}
// 初始化网速趋势图表
const speedCtx = document.getElementById('speedChart');
if (speedCtx && !charts.speed) {
charts.speed = new Chart(speedCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '发送速率 (MB/s)',
data: [],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
},
{
label: '接收速率 (MB/s)',
data: [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
spanGaps: true,
plugins: {
legend: {
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
},
zoom: {
pan: {
enabled: true,
mode: 'x'
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoom: function({chart}) {
console.log(chart.getZoomLevel());
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + ' MB/s';
}
}
}
}
}
});
}
}
// 初始化告警趋势图
function initAlarmTrendChart() {
const ctx = document.getElementById('alarmTrendChart');
if (!ctx) return;
charts.alarmTrend = new Chart(ctx, {
type: 'line',
data: {
labels: ['11-24', '11-25', '11-26', '11-27', '11-28', '11-29', '11-30'],
datasets: [
{
label: '报警',
data: [50, 60, 75, 80, 90, 100, 112],
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
},
{
label: '提醒',
data: [20, 25, 30, 35, 38, 38, 38],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 格式化字节数,根据用户要求:只在MB和GB之间转换
function formatBytes(bytes, decimals = 2, isRate = false) {
if (bytes === 0) {
return isRate ? '0 MB/s' : '0 MB';
}
const mb = 1024 * 1024;
const gb = mb * 1024;
const dm = decimals < 0 ? 0 : decimals;
// 对于速率(bytes/s):默认显示MB/s,超过1024MB/s显示GB/s
if (isRate) {
if (bytes >= gb) {
return parseFloat((bytes / gb).toFixed(dm)) + ' GB/s';
} else {
return parseFloat((bytes / mb).toFixed(dm)) + ' MB/s';
}
}
// 对于流量(bytes):默认显示MB,超过1024MB显示GB
else {
if (bytes >= gb) {
return parseFloat((bytes / gb).toFixed(dm)) + ' GB';
} else {
return parseFloat((bytes / mb).toFixed(dm)) + ' MB';
}
}
}
// 获取单个指标数据
async function fetchMetric(metricType, aggregation = 'average') {
// 构建查询参数
const params = new URLSearchParams();
// 设置时间范围参数
if (state.customStartTime && state.customEndTime) {
// 自定义时间范围
params.append('start_time', state.customStartTime);
params.append('end_time', state.customEndTime);
} else {
// 默认显示24小时的数据
params.append('start_time', '-24h');
params.append('end_time', 'now()');
}
// 设置聚合方式
params.append('aggregation', aggregation);
// 固定时间区间为10分钟
params.append('interval', '10m');
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/${metricType}?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${metricType} metrics`);
}
const data = await response.json();
return data.data;
}
// 格式化时间,统一格式避免图表误解
function formatTime(timeStr) {
// timeStr是ISO格式的UTC时间字符串,如"2025-12-02T01:53:19Z"
const date = new Date(timeStr);
// 格式化年、月、日(使用本地时间)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
// 格式化时、分、秒(使用本地时间)
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// 总是显示完整的日期和时间,方便区分不同日期的数据
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 加载监控指标
async function loadMetrics() {
try {
// 并行加载所有指标
const [cpuData, memoryData, diskData, networkSumData] = await Promise.all([
fetchMetric('cpu'),
fetchMetric('memory'),
fetchMetric('disk'),
fetchMetric('network')
]);
// 更新状态卡片
updateStatusCards({ cpu: cpuData, memory: memoryData, disk: diskData, network: networkSumData });
// 更新图表
updateCharts(cpuData, memoryData, diskData, networkSumData);
} catch (error) {
console.error('Failed to load metrics:', error);
// 显示友好的错误提示
showToast('加载监控数据失败,请稍后重试', 'error');
}
}
// WebSocket初始化
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket连接已打开');
wsReconnectAttempts = 0;
wsReconnectDelay = 1000;
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
ws.onclose = (event) => {
console.log(`WebSocket连接已关闭: ${event.code} - ${event.reason}`);
attemptReconnect();
};
ws.onerror = (error) => {
console.error('WebSocket连接错误:', error);
};
}
// 处理WebSocket消息
function handleWebSocketMessage(message) {
if (message.type === 'metrics_update') {
handleMetricsUpdate(message);
}
}
// 加载服务器信息
async function loadServerInfo(deviceId) {
try {
const response = await fetch(`${API_BASE_URL}/devices/${deviceId}`);
if (!response.ok) {
throw new Error('Failed to fetch server info');
}
const deviceData = await response.json();
// 更新服务器信息显示
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `服务器名称: ${deviceData.name || deviceId} | IP地址: ${deviceData.ip || '未知'}
`;
}
} catch (error) {
console.error('Failed to load server info:', error);
// 使用模拟数据
const serverInfoDisplay = document.getElementById('serverInfoDisplay');
if (serverInfoDisplay) {
serverInfoDisplay.innerHTML = `服务器名称: 服务器 ${deviceId} | IP地址: 192.168.1.${deviceId}
`;
}
}
}
// 处理指标更新
function handleMetricsUpdate(message) {
const { device_id, metrics } = message;
// 直接更新,不再检查device_id
updateStatusCards(metrics);
// 立即刷新数据以确保图表也更新
setTimeout(() => loadMetrics(), 500);
}
// 更新状态卡片
function updateStatusCards(metrics) {
// 更新历史指标数据
updateHistoryMetrics(metrics);
// 使用历史数据或当前数据
const displayMetrics = {
cpu: metrics.cpu || state.historyMetrics.cpu,
memory: metrics.memory || state.historyMetrics.memory,
disk: metrics.disk || state.historyMetrics.disk,
network: metrics.network || state.historyMetrics.network
};
// 更新CPU状态卡片
if (displayMetrics.cpu) {
let cpuUsage = 0;
let cpuGhz = 0;
let cpuLoad = 0;
// 解析CPU数据
if (Array.isArray(displayMetrics.cpu) && displayMetrics.cpu.length > 0) {
cpuUsage = displayMetrics.cpu[displayMetrics.cpu.length - 1].value;
} else if (typeof displayMetrics.cpu === 'number') {
cpuUsage = displayMetrics.cpu;
} else if (typeof displayMetrics.cpu === 'object' && displayMetrics.cpu.usage) {
cpuUsage = displayMetrics.cpu.usage;
cpuGhz = displayMetrics.cpu.frequency || 0;
cpuLoad = displayMetrics.cpu.load || 0;
}
// 更新显示
const cpuElement = document.getElementById('cpuValue');
const cpuDetailsElement = document.getElementById('cpuDetails');
if (cpuElement) {
cpuElement.textContent = `${cpuUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰
cpuElement.className = cpuUsage > 90 ? 'text-red-500' : '';
}
if (cpuDetailsElement) {
cpuDetailsElement.textContent = `${cpuGhz.toFixed(2)} GHz | 负载: ${cpuLoad.toFixed(2)}`;
}
}
// 更新内存状态卡片
if (displayMetrics.memory) {
let memoryUsage = 0;
let memoryUsed = 0;
let memoryTotal = 0;
// 解析内存数据
if (Array.isArray(displayMetrics.memory) && displayMetrics.memory.length > 0) {
memoryUsage = displayMetrics.memory[displayMetrics.memory.length - 1].value;
} else if (typeof displayMetrics.memory === 'number') {
memoryUsage = displayMetrics.memory;
} else if (typeof displayMetrics.memory === 'object') {
memoryUsage = displayMetrics.memory.usage || 0;
memoryUsed = displayMetrics.memory.used || 0;
memoryTotal = displayMetrics.memory.total || 0;
}
// 更新显示
const memoryElement = document.getElementById('memoryValue');
const memoryDetailsElement = document.getElementById('memoryDetails');
if (memoryElement) {
memoryElement.textContent = `${memoryUsage.toFixed(1)}%`;
// 设置红色显示如果达到顶峰
memoryElement.className = memoryUsage > 90 ? 'text-red-500' : '';
}
if (memoryDetailsElement) {
memoryDetailsElement.textContent = `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}`;
}
}
// 更新磁盘状态卡片
if (displayMetrics.disk) {
let totalUsed = 0;
let totalSize = 0;
let usagePercent = 0;
// 过滤掉不需要的挂载点
const excludedMountpoints = ['/usr', '/boot', '/boot/efi'];
// 解析磁盘数据
if (typeof displayMetrics.disk === 'object' && displayMetrics.disk !== null && !Array.isArray(displayMetrics.disk)) {
// 按挂载点分组的数据
for (const mountpoint in displayMetrics.disk) {
// 跳过排除的挂载点
if (excludedMountpoints.includes(mountpoint)) {
continue;
}
const data = displayMetrics.disk[mountpoint];
if (data && typeof data === 'object' && data.used !== undefined && data.total !== undefined) {
totalUsed += data.used;
totalSize += data.total;
}
}
} else if (typeof displayMetrics.disk === 'object' && displayMetrics.disk.used !== undefined && displayMetrics.disk.total !== undefined) {
// 单磁盘数据
totalUsed = displayMetrics.disk.used;
totalSize = displayMetrics.disk.total;
}
// 计算使用率
if (totalSize > 0) {
usagePercent = (totalUsed / totalSize) * 100;
}
// 更新显示
const diskElement = document.getElementById('diskValue');
const diskDetailsElement = document.getElementById('diskDetails');
if (diskElement) {
diskElement.textContent = `${usagePercent.toFixed(1)}%`;
// 设置红色显示如果达到顶峰
diskElement.className = usagePercent > 90 ? 'text-red-500' : '';
}
if (diskDetailsElement) {
diskDetailsElement.textContent = `${formatBytes(totalUsed)} / ${formatBytes(totalSize)}`;
}
}
// 更新网络状态卡片
if (displayMetrics.network) {
let sentRate = 0;
let receivedRate = 0;
let sentTotal = 0;
let receivedTotal = 0;
// 解析网络数据
if (displayMetrics.network.sent && displayMetrics.network.received) {
// 处理数组格式的数据
if (Array.isArray(displayMetrics.network.sent) && displayMetrics.network.sent.length > 0 &&
Array.isArray(displayMetrics.network.received) && displayMetrics.network.received.length > 0) {
sentRate = displayMetrics.network.sent[displayMetrics.network.sent.length - 1].value;
receivedRate = displayMetrics.network.received[displayMetrics.network.received.length - 1].value;
// 计算总量(假设数组中的值是累积的)
sentTotal = displayMetrics.network.sent.reduce((sum, item) => sum + item.value, 0);
receivedTotal = displayMetrics.network.received.reduce((sum, item) => sum + item.value, 0);
}
// 处理数值格式的数据
else if (typeof displayMetrics.network.sent === 'number' && typeof displayMetrics.network.received === 'number') {
sentRate = displayMetrics.network.sent;
receivedRate = displayMetrics.network.received;
sentTotal = displayMetrics.network.sent_total || sentRate;
receivedTotal = displayMetrics.network.received_total || receivedRate;
}
} else if (typeof displayMetrics.network === 'object' &&
displayMetrics.network.bytes_sent !== undefined &&
displayMetrics.network.bytes_received !== undefined) {
// WebSocket消息格式 - 总流量
sentRate = displayMetrics.network.bytes_sent;
receivedRate = displayMetrics.network.bytes_received;
sentTotal = displayMetrics.network.bytes_sent_total || sentRate;
receivedTotal = displayMetrics.network.bytes_received_total || receivedRate;
} else if (typeof displayMetrics.network === 'object' && !Array.isArray(displayMetrics.network)) {
// 按网卡分组的数据,计算总流量
for (const iface in displayMetrics.network) {
const ifaceMetrics = displayMetrics.network[iface];
if (ifaceMetrics.bytes_sent !== undefined && ifaceMetrics.bytes_received !== undefined) {
sentRate += ifaceMetrics.bytes_sent;
receivedRate += ifaceMetrics.bytes_received;
sentTotal += ifaceMetrics.bytes_sent_total || ifaceMetrics.bytes_sent;
receivedTotal += ifaceMetrics.bytes_received_total || ifaceMetrics.bytes_received;
} else if (ifaceMetrics.BytesSent !== undefined && ifaceMetrics.BytesReceived !== undefined) {
sentRate += ifaceMetrics.BytesSent;
receivedRate += ifaceMetrics.BytesReceived;
}
}
}
// 计算比率
let ratioText = '0.0';
if (sentRate > 0) {
ratioText = (receivedRate / sentRate).toFixed(1);
}
// 更新显示
const networkValueElement = document.getElementById('networkValue');
const networkDetailsElement = document.getElementById('networkDetails');
if (networkValueElement) {
networkValueElement.innerHTML = `${ratioText} ↓ / ↑`;
}
if (networkDetailsElement) {
networkDetailsElement.innerHTML =
`接收: ${formatBytes(receivedRate, 2, true)} | ` +
`发送: ${formatBytes(sentRate, 2, true)}
` +
`总量: 接收 ${formatBytes(receivedTotal)} | 发送 ${formatBytes(sentTotal)}`;
}
}
}
// 更新历史指标数据
function updateHistoryMetrics(metrics) {
// 只更新有有效数据的指标
if (metrics.cpu) {
state.historyMetrics.cpu = metrics.cpu;
}
if (metrics.memory) {
state.historyMetrics.memory = metrics.memory;
}
if (metrics.disk) {
state.historyMetrics.disk = metrics.disk;
}
if (metrics.network) {
state.historyMetrics.network = metrics.network;
}
}
// 更新图表数据
function updateCharts(cpuData, memoryData, diskData, networkData) {
// 数据点排序函数
const sortDataByTime = (data) => {
return [...data].sort((a, b) => {
return new Date(a.time) - new Date(b.time);
});
};
// 计算固定份数X轴数据
const getFixedPointsData = (data) => {
if (!Array.isArray(data) || data.length === 0) {
return [];
}
// 根据时间范围计算需要的份数(每份10分钟)
let expectedPoints = 6; // 默认1小时,6份
let timeRange = state.currentTimeRange || '1h';
// 如果使用了自定义时间,检查是否是24小时范围
if (state.customStartTime && state.customEndTime) {
const startTime = new Date(state.customStartTime);
const endTime = new Date(state.customEndTime);
const durationHours = (endTime - startTime) / (1000 * 60 * 60);
// 根据实际时长设置预期点数
if (durationHours <= 0.5) {
timeRange = '30m';
} else if (durationHours <= 1) {
timeRange = '1h';
} else if (durationHours <= 2) {
timeRange = '2h';
} else if (durationHours <= 6) {
timeRange = '6h';
} else if (durationHours <= 12) {
timeRange = '12h';
} else {
timeRange = '24h';
}
}
switch(timeRange) {
case '30m':
expectedPoints = 3; // 30分钟,3份
break;
case '1h':
expectedPoints = 6; // 1小时,6份
break;
case '2h':
expectedPoints = 12; // 2小时,12份
break;
case '6h':
expectedPoints = 36; // 6小时,36份
break;
case '12h':
expectedPoints = 72; // 12小时,72份
break;
case '24h':
expectedPoints = 144; // 24小时,144份
break;
}
// 排序数据
const sortedData = sortDataByTime(data);
// 如果数据点足够,直接返回
if (sortedData.length <= expectedPoints) {
return sortedData;
}
// 计算采样步长
const step = Math.ceil(sortedData.length / expectedPoints);
const sampled = [];
// 采样数据,确保得到期望的份数
for (let i = 0; i < sortedData.length; i += step) {
sampled.push(sortedData[i]);
if (sampled.length >= expectedPoints) {
break;
}
}
// 确保包含最后一个数据点
if (sampled.length < expectedPoints && sortedData.length > 0) {
// 如果采样点不够,从末尾补充
const remaining = expectedPoints - sampled.length;
for (let i = 1; i <= remaining; i++) {
const index = Math.max(0, sortedData.length - i);
if (!sampled.some(item => item.time === sortedData[index].time)) {
sampled.push(sortedData[index]);
}
}
}
// 再次排序,确保时间顺序
return sortDataByTime(sampled);
};
// 更新CPU图表
if (cpuData && Array.isArray(cpuData) && cpuData.length > 0 && charts.cpu) {
const fixedData = getFixedPointsData(cpuData);
charts.cpu.data.datasets[0].data = fixedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.cpu.update();
}
// 更新内存图表
if (memoryData && Array.isArray(memoryData) && memoryData.length > 0 && charts.memory) {
const fixedData = getFixedPointsData(memoryData);
charts.memory.data.datasets[0].data = fixedData.map(item => ({
x: formatTime(item.time),
y: item.value
}));
charts.memory.update();
}
// 更新磁盘图表,支持多个挂载点
if (diskData && charts.disk) {
// 定义不同的颜色,用于区分不同的挂载点
const colors = [
{ border: '#f59e0b', background: 'rgba(245, 158, 11, 0.1)' }, // 黄色
{ border: '#ef4444', background: 'rgba(239, 68, 68, 0.1)' }, // 红色
{ border: '#10b981', background: 'rgba(16, 185, 129, 0.1)' }, // 绿色
{ border: '#3b82f6', background: 'rgba(59, 130, 246, 0.1)' }, // 蓝色
{ border: '#8b5cf6', background: 'rgba(139, 92, 246, 0.1)' }, // 紫色
{ border: '#ec4899', background: 'rgba(236, 72, 153, 0.1)' }, // 粉色
];
// 清空现有的数据集
charts.disk.data.datasets = [];
// 为每个挂载点创建独立的数据集
let colorIndex = 0;
if (typeof diskData === 'object' && diskData !== null && !Array.isArray(diskData)) {
// 处理按挂载点分组的数据
for (const [mountpoint, data] of Object.entries(diskData)) {
if (data && Array.isArray(data) && data.length > 0) {
// 获取颜色
const color = colors[colorIndex % colors.length];
colorIndex++;
// 排序数据
const sortedData = sortDataByTime(data);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
// 创建数据集
const dataset = {
label: `磁盘使用率 (${mountpoint})`,
data: fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value
})),
borderColor: color.border,
backgroundColor: color.background,
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3,
};
// 添加数据集
charts.disk.data.datasets.push(dataset);
}
}
} else if (Array.isArray(diskData) && diskData.length > 0) {
// 处理单一磁盘数据
const sortedData = sortDataByTime(diskData);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
const dataset = {
label: '磁盘使用率',
data: fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value
})),
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.7,
pointRadius: 0,
pointHoverRadius: 3,
};
charts.disk.data.datasets.push(dataset);
}
// 更新图表
charts.disk.update();
}
// 保存当前选中的网卡
state.currentInterface = state.currentInterface || 'all';
// 更新网络流量趋势图表(发送总和和接收总和)
if (networkData && charts.network) {
let selectedNetworkData = networkData;
// 如果是按网卡分组的数据,选择当前选中的网卡数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
}
if (selectedNetworkData.sent && selectedNetworkData.received) {
// 计算发送总和(时间段内的累积值)
if (Array.isArray(selectedNetworkData.sent) && selectedNetworkData.sent.length > 0) {
// 排序发送数据
const sortedSent = sortDataByTime(selectedNetworkData.sent);
// 计算累积发送总和(MB)
let cumulativeSent = 0;
const sentSumData = sortedSent.map(item => {
// 转换为MB并累积
const mbValue = item.value / (1024 * 1024);
cumulativeSent += mbValue;
return {
time: item.time,
value: cumulativeSent
};
});
// 使用固定份数X轴数据计算
const fixedPointsSentSum = getFixedPointsData(sentSumData);
charts.network.data.datasets[0].data = fixedPointsSentSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
// 计算接收总和(时间段内的累积值)
if (Array.isArray(selectedNetworkData.received) && selectedNetworkData.received.length > 0) {
// 排序接收数据
const sortedReceived = sortDataByTime(selectedNetworkData.received);
// 计算累积接收总和(MB)
let cumulativeReceived = 0;
const receivedSumData = sortedReceived.map(item => {
// 转换为MB并累积
const mbValue = item.value / (1024 * 1024);
cumulativeReceived += mbValue;
return {
time: item.time,
value: cumulativeReceived
};
});
// 使用固定份数X轴数据计算
const fixedPointsReceivedSum = getFixedPointsData(receivedSumData);
charts.network.data.datasets[1].data = fixedPointsReceivedSum.map(item => ({
x: formatTime(item.time),
y: item.value
}));
}
charts.network.update();
}
}
// 更新网速趋势图表
if (networkData && charts.speed) {
let selectedNetworkData = networkData;
// 如果是按网卡分组的数据,选择当前选中的网卡数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
selectedNetworkData = networkData[state.currentInterface] || networkData['all'] || {};
}
if (selectedNetworkData.sent && selectedNetworkData.received) {
// 更新发送流量
if (Array.isArray(selectedNetworkData.sent) && selectedNetworkData.sent.length > 0) {
const sortedData = sortDataByTime(selectedNetworkData.sent);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
charts.speed.data.datasets[0].data = fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s
}));
}
// 更新接收流量
if (Array.isArray(selectedNetworkData.received) && selectedNetworkData.received.length > 0) {
const sortedData = sortDataByTime(selectedNetworkData.received);
// 使用固定份数X轴数据计算
const fixedPointsData = getFixedPointsData(sortedData);
charts.speed.data.datasets[1].data = fixedPointsData.map(item => ({
x: formatTime(item.time),
y: item.value / (1024 * 1024) // 转换为MB/s
}));
}
charts.speed.update();
}
}
// 更新网卡选择下拉框
updateInterfaceDropdown(networkData);
// 初始化图表(如果尚未初始化)
initDetailedCharts();
}
// 更新网卡选择下拉框
function updateInterfaceDropdown(networkData) {
// 创建或获取网卡选择容器
let interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (!interfaceContainer) {
// 获取图表选项卡导航容器
const chartTabs = document.getElementById('chartTabs');
if (!chartTabs) {
return;
}
// 创建网卡选择容器
interfaceContainer = document.createElement('div');
interfaceContainer.id = 'interfaceSelectorContainer';
interfaceContainer.className = 'flex items-center gap-2 ml-4';
// 创建标签
const label = document.createElement('label');
label.htmlFor = 'interfaceSelector';
label.className = 'text-sm text-gray-600';
label.textContent = '网卡:';
// 创建下拉框
const select = document.createElement('select');
select.id = 'interfaceSelector';
select.className = 'px-3 py-1 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加事件监听
select.addEventListener('change', (e) => {
state.currentInterface = e.target.value;
// 重新加载指标
loadMetrics();
});
// 组装容器
interfaceContainer.appendChild(label);
interfaceContainer.appendChild(select);
// 将容器添加到图表选项卡导航中
const tabsContainer = chartTabs.querySelector('.flex.flex-wrap');
if (tabsContainer) {
tabsContainer.appendChild(interfaceContainer);
}
}
// 更新下拉框选项
const select = document.getElementById('interfaceSelector');
if (select) {
// 保存当前选中的值
const currentValue = select.value;
// 清空现有选项
select.innerHTML = '';
// 添加默认选项
const defaultOption = document.createElement('option');
defaultOption.value = 'all';
defaultOption.textContent = '所有网卡';
select.appendChild(defaultOption);
// 添加所有网卡选项
// 检查是否有按网卡分组的网络数据
if (typeof networkData === 'object' && networkData.sent === undefined && networkData.received === undefined) {
// 获取所有网卡名称
const interfaces = Object.keys(networkData);
// 添加所有网卡选项
interfaces.forEach(iface => {
// 只添加实际的网卡名称,不添加"all"选项
if (iface !== 'all') {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
select.appendChild(option);
}
});
} else {
// 如果没有按网卡分组的数据,使用模拟网卡名称
// 这里可以根据实际情况修改,比如从其他地方获取网卡名称列表
const mockInterfaces = ['eth0', 'eth1', 'lo'];
mockInterfaces.forEach(iface => {
const option = document.createElement('option');
option.value = iface;
option.textContent = iface;
select.appendChild(option);
});
}
// 恢复当前选中的值
select.value = currentValue;
}
// 检查当前选中的选项卡,如果不是"网络"或"网速",隐藏网卡选择下拉框
const activeTab = document.querySelector('.chart-tab.active');
if (activeTab && activeTab.dataset.tab !== 'network' && activeTab.dataset.tab !== 'speed') {
interfaceContainer.classList.add('hidden');
} else {
interfaceContainer.classList.remove('hidden');
}
}
// 尝试重连WebSocket
function attemptReconnect() {
if (wsReconnectAttempts < wsMaxReconnectAttempts) {
wsReconnectAttempts++;
wsReconnectDelay *= 2;
setTimeout(() => {
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${wsMaxReconnectAttempts})`);
initWebSocket();
}, wsReconnectDelay);
}
}
// 打开模态框
function openModal(isEdit = false, deviceData = null) {
const modal = document.getElementById('deviceModal');
const modalContent = document.getElementById('modalContent');
const modalTitle = document.getElementById('modalTitle');
const deviceId = document.getElementById('deviceId');
const deviceName = document.getElementById('deviceName');
const deviceIp = document.getElementById('deviceIp');
const deviceStatus = document.getElementById('deviceStatus');
// 重置表单
document.getElementById('deviceForm').reset();
// 设置模态框标题和数据
if (isEdit && deviceData) {
modalTitle.textContent = '编辑设备';
deviceId.value = deviceData.id;
deviceName.value = deviceData.name || '';
deviceIp.value = deviceData.ip || '';
deviceStatus.value = deviceData.status || 'inactive';
} else {
modalTitle.textContent = '添加设备';
deviceId.value = '';
}
// 显示模态框并添加动画
modal.classList.remove('hidden');
// 触发重排后再添加动画类
setTimeout(() => {
modalContent.classList.remove('scale-95', 'opacity-0');
modalContent.classList.add('scale-100', 'opacity-100');
}, 10);
}
// 关闭模态框
function closeModal() {
const modal = document.getElementById('deviceModal');
const modalContent = document.getElementById('modalContent');
// 先应用离开动画
modalContent.classList.remove('scale-100', 'opacity-100');
modalContent.classList.add('scale-95', 'opacity-0');
// 动画结束后隐藏模态框
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
// 编辑设备
function editDevice(deviceId) {
// 直接从API获取设备详情,确保数据最新
fetch(`${API_BASE_URL}/devices/${deviceId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch device details');
}
return response.json();
})
.then(data => {
// 提取设备数据(API返回的是 {"device": {...}} 格式)
const deviceData = data.device;
openModal(true, deviceData);
})
.catch(error => {
console.error('Failed to get device details:', error);
showToast('获取设备详情失败', 'error');
});
}
// 处理设备表单提交
async function handleDeviceFormSubmit(event) {
event.preventDefault();
const deviceId = document.getElementById('deviceId').value;
const deviceName = document.getElementById('deviceName').value;
const deviceIp = document.getElementById('deviceIp').value;
const deviceStatus = document.getElementById('deviceStatus').value;
// 生成或使用现有ID
let idToUse = deviceId;
if (!idToUse) {
// 为新设备生成ID
idToUse = 'device-' + Date.now();
}
// 获取当前时间戳
const timestamp = Math.floor(Date.now() / 1000);
// 为新设备生成token
let token = '';
if (!deviceId) {
token = 'token-' + Math.random().toString(36).substring(2, 15);
}
// 构建完整的设备数据对象,包含所有必需字段
const deviceData = {
id: idToUse,
name: deviceName,
ip: deviceIp,
status: deviceStatus,
token: token, // 新设备生成token,编辑时不提供(由后端保持原值)
created_at: timestamp, // 新设备的创建时间
updated_at: timestamp // 更新时间
};
try {
let url = `${API_BASE_URL}/devices/`;
let method = 'POST';
// 如果是编辑操作,修改URL和方法
if (deviceId) {
url = `${API_BASE_URL}/devices/${deviceId}`;
method = 'PUT';
}
// 发送请求
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(deviceData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// 显示成功提示
showToast(deviceId ? '设备更新成功' : '设备添加成功', 'success');
// 关闭模态框
closeModal();
// 重新加载设备列表
await loadDeviceManagementList();
} catch (error) {
console.error('保存设备失败:', error);
showToast(deviceId ? '设备更新失败' : '设备添加失败', 'error');
}
}
// 绑定事件
function bindEvents() {
// 添加设备按钮事件
const addDeviceBtn = document.getElementById('addDeviceBtn');
if (addDeviceBtn) {
addDeviceBtn.addEventListener('click', () => {
openModal(false);
});
}
// 关闭模态框按钮
const closeModalBtn = document.getElementById('closeModalBtn');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeModal);
}
// 取消按钮
const cancelModalBtn = document.getElementById('cancelModalBtn');
if (cancelModalBtn) {
cancelModalBtn.addEventListener('click', closeModal);
}
// 设备表单提交
const deviceForm = document.getElementById('deviceForm');
if (deviceForm) {
deviceForm.addEventListener('submit', handleDeviceFormSubmit);
}
// 点击模态框背景关闭
const modal = document.getElementById('deviceModal');
if (modal) {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
// 设备搜索事件
const deviceSearch = document.getElementById('deviceSearch');
if (deviceSearch) {
deviceSearch.addEventListener('input', applyDeviceFilters);
}
// 设备状态筛选事件
const deviceStatusFilter = document.getElementById('deviceStatusFilter');
if (deviceStatusFilter) {
deviceStatusFilter.addEventListener('change', applyDeviceFilters);
}
// 清除筛选按钮事件
const clearFilterBtn = document.getElementById('clearFilterBtn');
if (clearFilterBtn) {
clearFilterBtn.addEventListener('click', clearDeviceFilters);
}
// 自定义时间查询事件
const customTimeQuery = document.getElementById('customTimeQuery');
if (customTimeQuery) {
customTimeQuery.addEventListener('click', () => {
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 获取本地时间
const localStartTime = new Date(startTimeInput.value);
const localEndTime = new Date(endTimeInput.value);
// 转换为ISO字符串(UTC时间)
state.customStartTime = localStartTime.toISOString();
state.customEndTime = localEndTime.toISOString();
// 清空当前时间范围状态,只使用自定义时间
state.currentTimeRange = '';
// 重新加载数据
loadMetrics();
}
});
}
// 初始化自定义时间输入框
const now = new Date();
const startTimeInput = document.getElementById('customStartTime');
const endTimeInput = document.getElementById('customEndTime');
if (startTimeInput && endTimeInput) {
// 默认显示过去24小时
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// 显示本地时间格式,YYYY-MM-DDTHH:MM
startTimeInput.value = twentyFourHoursAgo.toISOString().slice(0, 16);
endTimeInput.value = now.toISOString().slice(0, 16);
}
// 缩放控件事件处理
const zoomOutBtn = document.getElementById('zoomOutBtn');
const zoomInBtn = document.getElementById('zoomInBtn');
const resetZoomBtn = document.getElementById('resetZoomBtn');
const currentTimeRangeDisplay = document.getElementById('currentTimeRangeDisplay');
if (zoomOutBtn && zoomInBtn && currentTimeRangeDisplay) {
// 时间范围选项列表,用于缩放
const timeRanges = ['30m', '1h', '2h', '6h', '12h', '24h'];
// 更新当前时间范围显示
const updateTimeRangeDisplay = () => {
switch(state.currentTimeRange) {
case '30m':
currentTimeRangeDisplay.textContent = '过去30分钟';
break;
case '1h':
currentTimeRangeDisplay.textContent = '过去1小时';
break;
case '2h':
currentTimeRangeDisplay.textContent = '过去2小时';
break;
case '6h':
currentTimeRangeDisplay.textContent = '过去6小时';
break;
case '12h':
currentTimeRangeDisplay.textContent = '过去12小时';
break;
case '24h':
currentTimeRangeDisplay.textContent = '过去24小时';
break;
default:
currentTimeRangeDisplay.textContent = '自定义时间范围';
}
};
// 初始化显示
updateTimeRangeDisplay();
// 放大事件
zoomInBtn.addEventListener('click', () => {
// 只在使用预设时间范围时生效
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间时,先清除自定义时间
state.customStartTime = '';
state.customEndTime = '';
state.currentTimeRange = '1h';
} else {
// 查找当前时间范围在列表中的索引
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
if (currentIndex > 0) {
// 放大:使用更小的时间范围
state.currentTimeRange = timeRanges[currentIndex - 1];
}
}
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
});
// 缩小事件
zoomOutBtn.addEventListener('click', () => {
// 只在使用预设时间范围时生效
if (state.customStartTime && state.customEndTime) {
// 使用自定义时间时,先清除自定义时间
state.customStartTime = '';
state.customEndTime = '';
state.currentTimeRange = '1h';
} else {
// 查找当前时间范围在列表中的索引
const currentIndex = timeRanges.indexOf(state.currentTimeRange);
if (currentIndex < timeRanges.length - 1) {
// 缩小:使用更大的时间范围
state.currentTimeRange = timeRanges[currentIndex + 1];
}
}
// 更新显示
updateTimeRangeDisplay();
// 重新加载数据
loadMetrics();
});
}
// 重置缩放按钮事件处理
if (resetZoomBtn) {
resetZoomBtn.addEventListener('click', () => {
// 重置所有图表的缩放
Object.values(charts).forEach(chart => {
if (chart && typeof chart.resetZoom === 'function') {
chart.resetZoom();
}
});
});
}
}
// 工具函数
function showContent(contentId) {
const element = document.getElementById(contentId);
if (element) {
element.classList.remove('hidden');
}
}
// 跳转到服务器监控详情页面
function goToServerMonitor(deviceId) {
state.currentDeviceID = deviceId;
window.location.hash = `#serverMonitor/${deviceId}`;
// 加载选中设备的监控数据
setTimeout(() => {
loadMetrics();
}, 100);
}
// 初始化图表选项卡
function initChartTabs() {
const tabs = document.querySelectorAll('.chart-tab');
if (tabs.length === 0) return;
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// 移除所有选项卡的激活状态
tabs.forEach(t => {
t.classList.remove('active', 'text-blue-600', 'border-blue-600');
t.classList.add('text-gray-600', 'border-transparent');
});
// 添加当前选项卡的激活状态
tab.classList.add('active', 'text-blue-600', 'border-blue-600');
tab.classList.remove('text-gray-600', 'border-transparent');
// 隐藏所有图表容器
const tabId = tab.dataset.tab;
const chartContainers = document.querySelectorAll('.chart-container');
chartContainers.forEach(container => {
container.classList.add('hidden');
});
// 显示当前选中的图表容器
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
}
// 显示/隐藏网卡选择下拉框
const interfaceContainer = document.getElementById('interfaceSelectorContainer');
if (interfaceContainer) {
// 只有在点击"网络"或"网速"选项卡时,显示网卡选择下拉框
if (tabId === 'network' || tabId === 'speed') {
interfaceContainer.classList.remove('hidden');
} else {
interfaceContainer.classList.add('hidden');
}
}
});
});
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
initApp();
// 初始化图表选项卡
initChartTabs();
// 添加路由监听,处理hash变化
window.addEventListener('hashchange', handleHashChange);
// 初始检查hash
handleHashChange();
});
// 处理hash变化
function handleHashChange() {
const hash = window.location.hash;
if (hash === '#serverMonitor' || hash.startsWith('#serverMonitor/')) {
// 延迟一下,确保DOM已经渲染完成
setTimeout(() => {
loadMetrics();
}, 300);
}
}