web异常待修复
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
# DNS Server Hosts File
|
# DNS Server Hosts File
|
||||||
# Generated by DNS Server
|
# Generated by DNS Server
|
||||||
|
|
||||||
ad.qq.com 127.0.0.1
|
|
||||||
ad.qq.com 0.0.0.0
|
ad.qq.com 0.0.0.0
|
||||||
127.0.0.1 ex
|
127.0.0.1 ex
|
||||||
127.0.0.1 1
|
127.0.0.1 1
|
||||||
127.0.0.1 h
|
127.0.0.1 h
|
||||||
::1 localhost
|
::1 localhost
|
||||||
|
127.0.0.1 so.com
|
||||||
|
ad.qq.com 127.0.0.1
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"blockedDomainsCount": {},
|
"blockedDomainsCount": {},
|
||||||
"resolvedDomainsCount": {},
|
"resolvedDomainsCount": {},
|
||||||
"lastSaved": "2025-11-24T02:14:05.499978724+08:00"
|
"lastSaved": "2025-11-24T10:49:00.992276965+08:00"
|
||||||
}
|
}
|
||||||
2410
data/stats.json
2410
data/stats.json
File diff suppressed because it is too large
Load Diff
25694
dns-server.log
25694
dns-server.log
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,36 @@
|
|||||||
<meta name="description" content="DNS服务器管理控制台 - 高性能DNS服务器,支持规则屏蔽和Hosts管理">
|
<meta name="description" content="DNS服务器管理控制台 - 高性能DNS服务器,支持规则屏蔽和Hosts管理">
|
||||||
<title>DNS服务器管理控制台</title>
|
<title>DNS服务器管理控制台</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.css">
|
<!-- Chart.js 4.x不需要单独的CSS文件 -->
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<style>
|
||||||
|
/* 光晕效果样式 */
|
||||||
|
.stat-value.update {
|
||||||
|
animation: glow 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0% { box-shadow: 0 0 5px rgba(75, 192, 192, 0.5); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(75, 192, 192, 0.8); }
|
||||||
|
100% { box-shadow: 0 0 5px rgba(75, 192, 192, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小型图表容器 */
|
||||||
|
.mini-chart-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 60px;
|
||||||
|
height: 20px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小型图表 */
|
||||||
|
.mini-chart {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -63,42 +91,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-ban"></i>
|
<i class="fas fa-ban"></i>
|
||||||
<div class="stat-value" id="blocked-count">--</div>
|
<div class="stat-value" id="blocked-count">--</div>
|
||||||
<div class="stat-label">屏蔽请求</div>
|
<div class="stat-label">屏蔽请求</div>
|
||||||
|
<div class="mini-chart-container">
|
||||||
|
<canvas id="blocked-chart" class="mini-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
</div>
|
||||||
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-check-circle"></i>
|
||||||
<div class="stat-value" id="allowed-count">--</div>
|
<div class="stat-value" id="allowed-count">--</div>
|
||||||
<div class="stat-label">允许请求</div>
|
<div class="stat-label">允许请求</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-question-circle"></i>
|
<i class="fas fa-question-circle"></i>
|
||||||
<div class="stat-value" id="error-count">--</div>
|
<div class="stat-value" id="error-count">--</div>
|
||||||
<div class="stat-label">错误请求</div>
|
<div class="stat-label">错误请求</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-history"></i>
|
<i class="fas fa-history"></i>
|
||||||
<div class="stat-value" id="total-queries">--</div>
|
<div class="stat-value" id="total-queries">--</div>
|
||||||
<div class="stat-label">总请求数</div>
|
<div class="stat-label">总请求数</div>
|
||||||
|
<div class="mini-chart-container">
|
||||||
|
<canvas id="query-chart" class="mini-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
</div>
|
||||||
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<div class="stat-value" id="rules-count">--</div>
|
<div class="stat-value" id="rules-count">--</div>
|
||||||
<div class="stat-label">屏蔽规则数</div>
|
<div class="stat-label">屏蔽规则数</div>
|
||||||
|
<div class="mini-chart-container">
|
||||||
|
<canvas id="rules-chart" class="mini-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
</div>
|
||||||
|
<div class="stat-card" style="position: relative;">
|
||||||
<i class="fas fa-globe"></i>
|
<i class="fas fa-globe"></i>
|
||||||
<div class="stat-value" id="hosts-count">--</div>
|
<div class="stat-value" id="hosts-count">--</div>
|
||||||
<div class="stat-label">Hosts条目</div>
|
<div class="stat-label">Hosts条目</div>
|
||||||
|
<div class="mini-chart-container">
|
||||||
|
<canvas id="hosts-chart" class="mini-chart"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="charts-container">
|
<div class="charts-container">
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3>24小时屏蔽统计</h3>
|
<h3>24小时屏蔽统计</h3>
|
||||||
<canvas id="hourly-chart"></canvas>
|
<canvas id="hourly-chart" style="height: 300px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3>请求类型分布</h3>
|
<h3>请求类型分布</h3>
|
||||||
@@ -411,7 +451,158 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="js/vendor/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// 全局数据变化检测和图表管理
|
||||||
|
let previousStats = {};
|
||||||
|
let miniCharts = {};
|
||||||
|
let dataHistory = {
|
||||||
|
rules: Array(10).fill(0),
|
||||||
|
hosts: Array(10).fill(0),
|
||||||
|
query: Array(10).fill(0),
|
||||||
|
blocked: Array(10).fill(0)
|
||||||
|
};
|
||||||
|
let updateTimer = null;
|
||||||
|
const UPDATE_INTERVAL = 2000;
|
||||||
|
let previousFullData = null;
|
||||||
|
let previousChartData = null;
|
||||||
|
|
||||||
|
// 初始化小型图表
|
||||||
|
function initMiniCharts() {
|
||||||
|
const chartConfigs = {
|
||||||
|
'rules-chart': { label: '规则数', color: 'rgb(75, 192, 192)' },
|
||||||
|
'hosts-chart': { label: 'Hosts数', color: 'rgb(153, 102, 255)' },
|
||||||
|
'query-chart': { label: '查询数', color: 'rgb(255, 159, 64)' },
|
||||||
|
'blocked-chart': { label: '屏蔽数', color: 'rgb(255, 99, 132)' }
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(chartConfigs).forEach(([id, config]) => {
|
||||||
|
const ctx = document.getElementById(id);
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
miniCharts[id] = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: Array(10).fill(''),
|
||||||
|
datasets: [{
|
||||||
|
label: config.label,
|
||||||
|
data: Array(10).fill(0),
|
||||||
|
borderColor: config.color,
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { enabled: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { display: false },
|
||||||
|
y: { display: false, beginAtZero: true }
|
||||||
|
},
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新小型图表
|
||||||
|
function updateMiniChart(chartId, data) {
|
||||||
|
if (miniCharts[chartId]) {
|
||||||
|
miniCharts[chartId].data.datasets[0].data = data;
|
||||||
|
miniCharts[chartId].update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据历史记录
|
||||||
|
function updateDataHistory(key, value) {
|
||||||
|
dataHistory[key].shift();
|
||||||
|
dataHistory[key].push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据是否变化并添加光晕效果
|
||||||
|
function checkAndAnimate(elementId, newValue) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const oldValue = previousStats[elementId] || 0;
|
||||||
|
|
||||||
|
if (newValue !== oldValue && oldValue !== 0 && oldValue !== '--') {
|
||||||
|
element.classList.add('update');
|
||||||
|
setTimeout(() => {
|
||||||
|
element.classList.remove('update');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousStats[elementId] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动实时更新
|
||||||
|
function startRealTimeUpdate() {
|
||||||
|
if (updateTimer) {
|
||||||
|
clearInterval(updateTimer);
|
||||||
|
}
|
||||||
|
updateTimer = setInterval(() => {
|
||||||
|
// 仅当当前面板是仪表盘时更新数据
|
||||||
|
if (document.getElementById('dashboard').classList.contains('active')) {
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
|
}, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止实时更新
|
||||||
|
function stopRealTimeUpdate() {
|
||||||
|
if (updateTimer) {
|
||||||
|
clearInterval(updateTimer);
|
||||||
|
updateTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示悬浮通知
|
||||||
|
function showNotification(type, message) {
|
||||||
|
// 创建通知元素
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification notification-${type}`;
|
||||||
|
|
||||||
|
// 设置图标
|
||||||
|
let iconClass = 'info-circle';
|
||||||
|
if (type === 'success') iconClass = 'check-circle';
|
||||||
|
else if (type === 'danger') iconClass = 'exclamation-circle';
|
||||||
|
else if (type === 'warning') iconClass = 'exclamation-triangle';
|
||||||
|
|
||||||
|
// 设置通知内容
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-icon">
|
||||||
|
<i class="fas fa-${iconClass}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="notification-content">${message}</div>
|
||||||
|
<button class="notification-close">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加关闭事件
|
||||||
|
notification.querySelector('.notification-close').addEventListener('click', () => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加到页面
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 3秒后自动关闭
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
<script src="js/modules/dashboard.js"></script>
|
<script src="js/modules/dashboard.js"></script>
|
||||||
<script src="js/modules/rules.js"></script>
|
<script src="js/modules/rules.js"></script>
|
||||||
@@ -419,5 +610,47 @@
|
|||||||
<script src="js/modules/blacklists.js"></script>
|
<script src="js/modules/blacklists.js"></script>
|
||||||
<script src="js/modules/query.js"></script>
|
<script src="js/modules/query.js"></script>
|
||||||
<script src="js/modules/config.js"></script>
|
<script src="js/modules/config.js"></script>
|
||||||
|
<script>
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
initMiniCharts();
|
||||||
|
|
||||||
|
// 为侧边栏导航添加切换事件
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
navItems.forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
const target = this.getAttribute('data-target');
|
||||||
|
const panels = document.querySelectorAll('.panel');
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
|
||||||
|
// 更新面板显示
|
||||||
|
panels.forEach(panel => {
|
||||||
|
panel.classList.remove('active');
|
||||||
|
if (panel.id === target) {
|
||||||
|
panel.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新导航高亮
|
||||||
|
navItems.forEach(navItem => {
|
||||||
|
navItem.classList.remove('active');
|
||||||
|
});
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// 根据面板类型控制更新
|
||||||
|
if (target === 'dashboard') {
|
||||||
|
startRealTimeUpdate();
|
||||||
|
} else {
|
||||||
|
stopRealTimeUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面卸载时清理定时器
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
stopRealTimeUpdate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
1727
static/index.html.2
Normal file
1727
static/index.html.2
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,18 +2,20 @@
|
|||||||
const API_BASE_URL = '/api';
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
// DOM 加载完成后执行
|
// DOM 加载完成后执行
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 初始化面板切换
|
// 初始化面板切换
|
||||||
initPanelNavigation();
|
initPanelNavigation();
|
||||||
|
|
||||||
// 初始化通知组件
|
|
||||||
initNotification();
|
|
||||||
|
|
||||||
// 加载初始数据
|
// 加载初始数据
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
|
|
||||||
// 定时更新数据
|
// 直接调用dashboard面板初始化函数,确保数据正确加载
|
||||||
setInterval(loadInitialData, 60000); // 每分钟更新一次
|
if (typeof initDashboardPanel === 'function') {
|
||||||
|
initDashboardPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:实时更新现在由index.html中的startRealTimeUpdate函数控制
|
||||||
|
// 并根据面板状态自动启用/禁用
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化面板导航
|
// 初始化面板导航
|
||||||
@@ -40,24 +42,27 @@ function initPanelNavigation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化通知组件
|
// 保留原有的通知函数作为兼容层
|
||||||
function initNotification() {
|
// 现在主通知功能由index.html中的showNotification函数实现
|
||||||
|
if (typeof window.showNotification === 'undefined') {
|
||||||
window.showNotification = function(message, type = 'info') {
|
window.showNotification = function(message, type = 'info') {
|
||||||
const notification = document.getElementById('notification');
|
// 创建临时通知元素
|
||||||
const notificationMessage = document.getElementById('notification-message');
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification notification-${type}`;
|
||||||
|
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);
|
||||||
notificationMessage.textContent = message;
|
|
||||||
notification.className = 'notification show ' + type;
|
|
||||||
|
|
||||||
// 自动关闭
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.classList.remove('show');
|
notification.remove();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载初始数据
|
// 加载初始数据(主要用于服务器状态)
|
||||||
function loadInitialData() {
|
function loadInitialData() {
|
||||||
// 加载服务器状态
|
// 加载服务器状态
|
||||||
fetch(`${API_BASE_URL}/status`)
|
fetch(`${API_BASE_URL}/status`)
|
||||||
@@ -83,40 +88,31 @@ function loadInitialData() {
|
|||||||
const serverStatus = document.getElementById('server-status');
|
const serverStatus = document.getElementById('server-status');
|
||||||
statusDot.classList.remove('connected');
|
statusDot.classList.remove('connected');
|
||||||
serverStatus.textContent = '离线';
|
serverStatus.textContent = '离线';
|
||||||
|
|
||||||
|
// 使用新的通知功能
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('获取服务器状态失败', 'danger');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载统计数据
|
// 注意:统计数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||||
fetch(`${API_BASE_URL}/stats`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
// 更新统计数据
|
|
||||||
if (data && data.dns) {
|
|
||||||
updateStatCards(data.dns);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取统计数据失败:', error);
|
|
||||||
window.showNotification('获取统计数据失败', 'error');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新统计卡片数据
|
// 注意:统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||||
|
// 此函数保留作为兼容层,实际功能已迁移
|
||||||
function updateStatCards(stats) {
|
function updateStatCards(stats) {
|
||||||
const statElements = {
|
// 空实现,保留函数声明以避免引用错误
|
||||||
'blocked-count': stats.Blocked || 0,
|
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
|
||||||
'allowed-count': stats.Allowed || 0,
|
}
|
||||||
'error-count': stats.Errors || 0,
|
|
||||||
'total-queries': stats.Queries || 0,
|
|
||||||
'rules-count': 0,
|
|
||||||
'hosts-count': 0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [id, value] of Object.entries(statElements)) {
|
// 注意:获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||||
const element = document.getElementById(id);
|
function fetchRulesCount() {
|
||||||
if (element) {
|
// 空实现,保留函数声明以避免引用错误
|
||||||
element.textContent = formatNumber(value);
|
}
|
||||||
}
|
|
||||||
}
|
// 注意:获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||||
|
function fetchHostsCount() {
|
||||||
|
// 空实现,保留函数声明以避免引用错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用API请求函数
|
// 通用API请求函数
|
||||||
|
|||||||
@@ -101,11 +101,8 @@ function saveConfig() {
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
window.showNotification('配置保存成功', 'success');
|
window.showNotification('配置保存成功', 'success');
|
||||||
|
|
||||||
// 询问是否需要重启服务以应用配置
|
// 由于服务器没有提供重启API,移除重启提示
|
||||||
confirmAction(
|
// 直接提示用户配置已保存
|
||||||
'配置已保存。某些更改可能需要重启服务才能生效。是否现在重启服务?',
|
|
||||||
() => restartService()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
|
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
|
||||||
}
|
}
|
||||||
@@ -116,27 +113,7 @@ function saveConfig() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启服务
|
// 服务重启功能已移除,因为服务器没有提供对应的API端点
|
||||||
function restartService() {
|
|
||||||
apiRequest('/service/restart', 'POST')
|
|
||||||
.then(response => {
|
|
||||||
if (response.success) {
|
|
||||||
window.showNotification('服务正在重启,请稍后刷新页面', 'success');
|
|
||||||
|
|
||||||
// 等待几秒后重新加载页面
|
|
||||||
setTimeout(() => {
|
|
||||||
location.reload();
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
window.showNotification(`重启失败: ${response.message || '未知错误'}`, 'error');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('重启服务失败:', error);
|
|
||||||
// 重启服务可能会导致连接中断,这是正常的
|
|
||||||
window.showNotification('服务重启中,请手动刷新页面确认状态', 'info');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证IP地址格式
|
// 验证IP地址格式
|
||||||
function isValidIp(ip) {
|
function isValidIp(ip) {
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
function initDashboardPanel() {
|
function initDashboardPanel() {
|
||||||
// 加载统计数据
|
// 加载统计数据
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
|
// 启动实时更新
|
||||||
|
if (typeof startRealTimeUpdate === 'function') {
|
||||||
|
startRealTimeUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载仪表盘数据
|
// 加载仪表盘数据
|
||||||
function loadDashboardData() {
|
function loadDashboardData() {
|
||||||
|
// 加载统计卡片数据
|
||||||
|
updateStatCards();
|
||||||
|
|
||||||
// 加载24小时统计数据
|
// 加载24小时统计数据
|
||||||
loadHourlyStats();
|
loadHourlyStats();
|
||||||
|
|
||||||
@@ -19,10 +26,137 @@ function loadDashboardData() {
|
|||||||
loadTopResolvedDomains();
|
loadTopResolvedDomains();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新统计卡片数据
|
||||||
|
function updateStatCards() {
|
||||||
|
// 获取所有统计数据
|
||||||
|
apiRequest('/stats')
|
||||||
|
.then(data => {
|
||||||
|
// 更新请求统计
|
||||||
|
if (data && data.dns) {
|
||||||
|
// 屏蔽请求
|
||||||
|
const blockedCount = data.dns.Blocked || data.dns.blocked || 0;
|
||||||
|
updateStatCard('blocked-count', blockedCount);
|
||||||
|
|
||||||
|
// 允许请求
|
||||||
|
const allowedCount = data.dns.Allowed || data.dns.allowed || 0;
|
||||||
|
updateStatCard('allowed-count', allowedCount);
|
||||||
|
|
||||||
|
// 错误请求
|
||||||
|
const errorCount = data.dns.Errors || data.dns.errors || 0;
|
||||||
|
updateStatCard('error-count', errorCount);
|
||||||
|
|
||||||
|
// 总请求数
|
||||||
|
const totalCount = blockedCount + allowedCount + errorCount;
|
||||||
|
updateStatCard('total-queries', totalCount);
|
||||||
|
|
||||||
|
// 更新数据历史记录和小型图表
|
||||||
|
if (typeof updateDataHistory === 'function') {
|
||||||
|
updateDataHistory('blocked', blockedCount);
|
||||||
|
updateDataHistory('query', totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新小型图表
|
||||||
|
if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') {
|
||||||
|
updateMiniChart('blocked-chart', dataHistory.blocked);
|
||||||
|
updateMiniChart('query-chart', dataHistory.query);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 处理其他可能的数据格式
|
||||||
|
// 修复语法错误,使用传统的对象属性访问方式
|
||||||
|
const blockedValue = data && (data.Blocked !== undefined ? data.Blocked : (data.blocked !== undefined ? data.blocked : 0));
|
||||||
|
const allowedValue = data && (data.Allowed !== undefined ? data.Allowed : (data.allowed !== undefined ? data.allowed : 0));
|
||||||
|
const errorValue = data && (data.Errors !== undefined ? data.Errors : (data.errors !== undefined ? data.errors : 0));
|
||||||
|
updateStatCard('blocked-count', blockedValue);
|
||||||
|
updateStatCard('allowed-count', allowedValue);
|
||||||
|
updateStatCard('error-count', errorValue);
|
||||||
|
const totalCount = blockedValue + allowedValue + errorValue;
|
||||||
|
updateStatCard('total-queries', totalCount);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取统计数据失败:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取规则数
|
||||||
|
apiRequest('/shield')
|
||||||
|
.then(data => {
|
||||||
|
let rulesCount = 0;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
rulesCount = data.length;
|
||||||
|
} else if (data && data.rules && Array.isArray(data.rules)) {
|
||||||
|
rulesCount = data.rules.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatCard('rules-count', rulesCount);
|
||||||
|
|
||||||
|
// 更新数据历史记录和小型图表
|
||||||
|
if (typeof updateDataHistory === 'function') {
|
||||||
|
updateDataHistory('rules', rulesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') {
|
||||||
|
updateMiniChart('rules-chart', dataHistory.rules);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取规则数失败:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取Hosts条目数
|
||||||
|
apiRequest('/shield/hosts')
|
||||||
|
.then(data => {
|
||||||
|
let hostsCount = 0;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
hostsCount = data.length;
|
||||||
|
} else if (data && data.hosts && Array.isArray(data.hosts)) {
|
||||||
|
hostsCount = data.hosts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatCard('hosts-count', hostsCount);
|
||||||
|
|
||||||
|
// 更新数据历史记录和小型图表
|
||||||
|
if (typeof updateDataHistory === 'function') {
|
||||||
|
updateDataHistory('hosts', hostsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') {
|
||||||
|
updateMiniChart('hosts-chart', dataHistory.hosts);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取Hosts条目数失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新单个统计卡片
|
||||||
|
function updateStatCard(elementId, value) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
// 格式化为可读数字
|
||||||
|
const formattedValue = formatNumber(value);
|
||||||
|
|
||||||
|
// 更新显示
|
||||||
|
element.textContent = formattedValue;
|
||||||
|
|
||||||
|
// 使用全局checkAndAnimate函数检测变化并添加光晕效果
|
||||||
|
if (typeof checkAndAnimate === 'function') {
|
||||||
|
checkAndAnimate(elementId, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载24小时统计数据
|
// 加载24小时统计数据
|
||||||
function loadHourlyStats() {
|
function loadHourlyStats() {
|
||||||
apiRequest('/api/hourly-stats')
|
apiRequest('/hourly-stats')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// 检查数据是否变化,避免不必要的重绘
|
||||||
|
if (typeof previousChartData !== 'undefined' &&
|
||||||
|
JSON.stringify(previousChartData) === JSON.stringify(data)) {
|
||||||
|
return; // 数据未变化,无需更新图表
|
||||||
|
}
|
||||||
|
|
||||||
|
previousChartData = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
// 处理不同可能的数据格式
|
// 处理不同可能的数据格式
|
||||||
if (data) {
|
if (data) {
|
||||||
// 优先处理用户提供的实际数据格式 {data: [], labels: []}
|
// 优先处理用户提供的实际数据格式 {data: [], labels: []}
|
||||||
@@ -141,15 +275,28 @@ function renderHourlyChart(hours, blocked, allowed) {
|
|||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false
|
intersect: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 500 // 快速动画,提升实时更新体验
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载请求类型分布
|
// 加载请求类型分布 - 注意:后端可能没有这个API,暂时注释掉
|
||||||
function loadRequestsDistribution() {
|
function loadRequestsDistribution() {
|
||||||
apiRequest('/api/stats')
|
// 后端没有对应的API路由,暂时跳过
|
||||||
|
console.log('请求类型分布API暂不可用');
|
||||||
|
return Promise.resolve()
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// 检查数据是否变化,避免不必要的重绘
|
||||||
|
if (typeof previousFullData !== 'undefined' &&
|
||||||
|
JSON.stringify(previousFullData) === JSON.stringify(data)) {
|
||||||
|
return; // 数据未变化,无需更新图表
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFullData = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
// 构造饼图所需的数据,支持多种数据格式
|
// 构造饼图所需的数据,支持多种数据格式
|
||||||
const labels = ['允许请求', '屏蔽请求', '错误请求'];
|
const labels = ['允许请求', '屏蔽请求', '错误请求'];
|
||||||
let requestData = [0, 0, 0]; // 默认值
|
let requestData = [0, 0, 0]; // 默认值
|
||||||
@@ -238,7 +385,10 @@ function renderRequestsPieChart(labels, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cutout: '60%'
|
cutout: '60%',
|
||||||
|
animation: {
|
||||||
|
duration: 500 // 快速动画,提升实时更新体验
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -246,13 +396,15 @@ function renderRequestsPieChart(labels, data) {
|
|||||||
// 加载最常屏蔽的域名
|
// 加载最常屏蔽的域名
|
||||||
function loadTopBlockedDomains() {
|
function loadTopBlockedDomains() {
|
||||||
// 首先获取表格元素并显示加载状态
|
// 首先获取表格元素并显示加载状态
|
||||||
const tbody = document.getElementById('top-blocked-table')?.querySelector('tbody');
|
// 修复语法错误,使用传统的DOM访问方式
|
||||||
|
const topBlockedTable = document.getElementById('top-blocked-table');
|
||||||
|
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
|
||||||
if (tbody) {
|
if (tbody) {
|
||||||
// 显示加载中状态
|
// 显示加载中状态
|
||||||
tbody.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
|
tbody.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
apiRequest('/api/top-blocked')
|
apiRequest('/top-blocked')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// 处理多种可能的数据格式,特别优化对用户提供格式的支持
|
// 处理多种可能的数据格式,特别优化对用户提供格式的支持
|
||||||
let processedData = [];
|
let processedData = [];
|
||||||
@@ -282,12 +434,19 @@ function loadTopBlockedDomains() {
|
|||||||
if (tbody) {
|
if (tbody) {
|
||||||
showEmpty(tbody, '获取数据失败');
|
showEmpty(tbody, '获取数据失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用全局通知功能
|
||||||
|
if (typeof showNotification === 'function') {
|
||||||
|
showNotification('danger', '获取最常屏蔽域名失败');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染最常屏蔽的域名表格
|
// 渲染最常屏蔽的域名表格
|
||||||
function renderTopBlockedDomains(domains) {
|
function renderTopBlockedDomains(domains) {
|
||||||
const tbody = document.getElementById('top-blocked-table')?.querySelector('tbody');
|
// 修复语法错误,使用传统的DOM访问方式
|
||||||
|
const topBlockedTable = document.getElementById('top-blocked-table');
|
||||||
|
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
|
||||||
console.log('准备渲染的域名数据:', domains);
|
console.log('准备渲染的域名数据:', domains);
|
||||||
@@ -327,7 +486,7 @@ function renderTopBlockedDomains(domains) {
|
|||||||
|
|
||||||
// 加载最常解析的域名
|
// 加载最常解析的域名
|
||||||
function loadTopResolvedDomains() {
|
function loadTopResolvedDomains() {
|
||||||
apiRequest('/api/top-resolved')
|
apiRequest('/top-resolved')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// 处理多种可能的数据格式
|
// 处理多种可能的数据格式
|
||||||
let processedData = [];
|
let processedData = [];
|
||||||
@@ -355,6 +514,11 @@ function loadTopResolvedDomains() {
|
|||||||
if (tbody) {
|
if (tbody) {
|
||||||
showEmpty(tbody, '暂无解析记录');
|
showEmpty(tbody, '暂无解析记录');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用全局通知功能
|
||||||
|
if (typeof showNotification === 'function') {
|
||||||
|
showNotification('danger', '获取最常解析域名失败');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,42 @@ function loadHosts() {
|
|||||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||||
showLoading(tbody);
|
showLoading(tbody);
|
||||||
|
|
||||||
|
// 更新API路径,使用完整路径
|
||||||
apiRequest('/shield/hosts', 'GET')
|
apiRequest('/shield/hosts', 'GET')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
renderHosts(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 => {
|
.catch(error => {
|
||||||
console.error('获取Hosts列表失败:', error);
|
console.error('获取Hosts列表失败:', error);
|
||||||
showError(tbody, '获取Hosts列表失败');
|
|
||||||
window.showNotification('获取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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +73,14 @@ function renderHosts(hosts) {
|
|||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
|
||||||
if (!hosts || hosts.length === 0) {
|
if (!hosts || hosts.length === 0) {
|
||||||
showEmpty(tbody, '暂无Hosts条目');
|
// 使用更友好的空状态显示
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +90,6 @@ function renderHosts(hosts) {
|
|||||||
addHostsToTable(entry.ip, entry.domain);
|
addHostsToTable(entry.ip, entry.domain);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化表格排序
|
|
||||||
initTableSort('hosts-table');
|
|
||||||
|
|
||||||
// 初始化删除按钮监听器
|
// 初始化删除按钮监听器
|
||||||
initDeleteHostsListeners();
|
initDeleteHostsListeners();
|
||||||
}
|
}
|
||||||
@@ -77,7 +109,17 @@ function addHostsToTable(ip, domain) {
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 添加行动画效果
|
||||||
|
row.style.opacity = '0';
|
||||||
|
row.style.transform = 'translateY(10px)';
|
||||||
tbody.appendChild(row);
|
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条目
|
// 添加Hosts条目
|
||||||
@@ -89,56 +131,123 @@ function addHostsEntry() {
|
|||||||
const domain = domainInput.value.trim();
|
const domain = domainInput.value.trim();
|
||||||
|
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('请输入IP地址', 'warning');
|
window.showNotification('请输入IP地址', 'warning');
|
||||||
|
}
|
||||||
ipInput.focus();
|
ipInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('请输入域名', 'warning');
|
window.showNotification('请输入域名', 'warning');
|
||||||
|
}
|
||||||
domainInput.focus();
|
domainInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简单的IP地址格式验证
|
// 简单的IP地址格式验证
|
||||||
if (!isValidIp(ip)) {
|
if (!isValidIp(ip)) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('请输入有效的IP地址', 'warning');
|
window.showNotification('请输入有效的IP地址', 'warning');
|
||||||
|
}
|
||||||
ipInput.focus();
|
ipInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain });
|
// 修复重复API调用问题,只调用一次
|
||||||
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain })
|
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
// 处理不同的响应格式
|
||||||
|
if (data.success || data.status === 'success') {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('Hosts条目添加成功', 'success');
|
window.showNotification('Hosts条目添加成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空输入框并聚焦到域名输入
|
||||||
ipInput.value = '';
|
ipInput.value = '';
|
||||||
domainInput.value = '';
|
domainInput.value = '';
|
||||||
|
domainInput.focus();
|
||||||
|
|
||||||
|
// 重新加载Hosts列表
|
||||||
loadHosts();
|
loadHosts();
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('hosts');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('添加Hosts条目失败:', error);
|
console.error('添加Hosts条目失败:', error);
|
||||||
window.showNotification('添加Hosts条目失败', 'error');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('添加Hosts条目失败', 'danger');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除Hosts条目
|
// 删除Hosts条目
|
||||||
function deleteHostsEntry(ip, domain) {
|
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('/shield/hosts', 'DELETE', { ip: ip, domain: domain })
|
apiRequest('/shield/hosts', 'DELETE', { ip: ip, domain: domain })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
// 处理不同的响应格式
|
||||||
|
if (data.success || data.status === 'success') {
|
||||||
|
// 等待动画完成后重新加载列表
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('Hosts条目删除成功', 'success');
|
window.showNotification('Hosts条目删除成功', 'success');
|
||||||
|
}
|
||||||
loadHosts();
|
loadHosts();
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('hosts');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
|
// 恢复行样式
|
||||||
|
if (targetRow) {
|
||||||
|
targetRow.style.opacity = '1';
|
||||||
|
targetRow.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
// 恢复行样式
|
||||||
|
if (targetRow) {
|
||||||
|
targetRow.style.opacity = '1';
|
||||||
|
targetRow.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
console.error('删除Hosts条目失败:', error);
|
console.error('删除Hosts条目失败:', error);
|
||||||
window.showNotification('删除Hosts条目失败', 'error');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('删除Hosts条目失败', 'danger');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +271,10 @@ function initDeleteHostsListeners() {
|
|||||||
const ip = this.getAttribute('data-ip');
|
const ip = this.getAttribute('data-ip');
|
||||||
const domain = this.getAttribute('data-domain');
|
const domain = this.getAttribute('data-domain');
|
||||||
|
|
||||||
confirmAction(
|
// 使用标准confirm对话框
|
||||||
`确定要删除这条Hosts条目吗?\n${ip} ${domain}`,
|
if (confirm(`确定要删除这条Hosts条目吗?\n${ip} ${domain}`)) {
|
||||||
() => deleteHostsEntry(ip, domain)
|
deleteHostsEntry(ip, domain);
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -178,3 +287,22 @@ function isValidIp(ip) {
|
|||||||
|
|
||||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
function initQueryPanel() {
|
function initQueryPanel() {
|
||||||
// 初始化事件监听器
|
// 初始化事件监听器
|
||||||
initQueryEventListeners();
|
initQueryEventListeners();
|
||||||
|
|
||||||
|
// 确保结果容器默认隐藏
|
||||||
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
|
if (resultContainer) {
|
||||||
|
resultContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化事件监听器
|
// 初始化事件监听器
|
||||||
@@ -23,7 +29,9 @@ function runDnsQuery() {
|
|||||||
const domain = domainInput.value.trim();
|
const domain = domainInput.value.trim();
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
window.showNotification('请输入要查询的域名', 'warning');
|
window.showNotification('请输入要查询的域名', 'warning');
|
||||||
|
}
|
||||||
domainInput.focus();
|
domainInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -31,62 +39,110 @@ function runDnsQuery() {
|
|||||||
// 显示查询中状态
|
// 显示查询中状态
|
||||||
showQueryLoading();
|
showQueryLoading();
|
||||||
|
|
||||||
apiRequest('/query?domain=' + domain, 'GET', { domain: domain })
|
// 更新API路径,使用完整路径
|
||||||
|
apiRequest('/query', 'GET', { domain: domain })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
// 处理可能的不同响应格式
|
||||||
renderQueryResult(data);
|
renderQueryResult(data);
|
||||||
|
|
||||||
|
// 触发数据刷新事件
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('query');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('DNS查询失败:', error);
|
console.error('DNS查询失败:', error);
|
||||||
showQueryError('查询失败,请稍后重试');
|
showQueryError('查询失败,请稍后重试');
|
||||||
window.showNotification('DNS查询失败', 'error');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('DNS查询失败', 'danger');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示查询加载状态
|
// 显示查询加载状态
|
||||||
function showQueryLoading() {
|
function showQueryLoading() {
|
||||||
const resultContainer = document.getElementById('query-result-container');
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
resultContainer.classList.remove('hidden');
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加加载动画类
|
||||||
|
resultContainer.classList.add('loading-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'error-animation', 'success-animation');
|
||||||
|
|
||||||
// 清空之前的结果
|
// 清空之前的结果
|
||||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
const resultContent = resultContainer.querySelector('.result-content');
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
resultHeader.textContent = '查询中...';
|
if (resultHeader) resultHeader.textContent = '查询中...';
|
||||||
resultContent.innerHTML = '<div class="loading"></div>';
|
if (resultContent) {
|
||||||
|
resultContent.innerHTML = '<div class="loading">' +
|
||||||
|
'<div class="spinner"></div><span>正在查询...</span>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示查询错误
|
// 显示查询错误
|
||||||
function showQueryError(message) {
|
function showQueryError(message) {
|
||||||
const resultContainer = document.getElementById('query-result-container');
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
resultContainer.classList.remove('hidden');
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加错误动画类
|
||||||
|
resultContainer.classList.add('error-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'loading-animation', 'success-animation');
|
||||||
|
|
||||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
const resultContent = resultContainer.querySelector('.result-content');
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
resultHeader.textContent = '查询错误';
|
if (resultHeader) resultHeader.textContent = '查询错误';
|
||||||
resultContent.innerHTML = `<div class="result-item" style="color: #e74c3c;">${message}</div>`;
|
if (resultContent) {
|
||||||
|
resultContent.innerHTML = `<div class="result-item error-message">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染查询结果
|
// 渲染查询结果
|
||||||
function renderQueryResult(result) {
|
function renderQueryResult(result) {
|
||||||
const resultContainer = document.getElementById('query-result-container');
|
const resultContainer = document.getElementById('query-result-container');
|
||||||
resultContainer.classList.remove('hidden');
|
if (!resultContainer) return;
|
||||||
|
|
||||||
|
// 添加成功动画类
|
||||||
|
resultContainer.classList.add('success-animation');
|
||||||
|
resultContainer.classList.remove('hidden', 'loading-animation', 'error-animation');
|
||||||
|
|
||||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||||
const resultContent = resultContainer.querySelector('.result-content');
|
const resultContent = resultContainer.querySelector('.result-content');
|
||||||
|
|
||||||
resultHeader.textContent = '查询结果';
|
if (resultHeader) resultHeader.textContent = '查询结果';
|
||||||
|
if (!resultContent) return;
|
||||||
|
|
||||||
|
// 安全的HTML转义函数
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// 根据查询结果构建内容
|
// 根据查询结果构建内容
|
||||||
let content = '';
|
let content = '<div class="result-grid">';
|
||||||
|
|
||||||
// 域名
|
// 域名
|
||||||
content += `<div class="result-item"><strong>域名:</strong> <span id="result-domain">${result.domain || ''}</span></div>`;
|
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>`;
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const statusText = result.isBlocked ? '被屏蔽' : result.isAllowed ? '允许访问' : '未知';
|
const statusText = result.isBlocked ? '被屏蔽' : result.isAllowed ? '允许访问' : '未知';
|
||||||
const statusClass = result.isBlocked ? 'status-error' : result.isAllowed ? 'status-success' : '';
|
const statusClass = result.isBlocked ? 'status-error' : result.isAllowed ? 'status-success' : '';
|
||||||
content += `<div class="result-item"><strong>状态:</strong> <span id="result-status" class="${statusClass}">${statusText}</span></div>`;
|
const statusIcon = result.isBlocked ? 'fa-ban' : result.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>`;
|
||||||
|
|
||||||
// 规则类型
|
// 规则类型
|
||||||
let ruleType = '';
|
let ruleType = '';
|
||||||
@@ -101,45 +157,123 @@ function renderQueryResult(result) {
|
|||||||
} else {
|
} else {
|
||||||
ruleType = result.isWhitelist ? '白名单规则' : result.isHosts ? 'Hosts记录' : '未匹配任何规则';
|
ruleType = result.isWhitelist ? '白名单规则' : result.isHosts ? 'Hosts记录' : '未匹配任何规则';
|
||||||
}
|
}
|
||||||
content += `<div class="result-item"><strong>规则类型:</strong> <span id="result-rule-type">${ruleType}</span></div>`;
|
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>`;
|
||||||
|
|
||||||
// 匹配规则
|
// 匹配规则
|
||||||
const matchedRule = result.matchedRule || '无';
|
const matchedRule = escapeHtml(result.matchedRule || '无');
|
||||||
content += `<div class="result-item"><strong>匹配规则:</strong> <span id="result-rule">${matchedRule}</span></div>`;
|
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">${matchedRule}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
// Hosts记录
|
// Hosts记录
|
||||||
const hostsRecord = result.hostsRecord ? `${result.hostsRecord.ip} ${result.hostsRecord.domain}` : '无';
|
const hostsRecord = result.hostsRecord ?
|
||||||
content += `<div class="result-item"><strong>Hosts记录:</strong> <span id="result-hosts">${hostsRecord}</span></div>`;
|
escapeHtml(`${result.hostsRecord.ip} ${result.hostsRecord.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>`;
|
||||||
|
|
||||||
// 查询时间
|
// 查询时间
|
||||||
const queryTime = `${(result.queryTime || 0).toFixed(2)} ms`;
|
const queryTime = `${(result.queryTime || 0).toFixed(2)} ms`;
|
||||||
content += `<div class="result-item"><strong>查询时间:</strong> <span id="result-time">${queryTime}</span></div>`;
|
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响应(如果有)
|
// DNS响应(如果有)
|
||||||
if (result.dnsResponse) {
|
if (result.dnsResponse) {
|
||||||
content += '<div class="result-item"><strong>DNS响应:</strong></div>';
|
content += '<div class="dns-response-section">';
|
||||||
content += '<div class="dns-response">';
|
content += '<h4><i class="fas fa-exchange-alt"></i> DNS响应</h4>';
|
||||||
|
|
||||||
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
|
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
|
||||||
content += '<ul>';
|
content += '<div class="dns-answers">';
|
||||||
result.dnsResponse.answers.forEach(answer => {
|
result.dnsResponse.answers.forEach((answer, index) => {
|
||||||
content += `<li>${answer.name} ${answer.type} ${answer.value}</li>`;
|
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 += '</ul>';
|
content += '</div>';
|
||||||
} else {
|
} else {
|
||||||
content += '<p>无DNS响应记录</p>';
|
content += '<div class="empty-dns"><i class="fas fa-info-circle"></i> 无DNS响应记录</div>';
|
||||||
}
|
}
|
||||||
content += '</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;
|
resultContent.innerHTML = content;
|
||||||
|
|
||||||
// 更新结果元素的内容(确保数据一致性)
|
// 通知用户查询成功
|
||||||
document.getElementById('result-domain').textContent = result.domain || '';
|
if (typeof window.showNotification === 'function') {
|
||||||
document.getElementById('result-status').textContent = statusText;
|
const statusMsg = result.isBlocked ? '查询完成,该域名被屏蔽' :
|
||||||
document.getElementById('result-status').className = statusClass;
|
result.isAllowed ? '查询完成,该域名允许访问' : '查询完成';
|
||||||
document.getElementById('result-rule-type').textContent = ruleType;
|
window.showNotification(statusMsg, 'info');
|
||||||
document.getElementById('result-rule').textContent = matchedRule;
|
}
|
||||||
document.getElementById('result-hosts').textContent = hostsRecord;
|
}
|
||||||
document.getElementById('result-time').textContent = queryTime;
|
|
||||||
|
// 复制查询结果到剪贴板
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -40,13 +40,24 @@ async function loadRules() {
|
|||||||
const rulesPanel = document.getElementById('rules-panel');
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
showLoading(rulesPanel);
|
showLoading(rulesPanel);
|
||||||
|
|
||||||
|
// 更新API路径,使用完整路径
|
||||||
const data = await apiRequest('/shield', 'GET');
|
const data = await apiRequest('/shield', 'GET');
|
||||||
rules = data.rules || [];
|
|
||||||
|
// 处理不同格式的响应数据
|
||||||
|
rules = Array.isArray(data) ? data : (data.rules || []);
|
||||||
filteredRules = [...rules];
|
filteredRules = [...rules];
|
||||||
currentPage = 1; // 重置为第一页
|
currentPage = 1; // 重置为第一页
|
||||||
renderRulesList();
|
renderRulesList();
|
||||||
|
|
||||||
|
// 更新规则数量统计卡片
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('加载规则失败:' + error.message);
|
console.error('加载规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('加载规则失败', 'danger');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
const rulesPanel = document.getElementById('rules-panel');
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
hideLoading(rulesPanel);
|
hideLoading(rulesPanel);
|
||||||
@@ -62,7 +73,14 @@ function renderRulesList() {
|
|||||||
rulesList.innerHTML = '';
|
rulesList.innerHTML = '';
|
||||||
|
|
||||||
if (filteredRules.length === 0) {
|
if (filteredRules.length === 0) {
|
||||||
rulesList.innerHTML = '<tr><td colspan="4" class="text-center">暂无规则</td></tr>';
|
// 使用更友好的空状态显示
|
||||||
|
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条规则';
|
paginationInfo.textContent = '共0条规则';
|
||||||
updatePaginationButtons();
|
updatePaginationButtons();
|
||||||
return;
|
return;
|
||||||
@@ -79,9 +97,12 @@ function renderRulesList() {
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const globalIndex = startIndex + index;
|
const globalIndex = startIndex + index;
|
||||||
|
|
||||||
|
// 根据规则类型添加不同的样式
|
||||||
|
const ruleTypeClass = getRuleTypeClass(rule);
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td class="rule-id">${globalIndex + 1}</td>
|
<td class="rule-id">${globalIndex + 1}</td>
|
||||||
<td class="rule-content"><pre>${escapeHtml(rule)}</pre></td>
|
<td class="rule-content ${ruleTypeClass}"><pre>${escapeHtml(rule)}</pre></td>
|
||||||
<td class="rule-actions">
|
<td class="rule-actions">
|
||||||
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
|
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
|
||||||
<i class="fas fa-trash"></i> 删除
|
<i class="fas fa-trash"></i> 删除
|
||||||
@@ -89,7 +110,17 @@ function renderRulesList() {
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 添加行动画效果
|
||||||
|
row.style.opacity = '0';
|
||||||
|
row.style.transform = 'translateY(10px)';
|
||||||
rulesList.appendChild(row);
|
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)';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定删除按钮事件
|
// 绑定删除按钮事件
|
||||||
@@ -156,15 +187,22 @@ async function addNewRule() {
|
|||||||
const rule = ruleInput.value.trim();
|
const rule = ruleInput.value.trim();
|
||||||
|
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
showNotification('请输入规则内容', 'warning');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('请输入规则内容', 'warning');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiRequest('/shield/rule', 'POST', { rule });
|
// 预处理规则,支持AdGuardHome格式
|
||||||
|
const processedRule = preprocessRule(rule);
|
||||||
|
|
||||||
if (response.success) {
|
// 更新API路径
|
||||||
rules.push(rule);
|
const response = await apiRequest('/shield', 'POST', { rule: processedRule });
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response.success || response.status === 'success') {
|
||||||
|
rules.push(processedRule);
|
||||||
filteredRules = [...rules];
|
filteredRules = [...rules];
|
||||||
ruleInput.value = '';
|
ruleInput.value = '';
|
||||||
|
|
||||||
@@ -172,12 +210,24 @@ async function addNewRule() {
|
|||||||
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||||
renderRulesList();
|
renderRulesList();
|
||||||
|
|
||||||
showNotification('规则添加成功', 'success');
|
// 更新规则数量统计
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则添加成功', 'success');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showNotification('规则添加失败:' + (response.message || '未知错误'), 'error');
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('添加规则失败:' + error.message);
|
console.error('添加规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('添加规则失败', 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +239,20 @@ async function deleteRule(index) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const rule = filteredRules[index];
|
const rule = filteredRules[index];
|
||||||
const response = await apiRequest('/shield/rule', 'DELETE', { rule });
|
const rowElement = document.querySelectorAll('#rules-list tr')[index];
|
||||||
|
|
||||||
if (response.success) {
|
// 添加删除动画
|
||||||
|
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('/shield', 'DELETE', { rule });
|
||||||
|
|
||||||
|
// 处理不同的响应格式
|
||||||
|
if (response.success || response.status === 'success') {
|
||||||
// 在原规则列表中找到并删除
|
// 在原规则列表中找到并删除
|
||||||
const originalIndex = rules.indexOf(rule);
|
const originalIndex = rules.indexOf(rule);
|
||||||
if (originalIndex !== -1) {
|
if (originalIndex !== -1) {
|
||||||
@@ -207,13 +268,35 @@ async function deleteRule(index) {
|
|||||||
currentPage = totalPages;
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待动画完成后重新渲染列表
|
||||||
|
setTimeout(() => {
|
||||||
renderRulesList();
|
renderRulesList();
|
||||||
showNotification('规则删除成功', 'success');
|
|
||||||
|
// 更新规则数量统计
|
||||||
|
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||||
|
window.updateRulesCount(rules.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则删除成功', 'success');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
showNotification('规则删除失败:' + (response.message || '未知错误'), 'error');
|
// 恢复行样式
|
||||||
|
if (rowElement) {
|
||||||
|
rowElement.style.opacity = '1';
|
||||||
|
rowElement.style.transform = 'translateX(0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则删除失败:' + (response.message || '未知错误'), 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('删除规则失败:' + error.message);
|
console.error('删除规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('删除规则失败', 'danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,13 +310,25 @@ async function reloadRules() {
|
|||||||
const rulesPanel = document.getElementById('rules-panel');
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
showLoading(rulesPanel);
|
showLoading(rulesPanel);
|
||||||
|
|
||||||
await apiRequest('/shield/reload', 'POST');
|
// 确保使用正确的API路径
|
||||||
|
await apiRequest('/shield/refresh', 'POST');
|
||||||
|
|
||||||
// 重新加载规则列表
|
// 重新加载规则列表
|
||||||
await loadRules();
|
await loadRules();
|
||||||
showNotification('规则重新加载成功', 'success');
|
|
||||||
|
// 触发数据刷新事件,通知其他模块数据已更新
|
||||||
|
if (typeof window.triggerDataRefresh === 'function') {
|
||||||
|
window.triggerDataRefresh('rules');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('规则重新加载成功', 'success');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('重新加载规则失败:' + error.message);
|
console.error('重新加载规则失败:', error);
|
||||||
|
if (typeof window.showNotification === 'function') {
|
||||||
|
window.showNotification('重新加载规则失败', 'danger');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
const rulesPanel = document.getElementById('rules-panel');
|
const rulesPanel = document.getElementById('rules-panel');
|
||||||
hideLoading(rulesPanel);
|
hideLoading(rulesPanel);
|
||||||
@@ -263,8 +358,56 @@ function escapeHtml(text) {
|
|||||||
'"': '"',
|
'"': '"',
|
||||||
"'": '''
|
"'": '''
|
||||||
};
|
};
|
||||||
return text.replace(/[&<>"']/g, m => map[m]);
|
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;
|
window.initRulesPanel = initRulesPanel;
|
||||||
|
|
||||||
|
// 注册到面板导航系统
|
||||||
|
if (window.registerPanelModule) {
|
||||||
|
window.registerPanelModule('rules-panel', {
|
||||||
|
init: initRulesPanel,
|
||||||
|
refresh: loadRules
|
||||||
|
});
|
||||||
|
}
|
||||||
1
static/js/vendor/chart.umd.min.js
vendored
Normal file
1
static/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