304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
// 全局配置
|
||
const API_BASE_URL = '/api';
|
||
|
||
// DOM 加载完成后执行
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 初始化面板切换
|
||
initPanelNavigation();
|
||
|
||
// 加载初始数据
|
||
loadInitialData();
|
||
|
||
// 直接调用dashboard面板初始化函数,确保数据正确加载
|
||
if (typeof initDashboardPanel === 'function') {
|
||
initDashboardPanel();
|
||
}
|
||
|
||
// 注意:实时更新现在由index.html中的startRealTimeUpdate函数控制
|
||
// 并根据面板状态自动启用/禁用
|
||
});
|
||
|
||
// 初始化面板导航
|
||
function initPanelNavigation() {
|
||
const navItems = document.querySelectorAll('.nav-item');
|
||
const panels = document.querySelectorAll('.panel');
|
||
|
||
navItems.forEach(item => {
|
||
item.addEventListener('click', function() {
|
||
// 移除所有活动类
|
||
navItems.forEach(nav => nav.classList.remove('active'));
|
||
panels.forEach(panel => panel.classList.remove('active'));
|
||
|
||
// 添加当前活动类
|
||
this.classList.add('active');
|
||
const target = this.getAttribute('data-target');
|
||
document.getElementById(target).classList.add('active');
|
||
|
||
// 面板激活时执行相应的初始化函数
|
||
if (window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]) {
|
||
window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 保留原有的通知函数作为兼容层
|
||
// 现在主通知功能由index.html中的showNotification函数实现
|
||
if (typeof window.showNotification === 'undefined') {
|
||
window.showNotification = function(message, type = 'info') {
|
||
// 创建临时通知元素
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification notification-${type} show`;
|
||
notification.innerHTML = `
|
||
<div class="notification-content">${message}</div>
|
||
`;
|
||
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #333; color: white; padding: 10px 15px; border-radius: 4px; z-index: 10000;';
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 3000);
|
||
};
|
||
}
|
||
|
||
// 加载初始数据(主要用于服务器状态)
|
||
function loadInitialData() {
|
||
// 加载服务器状态
|
||
fetch(`${API_BASE_URL}/status`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// 更新服务器状态指示器
|
||
const statusDot = document.querySelector('.status-dot');
|
||
const serverStatus = document.getElementById('server-status');
|
||
|
||
if (data && data.status === 'running') {
|
||
statusDot.classList.add('connected');
|
||
serverStatus.textContent = '运行中';
|
||
} else {
|
||
statusDot.classList.remove('connected');
|
||
serverStatus.textContent = '离线';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取服务器状态失败:', error);
|
||
|
||
// 更新状态为离线
|
||
const statusDot = document.querySelector('.status-dot');
|
||
const serverStatus = document.getElementById('server-status');
|
||
statusDot.classList.remove('connected');
|
||
serverStatus.textContent = '离线';
|
||
|
||
// 使用新的通知功能
|
||
if (typeof window.showNotification === 'function') {
|
||
window.showNotification('获取服务器状态失败', 'danger');
|
||
}
|
||
});
|
||
|
||
// 注意:统计数据更新现在由dashboard.js中的updateStatCards函数处理
|
||
}
|
||
|
||
// 注意:统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
|
||
// 此函数保留作为兼容层,实际功能已迁移
|
||
function updateStatCards(stats) {
|
||
// 空实现,保留函数声明以避免引用错误
|
||
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
|
||
}
|
||
|
||
// 注意:获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
|
||
function fetchRulesCount() {
|
||
// 空实现,保留函数声明以避免引用错误
|
||
}
|
||
|
||
// 注意:获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
|
||
function fetchHostsCount() {
|
||
// 空实现,保留函数声明以避免引用错误
|
||
}
|
||
|
||
// 通用API请求函数 - 添加错误处理和重试机制
|
||
function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
|
||
const config = {
|
||
method,
|
||
headers,
|
||
timeout: 10000, // 设置超时时间为10秒
|
||
};
|
||
|
||
// 处理请求URL和参数
|
||
let url = `${API_BASE_URL}${endpoint}`;
|
||
|
||
if (data) {
|
||
if (method === 'GET') {
|
||
// 为GET请求拼接查询参数
|
||
const params = new URLSearchParams();
|
||
Object.keys(data).forEach(key => {
|
||
params.append(key, data[key]);
|
||
});
|
||
url += `?${params.toString()}`;
|
||
} else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
||
// 为其他方法设置body
|
||
config.body = JSON.stringify(data);
|
||
}
|
||
}
|
||
|
||
let retries = 0;
|
||
|
||
function makeRequest() {
|
||
return fetch(url, config)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
// 检查响应是否完整
|
||
const contentType = response.headers.get('content-type');
|
||
if (contentType && contentType.includes('application/json')) {
|
||
// 使用.text()先获取响应文本,处理可能的JSON解析错误
|
||
return response.text().then(text => {
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (e) {
|
||
console.error('JSON解析错误:', e, '响应文本:', text);
|
||
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试
|
||
if (retries < maxRetries) {
|
||
retries++;
|
||
console.warn(`请求失败,正在进行第${retries}次重试...`);
|
||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||
}
|
||
throw new Error('JSON解析失败且重试次数已达上限');
|
||
}
|
||
});
|
||
}
|
||
return response.json();
|
||
})
|
||
.catch(error => {
|
||
console.error('API请求错误:', error);
|
||
|
||
// 检查是否为网络错误或ERR_INCOMPLETE_CHUNKED_ENCODING相关错误
|
||
if ((error.name === 'TypeError' && error.message.includes('Failed to fetch')) ||
|
||
error.message.includes('incomplete chunked encoding')) {
|
||
|
||
if (retries < maxRetries) {
|
||
retries++;
|
||
console.warn(`网络错误,正在进行第${retries}次重试...`);
|
||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||
}
|
||
}
|
||
|
||
throw error;
|
||
});
|
||
}
|
||
|
||
return makeRequest();
|
||
}
|
||
|
||
// 数字格式化函数
|
||
function formatNumber(num) {
|
||
if (num >= 1000000) {
|
||
return (num / 1000000).toFixed(1) + 'M';
|
||
} else if (num >= 1000) {
|
||
return (num / 1000).toFixed(1) + 'K';
|
||
}
|
||
return num.toString();
|
||
}
|
||
|
||
// 确认对话框函数
|
||
function confirmAction(message, onConfirm) {
|
||
if (confirm(message)) {
|
||
onConfirm();
|
||
}
|
||
}
|
||
|
||
// 加载状态函数
|
||
function showLoading(element) {
|
||
if (element) {
|
||
element.innerHTML = '<td colspan="100%" class="loading">加载中...</td>';
|
||
}
|
||
}
|
||
|
||
// 错误状态函数
|
||
function showError(element, message) {
|
||
if (element) {
|
||
element.innerHTML = `<td colspan="100%" style="color: #e74c3c;">${message}</td>`;
|
||
}
|
||
}
|
||
|
||
// 空状态函数
|
||
function showEmpty(element, message) {
|
||
if (element) {
|
||
element.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">${message}</td>`;
|
||
}
|
||
}
|
||
|
||
// 表格排序功能
|
||
function initTableSort(tableId) {
|
||
const table = document.getElementById(tableId);
|
||
if (!table) return;
|
||
|
||
const headers = table.querySelectorAll('thead th');
|
||
headers.forEach(header => {
|
||
header.addEventListener('click', function() {
|
||
const columnIndex = Array.from(headers).indexOf(this);
|
||
const isAscending = this.getAttribute('data-sort') !== 'asc';
|
||
|
||
// 重置所有标题
|
||
headers.forEach(h => h.setAttribute('data-sort', ''));
|
||
this.setAttribute('data-sort', isAscending ? 'asc' : 'desc');
|
||
|
||
// 排序行
|
||
sortTable(table, columnIndex, isAscending);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 表格排序实现
|
||
function sortTable(table, columnIndex, isAscending) {
|
||
const tbody = table.querySelector('tbody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
|
||
// 排序行
|
||
rows.sort((a, b) => {
|
||
const aValue = a.cells[columnIndex].textContent.trim();
|
||
const bValue = b.cells[columnIndex].textContent.trim();
|
||
|
||
// 尝试数字排序
|
||
const aNum = parseFloat(aValue);
|
||
const bNum = parseFloat(bValue);
|
||
|
||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||
return isAscending ? aNum - bNum : bNum - aNum;
|
||
}
|
||
|
||
// 字符串排序
|
||
return isAscending
|
||
? aValue.localeCompare(bValue)
|
||
: bValue.localeCompare(aValue);
|
||
});
|
||
|
||
// 重新添加行
|
||
rows.forEach(row => tbody.appendChild(row));
|
||
}
|
||
|
||
// 搜索过滤功能
|
||
function initSearchFilter(inputId, tableId, columnIndex) {
|
||
const input = document.getElementById(inputId);
|
||
const table = document.getElementById(tableId);
|
||
|
||
if (!input || !table) return;
|
||
|
||
input.addEventListener('input', function() {
|
||
const filter = this.value.toLowerCase();
|
||
const rows = table.querySelectorAll('tbody tr');
|
||
|
||
rows.forEach(row => {
|
||
const cell = row.cells[columnIndex];
|
||
if (cell) {
|
||
const text = cell.textContent.toLowerCase();
|
||
row.style.display = text.includes(filter) ? '' : 'none';
|
||
}
|
||
});
|
||
});
|
||
} |