web异常待修复

This commit is contained in:
Alex Yang
2025-11-24 10:50:03 +08:00
parent 534878fa4d
commit 4467a0bf4c
14 changed files with 30715 additions and 243 deletions

View File

@@ -6,8 +6,36 @@
<meta name="description" content="DNS服务器管理控制台 - 高性能DNS服务器支持规则屏蔽和Hosts管理">
<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://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">
<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>
<body>
<div class="container">
@@ -63,42 +91,54 @@
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card" style="position: relative;">
<i class="fas fa-ban"></i>
<div class="stat-value" id="blocked-count">--</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 class="stat-card" style="position: relative;">
<i class="fas fa-check-circle"></i>
<div class="stat-value" id="allowed-count">--</div>
<div class="stat-label">允许请求</div>
</div>
<div class="stat-card">
<div class="stat-card" style="position: relative;">
<i class="fas fa-question-circle"></i>
<div class="stat-value" id="error-count">--</div>
<div class="stat-label">错误请求</div>
</div>
<div class="stat-card">
<div class="stat-card" style="position: relative;">
<i class="fas fa-history"></i>
<div class="stat-value" id="total-queries">--</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 class="stat-card" style="position: relative;">
<i class="fas fa-list"></i>
<div class="stat-value" id="rules-count">--</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 class="stat-card" style="position: relative;">
<i class="fas fa-globe"></i>
<div class="stat-value" id="hosts-count">--</div>
<div class="stat-label">Hosts条目</div>
<div class="mini-chart-container">
<canvas id="hosts-chart" class="mini-chart"></canvas>
</div>
</div>
</div>
<div class="charts-container">
<div class="chart-card">
<h3>24小时屏蔽统计</h3>
<canvas id="hourly-chart"></canvas>
<canvas id="hourly-chart" style="height: 300px;"></canvas>
</div>
<div class="chart-card">
<h3>请求类型分布</h3>
@@ -411,7 +451,158 @@
</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/modules/dashboard.js"></script>
<script src="js/modules/rules.js"></script>
@@ -419,5 +610,47 @@
<script src="js/modules/blacklists.js"></script>
<script src="js/modules/query.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>
</html>

1727
static/index.html.2 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,20 @@
const API_BASE_URL = '/api';
// DOM 加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function() {
// 初始化面板切换
initPanelNavigation();
// 初始化通知组件
initNotification();
// 加载初始数据
loadInitialData();
// 定时更新数据
setInterval(loadInitialData, 60000); // 每分钟更新一次
// 直接调用dashboard面板初始化函数确保数据正确加载
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') {
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;';
// 设置消息和类型
notificationMessage.textContent = message;
notification.className = 'notification show ' + type;
document.body.appendChild(notification);
// 自动关闭
setTimeout(() => {
notification.classList.remove('show');
notification.remove();
}, 3000);
};
}
// 加载初始数据
// 加载初始数据(主要用于服务器状态)
function loadInitialData() {
// 加载服务器状态
fetch(`${API_BASE_URL}/status`)
@@ -83,40 +88,31 @@ function loadInitialData() {
const serverStatus = document.getElementById('server-status');
statusDot.classList.remove('connected');
serverStatus.textContent = '离线';
// 使用新的通知功能
if (typeof window.showNotification === 'function') {
window.showNotification('获取服务器状态失败', 'danger');
}
});
// 加载统计数据
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函数处理
}
// 更新统计卡片数据
// 注意:统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
// 此函数保留作为兼容层,实际功能已迁移
function updateStatCards(stats) {
const statElements = {
'blocked-count': stats.Blocked || 0,
'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)) {
const element = document.getElementById(id);
if (element) {
element.textContent = formatNumber(value);
}
}
// 空实现,保留函数声明以避免引用错误
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
}
// 注意获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
function fetchRulesCount() {
// 空实现,保留函数声明以避免引用错误
}
// 注意获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
function fetchHostsCount() {
// 空实现,保留函数声明以避免引用错误
}
// 通用API请求函数

View File

@@ -101,11 +101,8 @@ function saveConfig() {
if (response.success) {
window.showNotification('配置保存成功', 'success');
// 询问是否需要重启服务以应用配置
confirmAction(
'配置已保存。某些更改可能需要重启服务才能生效。是否现在重启服务?',
() => restartService()
);
// 由于服务器没有提供重启API移除重启提示
// 直接提示用户配置已保存
} else {
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
}
@@ -116,27 +113,7 @@ function saveConfig() {
});
}
// 重启服务
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');
});
}
// 服务重启功能已移除因为服务器没有提供对应的API端点
// 验证IP地址格式
function isValidIp(ip) {

View File

@@ -2,10 +2,17 @@
function initDashboardPanel() {
// 加载统计数据
loadDashboardData();
// 启动实时更新
if (typeof startRealTimeUpdate === 'function') {
startRealTimeUpdate();
}
}
// 加载仪表盘数据
function loadDashboardData() {
// 加载统计卡片数据
updateStatCards();
// 加载24小时统计数据
loadHourlyStats();
@@ -19,10 +26,137 @@ function loadDashboardData() {
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小时统计数据
function loadHourlyStats() {
apiRequest('/api/hourly-stats')
apiRequest('/hourly-stats')
.then(data => {
// 检查数据是否变化,避免不必要的重绘
if (typeof previousChartData !== 'undefined' &&
JSON.stringify(previousChartData) === JSON.stringify(data)) {
return; // 数据未变化,无需更新图表
}
previousChartData = JSON.parse(JSON.stringify(data));
// 处理不同可能的数据格式
if (data) {
// 优先处理用户提供的实际数据格式 {data: [], labels: []}
@@ -141,15 +275,28 @@ function renderHourlyChart(hours, blocked, allowed) {
mode: 'index',
intersect: false
}
},
animation: {
duration: 500 // 快速动画,提升实时更新体验
}
}
});
}
// 加载请求类型分布
// 加载请求类型分布 - 注意后端可能没有这个API暂时注释掉
function loadRequestsDistribution() {
apiRequest('/api/stats')
// 后端没有对应的API路由暂时跳过
console.log('请求类型分布API暂不可用');
return Promise.resolve()
.then(data => {
// 检查数据是否变化,避免不必要的重绘
if (typeof previousFullData !== 'undefined' &&
JSON.stringify(previousFullData) === JSON.stringify(data)) {
return; // 数据未变化,无需更新图表
}
previousFullData = JSON.parse(JSON.stringify(data));
// 构造饼图所需的数据,支持多种数据格式
const labels = ['允许请求', '屏蔽请求', '错误请求'];
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() {
// 首先获取表格元素并显示加载状态
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) {
// 显示加载中状态
tbody.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
}
apiRequest('/api/top-blocked')
apiRequest('/top-blocked')
.then(data => {
// 处理多种可能的数据格式,特别优化对用户提供格式的支持
let processedData = [];
@@ -282,12 +434,19 @@ function loadTopBlockedDomains() {
if (tbody) {
showEmpty(tbody, '获取数据失败');
}
// 使用全局通知功能
if (typeof showNotification === 'function') {
showNotification('danger', '获取最常屏蔽域名失败');
}
});
}
// 渲染最常屏蔽的域名表格
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;
console.log('准备渲染的域名数据:', domains);
@@ -327,7 +486,7 @@ function renderTopBlockedDomains(domains) {
// 加载最常解析的域名
function loadTopResolvedDomains() {
apiRequest('/api/top-resolved')
apiRequest('/top-resolved')
.then(data => {
// 处理多种可能的数据格式
let processedData = [];
@@ -355,6 +514,11 @@ function loadTopResolvedDomains() {
if (tbody) {
showEmpty(tbody, '暂无解析记录');
}
// 使用全局通知功能
if (typeof showNotification === 'function') {
showNotification('danger', '获取最常解析域名失败');
}
});
}

View File

@@ -28,14 +28,42 @@ function loadHosts() {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
showLoading(tbody);
// 更新API路径使用完整路径
apiRequest('/shield/hosts', 'GET')
.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 => {
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 (!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;
}
@@ -55,9 +90,6 @@ function renderHosts(hosts) {
addHostsToTable(entry.ip, entry.domain);
});
// 初始化表格排序
initTableSort('hosts-table');
// 初始化删除按钮监听器
initDeleteHostsListeners();
}
@@ -77,7 +109,17 @@ function addHostsToTable(ip, domain) {
</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条目
@@ -89,56 +131,123 @@ function addHostsEntry() {
const domain = domainInput.value.trim();
if (!ip) {
window.showNotification('请输入IP地址', 'warning');
if (typeof window.showNotification === 'function') {
window.showNotification('请输入IP地址', 'warning');
}
ipInput.focus();
return;
}
if (!domain) {
window.showNotification('请输入域名', 'warning');
if (typeof window.showNotification === 'function') {
window.showNotification('请输入域名', 'warning');
}
domainInput.focus();
return;
}
// 简单的IP地址格式验证
if (!isValidIp(ip)) {
window.showNotification('请输入有效的IP地址', 'warning');
if (typeof window.showNotification === 'function') {
window.showNotification('请输入有效的IP地址', 'warning');
}
ipInput.focus();
return;
}
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain });
// 修复重复API调用问题只调用一次
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain })
.then(data => {
if (data.success) {
window.showNotification('Hosts条目添加成功', 'success');
// 处理不同的响应格式
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 {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
if (typeof window.showNotification === 'function') {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
}
}
})
.catch(error => {
console.error('添加Hosts条目失败:', error);
window.showNotification('添加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('/shield/hosts', 'DELETE', { ip: ip, domain: domain })
.then(data => {
if (data.success) {
window.showNotification('Hosts条目删除成功', 'success');
loadHosts();
// 处理不同的响应格式
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 {
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 => {
// 恢复行样式
if (targetRow) {
targetRow.style.opacity = '1';
targetRow.style.transform = 'translateX(0)';
}
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 domain = this.getAttribute('data-domain');
confirmAction(
`确定要删除这条Hosts条目吗\n${ip} ${domain}`,
() => deleteHostsEntry(ip, domain)
);
// 使用标准confirm对话框
if (confirm(`确定要删除这条Hosts条目吗\n${ip} ${domain}`)) {
deleteHostsEntry(ip, domain);
}
});
});
}
@@ -177,4 +286,23 @@ function isValidIp(ip) {
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
});
}

View File

@@ -2,6 +2,12 @@
function initQueryPanel() {
// 初始化事件监听器
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();
if (!domain) {
window.showNotification('请输入要查询的域名', 'warning');
if (typeof window.showNotification === 'function') {
window.showNotification('请输入要查询的域名', 'warning');
}
domainInput.focus();
return;
}
@@ -31,62 +39,110 @@ function runDnsQuery() {
// 显示查询中状态
showQueryLoading();
apiRequest('/query?domain=' + domain, 'GET', { domain: domain })
// 更新API路径使用完整路径
apiRequest('/query', 'GET', { domain: domain })
.then(data => {
// 处理可能的不同响应格式
renderQueryResult(data);
// 触发数据刷新事件
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('query');
}
})
.catch(error => {
console.error('DNS查询失败:', error);
showQueryError('查询失败,请稍后重试');
window.showNotification('DNS查询失败', 'error');
if (typeof window.showNotification === 'function') {
window.showNotification('DNS查询失败', 'danger');
}
});
}
// 显示查询加载状态
function showQueryLoading() {
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 resultContent = resultContainer.querySelector('.result-content');
resultHeader.textContent = '查询中...';
resultContent.innerHTML = '<div class="loading"></div>';
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');
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 resultContent = resultContainer.querySelector('.result-content');
resultHeader.textContent = '查询错误';
resultContent.innerHTML = `<div class="result-item" style="color: #e74c3c;">${message}</div>`;
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');
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 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 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 = '';
@@ -101,45 +157,123 @@ function renderQueryResult(result) {
} else {
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 || '无';
content += `<div class="result-item"><strong>匹配规则:</strong> <span id="result-rule">${matchedRule}</span></div>`;
const matchedRule = escapeHtml(result.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">${matchedRule}</div>
</div>`;
// Hosts记录
const hostsRecord = result.hostsRecord ? `${result.hostsRecord.ip} ${result.hostsRecord.domain}` : '无';
content += `<div class="result-item"><strong>Hosts记录:</strong> <span id="result-hosts">${hostsRecord}</span></div>`;
const hostsRecord = result.hostsRecord ?
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`;
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响应如果有
if (result.dnsResponse) {
content += '<div class="result-item"><strong>DNS响应:</strong></div>';
content += '<div class="dns-response">';
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 += '<ul>';
result.dnsResponse.answers.forEach(answer => {
content += `<li>${answer.name} ${answer.type} ${answer.value}</li>`;
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 += '</ul>';
content += '</div>';
} else {
content += '<p>无DNS响应记录</p>';
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;
// 更新结果元素的内容(确保数据一致性)
document.getElementById('result-domain').textContent = result.domain || '';
document.getElementById('result-status').textContent = statusText;
document.getElementById('result-status').className = statusClass;
document.getElementById('result-rule-type').textContent = ruleType;
document.getElementById('result-rule').textContent = matchedRule;
document.getElementById('result-hosts').textContent = hostsRecord;
document.getElementById('result-time').textContent = queryTime;
// 通知用户查询成功
if (typeof window.showNotification === 'function') {
const statusMsg = result.isBlocked ? '查询完成,该域名被屏蔽' :
result.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');
}
}
});
}

View File

@@ -40,13 +40,24 @@ async function loadRules() {
const rulesPanel = document.getElementById('rules-panel');
showLoading(rulesPanel);
// 更新API路径使用完整路径
const data = await apiRequest('/shield', 'GET');
rules = data.rules || [];
// 处理不同格式的响应数据
rules = Array.isArray(data) ? data : (data.rules || []);
filteredRules = [...rules];
currentPage = 1; // 重置为第一页
renderRulesList();
// 更新规则数量统计卡片
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
} catch (error) {
showError('加载规则失败' + error.message);
console.error('加载规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('加载规则失败', 'danger');
}
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
@@ -62,7 +73,14 @@ function renderRulesList() {
rulesList.innerHTML = '';
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条规则';
updatePaginationButtons();
return;
@@ -79,9 +97,12 @@ function renderRulesList() {
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"><pre>${escapeHtml(rule)}</pre></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> 删除
@@ -89,7 +110,17 @@ function renderRulesList() {
</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)';
});
});
// 绑定删除按钮事件
@@ -156,15 +187,22 @@ async function addNewRule() {
const rule = ruleInput.value.trim();
if (!rule) {
showNotification('请输入规则内容', 'warning');
if (typeof window.showNotification === 'function') {
window.showNotification('请输入规则内容', 'warning');
}
return;
}
try {
const response = await apiRequest('/shield/rule', 'POST', { rule });
// 预处理规则支持AdGuardHome格式
const processedRule = preprocessRule(rule);
if (response.success) {
rules.push(rule);
// 更新API路径
const response = await apiRequest('/shield', 'POST', { rule: processedRule });
// 处理不同的响应格式
if (response.success || response.status === 'success') {
rules.push(processedRule);
filteredRules = [...rules];
ruleInput.value = '';
@@ -172,12 +210,24 @@ async function addNewRule() {
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
renderRulesList();
showNotification('规则添加成功', 'success');
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则添加成功', 'success');
}
} else {
showNotification('规则添加失败:' + (response.message || '未知错误'), 'error');
if (typeof window.showNotification === 'function') {
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
}
}
} 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 {
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);
if (originalIndex !== -1) {
@@ -207,13 +268,35 @@ async function deleteRule(index) {
currentPage = totalPages;
}
renderRulesList();
showNotification('规则删除成功', 'success');
// 等待动画完成后重新渲染列表
setTimeout(() => {
renderRulesList();
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则删除成功', 'success');
}
}, 300);
} 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) {
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');
showLoading(rulesPanel);
await apiRequest('/shield/reload', 'POST');
// 确保使用正确的API路径
await apiRequest('/shield/refresh', 'POST');
// 重新加载规则列表
await loadRules();
showNotification('规则重新加载成功', 'success');
// 触发数据刷新事件,通知其他模块数据已更新
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('rules');
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则重新加载成功', 'success');
}
} catch (error) {
showError('重新加载规则失败' + error.message);
console.error('重新加载规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('重新加载规则失败', 'danger');
}
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
@@ -263,8 +358,56 @@ function escapeHtml(text) {
'"': '&quot;',
"'": '&#039;'
};
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

File diff suppressed because one or more lines are too long