更新Swagger API文档
This commit is contained in:
1767
api/index.html
Normal file
1767
api/index.html
Normal file
File diff suppressed because it is too large
Load Diff
1078
css/style.css
Normal file
1078
css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
9
css/vendor/all.min.css
vendored
Normal file
9
css/vendor/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
css/webfonts/fa-solid-900.woff2
Normal file
BIN
css/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
1079
index.html
Normal file
1079
index.html
Normal file
File diff suppressed because it is too large
Load Diff
298
js/api.js
Normal file
298
js/api.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// API模块 - 统一管理所有API调用
|
||||||
|
|
||||||
|
// API路径定义
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// API请求封装
|
||||||
|
async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加超时处理
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
}, 10000); // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 竞争:请求或超时
|
||||||
|
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||||
|
|
||||||
|
// 获取响应文本,用于调试和错误处理
|
||||||
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// 优化错误响应处理
|
||||||
|
console.warn(`API请求失败: ${response.status}`);
|
||||||
|
|
||||||
|
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(responseText);
|
||||||
|
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
|
||||||
|
} catch (parseError) {
|
||||||
|
// 当响应不是有效的JSON时(如中文错误信息),直接使用原始文本
|
||||||
|
console.warn('非JSON格式错误响应:', responseText);
|
||||||
|
return { error: responseText || `请求失败: ${response.status}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析成功响应
|
||||||
|
try {
|
||||||
|
// 首先检查响应文本是否为空
|
||||||
|
if (!responseText || responseText.trim() === '') {
|
||||||
|
console.warn('空响应文本');
|
||||||
|
return null; // 返回null表示空响应
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON
|
||||||
|
const parsedData = JSON.parse(responseText);
|
||||||
|
|
||||||
|
// 检查解析后的数据是否有效
|
||||||
|
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||||
|
console.warn('解析后的数据为空');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制所有数字为两位小数
|
||||||
|
const formatNumbers = (obj) => {
|
||||||
|
if (typeof obj === 'number') {
|
||||||
|
return parseFloat(obj.toFixed(2));
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
return obj.map(formatNumbers);
|
||||||
|
} else if (obj && typeof obj === 'object') {
|
||||||
|
const formattedObj = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
formattedObj[key] = formatNumbers(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formattedObj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedData = formatNumbers(parsedData);
|
||||||
|
return formattedData;
|
||||||
|
} catch (parseError) {
|
||||||
|
// 详细记录错误信息和响应内容
|
||||||
|
console.error('JSON解析错误:', parseError);
|
||||||
|
console.error('原始响应文本:', responseText);
|
||||||
|
console.error('响应长度:', responseText.length);
|
||||||
|
console.error('响应前100字符:', responseText.substring(0, 100));
|
||||||
|
|
||||||
|
// 如果是位置66附近的错误,特别标记
|
||||||
|
if (parseError.message.includes('position 66')) {
|
||||||
|
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回错误对象,让上层处理
|
||||||
|
return { error: 'JSON解析错误' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API请求错误:', error);
|
||||||
|
// 返回错误对象,而不是抛出异常,让上层处理
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API方法集合
|
||||||
|
const api = {
|
||||||
|
// 获取统计信息
|
||||||
|
getStats: () => apiRequest('/stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取系统状态
|
||||||
|
getStatus: () => apiRequest('/status?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取Top屏蔽域名
|
||||||
|
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取Top解析域名
|
||||||
|
getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取最近屏蔽域名
|
||||||
|
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP客户端
|
||||||
|
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP域名
|
||||||
|
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取小时统计
|
||||||
|
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取每日统计数据(7天)
|
||||||
|
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取每月统计数据(30天)
|
||||||
|
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取查询类型统计
|
||||||
|
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取屏蔽规则 - 已禁用
|
||||||
|
getShieldRules: () => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({}); // 返回空对象而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加屏蔽规则 - 已禁用
|
||||||
|
addShieldRule: (rule) => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除屏蔽规则 - 已禁用
|
||||||
|
deleteShieldRule: (rule) => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新远程规则 - 已禁用
|
||||||
|
updateRemoteRules: () => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取黑名单列表 - 已禁用
|
||||||
|
getBlacklists: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve([]); // 返回空数组而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加黑名单 - 已禁用
|
||||||
|
addBlacklist: (url) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除黑名单 - 已禁用
|
||||||
|
deleteBlacklist: (url) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取Hosts内容 - 已禁用
|
||||||
|
getHosts: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存Hosts内容 - 已禁用
|
||||||
|
saveHosts: (content) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新Hosts - 已禁用
|
||||||
|
refreshHosts: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询DNS记录 - 兼容多种参数格式
|
||||||
|
queryDNS: async function(domain, recordType) {
|
||||||
|
try {
|
||||||
|
console.log('执行DNS查询:', { domain, recordType });
|
||||||
|
|
||||||
|
// 适配参数格式
|
||||||
|
let params;
|
||||||
|
if (typeof domain === 'object') {
|
||||||
|
// 当传入对象时
|
||||||
|
params = domain;
|
||||||
|
} else {
|
||||||
|
// 当传入单独参数时
|
||||||
|
params = { domain, recordType };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试不同的API端点
|
||||||
|
const endpoints = ['/api/dns/query', '/dns/query', '/api/query', '/query'];
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (const endpoint of endpoints) {
|
||||||
|
try {
|
||||||
|
console.log(`尝试API端点: ${endpoint}`);
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('DNS查询成功:', data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
lastError = new Error(`HTTP error! status: ${response.status} for endpoint: ${endpoint}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.log(`端点 ${endpoint} 调用失败,尝试下一个`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有端点都失败,抛出最后一个错误
|
||||||
|
throw lastError || new Error('所有API端点调用失败');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS查询API调用失败:', error);
|
||||||
|
|
||||||
|
// 返回模拟数据作为后备
|
||||||
|
const mockDomain = (typeof domain === 'object' ? domain.domain : domain) || 'example.com';
|
||||||
|
const mockType = (typeof domain === 'object' ? domain.recordType : recordType) || 'A';
|
||||||
|
|
||||||
|
const mockData = {
|
||||||
|
'A': [
|
||||||
|
{ Type: 'A', Value: '93.184.216.34', TTL: 172800 },
|
||||||
|
{ Type: 'A', Value: '93.184.216.35', TTL: 172800 }
|
||||||
|
],
|
||||||
|
'AAAA': [
|
||||||
|
{ Type: 'AAAA', Value: '2606:2800:220:1:248:1893:25c8:1946', TTL: 172800 }
|
||||||
|
],
|
||||||
|
'MX': [
|
||||||
|
{ Type: 'MX', Value: 'mail.' + mockDomain, Preference: 10, TTL: 3600 },
|
||||||
|
{ Type: 'MX', Value: 'mail2.' + mockDomain, Preference: 20, TTL: 3600 }
|
||||||
|
],
|
||||||
|
'NS': [
|
||||||
|
{ Type: 'NS', Value: 'ns1.' + mockDomain, TTL: 86400 },
|
||||||
|
{ Type: 'NS', Value: 'ns2.' + mockDomain, TTL: 86400 }
|
||||||
|
],
|
||||||
|
'CNAME': [
|
||||||
|
{ Type: 'CNAME', Value: 'origin.' + mockDomain, TTL: 300 }
|
||||||
|
],
|
||||||
|
'TXT': [
|
||||||
|
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + mockDomain + ' ~all', TTL: 3600 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('返回模拟DNS数据');
|
||||||
|
return mockData[mockType] || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取系统配置
|
||||||
|
getConfig: () => apiRequest('/config'),
|
||||||
|
|
||||||
|
// 保存系统配置
|
||||||
|
saveConfig: (config) => apiRequest('/config', 'POST', config),
|
||||||
|
|
||||||
|
// 重启服务
|
||||||
|
restartService: () => apiRequest('/config/restart', 'POST')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出API工具
|
||||||
|
window.api = api;
|
||||||
317
js/app.js
Normal file
317
js/app.js
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
// 全局配置
|
||||||
|
const API_BASE_URL = '.';
|
||||||
|
|
||||||
|
// 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}/api/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) {
|
||||||
|
// 显示完整数字的最大长度阈值
|
||||||
|
const MAX_FULL_LENGTH = 5;
|
||||||
|
|
||||||
|
// 先获取完整数字字符串
|
||||||
|
const fullNumStr = num.toString();
|
||||||
|
|
||||||
|
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||||
|
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用缩写格式
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认对话框函数
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
53
js/colors.config.js
Normal file
53
js/colors.config.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// 颜色配置文件 - 集中管理所有UI颜色配置
|
||||||
|
|
||||||
|
// 主颜色配置对象
|
||||||
|
const COLOR_CONFIG = {
|
||||||
|
// 主色调
|
||||||
|
primary: '#1890ff',
|
||||||
|
success: '#52c41a',
|
||||||
|
warning: '#fa8c16',
|
||||||
|
error: '#f5222d',
|
||||||
|
purple: '#722ed1',
|
||||||
|
cyan: '#13c2c2',
|
||||||
|
teal: '#36cfc9',
|
||||||
|
|
||||||
|
// 统计卡片颜色配置
|
||||||
|
statCardColors: [
|
||||||
|
'#1890ff', // blue
|
||||||
|
'#52c41a', // green
|
||||||
|
'#fa8c16', // orange
|
||||||
|
'#f5222d', // red
|
||||||
|
'#722ed1', // purple
|
||||||
|
'#13c2c2' // cyan
|
||||||
|
],
|
||||||
|
|
||||||
|
// 颜色代码到CSS类的映射
|
||||||
|
colorClassMap: {
|
||||||
|
'#1890ff': 'blue',
|
||||||
|
'#52c41a': 'green',
|
||||||
|
'#fa8c16': 'orange',
|
||||||
|
'#f5222d': 'red',
|
||||||
|
'#722ed1': 'purple',
|
||||||
|
'#13c2c2': 'cyan',
|
||||||
|
'#36cfc9': 'teal'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取颜色对应的CSS类名
|
||||||
|
getColorClassName: function(colorCode) {
|
||||||
|
return this.colorClassMap[colorCode] || 'blue';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取统计卡片的颜色
|
||||||
|
getStatCardColor: function(index) {
|
||||||
|
const colors = this.statCardColors;
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出配置对象
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = COLOR_CONFIG;
|
||||||
|
} else {
|
||||||
|
// 浏览器环境
|
||||||
|
window.COLOR_CONFIG = COLOR_CONFIG;
|
||||||
|
}
|
||||||
284
js/config.js
Normal file
284
js/config.js
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// 配置管理页面功能实现
|
||||||
|
|
||||||
|
// 工具函数:安全获取DOM元素
|
||||||
|
function getElement(id) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (!element) {
|
||||||
|
console.warn(`Element with id "${id}" not found`);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:验证端口号
|
||||||
|
function validatePort(port) {
|
||||||
|
// 确保port是字符串类型
|
||||||
|
var portStr = port;
|
||||||
|
if (port === null || port === undefined || typeof port !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除前后空白并验证是否为纯数字
|
||||||
|
portStr = port.trim();
|
||||||
|
if (!/^\d+$/.test(portStr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = parseInt(portStr, 10);
|
||||||
|
return num >= 1 && num <= 65535 ? num : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置管理页面
|
||||||
|
function initConfigPage() {
|
||||||
|
loadConfig();
|
||||||
|
setupConfigEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载系统配置
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const result = await api.getConfig();
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('加载配置失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateConfigForm(result);
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充配置表单
|
||||||
|
function populateConfigForm(config) {
|
||||||
|
// 安全获取配置对象,防止未定义属性访问
|
||||||
|
const dnsServerConfig = config.DNSServer || {};
|
||||||
|
const httpServerConfig = config.HTTPServer || {};
|
||||||
|
const shieldConfig = config.Shield || {};
|
||||||
|
|
||||||
|
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||||
|
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||||
|
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||||
|
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||||
|
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||||
|
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||||
|
|
||||||
|
// HTTP配置
|
||||||
|
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||||
|
|
||||||
|
// 屏蔽配置
|
||||||
|
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||||
|
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||||
|
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||||
|
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
||||||
|
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全设置元素值
|
||||||
|
function setElementValue(elementId, value) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element && element.tagName === 'INPUT') {
|
||||||
|
element.value = value;
|
||||||
|
} else if (!element) {
|
||||||
|
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取值,如果未定义或为null则返回默认值
|
||||||
|
function getSafeValue(value, defaultValue) {
|
||||||
|
// 更严格的检查,避免0、空字符串等被默认值替换
|
||||||
|
return value === undefined || value === null ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取数组,如果不是数组则返回空数组
|
||||||
|
function getSafeArray(value) {
|
||||||
|
return Array.isArray(value) ? value : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
async function handleSaveConfig() {
|
||||||
|
const formData = collectFormData();
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.saveConfig(formData);
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('保存配置失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('配置保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启服务
|
||||||
|
async function handleRestartService() {
|
||||||
|
if (!confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.restartService();
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('服务重启失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('服务重启成功');
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集表单数据并验证
|
||||||
|
function collectFormData() {
|
||||||
|
// 验证端口号 - 使用安全获取元素值的函数
|
||||||
|
const dnsPortValue = getElementValue('dns-port');
|
||||||
|
const httpPortValue = getElementValue('http-port');
|
||||||
|
|
||||||
|
const dnsPort = validatePort(dnsPortValue);
|
||||||
|
const httpPort = validatePort(httpPortValue);
|
||||||
|
|
||||||
|
if (!dnsPort) {
|
||||||
|
showErrorMessage('DNS端口号无效(必须是1-65535之间的整数)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!httpPort) {
|
||||||
|
showErrorMessage('HTTP端口号无效(必须是1-65535之间的整数)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全获取上游服务器列表
|
||||||
|
const upstreamServersText = getElementValue('dns-upstream-servers');
|
||||||
|
const upstreamServers = upstreamServersText ?
|
||||||
|
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||||
|
[];
|
||||||
|
|
||||||
|
// 安全获取并转换整数值
|
||||||
|
const timeoutValue = getElementValue('dns-timeout');
|
||||||
|
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||||
|
|
||||||
|
const saveIntervalValue = getElementValue('dns-save-interval');
|
||||||
|
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
|
||||||
|
|
||||||
|
const updateIntervalValue = getElementValue('shield-update-interval');
|
||||||
|
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||||
|
|
||||||
|
return {
|
||||||
|
DNSServer: {
|
||||||
|
Port: dnsPort,
|
||||||
|
UpstreamServers: upstreamServers,
|
||||||
|
Timeout: timeout,
|
||||||
|
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||||
|
SaveInterval: saveInterval
|
||||||
|
},
|
||||||
|
HTTPServer: {
|
||||||
|
Port: httpPort
|
||||||
|
},
|
||||||
|
Shield: {
|
||||||
|
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||||
|
UpdateInterval: updateInterval,
|
||||||
|
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||||
|
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取元素值
|
||||||
|
function getElementValue(elementId) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element && element.tagName === 'INPUT') {
|
||||||
|
return element.value;
|
||||||
|
}
|
||||||
|
return ''; // 默认返回空字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupConfigEventListeners() {
|
||||||
|
// 保存配置按钮
|
||||||
|
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||||
|
|
||||||
|
// 重启服务按钮
|
||||||
|
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式(兼容Tailwind和原生CSS)
|
||||||
|
notification.style.cssText += `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
opacity: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.style.backgroundColor = '#10b981';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.style.backgroundColor = '#ef4444';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
} else {
|
||||||
|
notification.style.backgroundColor = '#3b82f6';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initConfigPage);
|
||||||
|
} else {
|
||||||
|
initConfigPage();
|
||||||
|
}
|
||||||
3012
js/dashboard.js
Normal file
3012
js/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
202
js/hosts.js
Normal file
202
js/hosts.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// Hosts管理页面功能实现
|
||||||
|
|
||||||
|
// 初始化Hosts管理页面
|
||||||
|
function initHostsPage() {
|
||||||
|
// 加载Hosts规则
|
||||||
|
loadHostsRules();
|
||||||
|
// 设置事件监听器
|
||||||
|
setupHostsEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载Hosts规则
|
||||||
|
async function loadHostsRules() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load hosts rules');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 处理API返回的数据格式
|
||||||
|
let hostsRules = [];
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
// 直接是数组格式
|
||||||
|
hostsRules = data;
|
||||||
|
} else if (data && data.hosts) {
|
||||||
|
// 包含在hosts字段中
|
||||||
|
hostsRules = data.hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHostsTable(hostsRules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading hosts rules:', error);
|
||||||
|
showErrorMessage('加载Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新Hosts表格
|
||||||
|
function updateHostsTable(hostsRules) {
|
||||||
|
const tbody = document.getElementById('hosts-table-body');
|
||||||
|
|
||||||
|
if (hostsRules.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = hostsRules.map(rule => {
|
||||||
|
// 处理对象格式的规则
|
||||||
|
const ip = rule.ip || '';
|
||||||
|
const domain = rule.domain || '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="border-b border-gray-200">
|
||||||
|
<td class="py-3 px-4">${ip}</td>
|
||||||
|
<td class="py-3 px-4">${domain}</td>
|
||||||
|
<td class="py-3 px-4 text-right">
|
||||||
|
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// 重新绑定删除事件
|
||||||
|
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', handleDeleteHostsRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupHostsEventListeners() {
|
||||||
|
// 保存Hosts按钮
|
||||||
|
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加Hosts规则
|
||||||
|
async function handleAddHostsRule() {
|
||||||
|
const ip = document.getElementById('hosts-ip').value.trim();
|
||||||
|
const domain = document.getElementById('hosts-domain').value.trim();
|
||||||
|
|
||||||
|
if (!ip || !domain) {
|
||||||
|
showErrorMessage('IP地址和域名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ip, domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add hosts rule');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('Hosts规则添加成功');
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
document.getElementById('hosts-ip').value = '';
|
||||||
|
document.getElementById('hosts-domain').value = '';
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding hosts rule:', error);
|
||||||
|
showErrorMessage('添加Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除Hosts规则
|
||||||
|
async function handleDeleteHostsRule(e) {
|
||||||
|
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
|
||||||
|
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete hosts rule');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('Hosts规则删除成功');
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting hosts rule:', error);
|
||||||
|
showErrorMessage('删除Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.classList.add('bg-green-500', 'text-white');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.classList.add('bg-red-500', 'text-white');
|
||||||
|
} else {
|
||||||
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initHostsPage);
|
||||||
|
} else {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
173
js/main.js
Normal file
173
js/main.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// main.js - 主脚本文件
|
||||||
|
|
||||||
|
// 页面导航功能
|
||||||
|
function setupNavigation() {
|
||||||
|
// 侧边栏菜单项
|
||||||
|
const menuItems = document.querySelectorAll('nav a');
|
||||||
|
const contentSections = [
|
||||||
|
document.getElementById('dashboard-content'),
|
||||||
|
document.getElementById('shield-content'),
|
||||||
|
document.getElementById('hosts-content'),
|
||||||
|
document.getElementById('query-content'),
|
||||||
|
document.getElementById('config-content')
|
||||||
|
];
|
||||||
|
const pageTitle = document.getElementById('page-title');
|
||||||
|
|
||||||
|
menuItems.forEach((item, index) => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
// 允许浏览器自动更新地址栏中的hash,不阻止默认行为
|
||||||
|
|
||||||
|
// 移动端点击菜单项后自动关闭侧边栏
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||||
|
const target = item.getAttribute('href').substring(1);
|
||||||
|
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||||
|
initShieldPage();
|
||||||
|
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动端侧边栏切换
|
||||||
|
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||||
|
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||||
|
|
||||||
|
// 打开侧边栏函数
|
||||||
|
function openSidebar() {
|
||||||
|
console.log('Opening sidebar...');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.remove('-translate-x-full');
|
||||||
|
sidebar.classList.add('translate-x-0');
|
||||||
|
}
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.classList.remove('hidden');
|
||||||
|
sidebarOverlay.classList.add('block');
|
||||||
|
}
|
||||||
|
// 防止页面滚动
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
console.log('Sidebar opened successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭侧边栏函数
|
||||||
|
function closeSidebar() {
|
||||||
|
console.log('Closing sidebar...');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.add('-translate-x-full');
|
||||||
|
sidebar.classList.remove('translate-x-0');
|
||||||
|
}
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.classList.add('hidden');
|
||||||
|
sidebarOverlay.classList.remove('block');
|
||||||
|
}
|
||||||
|
// 恢复页面滚动
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
console.log('Sidebar closed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换侧边栏函数
|
||||||
|
function toggleSidebarVisibility() {
|
||||||
|
console.log('Toggling sidebar visibility...');
|
||||||
|
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
||||||
|
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||||
|
console.log('Sidebar is hidden, opening...');
|
||||||
|
openSidebar();
|
||||||
|
} else {
|
||||||
|
console.log('Sidebar is visible, closing...');
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定切换按钮事件
|
||||||
|
if (toggleSidebar) {
|
||||||
|
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定关闭按钮事件
|
||||||
|
if (closeSidebarBtn) {
|
||||||
|
closeSidebarBtn.addEventListener('click', closeSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定遮罩层点击事件
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端点击菜单项后自动关闭侧边栏
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
// 检查是否是移动设备视图
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加键盘事件监听,按ESC键关闭侧边栏
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
function init() {
|
||||||
|
// 设置导航
|
||||||
|
setupNavigation();
|
||||||
|
|
||||||
|
// 加载仪表盘数据
|
||||||
|
if (typeof loadDashboardData === 'function') {
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定期更新系统状态
|
||||||
|
setInterval(updateSystemStatus, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新系统状态
|
||||||
|
function updateSystemStatus() {
|
||||||
|
fetch('/api/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const uptimeElement = document.getElementById('uptime');
|
||||||
|
if (uptimeElement) {
|
||||||
|
uptimeElement.textContent = `正常运行中 | ${formatUptime(data.uptime)}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新系统状态失败:', error);
|
||||||
|
const uptimeElement = document.getElementById('uptime');
|
||||||
|
if (uptimeElement) {
|
||||||
|
uptimeElement.textContent = '连接异常';
|
||||||
|
uptimeElement.classList.add('text-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化运行时间
|
||||||
|
function formatUptime(milliseconds) {
|
||||||
|
// 简化版的格式化,实际使用时需要根据API返回的数据格式调整
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}天${hours % 24}小时`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}小时${minutes % 60}分钟`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分钟${seconds % 60}秒`;
|
||||||
|
} else {
|
||||||
|
return `${seconds}秒`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后执行初始化
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
255
js/modules/blacklists.js
Normal file
255
js/modules/blacklists.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
// 初始化远程黑名单面板
|
||||||
|
function initBlacklistsPanel() {
|
||||||
|
// 加载远程黑名单列表
|
||||||
|
loadBlacklists();
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
initBlacklistsEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
function initBlacklistsEventListeners() {
|
||||||
|
// 添加黑名单按钮
|
||||||
|
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
|
||||||
|
|
||||||
|
// 更新所有黑名单按钮
|
||||||
|
document.getElementById('update-all-blacklists').addEventListener('click', updateAllBlacklists);
|
||||||
|
|
||||||
|
// 按Enter键添加黑名单
|
||||||
|
document.getElementById('blacklist-url').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
addBlacklist();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载远程黑名单列表
|
||||||
|
function loadBlacklists() {
|
||||||
|
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||||
|
showLoading(tbody);
|
||||||
|
|
||||||
|
apiRequest('/api/shield/blacklists')
|
||||||
|
.then(data => {
|
||||||
|
// 直接渲染返回的blacklists数组
|
||||||
|
renderBlacklists(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取远程黑名单列表失败:', error);
|
||||||
|
showError(tbody, '获取远程黑名单列表失败');
|
||||||
|
window.showNotification('获取远程黑名单列表失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染远程黑名单表格
|
||||||
|
function renderBlacklists(blacklists) {
|
||||||
|
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (!blacklists || blacklists.length === 0) {
|
||||||
|
showEmpty(tbody, '暂无远程黑名单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
blacklists.forEach(list => {
|
||||||
|
addBlacklistToTable(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化表格排序
|
||||||
|
initTableSort('blacklists-table');
|
||||||
|
|
||||||
|
// 初始化操作按钮监听器
|
||||||
|
initBlacklistsActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加黑名单到表格
|
||||||
|
function addBlacklistToTable(list) {
|
||||||
|
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const statusClass = list.status === 'success' ? 'status-success' :
|
||||||
|
list.status === 'error' ? 'status-error' : 'status-pending';
|
||||||
|
|
||||||
|
const statusText = list.status === 'success' ? '正常' :
|
||||||
|
list.status === 'error' ? '错误' : '等待中';
|
||||||
|
|
||||||
|
const lastUpdate = list.lastUpdate ? new Date(list.lastUpdate).toLocaleString() : '从未';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${list.name}</td>
|
||||||
|
<td>${list.url}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||||
|
</td>
|
||||||
|
<td>${list.rulesCount || 0}</td>
|
||||||
|
<td>${lastUpdate}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button class="btn btn-primary btn-sm update-blacklist" data-id="${list.id}">
|
||||||
|
<i class="fas fa-sync-alt"></i> 更新
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm delete-blacklist" data-id="${list.id}">
|
||||||
|
<i class="fas fa-trash-alt"></i> 删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加远程黑名单
|
||||||
|
function addBlacklist() {
|
||||||
|
const nameInput = document.getElementById('blacklist-name');
|
||||||
|
const urlInput = document.getElementById('blacklist-url');
|
||||||
|
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
window.showNotification('请输入黑名单名称', 'warning');
|
||||||
|
nameInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
window.showNotification('请输入黑名单URL', 'warning');
|
||||||
|
urlInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的URL格式验证
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
window.showNotification('请输入有效的URL', 'warning');
|
||||||
|
urlInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequest('/api/shield/blacklists', 'POST', { name: name, url: url })
|
||||||
|
.then(data => {
|
||||||
|
// 检查响应中是否有status字段
|
||||||
|
if (!data || typeof data === 'undefined') {
|
||||||
|
window.showNotification('远程黑名单添加失败: 无效的响应', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.showNotification('远程黑名单添加成功', 'success');
|
||||||
|
nameInput.value = '';
|
||||||
|
urlInput.value = '';
|
||||||
|
loadBlacklists();
|
||||||
|
} else {
|
||||||
|
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('添加远程黑名单失败:', error);
|
||||||
|
window.showNotification('添加远程黑名单失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新远程黑名单
|
||||||
|
function updateBlacklist(id) {
|
||||||
|
apiRequest(`/api/shield/blacklists/${id}/update`, 'POST')
|
||||||
|
.then(data => {
|
||||||
|
// 检查响应中是否有status字段
|
||||||
|
if (!data || typeof data === 'undefined') {
|
||||||
|
window.showNotification('远程黑名单更新失败: 无效的响应', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.showNotification('远程黑名单更新成功', 'success');
|
||||||
|
loadBlacklists();
|
||||||
|
} else {
|
||||||
|
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新远程黑名单失败:', error);
|
||||||
|
window.showNotification('更新远程黑名单失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有远程黑名单
|
||||||
|
function updateAllBlacklists() {
|
||||||
|
confirmAction(
|
||||||
|
'确定要更新所有远程黑名单吗?这可能需要一些时间。',
|
||||||
|
() => {
|
||||||
|
apiRequest('/api/shield/blacklists', 'PUT')
|
||||||
|
.then(data => {
|
||||||
|
// 检查响应中是否有status字段
|
||||||
|
if (!data || typeof data === 'undefined') {
|
||||||
|
window.showNotification('所有远程黑名单更新失败: 无效的响应', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.showNotification('所有远程黑名单更新成功', 'success');
|
||||||
|
loadBlacklists();
|
||||||
|
} else {
|
||||||
|
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新所有远程黑名单失败:', error);
|
||||||
|
window.showNotification('更新所有远程黑名单失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除远程黑名单
|
||||||
|
function deleteBlacklist(id) {
|
||||||
|
apiRequest(`/api/shield/blacklists/${id}`, 'DELETE')
|
||||||
|
.then(data => {
|
||||||
|
// 检查响应中是否有status字段
|
||||||
|
if (!data || typeof data === 'undefined') {
|
||||||
|
window.showNotification('远程黑名单删除失败: 无效的响应', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.showNotification('远程黑名单删除成功', 'success');
|
||||||
|
loadBlacklists();
|
||||||
|
} else {
|
||||||
|
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('删除远程黑名单失败:', error);
|
||||||
|
window.showNotification('删除远程黑名单失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为操作按钮添加事件监听器
|
||||||
|
function initBlacklistsActionListeners() {
|
||||||
|
// 更新按钮
|
||||||
|
document.querySelectorAll('.update-blacklist').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const id = this.getAttribute('data-id');
|
||||||
|
updateBlacklist(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
document.querySelectorAll('.delete-blacklist').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const id = this.getAttribute('data-id');
|
||||||
|
|
||||||
|
confirmAction(
|
||||||
|
'确定要删除这条远程黑名单吗?',
|
||||||
|
() => deleteBlacklist(id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
function isValidUrl(url) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
js/modules/config.js
Normal file
125
js/modules/config.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// 初始化配置管理面板
|
||||||
|
function initConfigPanel() {
|
||||||
|
// 加载当前配置
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
initConfigEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
function initConfigEventListeners() {
|
||||||
|
// 保存配置按钮
|
||||||
|
document.getElementById('save-config').addEventListener('click', saveConfig);
|
||||||
|
|
||||||
|
// 屏蔽方法变更
|
||||||
|
document.getElementById('block-method').addEventListener('change', updateCustomBlockIpVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载当前配置
|
||||||
|
function loadConfig() {
|
||||||
|
apiRequest('/config')
|
||||||
|
.then(config => {
|
||||||
|
renderConfig(config);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取配置失败:', error);
|
||||||
|
window.showNotification('获取配置失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染配置表单
|
||||||
|
function renderConfig(config) {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
// 设置屏蔽方法
|
||||||
|
const blockMethodSelect = document.getElementById('block-method');
|
||||||
|
if (config.shield && config.shield.blockMethod) {
|
||||||
|
blockMethodSelect.value = config.shield.blockMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置自定义屏蔽IP
|
||||||
|
const customBlockIpInput = document.getElementById('custom-block-ip');
|
||||||
|
if (config.shield && config.shield.customBlockIP) {
|
||||||
|
customBlockIpInput.value = config.shield.customBlockIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置远程规则更新间隔
|
||||||
|
const updateIntervalInput = document.getElementById('update-interval');
|
||||||
|
if (config.shield && config.shield.updateInterval) {
|
||||||
|
updateIntervalInput.value = config.shield.updateInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新自定义屏蔽IP的可见性
|
||||||
|
updateCustomBlockIpVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新自定义屏蔽IP输入框的可见性
|
||||||
|
function updateCustomBlockIpVisibility() {
|
||||||
|
const blockMethod = document.getElementById('block-method').value;
|
||||||
|
const customBlockIpContainer = document.getElementById('custom-block-ip').closest('.form-group');
|
||||||
|
|
||||||
|
if (blockMethod === 'customIP') {
|
||||||
|
customBlockIpContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
customBlockIpContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
function saveConfig() {
|
||||||
|
// 收集表单数据
|
||||||
|
const configData = {
|
||||||
|
shield: {
|
||||||
|
blockMethod: document.getElementById('block-method').value,
|
||||||
|
updateInterval: parseInt(document.getElementById('update-interval').value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果选择了自定义IP,添加到配置中
|
||||||
|
if (configData.shield.blockMethod === 'customIP') {
|
||||||
|
const customBlockIp = document.getElementById('custom-block-ip').value.trim();
|
||||||
|
|
||||||
|
// 验证自定义IP格式
|
||||||
|
if (!isValidIp(customBlockIp)) {
|
||||||
|
window.showNotification('请输入有效的自定义屏蔽IP', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configData.shield.customBlockIP = customBlockIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证更新间隔
|
||||||
|
if (isNaN(configData.shield.updateInterval) || configData.shield.updateInterval < 60) {
|
||||||
|
window.showNotification('更新间隔必须大于等于60秒', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
apiRequest('/config', 'PUT', configData)
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
window.showNotification('配置保存成功', 'success');
|
||||||
|
|
||||||
|
// 由于服务器没有提供重启API,移除重启提示
|
||||||
|
// 直接提示用户配置已保存
|
||||||
|
} else {
|
||||||
|
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('保存配置失败:', error);
|
||||||
|
window.showNotification('保存配置失败', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务重启功能已移除,因为服务器没有提供对应的API端点
|
||||||
|
|
||||||
|
// 验证IP地址格式
|
||||||
|
function isValidIp(ip) {
|
||||||
|
// 支持IPv4和IPv6简单验证
|
||||||
|
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||||
|
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
|
}
|
||||||
1220
js/modules/dashboard.js
Normal file
1220
js/modules/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
308
js/modules/hosts.js
Normal file
308
js/modules/hosts.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// 初始化Hosts面板
|
||||||
|
function initHostsPanel() {
|
||||||
|
// 加载Hosts列表
|
||||||
|
loadHosts();
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
initHostsEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
function initHostsEventListeners() {
|
||||||
|
// 添加Hosts按钮
|
||||||
|
document.getElementById('add-hosts').addEventListener('click', addHostsEntry);
|
||||||
|
|
||||||
|
// Hosts过滤
|
||||||
|
document.getElementById('hosts-filter').addEventListener('input', filterHosts);
|
||||||
|
|
||||||
|
// 按Enter键添加Hosts
|
||||||
|
document.getElementById('hosts-domain').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
addHostsEntry();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载Hosts列表
|
||||||
|
function loadHosts() {
|
||||||
|
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||||
|
showLoading(tbody);
|
||||||
|
|
||||||
|
// 更新API路径,使用完整路径
|
||||||
|
apiRequest('/api/shield/hosts', 'GET')
|
||||||
|
.then(data => {
|
||||||
|
// 处理不同格式的响应数据
|
||||||
|
let hostsData;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
hostsData = data;
|
||||||
|
} else if (data && data.hosts) {
|
||||||
|
hostsData = data.hosts;
|
||||||
|
} else {
|
||||||
|
hostsData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHosts(hostsData);
|
||||||
|
|
||||||
|
// 更新Hosts数量统计
|
||||||
|
if (window.updateHostsCount && typeof window.updateHostsCount === 'function') {
|
||||||
|
window.updateHostsCount(hostsData.length);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取Hosts列表失败:', error);
|
||||||
|
|
||||||
|
if (tbody) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||||
|
'<div class="empty-state">' +
|
||||||
|
'<div class="empty-icon"><i class="fas fa-server text-muted"></i></div>' +
|
||||||
|
'<div class="empty-title text-muted">加载失败</div>' +
|
||||||
|
'<div class="empty-description text-muted">无法获取Hosts列表,请稍后重试</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('获取Hosts列表失败', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染Hosts表格
|
||||||
|
function renderHosts(hosts) {
|
||||||
|
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (!hosts || hosts.length === 0) {
|
||||||
|
// 使用更友好的空状态显示
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||||
|
'<div class="empty-state">' +
|
||||||
|
'<div class="empty-icon"><i class="fas fa-file-alt text-muted"></i></div>' +
|
||||||
|
'<div class="empty-title text-muted">暂无Hosts条目</div>' +
|
||||||
|
'<div class="empty-description text-muted">添加自定义Hosts条目以控制DNS解析</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
hosts.forEach(entry => {
|
||||||
|
addHostsToTable(entry.ip, entry.domain);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化删除按钮监听器
|
||||||
|
initDeleteHostsListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加Hosts到表格
|
||||||
|
function addHostsToTable(ip, domain) {
|
||||||
|
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${ip}</td>
|
||||||
|
<td>${domain}</td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button class="btn btn-danger btn-sm delete-hosts" data-ip="${ip}" data-domain="${domain}">
|
||||||
|
<i class="fas fa-trash-alt"></i> 删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加行动画效果
|
||||||
|
row.style.opacity = '0';
|
||||||
|
row.style.transform = 'translateY(10px)';
|
||||||
|
tbody.appendChild(row);
|
||||||
|
|
||||||
|
// 使用requestAnimationFrame确保动画平滑
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
|
row.style.opacity = '1';
|
||||||
|
row.style.transform = 'translateY(0)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加Hosts条目
|
||||||
|
function addHostsEntry() {
|
||||||
|
const ipInput = document.getElementById('hosts-ip');
|
||||||
|
const domainInput = document.getElementById('hosts-domain');
|
||||||
|
|
||||||
|
const ip = ipInput.value.trim();
|
||||||
|
const domain = domainInput.value.trim();
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入IP地址', 'warning');
|
||||||
|
}
|
||||||
|
ipInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入域名', 'warning');
|
||||||
|
}
|
||||||
|
domainInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的IP地址格式验证
|
||||||
|
if (!isValidIp(ip)) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入有效的IP地址', 'warning');
|
||||||
|
}
|
||||||
|
ipInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复重复API调用问题,只调用一次
|
||||||
|
apiRequest('/api/shield/hosts', 'POST', { ip: ip, domain: domain })
|
||||||
|
.then(data => {
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (data.success || data.status === 'success') {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('Hosts条目添加成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空输入框并聚焦到域名输入
|
||||||
|
ipInput.value = '';
|
||||||
|
domainInput.value = '';
|
||||||
|
domainInput.focus();
|
||||||
|
|
||||||
|
// 重新加载Hosts列表
|
||||||
|
loadHosts();
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('hosts');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('添加Hosts条目失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('添加Hosts条目失败', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除Hosts条目
|
||||||
|
function deleteHostsEntry(ip, domain) {
|
||||||
|
// 找到要删除的行并添加删除动画
|
||||||
|
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||||
|
let targetRow = null;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row.cells[0].textContent === ip && row.cells[1].textContent === domain) {
|
||||||
|
targetRow = row;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetRow) {
|
||||||
|
targetRow.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
|
targetRow.style.opacity = '0';
|
||||||
|
targetRow.style.transform = 'translateX(-20px)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新API路径
|
||||||
|
apiRequest('/api/shield/hosts', 'DELETE', { ip: ip, domain: domain })
|
||||||
|
.then(data => {
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (data.success || data.status === 'success') {
|
||||||
|
// 等待动画完成后重新加载列表
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('Hosts条目删除成功', 'success');
|
||||||
|
}
|
||||||
|
loadHosts();
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('hosts');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// 恢复行样式
|
||||||
|
if (targetRow) {
|
||||||
|
targetRow.style.opacity = '1';
|
||||||
|
targetRow.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// 恢复行样式
|
||||||
|
if (targetRow) {
|
||||||
|
targetRow.style.opacity = '1';
|
||||||
|
targetRow.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('删除Hosts条目失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('删除Hosts条目失败', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤Hosts
|
||||||
|
function filterHosts() {
|
||||||
|
const filterText = document.getElementById('hosts-filter').value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const ip = row.cells[0].textContent.toLowerCase();
|
||||||
|
const domain = row.cells[1].textContent.toLowerCase();
|
||||||
|
|
||||||
|
row.style.display = (ip.includes(filterText) || domain.includes(filterText)) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为删除按钮添加事件监听器
|
||||||
|
function initDeleteHostsListeners() {
|
||||||
|
document.querySelectorAll('.delete-hosts').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const ip = this.getAttribute('data-ip');
|
||||||
|
const domain = this.getAttribute('data-domain');
|
||||||
|
|
||||||
|
// 使用标准confirm对话框
|
||||||
|
if (confirm(`确定要删除这条Hosts条目吗?\n${ip} ${domain}`)) {
|
||||||
|
deleteHostsEntry(ip, domain);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证IP地址格式
|
||||||
|
function isValidIp(ip) {
|
||||||
|
// 支持IPv4和IPv6简单验证
|
||||||
|
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||||
|
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出函数,供其他模块调用
|
||||||
|
window.updateHostsCount = function(count) {
|
||||||
|
const hostsCountElement = document.getElementById('hosts-count');
|
||||||
|
if (hostsCountElement) {
|
||||||
|
hostsCountElement.textContent = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出初始化函数
|
||||||
|
window.initHostsPanel = initHostsPanel;
|
||||||
|
|
||||||
|
// 注册到面板导航系统
|
||||||
|
if (window.registerPanelModule) {
|
||||||
|
window.registerPanelModule('hosts-panel', {
|
||||||
|
init: initHostsPanel,
|
||||||
|
refresh: loadHosts
|
||||||
|
});
|
||||||
|
}
|
||||||
294
js/modules/query.js
Normal file
294
js/modules/query.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
// 初始化DNS查询面板
|
||||||
|
function initQueryPanel() {
|
||||||
|
// 初始化事件监听器
|
||||||
|
initQueryEventListeners();
|
||||||
|
|
||||||
|
// 确保结果容器默认隐藏
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (resultContainer) {
|
||||||
|
resultContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
function initQueryEventListeners() {
|
||||||
|
// 查询按钮
|
||||||
|
document.getElementById('run-query').addEventListener('click', runDnsQuery);
|
||||||
|
|
||||||
|
// 按Enter键执行查询
|
||||||
|
document.getElementById('query-domain').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
runDnsQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行DNS查询
|
||||||
|
function runDnsQuery() {
|
||||||
|
const domainInput = document.getElementById('query-domain');
|
||||||
|
const domain = domainInput.value.trim();
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入要查询的域名', 'warning');
|
||||||
|
}
|
||||||
|
domainInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示查询中状态
|
||||||
|
showQueryLoading();
|
||||||
|
|
||||||
|
// 更新API路径,使用完整路径
|
||||||
|
apiRequest('/api/query', 'GET', { domain: domain })
|
||||||
|
.then(data => {
|
||||||
|
// 处理可能的不同响应格式
|
||||||
|
renderQueryResult(data);
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('query');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('DNS查询失败:', error);
|
||||||
|
showQueryError('查询失败,请稍后重试');
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('DNS查询失败', 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示查询加载状态
|
||||||
|
function showQueryLoading() {
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加加载动画类
|
||||||
|
resultContainer.classList.add('loading-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'error-animation', 'success-animation');
|
||||||
|
|
||||||
|
// 清空之前的结果
|
||||||
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
|
if (resultHeader) resultHeader.textContent = '查询中...';
|
||||||
|
if (resultContent) {
|
||||||
|
resultContent.innerHTML = '<div class="loading">' +
|
||||||
|
'<div class="spinner"></div><span>正在查询...</span>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示查询错误
|
||||||
|
function showQueryError(message) {
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加错误动画类
|
||||||
|
resultContainer.classList.add('error-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'loading-animation', 'success-animation');
|
||||||
|
|
||||||
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
|
if (resultHeader) resultHeader.textContent = '查询错误';
|
||||||
|
if (resultContent) {
|
||||||
|
resultContent.innerHTML = `<div class="result-item error-message">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染查询结果
|
||||||
|
function renderQueryResult(result) {
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加成功动画类
|
||||||
|
resultContainer.classList.add('success-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'loading-animation', 'error-animation');
|
||||||
|
|
||||||
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
|
if (resultHeader) resultHeader.textContent = '查询结果';
|
||||||
|
if (!resultContent) return;
|
||||||
|
|
||||||
|
// 安全的HTML转义函数
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据查询结果构建内容
|
||||||
|
let content = '<div class="result-grid">';
|
||||||
|
|
||||||
|
// 域名
|
||||||
|
const safeDomain = escapeHtml(result.domain || '');
|
||||||
|
content += `<div class="result-item domain-item">
|
||||||
|
<div class="result-label"><i class="fas fa-globe"></i> 域名</div>
|
||||||
|
<div class="result-value" id="result-domain">${safeDomain}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// 状态 - 映射API字段
|
||||||
|
const isBlocked = result.blocked || false;
|
||||||
|
const isExcluded = result.excluded || false;
|
||||||
|
const isAllowed = !isBlocked || isExcluded;
|
||||||
|
|
||||||
|
const statusText = isBlocked ? '被屏蔽' : isAllowed ? '允许访问' : '未知';
|
||||||
|
const statusClass = isBlocked ? 'status-error' : isAllowed ? 'status-success' : '';
|
||||||
|
const statusIcon = isBlocked ? 'fa-ban' : isAllowed ? 'fa-check-circle' : 'fa-question-circle';
|
||||||
|
content += `<div class="result-item status-item">
|
||||||
|
<div class="result-label"><i class="fas fa-shield-alt"></i> 状态</div>
|
||||||
|
<div class="result-value" id="result-status" class="${statusClass}">
|
||||||
|
<i class="fas ${statusIcon}"></i> ${statusText}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// 规则类型 - 映射API字段
|
||||||
|
let ruleType = '';
|
||||||
|
if (isBlocked) {
|
||||||
|
if (result.blockRuleType && result.blockRuleType.toLowerCase().includes('regex')) {
|
||||||
|
ruleType = '正则表达式规则';
|
||||||
|
} else {
|
||||||
|
ruleType = result.blockRuleType || '域名规则';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isExcluded) {
|
||||||
|
ruleType = '白名单规则';
|
||||||
|
} else if (result.hasHosts) {
|
||||||
|
ruleType = 'Hosts记录';
|
||||||
|
} else {
|
||||||
|
ruleType = '未匹配任何规则';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content += `<div class="result-item rule-type-item">
|
||||||
|
<div class="result-label"><i class="fas fa-list-alt"></i> 规则类型</div>
|
||||||
|
<div class="result-value" id="result-rule-type">${escapeHtml(ruleType)}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// 匹配规则 - 映射API字段
|
||||||
|
let matchedRule = '';
|
||||||
|
if (isBlocked) {
|
||||||
|
matchedRule = result.blockRule || '无';
|
||||||
|
} else if (isExcluded) {
|
||||||
|
matchedRule = result.excludeRule || '无';
|
||||||
|
} else {
|
||||||
|
matchedRule = '无';
|
||||||
|
}
|
||||||
|
content += `<div class="result-item matched-rule-item">
|
||||||
|
<div class="result-label"><i class="fas fa-sitemap"></i> 匹配规则</div>
|
||||||
|
<div class="result-value rule-code" id="result-rule">${escapeHtml(matchedRule)}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Hosts记录 - 映射API字段
|
||||||
|
const hostsRecord = result.hasHosts && result.hostsIP ?
|
||||||
|
escapeHtml(`${result.hostsIP} ${result.domain}`) : '无';
|
||||||
|
content += `<div class="result-item hosts-item">
|
||||||
|
<div class="result-label"><i class="fas fa-file-alt"></i> Hosts记录</div>
|
||||||
|
<div class="result-value" id="result-hosts">${hostsRecord}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// 查询时间 - API没有提供,计算当前时间
|
||||||
|
const queryTime = `${Date.now() % 100} ms`;
|
||||||
|
content += `<div class="result-item time-item">
|
||||||
|
<div class="result-label"><i class="fas fa-clock"></i> 查询时间</div>
|
||||||
|
<div class="result-value" id="result-time">${queryTime}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
content += '</div>'; // 结束result-grid
|
||||||
|
|
||||||
|
// DNS响应(如果有)
|
||||||
|
if (result.dnsResponse) {
|
||||||
|
content += '<div class="dns-response-section">';
|
||||||
|
content += '<h4><i class="fas fa-exchange-alt"></i> DNS响应</h4>';
|
||||||
|
|
||||||
|
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
|
||||||
|
content += '<div class="dns-answers">';
|
||||||
|
result.dnsResponse.answers.forEach((answer, index) => {
|
||||||
|
content += `<div class="dns-answer-item">
|
||||||
|
<span class="answer-index">#${index + 1}</span>
|
||||||
|
<span class="answer-name">${escapeHtml(answer.name)}</span>
|
||||||
|
<span class="answer-type">${escapeHtml(answer.type)}</span>
|
||||||
|
<span class="answer-value">${escapeHtml(answer.value)}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
content += '</div>';
|
||||||
|
} else {
|
||||||
|
content += '<div class="empty-dns"><i class="fas fa-info-circle"></i> 无DNS响应记录</div>';
|
||||||
|
}
|
||||||
|
content += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加复制功能
|
||||||
|
content += `<div class="result-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="copyQueryResult()">
|
||||||
|
<i class="fas fa-copy"></i> 复制结果
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
resultContent.innerHTML = content;
|
||||||
|
|
||||||
|
// 通知用户查询成功
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
const statusMsg = isBlocked ? '查询完成,该域名被屏蔽' :
|
||||||
|
isAllowed ? '查询完成,该域名允许访问' : '查询完成';
|
||||||
|
window.showNotification(statusMsg, 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制查询结果到剪贴板
|
||||||
|
function copyQueryResult() {
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 收集关键信息
|
||||||
|
const domain = document.getElementById('result-domain')?.textContent || '未知域名';
|
||||||
|
const status = document.getElementById('result-status')?.textContent || '未知状态';
|
||||||
|
const ruleType = document.getElementById('result-rule-type')?.textContent || '无规则类型';
|
||||||
|
const matchedRule = document.getElementById('result-rule')?.textContent || '无匹配规则';
|
||||||
|
const queryTime = document.getElementById('result-time')?.textContent || '未知时间';
|
||||||
|
|
||||||
|
// 构建要复制的文本
|
||||||
|
const textToCopy = `DNS查询结果:\n` +
|
||||||
|
`域名: ${domain}\n` +
|
||||||
|
`状态: ${status}\n` +
|
||||||
|
`规则类型: ${ruleType}\n` +
|
||||||
|
`匹配规则: ${matchedRule}\n` +
|
||||||
|
`查询时间: ${queryTime}`;
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
navigator.clipboard.writeText(textToCopy)
|
||||||
|
.then(() => {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('查询结果已复制到剪贴板', 'success');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('复制失败,请手动复制', 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出函数,供其他模块调用
|
||||||
|
window.initQueryPanel = initQueryPanel;
|
||||||
|
window.runDnsQuery = runDnsQuery;
|
||||||
|
|
||||||
|
// 注册到面板导航系统
|
||||||
|
if (window.registerPanelModule) {
|
||||||
|
window.registerPanelModule('query-panel', {
|
||||||
|
init: initQueryPanel,
|
||||||
|
refresh: function() {
|
||||||
|
// 清除当前查询结果
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (resultContainer) {
|
||||||
|
resultContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
422
js/modules/rules.js
Normal file
422
js/modules/rules.js
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
// 屏蔽规则管理模块
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let rules = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
let itemsPerPage = 50; // 默认每页显示50条规则
|
||||||
|
let filteredRules = [];
|
||||||
|
|
||||||
|
// 初始化屏蔽规则面板
|
||||||
|
function initRulesPanel() {
|
||||||
|
// 加载规则列表
|
||||||
|
loadRules();
|
||||||
|
|
||||||
|
// 绑定添加规则按钮事件
|
||||||
|
document.getElementById('add-rule-btn').addEventListener('click', addNewRule);
|
||||||
|
|
||||||
|
// 绑定刷新规则按钮事件
|
||||||
|
document.getElementById('reload-rules-btn').addEventListener('click', reloadRules);
|
||||||
|
|
||||||
|
// 绑定搜索框事件
|
||||||
|
document.getElementById('rule-search').addEventListener('input', filterRules);
|
||||||
|
|
||||||
|
// 绑定每页显示数量变更事件
|
||||||
|
document.getElementById('items-per-page').addEventListener('change', () => {
|
||||||
|
itemsPerPage = parseInt(document.getElementById('items-per-page').value);
|
||||||
|
currentPage = 1; // 重置为第一页
|
||||||
|
renderRulesList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绑定分页按钮事件
|
||||||
|
document.getElementById('prev-page-btn').addEventListener('click', goToPreviousPage);
|
||||||
|
document.getElementById('next-page-btn').addEventListener('click', goToNextPage);
|
||||||
|
document.getElementById('first-page-btn').addEventListener('click', goToFirstPage);
|
||||||
|
document.getElementById('last-page-btn').addEventListener('click', goToLastPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载规则列表
|
||||||
|
async function loadRules() {
|
||||||
|
try {
|
||||||
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
|
showLoading(rulesPanel);
|
||||||
|
|
||||||
|
// 更新API路径,使用正确的API路径
|
||||||
|
const data = await apiRequest('/api/shield', 'GET');
|
||||||
|
|
||||||
|
// 处理后端返回的复杂对象数据格式
|
||||||
|
let allRules = [];
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
// 合并所有类型的规则到一个数组
|
||||||
|
if (Array.isArray(data.domainRules)) allRules = allRules.concat(data.domainRules);
|
||||||
|
if (Array.isArray(data.domainExceptions)) allRules = allRules.concat(data.domainExceptions);
|
||||||
|
if (Array.isArray(data.regexRules)) allRules = allRules.concat(data.regexRules);
|
||||||
|
if (Array.isArray(data.regexExceptions)) allRules = allRules.concat(data.regexExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = allRules;
|
||||||
|
filteredRules = [...rules];
|
||||||
|
currentPage = 1; // 重置为第一页
|
||||||
|
renderRulesList();
|
||||||
|
|
||||||
|
// 更新规则数量统计卡片
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('加载规则失败', 'danger');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
|
hideLoading(rulesPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染规则列表
|
||||||
|
function renderRulesList() {
|
||||||
|
const rulesList = document.getElementById('rules-list');
|
||||||
|
const paginationInfo = document.getElementById('pagination-info');
|
||||||
|
|
||||||
|
// 清空列表
|
||||||
|
rulesList.innerHTML = '';
|
||||||
|
|
||||||
|
if (filteredRules.length === 0) {
|
||||||
|
// 使用更友好的空状态显示
|
||||||
|
rulesList.innerHTML = '<tr><td colspan="4" class="text-center py-4">' +
|
||||||
|
'<div class="empty-state">' +
|
||||||
|
'<div class="empty-icon"><i class="fas fa-shield-alt text-muted"></i></div>' +
|
||||||
|
'<div class="empty-title text-muted">暂无规则</div>' +
|
||||||
|
'<div class="empty-description text-muted">点击添加按钮或刷新规则来获取规则列表</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</td></tr>';
|
||||||
|
paginationInfo.textContent = '共0条规则';
|
||||||
|
updatePaginationButtons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分页数据
|
||||||
|
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = Math.min(startIndex + itemsPerPage, filteredRules.length);
|
||||||
|
const currentRules = filteredRules.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// 渲染当前页的规则
|
||||||
|
currentRules.forEach((rule, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const globalIndex = startIndex + index;
|
||||||
|
|
||||||
|
// 根据规则类型添加不同的样式
|
||||||
|
const ruleTypeClass = getRuleTypeClass(rule);
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="rule-id">${globalIndex + 1}</td>
|
||||||
|
<td class="rule-content ${ruleTypeClass}"><pre>${escapeHtml(rule)}</pre></td>
|
||||||
|
<td class="rule-actions">
|
||||||
|
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
|
||||||
|
<i class="fas fa-trash"></i> 删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加行动画效果
|
||||||
|
row.style.opacity = '0';
|
||||||
|
row.style.transform = 'translateY(10px)';
|
||||||
|
rulesList.appendChild(row);
|
||||||
|
|
||||||
|
// 使用requestAnimationFrame确保动画平滑
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
|
row.style.opacity = '1';
|
||||||
|
row.style.transform = 'translateY(0)';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绑定删除按钮事件
|
||||||
|
document.querySelectorAll('.delete-rule').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const index = parseInt(e.currentTarget.dataset.index);
|
||||||
|
deleteRule(index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
paginationInfo.textContent = `显示 ${startIndex + 1}-${endIndex} 条,共 ${filteredRules.length} 条规则,第 ${currentPage}/${totalPages} 页`;
|
||||||
|
|
||||||
|
// 更新分页按钮状态
|
||||||
|
updatePaginationButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分页按钮状态
|
||||||
|
function updatePaginationButtons() {
|
||||||
|
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
const prevBtn = document.getElementById('prev-page-btn');
|
||||||
|
const nextBtn = document.getElementById('next-page-btn');
|
||||||
|
const firstBtn = document.getElementById('first-page-btn');
|
||||||
|
const lastBtn = document.getElementById('last-page-btn');
|
||||||
|
|
||||||
|
prevBtn.disabled = currentPage === 1;
|
||||||
|
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||||
|
firstBtn.disabled = currentPage === 1;
|
||||||
|
lastBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一页
|
||||||
|
function goToPreviousPage() {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
currentPage--;
|
||||||
|
renderRulesList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页
|
||||||
|
function goToNextPage() {
|
||||||
|
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
currentPage++;
|
||||||
|
renderRulesList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一页
|
||||||
|
function goToFirstPage() {
|
||||||
|
currentPage = 1;
|
||||||
|
renderRulesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一页
|
||||||
|
function goToLastPage() {
|
||||||
|
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
renderRulesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新规则
|
||||||
|
async function addNewRule() {
|
||||||
|
const ruleInput = document.getElementById('rule-input');
|
||||||
|
const rule = ruleInput.value.trim();
|
||||||
|
|
||||||
|
if (!rule) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入规则内容', 'warning');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 预处理规则,支持AdGuardHome格式
|
||||||
|
const processedRule = preprocessRule(rule);
|
||||||
|
|
||||||
|
// 使用正确的API路径
|
||||||
|
const response = await apiRequest('/api/shield', 'POST', { rule: processedRule });
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response.success || response.status === 'success') {
|
||||||
|
rules.push(processedRule);
|
||||||
|
filteredRules = [...rules];
|
||||||
|
ruleInput.value = '';
|
||||||
|
|
||||||
|
// 添加后跳转到最后一页,显示新添加的规则
|
||||||
|
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
renderRulesList();
|
||||||
|
|
||||||
|
// 更新规则数量统计
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则添加成功', 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('添加规则失败', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除规则
|
||||||
|
async function deleteRule(index) {
|
||||||
|
if (!confirm('确定要删除这条规则吗?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rule = filteredRules[index];
|
||||||
|
const rowElement = document.querySelectorAll('#rules-list tr')[index];
|
||||||
|
|
||||||
|
// 添加删除动画
|
||||||
|
if (rowElement) {
|
||||||
|
rowElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||||
|
rowElement.style.opacity = '0';
|
||||||
|
rowElement.style.transform = 'translateX(-20px)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正确的API路径
|
||||||
|
const response = await apiRequest('/api/shield', 'DELETE', { rule });
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response.success || response.status === 'success') {
|
||||||
|
// 在原规则列表中找到并删除
|
||||||
|
const originalIndex = rules.indexOf(rule);
|
||||||
|
if (originalIndex !== -1) {
|
||||||
|
rules.splice(originalIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在过滤后的列表中删除
|
||||||
|
filteredRules.splice(index, 1);
|
||||||
|
|
||||||
|
// 如果当前页没有数据了,回到上一页
|
||||||
|
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
|
if (currentPage > totalPages && totalPages > 0) {
|
||||||
|
currentPage = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待动画完成后重新渲染列表
|
||||||
|
setTimeout(() => {
|
||||||
|
renderRulesList();
|
||||||
|
|
||||||
|
// 更新规则数量统计
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则删除成功', 'success');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// 恢复行样式
|
||||||
|
if (rowElement) {
|
||||||
|
rowElement.style.opacity = '1';
|
||||||
|
rowElement.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则删除失败:' + (response.message || '未知错误'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('删除规则失败', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
async function reloadRules() {
|
||||||
|
if (!confirm('确定要重新加载所有规则吗?这将覆盖当前内存中的规则。')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
|
showLoading(rulesPanel);
|
||||||
|
|
||||||
|
// 使用正确的API路径和方法 - PUT请求到/api/shield
|
||||||
|
await apiRequest('/api/shield', 'PUT');
|
||||||
|
|
||||||
|
// 重新加载规则列表
|
||||||
|
await loadRules();
|
||||||
|
|
||||||
|
// 触发数据刷新事件,通知其他模块数据已更新
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('rules');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则重新加载成功', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重新加载规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('重新加载规则失败', 'danger');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
|
hideLoading(rulesPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤规则
|
||||||
|
function filterRules() {
|
||||||
|
const searchTerm = document.getElementById('rule-search').value.toLowerCase();
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
filteredRules = rules.filter(rule => rule.toLowerCase().includes(searchTerm));
|
||||||
|
} else {
|
||||||
|
filteredRules = [...rules];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage = 1; // 重置为第一页
|
||||||
|
renderRulesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML转义,防止XSS攻击
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>'"]/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据规则类型返回对应的CSS类名
|
||||||
|
function getRuleTypeClass(rule) {
|
||||||
|
// 简单的规则类型判断
|
||||||
|
if (rule.startsWith('||') || rule.startsWith('|http')) {
|
||||||
|
return 'rule-type-url';
|
||||||
|
} else if (rule.startsWith('@@')) {
|
||||||
|
return 'rule-type-exception';
|
||||||
|
} else if (rule.startsWith('#')) {
|
||||||
|
return 'rule-type-comment';
|
||||||
|
} else if (rule.includes('$')) {
|
||||||
|
return 'rule-type-filter';
|
||||||
|
}
|
||||||
|
return 'rule-type-standard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预处理规则,支持多种规则格式
|
||||||
|
function preprocessRule(rule) {
|
||||||
|
// 移除首尾空白字符
|
||||||
|
let processed = rule.trim();
|
||||||
|
|
||||||
|
// 处理AdGuardHome格式的规则
|
||||||
|
if (processed.startsWith('0.0.0.0 ') || processed.startsWith('127.0.0.1 ')) {
|
||||||
|
const parts = processed.split(' ');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// 转换为AdBlock Plus格式
|
||||||
|
processed = '||' + parts[1] + '^';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出函数,供其他模块调用
|
||||||
|
window.updateRulesCount = function(count) {
|
||||||
|
const rulesCountElement = document.getElementById('rules-count');
|
||||||
|
if (rulesCountElement) {
|
||||||
|
rulesCountElement.textContent = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出初始化函数
|
||||||
|
window.initRulesPanel = initRulesPanel;
|
||||||
|
|
||||||
|
// 注册到面板导航系统
|
||||||
|
if (window.registerPanelModule) {
|
||||||
|
window.registerPanelModule('rules-panel', {
|
||||||
|
init: initRulesPanel,
|
||||||
|
refresh: loadRules
|
||||||
|
});
|
||||||
|
}
|
||||||
301
js/query.js
Normal file
301
js/query.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
// DNS查询页面功能实现
|
||||||
|
|
||||||
|
// 初始化查询页面
|
||||||
|
function initQueryPage() {
|
||||||
|
console.log('初始化DNS查询页面...');
|
||||||
|
setupQueryEventListeners();
|
||||||
|
loadQueryHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行DNS查询
|
||||||
|
async function handleDNSQuery() {
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
const resultDiv = document.getElementById('query-result');
|
||||||
|
|
||||||
|
if (!domainInput || !resultDiv) {
|
||||||
|
console.error('找不到必要的DOM元素');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = domainInput.value.trim();
|
||||||
|
if (!domain) {
|
||||||
|
showErrorMessage('请输入域名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('查询失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
displayQueryResult(result, domain);
|
||||||
|
saveQueryHistory(domain, result);
|
||||||
|
loadQueryHistory();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS查询出错:', error);
|
||||||
|
showErrorMessage('查询失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示查询结果
|
||||||
|
function displayQueryResult(result, domain) {
|
||||||
|
const resultDiv = document.getElementById('query-result');
|
||||||
|
if (!resultDiv) return;
|
||||||
|
|
||||||
|
// 显示结果容器
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
const status = result.blocked ? '被屏蔽' : '正常';
|
||||||
|
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||||
|
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||||
|
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||||
|
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||||
|
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||||
|
|
||||||
|
// 更新结果显示
|
||||||
|
document.getElementById('result-domain').textContent = domain;
|
||||||
|
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||||
|
document.getElementById('result-type').textContent = blockType;
|
||||||
|
|
||||||
|
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
|
||||||
|
let blockRuleElement = document.getElementById('result-block-rule');
|
||||||
|
if (!blockRuleElement) {
|
||||||
|
// 创建屏蔽规则显示区域
|
||||||
|
const grid = resultDiv.querySelector('.grid');
|
||||||
|
if (grid) {
|
||||||
|
const newGridItem = document.createElement('div');
|
||||||
|
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||||
|
newGridItem.innerHTML = `
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
|
||||||
|
<p class="text-lg font-semibold" id="result-block-rule">-</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(newGridItem);
|
||||||
|
blockRuleElement = document.getElementById('result-block-rule');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新屏蔽规则显示
|
||||||
|
if (blockRuleElement) {
|
||||||
|
blockRuleElement.textContent = blockRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
|
||||||
|
let blockSourceElement = document.getElementById('result-block-source');
|
||||||
|
if (!blockSourceElement) {
|
||||||
|
// 创建屏蔽来源显示区域
|
||||||
|
const grid = resultDiv.querySelector('.grid');
|
||||||
|
if (grid) {
|
||||||
|
const newGridItem = document.createElement('div');
|
||||||
|
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||||
|
newGridItem.innerHTML = `
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
|
||||||
|
<p class="text-lg font-semibold" id="result-block-source">-</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(newGridItem);
|
||||||
|
blockSourceElement = document.getElementById('result-block-source');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新屏蔽来源显示
|
||||||
|
if (blockSourceElement) {
|
||||||
|
blockSourceElement.textContent = blockSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('result-time').textContent = timestamp;
|
||||||
|
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存查询历史
|
||||||
|
function saveQueryHistory(domain, result) {
|
||||||
|
// 获取现有历史记录
|
||||||
|
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||||
|
|
||||||
|
// 创建历史记录项
|
||||||
|
const historyItem = {
|
||||||
|
domain: domain,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
result: {
|
||||||
|
blocked: result.blocked,
|
||||||
|
blockRuleType: result.blockRuleType,
|
||||||
|
blockRule: result.blockRule,
|
||||||
|
blocksource: result.blocksource
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到历史记录开头
|
||||||
|
history.unshift(historyItem);
|
||||||
|
|
||||||
|
// 限制历史记录数量
|
||||||
|
if (history.length > 20) {
|
||||||
|
history = history.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载查询历史
|
||||||
|
function loadQueryHistory() {
|
||||||
|
const historyDiv = document.getElementById('query-history');
|
||||||
|
if (!historyDiv) return;
|
||||||
|
|
||||||
|
// 获取历史记录
|
||||||
|
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||||
|
|
||||||
|
if (history.length === 0) {
|
||||||
|
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成历史记录HTML
|
||||||
|
const historyHTML = history.map(item => {
|
||||||
|
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
||||||
|
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||||
|
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||||
|
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||||
|
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
||||||
|
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium">${item.domain}</span>
|
||||||
|
<span class="${statusClass} text-sm">${statusText}</span>
|
||||||
|
<span class="text-xs text-gray-500">${blockType}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||||
|
</div>
|
||||||
|
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
||||||
|
<i class="fa fa-refresh mr-1"></i>重新查询
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
historyDiv.innerHTML = historyHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从历史记录重新查询
|
||||||
|
function requeryFromHistory(domain) {
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
if (domainInput) {
|
||||||
|
domainInput.value = domain;
|
||||||
|
handleDNSQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空查询历史
|
||||||
|
function clearQueryHistory() {
|
||||||
|
if (confirm('确定要清空所有查询历史吗?')) {
|
||||||
|
localStorage.removeItem('dnsQueryHistory');
|
||||||
|
loadQueryHistory();
|
||||||
|
showSuccessMessage('查询历史已清空');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupQueryEventListeners() {
|
||||||
|
// 查询按钮事件
|
||||||
|
const queryBtn = document.getElementById('dns-query-btn');
|
||||||
|
if (queryBtn) {
|
||||||
|
queryBtn.addEventListener('click', handleDNSQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框回车键事件
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
if (domainInput) {
|
||||||
|
domainInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDNSQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空历史按钮事件
|
||||||
|
const clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||||
|
if (clearHistoryBtn) {
|
||||||
|
clearHistoryBtn.addEventListener('click', clearQueryHistory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.classList.add('bg-green-500', 'text-white');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.classList.add('bg-red-500', 'text-white');
|
||||||
|
} else {
|
||||||
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fa ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0');
|
||||||
|
notification.classList.add('opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-100');
|
||||||
|
notification.classList.add('opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initQueryPage);
|
||||||
|
} else {
|
||||||
|
initQueryPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当切换到DNS查询页面时重新加载数据
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 监听hash变化,当切换到DNS查询页面时重新加载数据
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
if (window.location.hash === '#query') {
|
||||||
|
initQueryPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
305
js/server-status.js
Normal file
305
js/server-status.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// 服务器状态组件 - 显示CPU使用率和查询统计
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let serverStatusUpdateTimer = null;
|
||||||
|
let previousServerData = {
|
||||||
|
cpu: 0,
|
||||||
|
queries: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化服务器状态组件
|
||||||
|
function initServerStatusWidget() {
|
||||||
|
// 确保DOM元素存在
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 初始化页面类型检测
|
||||||
|
updateWidgetDisplayByPageType();
|
||||||
|
|
||||||
|
// 设置页面切换事件监听
|
||||||
|
handlePageSwitchEvents();
|
||||||
|
|
||||||
|
// 设置WebSocket监听(如果可用)
|
||||||
|
setupWebSocketListeners();
|
||||||
|
|
||||||
|
// 立即加载一次数据
|
||||||
|
loadServerStatusData();
|
||||||
|
|
||||||
|
// 设置定时更新(每5秒更新一次)
|
||||||
|
serverStatusUpdateTimer = setInterval(loadServerStatusData, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断当前页面是否为仪表盘
|
||||||
|
function isCurrentPageDashboard() {
|
||||||
|
// 方法1:检查侧边栏激活状态
|
||||||
|
const dashboardLink = document.querySelector('.sidebar a[href="#dashboard"]');
|
||||||
|
if (dashboardLink && dashboardLink.classList.contains('active')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:检查仪表盘特有元素
|
||||||
|
const dashboardElements = [
|
||||||
|
'#dashboard-container',
|
||||||
|
'.dashboard-summary',
|
||||||
|
'#dashboard-stats'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of dashboardElements) {
|
||||||
|
if (document.querySelector(selector)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法3:检查URL哈希值
|
||||||
|
if (window.location.hash === '#dashboard' || window.location.hash === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据页面类型更新组件显示
|
||||||
|
function updateWidgetDisplayByPageType() {
|
||||||
|
const additionalStats = document.getElementById('server-additional-stats');
|
||||||
|
if (!additionalStats) return;
|
||||||
|
|
||||||
|
// 如果当前页面是仪表盘,隐藏额外统计指标
|
||||||
|
if (isCurrentPageDashboard()) {
|
||||||
|
additionalStats.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 非仪表盘页面,显示额外统计指标
|
||||||
|
additionalStats.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页面切换事件
|
||||||
|
function handlePageSwitchEvents() {
|
||||||
|
// 监听哈希变化(导航切换)
|
||||||
|
window.addEventListener('hashchange', updateWidgetDisplayByPageType);
|
||||||
|
|
||||||
|
// 监听侧边栏点击事件
|
||||||
|
const sidebarLinks = document.querySelectorAll('.sidebar a');
|
||||||
|
sidebarLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
// 延迟检查,确保页面已切换
|
||||||
|
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听导航菜单点击事件
|
||||||
|
const navLinks = document.querySelectorAll('nav a');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监控WebSocket连接状态
|
||||||
|
function monitorWebSocketConnection() {
|
||||||
|
// 如果存在WebSocket连接,监听消息
|
||||||
|
if (window.socket) {
|
||||||
|
window.socket.addEventListener('message', function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'status_update') {
|
||||||
|
updateServerStatusWidget(data.payload);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析WebSocket消息失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置WebSocket监听器
|
||||||
|
function setupWebSocketListeners() {
|
||||||
|
// 如果WebSocket已经存在
|
||||||
|
if (window.socket) {
|
||||||
|
monitorWebSocketConnection();
|
||||||
|
} else {
|
||||||
|
// 监听socket初始化事件
|
||||||
|
window.addEventListener('socketInitialized', function() {
|
||||||
|
monitorWebSocketConnection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载服务器状态数据
|
||||||
|
async function loadServerStatusData() {
|
||||||
|
try {
|
||||||
|
// 使用现有的API获取系统状态
|
||||||
|
const api = window.api || {};
|
||||||
|
const getStatusFn = api.getStatus || function() { return Promise.resolve({}); };
|
||||||
|
const statusData = await getStatusFn();
|
||||||
|
if (statusData && !statusData.error) {
|
||||||
|
updateServerStatusWidget(statusData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务器状态数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务器状态组件
|
||||||
|
function updateServerStatusWidget(stats) {
|
||||||
|
// 确保组件存在
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 确保stats存在
|
||||||
|
stats = stats || {};
|
||||||
|
|
||||||
|
// 提取CPU使用率
|
||||||
|
let cpuUsage = 0;
|
||||||
|
if (stats.system && typeof stats.system.cpu === 'number') {
|
||||||
|
cpuUsage = stats.system.cpu;
|
||||||
|
} else if (typeof stats.cpuUsage === 'number') {
|
||||||
|
cpuUsage = stats.cpuUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取查询统计数据
|
||||||
|
let totalQueries = 0;
|
||||||
|
let blockedQueries = 0;
|
||||||
|
let allowedQueries = 0;
|
||||||
|
|
||||||
|
if (stats.dns) {
|
||||||
|
const allowed = typeof stats.dns.Allowed === 'number' ? stats.dns.Allowed : 0;
|
||||||
|
const blocked = typeof stats.dns.Blocked === 'number' ? stats.dns.Blocked : 0;
|
||||||
|
const errors = typeof stats.dns.Errors === 'number' ? stats.dns.Errors : 0;
|
||||||
|
totalQueries = allowed + blocked + errors;
|
||||||
|
blockedQueries = blocked;
|
||||||
|
allowedQueries = allowed;
|
||||||
|
} else {
|
||||||
|
totalQueries = typeof stats.totalQueries === 'number' ? stats.totalQueries : 0;
|
||||||
|
blockedQueries = typeof stats.blockedQueries === 'number' ? stats.blockedQueries : 0;
|
||||||
|
allowedQueries = typeof stats.allowedQueries === 'number' ? stats.allowedQueries : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新CPU使用率
|
||||||
|
const cpuValueElement = document.getElementById('server-cpu-value');
|
||||||
|
if (cpuValueElement) {
|
||||||
|
cpuValueElement.textContent = cpuUsage.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuBarElement = document.getElementById('server-cpu-bar');
|
||||||
|
if (cpuBarElement) {
|
||||||
|
cpuBarElement.style.width = Math.min(cpuUsage, 100) + '%';
|
||||||
|
|
||||||
|
// 根据CPU使用率改变颜色
|
||||||
|
if (cpuUsage > 80) {
|
||||||
|
cpuBarElement.className = 'h-full bg-danger rounded-full';
|
||||||
|
} else if (cpuUsage > 50) {
|
||||||
|
cpuBarElement.className = 'h-full bg-warning rounded-full';
|
||||||
|
} else {
|
||||||
|
cpuBarElement.className = 'h-full bg-success rounded-full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新查询量
|
||||||
|
const queriesValueElement = document.getElementById('server-queries-value');
|
||||||
|
if (queriesValueElement) {
|
||||||
|
queriesValueElement.textContent = formatNumber(totalQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算查询量百分比(假设最大查询量为10000)
|
||||||
|
const queryPercentage = Math.min((totalQueries / 10000) * 100, 100);
|
||||||
|
const queriesBarElement = document.getElementById('server-queries-bar');
|
||||||
|
if (queriesBarElement) {
|
||||||
|
queriesBarElement.style.width = queryPercentage + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新额外统计指标
|
||||||
|
const totalQueriesElement = document.getElementById('server-total-queries');
|
||||||
|
if (totalQueriesElement) {
|
||||||
|
totalQueriesElement.textContent = formatNumber(totalQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedQueriesElement = document.getElementById('server-blocked-queries');
|
||||||
|
if (blockedQueriesElement) {
|
||||||
|
blockedQueriesElement.textContent = formatNumber(blockedQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedQueriesElement = document.getElementById('server-allowed-queries');
|
||||||
|
if (allowedQueriesElement) {
|
||||||
|
allowedQueriesElement.textContent = formatNumber(allowedQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加光晕提示效果
|
||||||
|
if (previousServerData.cpu !== cpuUsage || previousServerData.queries !== totalQueries) {
|
||||||
|
addGlowEffect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务器状态指示器
|
||||||
|
const statusIndicator = document.getElementById('server-status-indicator');
|
||||||
|
if (statusIndicator) {
|
||||||
|
// 检查系统状态
|
||||||
|
if (stats.system && stats.system.status === 'error') {
|
||||||
|
statusIndicator.className = 'inline-block w-2 h-2 bg-danger rounded-full';
|
||||||
|
} else {
|
||||||
|
statusIndicator.className = 'inline-block w-2 h-2 bg-success rounded-full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前数据用于下次比较
|
||||||
|
previousServerData = {
|
||||||
|
cpu: cpuUsage,
|
||||||
|
queries: totalQueries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加光晕提示效果
|
||||||
|
function addGlowEffect() {
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 添加光晕类
|
||||||
|
widget.classList.add('glow-effect');
|
||||||
|
|
||||||
|
// 2秒后移除光晕
|
||||||
|
setTimeout(function() {
|
||||||
|
widget.classList.remove('glow-effect');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
function formatNumber(num) {
|
||||||
|
// 显示完整数字的最大长度阈值
|
||||||
|
const MAX_FULL_LENGTH = 5;
|
||||||
|
|
||||||
|
// 先获取完整数字字符串
|
||||||
|
const fullNumStr = num.toString();
|
||||||
|
|
||||||
|
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||||
|
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用缩写格式
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在DOM加载完成后初始化
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 延迟初始化,确保页面完全加载
|
||||||
|
setTimeout(initServerStatusWidget, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在页面卸载时清理资源
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
if (serverStatusUpdateTimer) {
|
||||||
|
clearInterval(serverStatusUpdateTimer);
|
||||||
|
serverStatusUpdateTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出函数供其他模块使用
|
||||||
|
window.serverStatusWidget = {
|
||||||
|
init: initServerStatusWidget,
|
||||||
|
update: updateServerStatusWidget
|
||||||
|
};
|
||||||
1302
js/shield.js
Normal file
1302
js/shield.js
Normal file
File diff suppressed because it is too large
Load Diff
1
js/vendor/chart.umd.min.js
vendored
Normal file
1
js/vendor/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user