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

2377 lines
92 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; }
.btn-secondary { background-color: var(--secondary-color); color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
.btn-secondary:hover { background-color: #1a252f; transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn-sm { padding: 4px 8px; font-size: 0.875rem; }
.form-label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
.form-text { display: block; margin-top: 0.25rem; font-size: 0.875rem; color: var(--gray-600); }
.mb-3 { margin-bottom: 1rem; }
.mt-3 { margin-top: 1rem; }
.rule-items { display: flex; flex-direction: column; gap: 0.5rem; }
.rule-item { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background-color: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 4px; }
.rule-text { flex: 1; word-break: break-all; margin-right: 0.5rem; }
/* 悬浮通知样式 */
.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);
}
.btn-edit {
background-color: var(--info-color);
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
}
.btn-edit:hover {
background-color: #2980b9;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
cursor: pointer;
width: 18px;
height: 18px;
}
.list-meta {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--gray-500);
}
.list-meta span {
display: flex;
align-items: center;
gap: 0.25rem;
}
.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 {
margin-top: 2rem;
padding: 1.5rem;
background-color: var(--gray-50);
border-radius: var(--border-radius-lg);
border-left: 4px solid var(--info-color);
box-shadow: var(--shadow);
}
.status-info h3 {
margin-bottom: 1rem;
}
.status-info p {
margin: 0.5rem 0;
line-height: 1.6;
}
.status-info .stat-card {
border-top: 4px solid var(--primary-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">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h2>服务器状态</h2>
<div style="display: flex; gap: 1rem;">
<div style="background-color: white; padding: 0.5rem 1rem; border-radius: var(--border-radius-md); box-shadow: var(--shadow); display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-ban" style="color: var(--primary-color);"></i>
<span style="font-size: 1rem; font-weight: 600;">规则: <span id="rules-count-inline">--</span></span>
</div>
<div style="background-color: white; padding: 0.5rem 1rem; border-radius: var(--border-radius-md); box-shadow: var(--shadow); display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-file-alt" style="color: var(--primary-color);"></i>
<span style="font-size: 1rem; font-weight: 600;">Hosts: <span id="hosts-count-inline">--</span></span>
</div>
</div>
</div>
<div class="stats-grid">
<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>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
<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>
<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-ban"></i> 黑名单管理</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="update-interval" class="form-label">更新间隔 (秒)</label>
<input type="number" id="update-interval" min="60" max="86400" placeholder="3600">
<small class="form-text">黑名单自动更新的时间间隔建议至少60秒</small>
</div>
<div class="mb-3">
<h4>添加新黑名单</h4>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<input type="text" id="blacklist-name" placeholder="黑名单名称">
<input type="text" id="blacklist-url" placeholder="黑名单URL">
<button id="add-blacklist" class="btn-secondary">
<i class="fas fa-plus"></i> 添加
</button>
</div>
<small class="form-text">添加远程黑名单源支持HTTP/HTTPS链接</small>
</div>
<div class="list-container" id="blacklists-list">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>加载中...</p>
</div>
</div>
<button id="save-blacklists-settings" class="btn-primary mt-3">
<i class="fas fa-save"></i> 保存设置
</button>
</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: true,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#333',
borderWidth: 1,
cornerRadius: 4,
displayColors: false,
callbacks: {
label: function(context) {
return context.dataset.label + ': ' + context.parsed.y;
}
}
}
},
scales: {
x: {
display: false
},
y: {
display: false,
beginAtZero: true
}
},
animation: false,
interaction: {
intersect: false,
mode: 'index'
}
}
});
});
}
// 更新小型图表
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 => {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('获取到统计数据:', data);
// 确保数据结构正确
if (!data || !data.shield) {
console.error('返回的数据结构不正确缺少shield字段');
// 即使数据结构不正确,也尝试更新服务器地址
document.getElementById('server-address').textContent = window.location.hostname + ':8080';
return;
}
// 获取各项统计数据
const domainRules = data.shield.domainRules || 0;
const regexRules = data.shield.regexRules || 0;
const rulesCount = domainRules + regexRules;
const hostsCount = data.shield.hostsRules || 0;
const queryCount = (data.dns && data.dns.Queries) || 0;
const blockedCount = (data.dns && data.dns.Blocked) || 0;
console.log(`规则统计: 域名规则=${domainRules}, 正则规则=${regexRules}, 总计=${rulesCount}`);
console.log(`Hosts统计: 条目数=${hostsCount}`);
// 直接更新显示,同时更新顶部状态区域和统计卡片
document.getElementById('rules-count').textContent = rulesCount;
document.getElementById('hosts-count').textContent = hostsCount;
document.getElementById('query-count').textContent = queryCount;
// 更新顶部服务器状态区域的数据
document.getElementById('rules-count-inline').textContent = rulesCount;
document.getElementById('hosts-count-inline').textContent = hostsCount;
// 更新屏蔽次数显示,包含百分比
const blockedPercentage = queryCount > 0 ? Math.round((blockedCount / queryCount) * 100) : 0;
const blockedElement = document.getElementById('blocked-count');
if (blockedElement) {
blockedElement.innerHTML = blockedCount +
'<span style="position: absolute; top: -10px; right: -20px; ' +
'background-color: #ff6b6b; color: white; border-radius: 50%; padding: 2px 6px; ' +
'font-size: 0.7rem; font-weight: bold;">' + blockedPercentage + '%</span>';
}
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();
})
.catch(error => {
console.error('加载统计数据失败:', error);
// 即使出错,也尝试更新服务器地址
document.getElementById('server-address').textContent = window.location.hostname + ':8080';
// 尝试直接从本地文件加载规则和hosts数量备用方案
try {
console.log('尝试备用方案加载规则和hosts数量...');
// 这里可以添加从其他接口或本地缓存获取数据的逻辑
} catch (e) {
console.error('备用方案也失败:', e);
}
});
}
// 保存上一次的图表数据,用于检测图表数据是否变化
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',
labels: {
padding: 20,
font: {
size: 14
}
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#333',
borderWidth: 1,
cornerRadius: 6,
padding: 12,
displayColors: false,
callbacks: {
title: function(tooltipItems) {
return tooltipItems[0].label + ' 时';
},
label: function(context) {
return '屏蔽次数: ' + context.parsed.y;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
},
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
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 => {
if (!response.ok) {
throw new Error('保存失败');
}
return response.json();
})
.then(data => {
showNotification('success', '黑名单设置已保存');
})
.catch(error => {
console.error('保存黑名单设置失败:', error);
showNotification('danger', '保存失败: ' + error.message);
});
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 => {
const hostsCount = data.hostsCount || 0;
const hostsList = data.hosts || [];
if (hostsCount > 0 && hostsList.length > 0) {
// 构建hosts列表HTML
let hostsHTML = `<div class="list-header">
<div class="list-title">Hosts条目列表</div>
<div class="list-description">共 ${hostsCount} 个Hosts条目</div>
</div>`;
hostsList.forEach(item => {
hostsHTML += `<div class="list-item">
<div class="list-content">
<div class="list-title">${item.domain}</div>
<div class="list-description">IP: ${item.ip}</div>
</div>
<div class="list-actions">
<button class="btn-danger btn-sm" onclick="deleteHostsEntry('${item.domain}')">删除</button>
</div>
</div>`;
});
hostsHTML += `<div class="list-footer">
<button class="btn-primary btn-sm" onclick="showAddHostsModal()">添加Hosts条目</button>
<button class="btn-outline btn-sm" onclick="loadHostsList()">刷新</button>
</div>`;
hostsContainer.innerHTML = hostsHTML;
} else {
hostsContainer.innerHTML = `<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>暂无Hosts条目</p>
<button class="btn-primary btn-sm" onclick="showAddHostsModal()">添加Hosts条目</button>
</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();
// 黑名单相关事件监听
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
document.getElementById('save-blacklists-settings').addEventListener('click', saveBlacklistsSettings);
// 加载黑名单设置
loadBlacklistsSettings();
};
// 加载当前屏蔽设置
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 loadBlacklistsSettings() {
fetch('/api/config')
.then(response => response.json())
.then(data => {
if (data.shield) {
// 设置更新间隔
document.getElementById('update-interval').value = data.shield.updateInterval || 3600;
// 加载黑名单列表
renderBlacklistsList(data.shield.blacklists || []);
}
})
.catch(error => {
console.error('加载黑名单设置失败:', error);
showNotification('danger', '加载黑名单设置失败');
renderBlacklistsList([]);
});
}
// 渲染黑名单列表
function renderBlacklistsList(blacklists) {
const listContainer = document.getElementById('blacklists-list');
if (!blacklists || blacklists.length === 0) {
listContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>暂无黑名单</p>
</div>
`;
return;
}
// 确保我们处理的是数组
const blacklistItems = Array.isArray(blacklists) ? blacklists : [];
let html = '';
blacklistItems.forEach((item, index) => {
html += `
<div class="list-item">
<div class="list-content">
<div class="list-title">
<label class="checkbox-label">
<input type="checkbox" ${item.enabled ? 'checked' : ''} class="blacklist-enabled" data-index="${index}">
<span>${item.name || '未命名黑名单'}</span>
</label>
</div>
<div class="list-description">${item.URL}</div>
<div class="list-meta">
<span>${item.ruleCount ? `规则数: ${item.ruleCount}` : '未加载规则'}</span>
${item.lastUpdateTime ? `<span>更新时间: ${item.lastUpdateTime}</span>` : ''}
</div>
</div>
<div class="list-actions">
<button class="btn-edit btn-sm" data-index="${index}">
<i class="fas fa-edit"></i>
</button>
<button class="btn-danger btn-sm delete-blacklist" data-index="${index}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
`;
});
listContainer.innerHTML = html;
// 添加事件监听
document.querySelectorAll('.blacklist-enabled').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const index = parseInt(this.getAttribute('data-index'));
toggleBlacklistStatus(index, this.checked);
});
});
document.querySelectorAll('.delete-blacklist').forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.getAttribute('data-index'));
deleteBlacklist(index);
});
});
document.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.getAttribute('data-index'));
editBlacklist(index);
});
});
}
// 添加黑名单
function addBlacklist() {
const name = document.getElementById('blacklist-name').value.trim();
const url = document.getElementById('blacklist-url').value.trim();
if (!name || !url) {
showNotification('warning', '请输入黑名单名称和URL');
return;
}
// 简单的URL验证
try {
new URL(url);
} catch (e) {
showNotification('warning', '请输入有效的URL格式');
return;
}
// 获取当前黑名单列表
const listItems = document.querySelectorAll('#blacklists-list .list-item');
let blacklists = [];
listItems.forEach((item, index) => {
const enabled = item.querySelector('.blacklist-enabled').checked;
const name = item.querySelector('.list-title span').textContent;
const url = item.querySelector('.list-description').textContent;
blacklists.push({ name, URL: url, enabled });
});
// 检查URL是否已存在
if (blacklists.some(item => item.URL === url)) {
showNotification('warning', '该URL已存在');
return;
}
// 添加新黑名单
blacklists.push({ name, URL: url, enabled: true });
renderBlacklistsList(blacklists);
// 清空输入框
document.getElementById('blacklist-name').value = '';
document.getElementById('blacklist-url').value = '';
showNotification('success', '黑名单已添加');
}
// 编辑黑名单
function editBlacklist(index) {
const listItems = document.querySelectorAll('#blacklists-list .list-item');
const item = listItems[index];
const currentName = item.querySelector('.list-title span').textContent;
const currentUrl = item.querySelector('.list-description').textContent;
const newName = prompt('请输入黑名单名称:', currentName);
if (newName === null) return;
const newUrl = prompt('请输入黑名单URL:', currentUrl);
if (newUrl === null) return;
if (!newName.trim() || !newUrl.trim()) {
showNotification('warning', '名称和URL不能为空');
return;
}
// URL验证
try {
new URL(newUrl);
} catch (e) {
showNotification('warning', '请输入有效的URL格式');
return;
}
// 获取所有黑名单
let blacklists = [];
listItems.forEach((item, idx) => {
const enabled = item.querySelector('.blacklist-enabled').checked;
let name = item.querySelector('.list-title span').textContent;
let url = item.querySelector('.list-description').textContent;
if (idx === index) {
name = newName.trim();
url = newUrl.trim();
}
blacklists.push({ name, URL: url, enabled });
});
renderBlacklistsList(blacklists);
showNotification('success', '黑名单已更新');
}
// 切换黑名单状态
function toggleBlacklistStatus(index, enabled) {
showNotification('info', enabled ? '黑名单已启用' : '黑名单已禁用');
}
// 删除黑名单
function deleteBlacklist(index) {
if (!confirm('确定要删除这条黑名单吗?')) {
return;
}
// 获取所有黑名单
const listItems = document.querySelectorAll('#blacklists-list .list-item');
let blacklists = [];
listItems.forEach((item, idx) => {
if (idx !== index) {
const enabled = item.querySelector('.blacklist-enabled').checked;
const name = item.querySelector('.list-title span').textContent;
const url = item.querySelector('.list-description').textContent;
blacklists.push({ name, URL: url, enabled });
}
});
}
// 加载hosts列表
function loadHostsList() {
const hostsContainer = document.getElementById('hosts-container');
hostsContainer.innerHTML = '<div class="loading-state"><i class="fas fa-spinner fa-spin"></i><p>加载中...</p></div>';
fetch('/api/shield/hosts')
.then(response => response.json())
.then(data => {
const hostsCount = data.hostsCount || 0;
const hostsList = data.hosts || [];
if (hostsCount > 0 && hostsList.length > 0) {
// 构建hosts列表HTML
let hostsHTML = `<div class="list-header">
<div class="list-title">Hosts条目列表</div>
<div class="list-description">共 ${hostsCount} 个Hosts条目</div>
</div>`;
hostsList.forEach(item => {
hostsHTML += `<div class="list-item">
<div class="list-content">
<div class="list-title">${item.domain}</div>
<div class="list-description">IP: ${item.ip}</div>
</div>
<div class="list-actions">
<button class="btn-danger btn-sm" onclick="deleteHostsEntry('${item.domain}')">删除</button>
</div>
</div>`;
});
hostsHTML += `<div class="list-footer">
<button class="btn-primary btn-sm" onclick="showAddHostsModal()">添加Hosts条目</button>
<button class="btn-outline btn-sm" onclick="loadHostsList()">刷新</button>
</div>`;
hostsContainer.innerHTML = hostsHTML;
} else {
hostsContainer.innerHTML = `<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>暂无Hosts条目</p>
<button class="btn-primary btn-sm" onclick="showAddHostsModal()">添加Hosts条目</button>
</div>`;
}
})
.catch(error => {
hostsContainer.innerHTML = `<div class="error-state"><i class="fas fa-exclamation-circle"></i><p>加载失败: ${error.message}</p></div>`;
console.error('加载hosts列表失败:', error);
});
}
// 显示添加hosts条目的模态框
function showAddHostsModal() {
// 创建模态框HTML
const modalHTML = `
<div class="modal-overlay" onclick="closeAddHostsModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3>添加Hosts条目</h3>
<button class="modal-close" onclick="closeAddHostsModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="hosts-domain">域名</label>
<input type="text" id="hosts-domain" placeholder="请输入域名,如 example.com" required>
</div>
<div class="form-group">
<label for="hosts-ip">IP地址</label>
<input type="text" id="hosts-ip" placeholder="请输入IP地址如 192.168.1.1" required>
</div>
</div>
<div class="modal-footer">
<button class="btn-outline" onclick="closeAddHostsModal()">取消</button>
<button class="btn-primary" onclick="addHostsEntry()">添加</button>
</div>
</div>
</div>`;
// 添加模态框到页面
const modalDiv = document.createElement('div');
modalDiv.id = 'add-hosts-modal';
modalDiv.innerHTML = modalHTML;
document.body.appendChild(modalDiv);
}
// 关闭添加hosts条目的模态框
function closeAddHostsModal() {
const modal = document.getElementById('add-hosts-modal');
if (modal) {
modal.remove();
}
}
// 添加hosts条目
function addHostsEntry() {
const domain = document.getElementById('hosts-domain').value.trim();
const ip = document.getElementById('hosts-ip').value.trim();
if (!domain || !ip) {
alert('请填写域名和IP地址');
return;
}
// 简单的IP地址验证
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(ip)) {
alert('请输入有效的IP地址');
return;
}
fetch('/api/shield/hosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain, ip })
})
.then(response => {
if (!response.ok) {
throw new Error('添加失败');
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
alert('添加成功');
closeAddHostsModal();
loadHostsList();
} else {
alert('添加失败');
}
})
.catch(error => {
console.error('添加hosts条目失败:', error);
alert('添加失败: ' + error.message);
});
}
// 删除hosts条目
function deleteHostsEntry(domain) {
if (confirm(`确定要删除域名 ${domain} 的hosts条目吗`)) {
fetch('/api/shield/hosts', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
})
.then(response => {
if (!response.ok) {
throw new Error('删除失败');
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
loadHostsList();
} else {
alert('删除失败');
}
})
.catch(error => {
console.error('删除hosts条目失败:', error);
alert('删除失败: ' + error.message);
});
}
}
renderBlacklistsList(blacklists);
showNotification('success', '黑名单已删除');
// 保存黑名单设置
function saveBlacklistsSettings() {
const updateInterval = parseInt(document.getElementById('update-interval').value);
// 验证更新间隔
if (isNaN(updateInterval) || updateInterval < 60) {
showNotification('warning', '更新间隔必须大于等于60秒');
return;
}
// 获取当前黑名单列表
const listItems = document.querySelectorAll('#blacklists-list .list-item');
let blacklists = [];
listItems.forEach(item => {
const enabled = item.querySelector('.blacklist-enabled').checked;
const name = item.querySelector('.list-title span').textContent;
const url = item.querySelector('.list-description').textContent;
blacklists.push({ name, URL: url, enabled });
});
fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shield: {
blacklists: blacklists,
updateInterval: updateInterval
}
})
})
.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>