Files
dns-server/static/index.html
2025-11-23 18:37:24 +08:00

1725 lines
63 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS服务器管理中心</title>
<!-- 引入Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2c3e50;
--success-color: #2ecc71;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--info-color: #3498db;
--light-color: #ecf0f1;
--dark-color: #34495e;
--gray-100: #f8f9fa;
--gray-200: #e9ecef;
--gray-300: #dee2e6;
--gray-400: #ced4da;
--gray-500: #adb5bd;
--gray-600: #6c757d;
--gray-700: #495057;
--gray-800: #343a40;
--gray-900: #212529;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
--border-radius-sm: 0.25rem;
--border-radius: 0.375rem;
--border-radius-md: 0.5rem;
--border-radius-lg: 0.75rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background-color: var(--gray-100);
color: var(--gray-800);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 2rem 0;
border-radius: var(--border-radius-lg);
margin-bottom: 2rem;
box-shadow: var(--shadow-md);
text-align: center;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
font-weight: 700;
}
header p {
font-size: 1.1rem;
opacity: 0.9;
}
.tabs {
background-color: white;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
margin-bottom: 2rem;
overflow: hidden;
}
.tab-nav {
display: flex;
background-color: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.tab-btn {
flex: 1;
padding: 1rem 1.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: var(--gray-600);
transition: all var(--transition-fast);
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.tab-btn:hover {
background-color: var(--gray-100);
color: var(--primary-color);
}
.tab-btn.active {
color: var(--primary-color);
background-color: white;
box-shadow: inset 0 3px 0 var(--primary-color);
}
.tab-content {
display: none;
padding: 2rem;
animation: fadeIn 0.3s ease-in-out;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
background-color: white;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card-header {
border-bottom: 1px solid var(--gray-200);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-800);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-body {
padding: 0.5rem 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: white;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
padding: 1rem;
text-align: center;
transition: transform var(--transition-fast);
border-top: 4px solid var(--primary-color);
}
.stat-card:hover {
transform: translateY(-3px);
}
.stat-card i {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--gray-800);
margin-bottom: 0.25rem;
position: relative;
transition: all 0.3s ease;
}
.stat-value.update {
animation: glow 1s ease-out;
}
@keyframes glow {
0% {
text-shadow: 0 0 5px var(--primary-color), 0 0 10px var(--primary-color);
transform: scale(1.1);
}
100% {
text-shadow: none;
transform: scale(1);
}
}
.mini-chart-container {
height: 60px;
margin-top: 0.5rem;
opacity: 0.8;
}
.stat-label {
font-size: 0.9rem;
color: var(--gray-600);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chart-container {
position: relative;
height: 300px;
margin: 1rem 0;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 1.5rem;
}
input[type="text"], select {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--border-radius);
font-size: 1rem;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
background-color: white;
}
input[type="text"]:focus, select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-success:hover {
background-color: #27ae60;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-outline {
background-color: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.btn-outline:hover {
background-color: var(--primary-color);
color: white;
}
/* 悬浮通知样式 */
.notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
padding: 1rem 1.5rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 0.75rem;
animation: slideIn 0.3s ease-out;
max-width: 400px;
}
.notification-success {
background-color: white;
border-left: 4px solid var(--success-color);
color: var(--gray-800);
}
.notification-danger {
background-color: white;
border-left: 4px solid var(--danger-color);
color: var(--gray-800);
}
.notification-warning {
background-color: white;
border-left: 4px solid var(--warning-color);
color: var(--gray-800);
}
.notification-info {
background-color: white;
border-left: 4px solid var(--info-color);
color: var(--gray-800);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-icon {
font-size: 1.25rem;
}
.notification-success .notification-icon {
color: var(--success-color);
}
.notification-danger .notification-icon {
color: var(--danger-color);
}
.notification-warning .notification-icon {
color: var(--warning-color);
}
.notification-info .notification-icon {
color: var(--info-color);
}
.notification-content {
flex: 1;
font-weight: 500;
}
.notification-close {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: var(--gray-500);
padding: 0;
margin-left: 0.5rem;
transition: color var(--transition-fast);
}
.notification-close:hover {
color: var(--gray-700);
}
.list-container {
max-height: 400px;
overflow-y: auto;
border-radius: var(--border-radius);
border: 1px solid var(--gray-200);
background-color: white;
}
.list-item {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--gray-100);
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color var(--transition-fast);
}
.list-item:hover {
background-color: var(--gray-50);
}
.list-item:last-child {
border-bottom: none;
}
.list-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.list-title {
font-weight: 500;
color: var(--gray-800);
}
.list-description {
font-size: 0.875rem;
color: var(--gray-600);
}
.list-actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: var(--border-radius-sm);
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--gray-500);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 1rem;
}
.alert {
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1rem;
border-left: 4px solid;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border-color: var(--success-color);
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border-color: var(--danger-color);
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border-color: var(--info-color);
}
.status-info {
background-color: var(--gray-50);
padding: 1.5rem;
border-radius: var(--border-radius-lg);
border-left: 4px solid var(--info-color);
}
.status-info p {
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-info strong {
color: var(--gray-700);
}
pre {
background-color: var(--gray-900);
color: var(--gray-100);
padding: 1.5rem;
border-radius: var(--border-radius);
overflow-x: auto;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 9999px;
text-transform: uppercase;
}
.badge-primary {
background-color: var(--primary-color);
color: white;
}
.badge-success {
background-color: var(--success-color);
color: white;
}
.badge-danger {
background-color: var(--danger-color);
color: white;
}
.badge-warning {
background-color: var(--warning-color);
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
header h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
}
.input-group {
flex-direction: column;
}
.tab-nav {
overflow-x: auto;
white-space: nowrap;
}
.tab-btn {
flex: none;
}
.tab-content {
padding: 1rem;
}
}
/* 加载动画 */
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--gray-200);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 0.8s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--gray-100);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--gray-400);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--gray-500);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-server"></i> DNS服务器管理中心</h1>
<p>高性能DNS服务器支持规则屏蔽和Hosts管理</p>
</header>
<div class="tabs">
<div class="tab-nav">
<button class="tab-btn active" onclick="openTab(event, 'dashboard')">
<i class="fas fa-tachometer-alt"></i> 概览
</button>
<button class="tab-btn" onclick="openTab(event, 'block-rules')">
<i class="fas fa-ban"></i> 屏蔽规则
</button>
<button class="tab-btn" onclick="openTab(event, 'hosts')">
<i class="fas fa-list-ul"></i> Hosts管理
</button>
<button class="tab-btn" onclick="openTab(event, 'query')">
<i class="fas fa-search"></i> DNS查询
</button>
</div>
<!-- 概览面板 -->
<div id="dashboard" class="tab-content active">
<h2 style="margin-bottom: 1.5rem;">服务器状态</h2>
<div class="stats-grid">
<div class="stat-card">
<i class="fas fa-ban"></i>
<div class="stat-value" id="rules-count">--</div>
<div class="stat-label">屏蔽规则数</div>
<div class="mini-chart-container">
<canvas id="rules-chart"></canvas>
</div>
</div>
<div class="stat-card">
<i class="fas fa-file-alt"></i>
<div class="stat-value" id="hosts-count">--</div>
<div class="stat-label">Hosts条目数</div>
<div class="mini-chart-container">
<canvas id="hosts-chart"></canvas>
</div>
</div>
<div class="stat-card">
<i class="fas fa-question-circle"></i>
<div class="stat-value" id="query-count">--</div>
<div class="stat-label">DNS查询次数</div>
<div class="mini-chart-container">
<canvas id="query-chart"></canvas>
</div>
</div>
<div class="stat-card">
<i class="fas fa-times-circle"></i>
<div class="stat-value" id="blocked-count">--</div>
<div class="stat-label">屏蔽次数</div>
<div class="mini-chart-container">
<canvas id="blocked-chart"></canvas>
</div>
</div>
</div>
<h2 style="margin-bottom: 1.5rem; margin-top: 2rem;">TOP域名统计</h2>
<div class="grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-ban"></i> TOP 10 屏蔽域名</h3>
</div>
<div class="card-body">
<div id="top-blocked-domains" class="list-container">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>加载中...</p>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-globe"></i> TOP 10 解析域名</h3>
</div>
<div class="card-body">
<div id="top-resolved-domains" class="list-container">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>加载中...</p>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-chart-line"></i> 24小时屏蔽统计</h3>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="blockChart"></canvas>
</div>
</div>
</div>
<div class="status-info">
<h3 style="margin-bottom: 1rem; font-size: 1.25rem; color: var(--gray-700);">服务器信息</h3>
<p><strong>服务器地址:</strong> <span id="server-address">--</span></p>
<p><strong>当前时间:</strong> <span id="current-time">--</span></p>
<p><strong>运行状态:</strong> <span class="badge badge-success">正常运行</span></p>
</div>
</div>
<!-- 屏蔽规则面板 -->
<div id="block-rules" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cog"></i> 屏蔽设置</h3>
</div>
<div class="card-body">
<div class="input-group">
<select id="block-method">
<option value="NXDOMAIN">返回NXDOMAIN</option>
<option value="refused">返回拒绝</option>
<option value="emptyIP">返回空IP (0.0.0.0)</option>
<option value="customIP">返回自定义IP</option>
</select>
<input type="text" id="custom-block-ip" placeholder="自定义IP地址" disabled>
<button id="save-block-settings" class="btn-primary">
<i class="fas fa-save"></i> 保存设置
</button>
</div>
<small style="display:block; margin-top:0.5rem; color:var(--gray-600); font-size:0.875rem;">
<strong>NXDOMAIN</strong>: 返回域名不存在错误<br>
<strong>refused</strong>: 返回查询拒绝错误<br>
<strong>emptyIP</strong>: 返回0.0.0.0<br>
<strong>customIP</strong>: 返回自定义IP地址
</small>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-plus-circle"></i> 添加屏蔽规则</h3>
</div>
<div class="card-body">
<div class="input-group">
<select id="rule-type">
<option value="domain">域名规则</option>
<option value="exception">排除规则</option>
<option value="regex">正则表达式</option>
<option value="wildcard">通配符规则</option>
<option value="start">URL开头</option>
<option value="end">URL结尾</option>
</select>
<input type="text" id="new-rule" placeholder="输入规则内容例如example.com">
<button id="add-rule-btn" class="btn-primary">
<i class="fas fa-plus"></i> 添加
</button>
</div>
<small style="display:block; margin-top:0.5rem; color:var(--gray-600); font-size:0.875rem;">支持AdGuardHome规则格式域名规则(||example.com^)、排除规则(@@||example.com^)、正则规则(/regex/)、通配符规则(*example.com)等</small>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-list"></i> 规则列表</h3>
</div>
<div class="card-body">
<div id="rules-container" class="list-container">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>规则列表加载中...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Hosts管理面板 -->
<div id="hosts" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-plus-circle"></i> 添加Hosts条目</h3>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" id="hosts-ip" placeholder="IP地址例如127.0.0.1">
<input type="text" id="hosts-domain" placeholder="域名例如localhost">
<button id="add-hosts-btn" class="btn-primary">
<i class="fas fa-plus"></i> 添加
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-list"></i> 当前Hosts条目</h3>
</div>
<div class="card-body">
<div id="hosts-container" class="list-container">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>Hosts列表加载中...</p>
</div>
</div>
</div>
</div>
</div>
<!-- DNS查询面板 -->
<div id="query" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-search"></i> DNS查询</h3>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" id="query-domain" placeholder="输入要查询的域名">
<button id="query-btn" class="btn-primary">
<i class="fas fa-search"></i> 查询
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-code"></i> 查询结果</h3>
</div>
<div class="card-body">
<pre id="query-result-text">请输入域名并点击查询按钮</pre>
</div>
</div>
</div>
</div>
</div>
<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)
};
// 标签页切换功能
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].className = tabcontent[i].className.replace(" active", "");
}
tablinks = document.getElementsByClassName("tab-btn");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).className += " active";
evt.currentTarget.className += " active";
// 当切换到特定标签时加载数据
if (tabName === 'dashboard') {
loadDashboardData();
// 启动定时更新,但确保只启动一次
if (!updateTimer) {
startRealTimeUpdate();
}
} else {
// 切换到其他标签页时停止定时更新
stopRealTimeUpdate();
if (tabName === 'block-rules') {
loadRules();
} else if (tabName === 'hosts') {
loadHosts();
}
}
}
// 定时更新的定时器
let updateTimer = null;
const UPDATE_INTERVAL = 2000; // 降低间隔以便更快地响应数据变化
// 启动实时更新
function startRealTimeUpdate() {
// 确保完全清除之前的定时器
if (updateTimer) {
clearInterval(updateTimer);
updateTimer = null;
}
// 启动新的定时器保持轮询机制但在loadDashboardData内部判断是否需要更新
updateTimer = setInterval(loadDashboardData, UPDATE_INTERVAL);
}
// 停止实时更新
function stopRealTimeUpdate() {
if (updateTimer) {
clearInterval(updateTimer);
updateTimer = null;
}
}
// 检查数据是否变化并添加光晕效果
function checkAndAnimate(elementId, newValue) {
const element = document.getElementById(elementId);
const oldValue = previousStats[elementId] || 0;
// 检查值是否发生变化
if (newValue !== oldValue && oldValue !== 0) {
// 添加更新类触发动画
element.classList.add('update');
// 动画结束后移除类
setTimeout(() => {
element.classList.remove('update');
}, 1000);
}
// 更新存储的上一次值
previousStats[elementId] = newValue;
}
// 更新数据历史记录
function updateDataHistory(key, value) {
dataHistory[key].shift(); // 移除最旧的数据点
dataHistory[key].push(value); // 添加新的数据点
}
// 初始化小型图表
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).getContext('2d');
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();
}
}
// 页面加载完成时初始化
window.addEventListener('load', function() {
// 初始化小型图表
initMiniCharts();
// 加载仪表盘数据
if (document.getElementById('dashboard').classList.contains('active')) {
loadDashboardData();
// 确保只启动一次定时器
if (!updateTimer) {
startRealTimeUpdate();
}
}
});
// 页面卸载时清理定时器
window.addEventListener('beforeunload', function() {
stopRealTimeUpdate();
});
// 保存上一次完整的API数据用于检测是否有任何变动
let previousFullData = null;
// 用于比较数据是否变化的辅助函数
function isDataChanged(newData, oldData) {
if (!oldData) return true;
// 检查关键统计数据是否有变化
const newRulesCount = (newData.shield && (newData.shield.domainRules + newData.shield.regexRules)) || 0;
const oldRulesCount = (oldData.shield && (oldData.shield.domainRules + oldData.shield.regexRules)) || 0;
const newHostsCount = (newData.shield && newData.shield.hostsRules) || 0;
const oldHostsCount = (oldData.shield && oldData.shield.hostsRules) || 0;
const newQueryCount = (newData.dns && newData.dns.Queries) || 0;
const oldQueryCount = (oldData.dns && oldData.dns.Queries) || 0;
const newBlockedCount = (newData.dns && newData.dns.Blocked) || 0;
const oldBlockedCount = (oldData.dns && oldData.dns.Blocked) || 0;
// 如果任何数据项发生变化返回true
return newRulesCount !== oldRulesCount ||
newHostsCount !== oldHostsCount ||
newQueryCount !== oldQueryCount ||
newBlockedCount !== oldBlockedCount;
}
// 加载概览数据 - 仅在数据变动时更新页面
function loadDashboardData() {
// 更新当前时间
document.getElementById('current-time').textContent = new Date().toLocaleString('zh-CN');
fetch('/api/stats')
.then(response => response.json())
.then(data => {
// 检查数据是否有变动
if (isDataChanged(data, previousFullData)) {
console.log('数据有变动,正在更新页面...');
// 获取各项统计数据
const rulesCount = (data.shield && (data.shield.domainRules + data.shield.regexRules)) || 0;
const hostsCount = (data.shield && data.shield.hostsRules) || 0;
const queryCount = (data.dns && data.dns.Queries) || 0;
const blockedCount = (data.dns && data.dns.Blocked) || 0;
// 检查数据变化并添加动画
checkAndAnimate('rules-count', rulesCount);
checkAndAnimate('hosts-count', hostsCount);
checkAndAnimate('query-count', queryCount);
checkAndAnimate('blocked-count', blockedCount);
// 更新显示
document.getElementById('rules-count').textContent = rulesCount;
document.getElementById('hosts-count').textContent = hostsCount;
document.getElementById('query-count').textContent = queryCount;
document.getElementById('blocked-count').textContent = blockedCount;
document.getElementById('server-address').textContent = window.location.hostname + ':8080';
// 更新数据历史记录
updateDataHistory('rules', rulesCount);
updateDataHistory('hosts', hostsCount);
updateDataHistory('query', queryCount);
updateDataHistory('blocked', blockedCount);
// 更新小型图表
updateMiniChart('rules-chart', dataHistory.rules);
updateMiniChart('hosts-chart', dataHistory.hosts);
updateMiniChart('query-chart', dataHistory.query);
updateMiniChart('blocked-chart', dataHistory.blocked);
// 更新完整数据缓存
previousFullData = JSON.parse(JSON.stringify(data));
// 数据有变动时才加载其他相关数据
loadChartData();
loadTopDomains();
} else {
console.log('数据无变动,不更新页面');
}
})
.catch(error => console.error('加载统计数据失败:', error));
}
// 保存上一次的图表数据,用于检测图表数据是否变化
let previousChartData = null;
// 加载图表数据并渲染 - 仅在数据变动时更新
function loadChartData() {
fetch('/api/hourly-stats')
.then(response => response.json())
.then(data => {
// 检查图表数据是否有变动
const dataChanged = !previousChartData ||
JSON.stringify(data.labels) !== JSON.stringify(previousChartData.labels) ||
JSON.stringify(data.data) !== JSON.stringify(previousChartData.data);
if (dataChanged) {
console.log('图表数据有变动,正在更新图表...');
renderBlockChart(data.labels, data.data);
// 更新缓存
previousChartData = { labels: [...data.labels], data: [...data.data] };
}
})
.catch(error => console.error('加载图表数据失败:', error));
}
// 渲染屏蔽统计图表
let blockChart = null;
function renderBlockChart(labels, data) {
const ctx = document.getElementById('blockChart').getContext('2d');
// 如果图表已存在,先销毁
if (blockChart) {
blockChart.destroy();
}
blockChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '屏蔽次数',
data: data,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 2,
pointBackgroundColor: 'rgb(255, 99, 132)',
pointRadius: 4,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false,
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
},
x: {
grid: {
display: false
}
}
},
animation: {
duration: 1000
}
}
});
}
// 加载屏蔽规则
function loadRules() {
const rulesContainer = document.getElementById('rules-container');
rulesContainer.innerHTML = '<div class="empty-state"><div class="loader"></div><p>规则列表加载中...</p></div>';
// 从服务器获取真实规则列表
fetch('/api/shield')
.then(response => {
if (!response.ok) {
throw new Error('获取规则失败');
}
return response.json();
})
.then(data => {
rulesContainer.innerHTML = '';
// 合并所有规则类型
let allRules = [];
// 添加域名规则
if (Array.isArray(data.domainRules)) {
data.domainRules.forEach(rule => {
allRules.push({ rule: rule, type: 'domain', block: true });
});
}
// 添加排除规则
if (Array.isArray(data.domainExceptions)) {
data.domainExceptions.forEach(rule => {
allRules.push({ rule: rule, type: 'exception', block: false });
});
}
// 添加正则规则
if (Array.isArray(data.regexRules)) {
data.regexRules.forEach(rule => {
allRules.push({ rule: rule, type: 'regex', block: true });
});
}
// 添加正则排除规则
if (Array.isArray(data.regexExceptions)) {
data.regexExceptions.forEach(rule => {
allRules.push({ rule: rule, type: 'regex_exception', block: false });
});
}
// 如果没有规则,显示空状态
if (allRules.length === 0) {
rulesContainer.innerHTML = '<div class="empty-state"><i class="fas fa-info-circle"></i><p>暂无规则</p></div>';
return;
}
// 渲染所有规则
allRules.forEach(item => {
const ruleItem = document.createElement('div');
ruleItem.className = 'list-item';
// 根据规则类型添加不同的样式和图标
let icon = 'ban';
let title = '屏蔽规则';
let badgeClass = 'badge-danger';
let typeText = '域名';
if (item.type === 'exception') {
icon = 'check-circle';
title = '排除规则';
badgeClass = 'badge-success';
typeText = '排除';
} else if (item.type === 'regex' || item.type === 'regex_exception') {
icon = 'code';
title = item.type === 'regex' ? '正则屏蔽规则' : '正则排除规则';
badgeClass = item.type === 'regex' ? 'badge-primary' : 'badge-success';
typeText = '正则';
}
// 转义规则中的特殊字符确保在HTML和JavaScript中正确处理
const escapedRule = item.rule.replace(/'/g, "\\'");
ruleItem.innerHTML = `
<div class="list-content">
<div class="list-title"><i class="fas fa-${icon}"></i> ${title}</div>
<div class="list-description">${item.rule}</div>
</div>
<div class="list-actions">
<span class="badge ${badgeClass}">${typeText}</span>
<button class="btn-danger btn-sm" onclick="deleteRule('${escapedRule}')">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
rulesContainer.appendChild(ruleItem);
});
})
.catch(error => {
console.error('加载规则失败:', error);
rulesContainer.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载规则失败</p></div>';
});
}
// 添加屏蔽规则
document.getElementById('add-rule-btn').addEventListener('click', function() {
const rule = document.getElementById('new-rule').value.trim();
const ruleType = document.getElementById('rule-type').value;
const btn = this;
const originalText = btn.innerHTML;
if (!rule) {
showNotification('warning', '请输入规则内容');
return;
}
// 显示加载状态
btn.innerHTML = '<div class="loader"></div> 添加中';
btn.disabled = true;
// 根据规则类型生成AdGuardHome格式的规则
let fullRule = '';
switch(ruleType) {
case 'domain':
// 标准域名规则: ||example.com^
fullRule = `||${rule}^`;
break;
case 'exception':
// 排除规则: @@||example.com^
fullRule = `@@||${rule}^`;
break;
case 'regex':
// 正则表达式规则: /pattern/
fullRule = `/${rule}/`;
break;
case 'wildcard':
// 通配符规则: *example.com
fullRule = `*${rule}`;
break;
case 'start':
// URL开头匹配: |http://example.com
fullRule = `|${rule}`;
break;
case 'end':
// URL结尾匹配: example.com|
fullRule = `${rule}|`;
break;
default:
fullRule = rule;
}
fetch('/api/shield', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ rule: fullRule })
})
.then(response => response.json())
.then(data => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
// 显示成功消息
showNotification('success', '规则添加成功');
// 清空输入框
document.getElementById('new-rule').value = '';
// 重新加载规则
loadRules();
})
.catch(error => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
console.error('添加规则失败:', error);
showNotification('danger', '添加规则失败,请稍后重试');
});
});
// 加载Hosts条目
function loadHosts() {
const hostsContainer = document.getElementById('hosts-container');
hostsContainer.innerHTML = '<div class="empty-state"><div class="loader"></div><p>Hosts列表加载中...</p></div>';
fetch('/api/shield/hosts')
.then(response => response.json())
.then(data => {
// 注意这需要在shieldManager中添加一个获取所有hosts条目的方法
// 暂时返回统计信息
const hostsCount = data.hostsCount || 0;
if (hostsCount > 0) {
hostsContainer.innerHTML = `<div class="list-item">
<div class="list-content">
<div class="list-title">Hosts概览</div>
<div class="list-description">共 ${hostsCount} 个Hosts条目</div>
</div>
<div class="list-actions">
<button class="btn-outline btn-sm" onclick="location.reload()">刷新</button>
</div>
</div>`;
} else {
hostsContainer.innerHTML = '<div class="empty-state"><i class="fas fa-info-circle"></i><p>暂无Hosts条目</p></div>';
}
})
.catch(error => {
console.error('加载Hosts失败:', error);
hostsContainer.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败,请稍后重试</p></div>';
});
}
// 添加Hosts条目
document.getElementById('add-hosts-btn').addEventListener('click', function() {
const ip = document.getElementById('hosts-ip').value.trim();
const domain = document.getElementById('hosts-domain').value.trim();
const btn = this;
const originalText = btn.innerHTML;
if (!ip || !domain) {
showNotification('warning', '请输入IP地址和域名');
return;
}
// 显示加载状态
btn.innerHTML = '<div class="loader"></div> 添加中';
btn.disabled = true;
fetch('/api/shield/hosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ip: ip, domain: domain })
})
.then(response => response.json())
.then(data => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
// 显示成功消息
showNotification('success', 'Hosts条目添加成功');
// 清空输入框
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
// 重新加载Hosts
loadHosts();
})
.catch(error => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
console.error('添加Hosts条目失败:', error);
showNotification('danger', '添加Hosts条目失败请稍后重试');
});
});
// DNS查询
document.getElementById('query-btn').addEventListener('click', function() {
const domain = document.getElementById('query-domain').value.trim();
const btn = this;
const originalText = btn.innerHTML;
const resultElement = document.getElementById('query-result-text');
if (!domain) {
showNotification('warning', '请输入要查询的域名');
return;
}
// 显示加载状态
btn.innerHTML = '<div class="loader"></div> 查询中';
btn.disabled = true;
resultElement.textContent = '正在查询...';
fetch(`/api/query?domain=${encodeURIComponent(domain)}`)
.then(response => response.json())
.then(data => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
// 格式化显示结果
resultElement.textContent = JSON.stringify(data, null, 2);
})
.catch(error => {
// 重置按钮状态
btn.innerHTML = originalText;
btn.disabled = false;
console.error('查询失败:', error);
resultElement.textContent = '查询失败: ' + error.message;
showNotification('danger', '查询失败,请稍后重试');
});
});
// 删除规则函数
function deleteRule(rule) {
if (!confirm('确定要删除这条规则吗?')) {
return;
}
fetch('/api/shield', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ rule: rule })
})
.then(response => {
if (!response.ok) {
throw new Error('删除失败');
}
return response.json();
})
.then(data => {
// 显示成功消息
showNotification('success', '规则删除成功');
// 重新加载规则列表
loadRules();
})
.catch(error => {
console.error('删除规则失败:', error);
showNotification('danger', '删除规则失败,请稍后重试');
});
}
// 加载TOP域名数据
function loadTopDomains() {
// 加载TOP屏蔽域名
fetch('/api/top-blocked')
.then(response => response.json())
.then(data => {
const container = document.getElementById('top-blocked-domains');
container.innerHTML = '';
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-info-circle"></i><p>暂无屏蔽域名统计</p></div>';
return;
}
data.forEach((item, index) => {
const listItem = document.createElement('div');
listItem.className = 'list-item';
listItem.innerHTML = `
<div class="list-content">
<div class="list-title">${index + 1}. ${item.domain}</div>
<div class="list-description">屏蔽次数: ${item.count}</div>
</div>
<div class="list-actions">
<span class="badge badge-danger">屏蔽</span>
</div>
`;
container.appendChild(listItem);
});
})
.catch(error => {
console.error('加载TOP屏蔽域名失败:', error);
document.getElementById('top-blocked-domains').innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
});
// 加载TOP解析域名
fetch('/api/top-resolved')
.then(response => response.json())
.then(data => {
const container = document.getElementById('top-resolved-domains');
container.innerHTML = '';
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-info-circle"></i><p>暂无解析域名统计</p></div>';
return;
}
data.forEach((item, index) => {
const listItem = document.createElement('div');
listItem.className = 'list-item';
listItem.innerHTML = `
<div class="list-content">
<div class="list-title">${index + 1}. ${item.domain}</div>
<div class="list-description">解析次数: ${item.count}</div>
</div>
<div class="list-actions">
<span class="badge badge-success">解析</span>
</div>
`;
container.appendChild(listItem);
});
})
.catch(error => {
console.error('加载TOP解析域名失败:', error);
document.getElementById('top-resolved-domains').innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
});
}
// 初始化页面
window.onload = function() {
loadDashboardData();
// 每秒更新时间
setInterval(() => {
document.getElementById('current-time').textContent = new Date().toLocaleString('zh-CN');
}, 1000);
// 屏蔽方法选择事件
document.getElementById('block-method').addEventListener('change', function() {
document.getElementById('custom-block-ip').disabled = this.value !== 'customIP';
});
// 保存屏蔽设置按钮点击事件
document.getElementById('save-block-settings').addEventListener('click', saveBlockSettings);
// 加载当前屏蔽设置
loadBlockSettings();
};
// 加载当前屏蔽设置
function loadBlockSettings() {
fetch('/api/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.shield) {
document.getElementById('block-method').value = data.shield.blockMethod || 'NXDOMAIN';
document.getElementById('custom-block-ip').value = data.shield.customBlockIP || '';
document.getElementById('custom-block-ip').disabled = data.shield.blockMethod !== 'customIP';
}
})
.catch(error => {
console.error('加载屏蔽设置失败:', error);
});
}
// 保存屏蔽设置
function saveBlockSettings() {
const blockMethod = document.getElementById('block-method').value;
const customBlockIP = document.getElementById('custom-block-ip').value;
// 验证自定义IP
if (blockMethod === 'customIP' && customBlockIP) {
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(customBlockIP)) {
showNotification('warning', '请输入有效的IP地址');
return;
}
}
fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shield: {
blockMethod: blockMethod,
customBlockIP: customBlockIP
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('success', '屏蔽设置已保存');
} else {
showNotification('danger', '保存失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
console.error('保存屏蔽设置失败:', error);
showNotification('danger', '保存失败: ' + error.message);
});
}
// 显示悬浮通知
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>
</body>
</html>