1728 lines
63 KiB
HTML
1728 lines
63 KiB
HTML
<!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解析域名(由于没有直接的API,这里使用模拟数据)
|
||
// 实际使用时,应该调用后端提供的相关API
|
||
setTimeout(() => {
|
||
const mockResolvedDomains = [
|
||
{ domain: 'baidu.com', count: 1532 },
|
||
{ domain: 'sina.com.cn', count: 987 },
|
||
{ domain: 'qq.com', count: 845 },
|
||
{ domain: 'aliyun.com', count: 732 },
|
||
{ domain: 'github.com', count: 654 },
|
||
{ domain: 'gitee.com', count: 521 },
|
||
{ domain: 'bing.com', count: 489 },
|
||
{ domain: 'sohu.com', count: 398 },
|
||
{ domain: 'jd.com', count: 345 },
|
||
{ domain: 'taobao.com', count: 298 }
|
||
];
|
||
|
||
const container = document.getElementById('top-resolved-domains');
|
||
container.innerHTML = '';
|
||
|
||
mockResolvedDomains.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);
|
||
});
|
||
}, 500);
|
||
}
|
||
|
||
// 初始化页面
|
||
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>
|