This commit is contained in:
Alex Yang
2025-12-16 00:11:42 +08:00
parent ba26e2b647
commit 11d39d6b76
35 changed files with 12378 additions and 1157 deletions

View File

@@ -3,462 +3,14 @@
<head>
<meta charset="UTF-8">
<title>DNS Server API 文档</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.3/swagger-ui.css">
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.swagger-ui .topbar {
background-color: #2c3e50;
padding: 10px 0;
}
.swagger-ui .topbar .topbar-wrapper .link {
color: #ecf0f1;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.3/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.3/swagger-ui-standalone-preset.js"></script>
<script>
// 定义API文档的JSON
const swaggerDocument = {
"openapi": "3.0.0",
"info": {
"title": "DNS Server API",
"description": "DNS服务器API文档",
"version": "1.0.0",
"contact": {
"name": "API Support",
"email": "support@example.com"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"servers": [
{
"url": "http://localhost:8080/api",
"description": "本地开发服务器"
}
],
"paths": {
"/stats": {
"get": {
"summary": "获取系统统计信息",
"description": "获取DNS服务器和Shield的统计信息",
"tags": ["stats"],
"responses": {
"200": {
"description": "成功获取统计信息",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"dns": {
"type": "object",
"properties": {
"Queries": {"type": "integer"},
"Blocked": {"type": "integer"},
"Allowed": {"type": "integer"},
"Errors": {"type": "integer"},
"LastQuery": {"type": "string"},
"AvgResponseTime": {"type": "number"},
"TotalResponseTime": {"type": "number"},
"QueryTypes": {"type": "object"},
"SourceIPs": {"type": "object"},
"CpuUsage": {"type": "number"}
}
},
"shield": {"type": "object"},
"topQueryType": {"type": "string"},
"activeIPs": {"type": "integer"},
"avgResponseTime": {"type": "number"},
"cpuUsage": {"type": "number"},
"time": {"type": "string"}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
}
},
"/shield": {
"get": {
"summary": "获取Shield配置",
"description": "获取Shield的配置信息",
"tags": ["shield"],
"responses": {
"200": {
"description": "成功获取配置信息",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
},
"post": {
"summary": "更新Shield配置",
"description": "更新Shield的配置信息",
"tags": ["shield"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "成功更新配置",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"}
}
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
}
},
"/shield/blacklists": {
"get": {
"summary": "获取黑名单列表",
"description": "获取所有远程黑名单的列表",
"tags": ["shield"],
"responses": {
"200": {
"description": "成功获取黑名单列表",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"url": {"type": "string"},
"enabled": {"type": "boolean"},
"lastUpdate": {"type": "string"},
"status": {"type": "string"}
}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
},
"post": {
"summary": "添加黑名单",
"description": "添加新的远程黑名单",
"tags": ["shield"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name", "url"],
"properties": {
"name": {"type": "string"},
"url": {"type": "string"},
"enabled": {"type": "boolean"}
}
}
}
}
},
"responses": {
"200": {
"description": "成功添加黑名单",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"}
}
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
},
"put": {
"summary": "更新黑名单",
"description": "更新黑名单的配置信息",
"tags": ["shield"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"url": {"type": "string"},
"enabled": {"type": "boolean"}
}
}
}
}
},
"responses": {
"200": {
"description": "成功更新黑名单",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"}
}
}
}
}
},
"400": {
"description": "请求参数错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
},
"404": {
"description": "黑名单不存在",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
}
},
"/shield/blacklists/{name}": {
"delete": {
"summary": "删除黑名单",
"description": "根据名称删除指定的远程黑名单",
"tags": ["shield"],
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "黑名单名称"
}
],
"responses": {
"200": {
"description": "成功删除黑名单",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"}
}
}
}
}
},
"404": {
"description": "黑名单不存在",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
},
"500": {
"description": "服务器内部错误",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {"type": "string"}
}
}
}
}
}
}
}
}
},
"tags": [
{
"name": "stats",
"description": "统计信息相关API"
},
{
"name": "shield",
"description": "Shield功能相关API"
}
]
};
// 初始化Swagger UI
window.onload = function() {
const ui = SwaggerUIBundle({
spec: swaggerDocument,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-standalone-preset.js"></script>
<script src="js/index.js"></script>
</body>
</html>

View File

@@ -132,26 +132,7 @@ header p {
/* 响应式布局 - 移动设备 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: -var(--sidebar-width);
top: var(--header-height);
z-index: 99;
height: calc(100vh - var(--header-height));
}
.sidebar.open {
left: 0;
width: var(--sidebar-width);
}
.sidebar.open .nav-item span {
display: block;
}
.sidebar.open .nav-item i {
margin-right: 1rem;
}
/* 这些样式已经通过Tailwind CSS类在HTML中实现这里移除避免冲突 */
}
.nav-menu {
@@ -1062,18 +1043,6 @@ tr:hover {
font-size: 0.9rem;
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View File

@@ -8,110 +8,17 @@
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Chart.js -->
<!-- Chart.js 本地备用 -->
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
<!-- Tailwind 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFFB',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
info: '#86909C',
dark: '#1D2129',
light: '#F2F3F5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
}
}
</script>
<script src="js/vendor/tailwind.js"></script>
<!-- 自定义工具类 -->
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.sidebar-item-active {
background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
}
}
</style>
<!-- 数字光晕效果样式 -->
<style>
/* 数字光晕效果基础样式
.number-glow {
animation: glow-pulse 2s ease-in-out;
}
/* 服务器状态组件光晕效果
.glow-effect {
animation: pulse 2s ease-in-out;
}
*/
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
}
}
/* 服务器状态组件样式优化 */
.server-status-widget {
min-width: 170px;
transition: all 0.3s ease;
}
.server-status-widget:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 加载状态样式 */
.status-loading {
animation: status-pulse 1.5s ease-in-out infinite;
}
/* 状态脉冲动画 */
@keyframes status-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 保存按钮状态样式 */
#save-blacklist-status {
transition: all 0.3s ease-in-out;
}
</style>
<style type="text/tailwindcss" src="css/index.css"></style>
</head>
<body class="bg-gray-50 text-dark font-sans">
<div class="flex h-screen overflow-hidden">
<!-- 侧边栏 -->
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg transform-gpu overflow-hidden">
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg">
<!-- 移动端关闭按钮 -->
<div class="absolute top-4 right-4 md:hidden">
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
@@ -149,7 +56,7 @@
<li>
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-search mr-3 text-lg"></i>
<span>DNS查询</span>
<span>DNS屏蔽查询</span>
</a>
</li>
<li>
@@ -176,7 +83,7 @@
<!-- 顶部导航栏 -->
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
<div class="flex items-center">
<button id="toggle-sidebar" class="md:hidden text-gray-500 hover:text-gray-700 focus:outline-none z-10">
<button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-bars text-xl"></i>
</button>
<h2 class="ml-4 text-xl font-semibold" id="page-title">仪表盘</h2>
@@ -476,7 +383,7 @@
<h3 class="text-lg font-semibold mb-4">被拦截域名排行</h3>
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<div class="space-y-3" id="top-blocked-table">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-danger">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">1</span>
@@ -530,7 +437,7 @@
<h3 class="text-lg font-semibold mb-4">请求域名排行</h3>
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<div class="space-y-3" id="top-domains-table">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-success">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">1</span>
@@ -562,7 +469,7 @@
</div>
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<div class="space-y-3" id="top-clients-table">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-primary">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">1</span>
@@ -736,7 +643,7 @@
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">更新状态</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500"></th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
@@ -876,15 +783,15 @@
</div>
<div class="md:col-span-2">
<label for="dns-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">上游DNS服务器 (逗号分隔)</label>
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="223.5.5.5,223.6.6.6">
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
</div>
<div>
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="data/stats.json">
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
</div>
<div>
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="30">
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
</div>
</div>
</div>
@@ -906,7 +813,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
<div>
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">本地规则文件</label>
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="data/rules.txt">
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
</div>
<div>
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>

View File

@@ -27,8 +27,6 @@ async function initDashboard() {
// 初始化图表
initCharts();
// 初始化统计卡片图表
initStatCardCharts();
// 初始化时间范围切换
@@ -124,9 +122,6 @@ function processRealTimeData(stats) {
// 更新统计卡片 - 这会更新所有统计卡片包括CPU使用率卡片
updateStatsCards(stats);
// 更新统计卡片图表
updateStatCardCharts(stats);
// 获取查询类型统计数据
let queryTypeStats = null;
if (stats.dns && stats.dns.QueryTypes) {
@@ -156,6 +151,8 @@ function processRealTimeData(stats) {
// 更新新卡片数据
if (document.getElementById('avg-response-time')) {
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
// 计算响应时间趋势
let responsePercent = '---';
let trendClass = 'text-gray-400';
@@ -185,16 +182,9 @@ function processRealTimeData(stats) {
}
}
// 使用滚轮效果更新响应时间
if (stats.avgResponseTime) {
animateValue('avg-response-time', stats.avgResponseTime + 'ms');
} else {
document.getElementById('avg-response-time').textContent = '---';
}
document.getElementById('avg-response-time').textContent = responseTime;
const responseTimePercentElem = document.getElementById('response-time-percent');
if (responseTimePercentElem) {
// 直接更新文本,移除动画效果
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
}
@@ -209,8 +199,7 @@ function processRealTimeData(stats) {
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
}
// 使用滚轮效果更新查询类型
animateValue('top-query-type', queryType);
document.getElementById('top-query-type').textContent = queryType;
}
if (document.getElementById('active-ips')) {
@@ -242,8 +231,7 @@ function processRealTimeData(stats) {
}
}
// 使用滚轮效果更新活跃IP数量
animateValue('active-ips', activeIPs);
document.getElementById('active-ips').textContent = activeIPs;
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
if (activeIpsPercentElem) {
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
@@ -252,7 +240,7 @@ function processRealTimeData(stats) {
}
// 实时更新TOP客户端和TOP域名数据
updateTopData(stats);
updateTopData();
} catch (error) {
console.error('处理实时数据失败:', error);
@@ -260,43 +248,25 @@ function processRealTimeData(stats) {
}
// 实时更新TOP客户端和TOP域名数据
async function updateTopData(stats = null) {
async function updateTopData() {
try {
// 如果提供了WebSocket数据直接使用
if (stats && stats.topClients) {
updateTopClientsTable(stats.topClients);
// 隐藏错误信息
const errorElement = document.getElementById('top-clients-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 否则从API获取最新的TOP客户端数据
let clientsData = [];
try {
clientsData = await api.getTopClients();
} catch (error) {
console.error('获取TOP客户端数据失败:', error);
}
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
if (clientsData.length > 0) {
// 使用真实数据
updateTopClientsTable(clientsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-clients-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 数据为空,使用模拟数据
const mockClients = [
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' }
];
updateTopClientsTable(mockClients);
}
// 获取最新的TOP客户端数据
let clientsData = [];
try {
clientsData = await api.getTopClients();
} catch (error) {
console.error('获取TOP客户端数据失败:', error);
}
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
if (clientsData.length > 0) {
// 使用真实数据
updateTopClientsTable(clientsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-clients-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// API调用失败或返回错误,使用模拟数据
// 数据为空,使用模拟数据
const mockClients = [
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
@@ -306,43 +276,35 @@ async function updateTopData(stats = null) {
];
updateTopClientsTable(mockClients);
}
} else {
// API调用失败或返回错误使用模拟数据
const mockClients = [
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' }
];
updateTopClientsTable(mockClients);
}
// 如果提供了WebSocket数据直接使用
if (stats && stats.topDomains) {
updateTopDomainsTable(stats.topDomains);
// 隐藏错误信息
const errorElement = document.getElementById('top-domains-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 否则从API获取最新的TOP域名数据
let domainsData = [];
try {
domainsData = await api.getTopDomains();
} catch (error) {
console.error('获取TOP域名数据失败:', error);
}
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
if (domainsData.length > 0) {
// 使用真实数据
updateTopDomainsTable(domainsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-domains-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 数据为空,使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
updateTopDomainsTable(mockDomains);
}
// 获取最新的TOP域名数据
let domainsData = [];
try {
domainsData = await api.getTopDomains();
} catch (error) {
console.error('获取TOP域名数据失败:', error);
}
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
if (domainsData.length > 0) {
// 使用真实数据
updateTopDomainsTable(domainsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-domains-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// API调用失败或返回错误,使用模拟数据
// 数据为空,使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
@@ -352,6 +314,16 @@ async function updateTopData(stats = null) {
];
updateTopDomainsTable(mockDomains);
}
} else {
// API调用失败或返回错误使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
updateTopDomainsTable(mockDomains);
}
} catch (error) {
console.error('更新TOP数据失败:', error);
@@ -737,20 +709,6 @@ async function loadDashboardData() {
}
// 更新统计卡片
// 格式化数字,添加千位分隔符
function formatNumber(num, element) {
// 如果是数字类型,转换为字符串
if (typeof num === 'number') {
// 处理浮点数(例如响应时间)
if (num % 1 !== 0 && element && element.id.includes('response-time')) {
return num.toFixed(2);
}
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 如果已经是字符串,直接返回
return num;
}
function updateStatsCards(stats) {
console.log('更新统计卡片,收到数据:', stats);
@@ -800,22 +758,184 @@ function updateStatsCards(stats) {
queryTypePercentage = stats[0].queryTypePercentage || 0;
activeIPs = stats[0].activeIPs || 0;
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
}
// 为数字元素添加滚轮式滚动特效
// 直接更新数字元素,移除滚动动画
// 存储正在进行的动画状态,避免动画重叠
const animationInProgress = {};
// 为数字元素添加翻页滚动特效
function animateValue(elementId, newValue) {
const element = document.getElementById(elementId);
if (!element) return;
// 先调用formatNumber获取格式化后的
const formattedNewValue = formatNumber(newValue, element);
const currentValue = element.textContent;
// 如果该元素正在进行动画,取消当前动画并立即更新
if (animationInProgress[elementId]) {
// 清除之前可能设置的定时器
clearTimeout(animationInProgress[elementId].timeout1);
clearTimeout(animationInProgress[elementId].timeout2);
clearTimeout(animationInProgress[elementId].timeout3);
// 立即设置新值,避免显示错乱
const formattedNewValue = formatNumber(newValue);
element.innerHTML = formattedNewValue;
return;
}
// 如果值没有变化,不执行更新
if (currentValue !== formattedNewValue) {
element.textContent = formattedNewValue;
const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0;
const formattedNewValue = formatNumber(newValue);
// 如果值没有变化,不执行动画
if (oldValue === newValue && element.textContent === formattedNewValue) {
return;
}
// 先移除可能存在的光晕效果类
element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow');
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
// 保存原始样式
const originalStyle = element.getAttribute('style') || '';
try {
// 配置翻页容器样式,确保与原始元素大小完全一致
const containerStyle =
'position: relative; '
+ 'display: ' + computedStyle.display + '; '
+ 'overflow: hidden; '
+ 'height: ' + element.offsetHeight + 'px; '
+ 'width: ' + element.offsetWidth + 'px; '
+ 'margin: ' + computedStyle.margin + '; '
+ 'padding: ' + computedStyle.padding + '; '
+ 'box-sizing: ' + computedStyle.boxSizing + '; '
+ 'line-height: ' + computedStyle.lineHeight + ';';
// 创建翻页容器
const flipContainer = document.createElement('div');
flipContainer.style.cssText = containerStyle;
flipContainer.className = 'number-flip-container';
// 创建旧值元素
const oldValueElement = document.createElement('div');
oldValueElement.textContent = element.textContent;
oldValueElement.style.cssText =
'position: absolute; ' +
'top: 0; ' +
'left: 0; ' +
'width: 100%; ' +
'height: 100%; ' +
'display: flex; ' +
'align-items: center; ' +
'justify-content: center; ' +
'transition: transform 400ms ease-in-out; ' +
'transform-origin: center;';
// 创建新值元素
const newValueElement = document.createElement('div');
newValueElement.textContent = formattedNewValue;
newValueElement.style.cssText =
'position: absolute; ' +
'top: 0; ' +
'left: 0; ' +
'width: 100%; ' +
'height: 100%; ' +
'display: flex; ' +
'align-items: center; ' +
'justify-content: center; ' +
'transition: transform 400ms ease-in-out; ' +
'transform-origin: center; ' +
'transform: translateY(100%);';
// 复制原始元素的样式到新元素,确保大小完全一致
const computedStyle = getComputedStyle(element);
[oldValueElement, newValueElement].forEach(el => {
el.style.fontSize = computedStyle.fontSize;
el.style.fontWeight = computedStyle.fontWeight;
el.style.color = computedStyle.color;
el.style.fontFamily = computedStyle.fontFamily;
el.style.textAlign = computedStyle.textAlign;
el.style.lineHeight = computedStyle.lineHeight;
el.style.width = '100%';
el.style.height = '100%';
el.style.margin = '0';
el.style.padding = '0';
el.style.boxSizing = 'border-box';
el.style.whiteSpace = computedStyle.whiteSpace;
el.style.overflow = 'hidden';
el.style.textOverflow = 'ellipsis';
// 确保垂直对齐正确
el.style.verticalAlign = 'middle';
});
// 替换原始元素的内容
element.textContent = '';
flipContainer.appendChild(oldValueElement);
flipContainer.appendChild(newValueElement);
element.appendChild(flipContainer);
// 标记该元素正在进行动画
animationInProgress[elementId] = {};
// 启动翻页动画
animationInProgress[elementId].timeout1 = setTimeout(() => {
if (oldValueElement && newValueElement) {
oldValueElement.style.transform = 'translateY(-100%)';
newValueElement.style.transform = 'translateY(0)';
}
}, 50);
// 动画结束后,恢复原始元素
animationInProgress[elementId].timeout2 = setTimeout(() => {
try {
// 清理并设置最终值
element.innerHTML = formattedNewValue;
if (originalStyle) {
element.setAttribute('style', originalStyle);
} else {
element.removeAttribute('style');
}
// 添加当前卡片颜色的深色光晕效果
const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50');
let glowColorClass = '';
if (card) {
if (card.classList.contains('bg-blue-50') || card.id.includes('total') || card.id.includes('response')) {
glowColorClass = 'number-glow-dark-blue';
} else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) {
glowColorClass = 'number-glow-dark-red';
} else if (card.classList.contains('bg-green-50') || card.id.includes('allowed') || card.id.includes('active')) {
glowColorClass = 'number-glow-dark-green';
} else if (card.classList.contains('bg-yellow-50') || card.id.includes('error') || card.id.includes('cpu')) {
glowColorClass = 'number-glow-dark-yellow';
}
}
if (glowColorClass) {
element.classList.add(glowColorClass);
// 2秒后移除光晕效果
animationInProgress[elementId].timeout3 = setTimeout(() => {
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
}, 2000);
}
} catch (e) {
console.error('更新元素失败:', e);
} finally {
// 清除动画状态标记
delete animationInProgress[elementId];
}
}, 450);
} catch (e) {
console.error('创建动画失败:', e);
// 出错时直接设置值
element.innerHTML = formattedNewValue;
if (originalStyle) {
element.setAttribute('style', originalStyle);
} else {
element.removeAttribute('style');
}
// 清除动画状态标记
delete animationInProgress[elementId];
}
}
@@ -824,8 +944,37 @@ function updateStatsCards(stats) {
const element = document.getElementById(elementId);
if (!element) return;
// 直接更新文本,移除所有动画效果
element.textContent = value;
// 检查是否有正在进行的动画
if (animationInProgress[elementId + '_percent']) {
clearTimeout(animationInProgress[elementId + '_percent']);
}
try {
element.style.opacity = '0';
element.style.transition = 'opacity 200ms ease-out';
// 保存定时器ID便于后续可能的取消
animationInProgress[elementId + '_percent'] = setTimeout(() => {
try {
element.textContent = value;
element.style.opacity = '1';
} catch (e) {
console.error('更新百分比元素失败:', e);
} finally {
// 清除动画状态标记
delete animationInProgress[elementId + '_percent'];
}
}, 200);
} catch (e) {
console.error('设置百分比动画失败:', e);
// 出错时直接设置值
try {
element.textContent = value;
element.style.opacity = '1';
} catch (e2) {
console.error('直接更新百分比元素也失败:', e2);
}
}
}
// 平滑更新数量显示
@@ -835,10 +984,14 @@ function updateStatsCards(stats) {
animateValue('error-queries', errorQueries);
animateValue('active-ips', activeIPs);
// 平滑更新文本和百分比
updatePercentage('top-query-type', topQueryType);
updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`);
updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`);
// 直接更新文本和百分比,移除动画效果
const topQueryTypeElement = document.getElementById('top-query-type');
const queryTypePercentageElement = document.getElementById('query-type-percentage');
const activeIpsPercentElement = document.getElementById('active-ips-percent');
if (topQueryTypeElement) topQueryTypeElement.textContent = topQueryType;
if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`;
if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`;
// 计算并平滑更新百分比
if (totalQueries > 0) {
@@ -880,11 +1033,9 @@ function updateTopBlockedTable(domains) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ name: 'example1.com', count: 150 },
{ name: 'example2.com', count: 130 },
{ name: 'example3.com', count: 120 },
{ name: 'example4.com', count: 110 },
{ name: 'example5.com', count: 100 }
{ name: '---.---.---', count: '---' },
{ name: '---.---.---', count: '---' },
{ name: '---.---.---', count: '---' }
];
console.log('使用示例数据填充Top屏蔽域名表格');
}
@@ -893,7 +1044,7 @@ function updateTopBlockedTable(domains) {
for (let i = 0; i < tableData.length && i < 5; i++) {
const domain = tableData[i];
html += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-danger">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
@@ -934,11 +1085,11 @@ function updateRecentBlockedTable(domains) {
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ name: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' },
{ name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' },
{ name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' },
{ name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' },
{ name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' }
{ name: '---.---.---', timestamp: now - 5 * 60 * 1000, type: '广告' },
{ name: '---.---.---', timestamp: now - 15 * 60 * 1000, type: '恶意' },
{ name: '---.---.---', timestamp: now - 30 * 60 * 1000, type: '广告' },
{ name: '---.---.---', timestamp: now - 45 * 60 * 1000, type: '追踪' },
{ name: '---.---.---', timestamp: now - 60 * 60 * 1000, type: '恶意' }
];
console.log('使用示例数据填充最近屏蔽域名表格');
}
@@ -948,7 +1099,7 @@ function updateRecentBlockedTable(domains) {
const domain = tableData[i];
const time = formatTime(domain.timestamp);
html += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-warning">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-warning">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">${domain.name}</div>
<div class="text-sm text-gray-500 mt-1">${time}</div>
@@ -991,11 +1142,11 @@ function updateTopClientsTable(clients) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ ip: '192.168.1.100', count: 120 },
{ ip: '192.168.1.101', count: 95 },
{ ip: '192.168.1.102', count: 80 },
{ ip: '192.168.1.103', count: 65 },
{ ip: '192.168.1.104', count: 50 }
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' }
];
console.log('使用示例数据填充TOP客户端表格');
}
@@ -1007,7 +1158,7 @@ function updateTopClientsTable(clients) {
for (let i = 0; i < tableData.length; i++) {
const client = tableData[i];
html += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-primary">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
@@ -1068,7 +1219,7 @@ function updateTopDomainsTable(domains) {
for (let i = 0; i < tableData.length; i++) {
const domain = tableData[i];
html += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-success">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
@@ -1141,7 +1292,8 @@ function initTimeRangeToggle() {
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
// 设置非选中状态样式,移除过渡动画
// 设置非选中状态样式
button.classList.add('transition-colors', 'duration-200');
button.classList.add(...styleConfig.normal);
button.classList.add(...styleConfig.hover);
@@ -1281,8 +1433,11 @@ function initCharts() {
options: {
responsive: true,
maintainAspectRatio: false,
// 禁用图表动画
animation: false,
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
animation: {
duration: 500, // 延长动画时间,使过渡更平滑
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
@@ -1350,8 +1505,11 @@ function initCharts() {
options: {
responsive: true,
maintainAspectRatio: false,
// 禁用图表动画
animation: false,
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
animation: {
duration: 300,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
@@ -1512,7 +1670,7 @@ function initDetailedTimeRangeToggle() {
'bg-green-500', 'bg-purple-500', 'bg-gray-100', 'mixed-view-active');
// 设置非选中状态样式
// 移除过渡动画类
button.classList.add('transition-colors', 'duration-200');
button.classList.add(...styleConfig.normal);
button.classList.add(...styleConfig.hover);
@@ -1650,8 +1808,11 @@ function drawDetailedDNSRequestsChart() {
detailedDnsRequestsChart.data.labels = results[0].labels;
detailedDnsRequestsChart.data.datasets = datasets;
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
// 更新图表,不使用动画
detailedDnsRequestsChart.update();
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
@@ -1662,8 +1823,10 @@ function drawDetailedDNSRequestsChart() {
options: {
responsive: true,
maintainAspectRatio: false,
// 禁用图表动画
animation: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: showLegend,
@@ -2216,8 +2379,15 @@ function updateChartData(chartId, newValue) {
chart.data.datasets[0].data = historyData;
chart.data.labels = generateTimeLabels(historyData.length);
// 更新图表,不使用动画
chart.update();
// 使用自定义动画配置更新图表,确保平滑过渡,避免空白区域
chart.update({
duration: 300, // 增加动画持续时间
easing: 'easeInOutQuart', // 使用平滑的缓动函数
transition: {
duration: 300,
easing: 'easeInOutQuart'
}
});
}
// 从统计数据中获取规则数
@@ -2323,8 +2493,11 @@ function initStatCardCharts() {
options: {
responsive: true,
maintainAspectRatio: false,
// 禁用图表动画
animation: false,
// 添加动画配置,确保平滑过渡
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: false
@@ -2426,85 +2599,32 @@ function generateTimeLabels(count) {
return labels;
}
// 检查元素内容是否溢出
function isContentOverflow(element) {
if (!element) return false;
return element.scrollWidth > element.clientWidth;
}
// 格式化数字显示使用K/M后缀
function formatNumber(num, element = null) {
function formatNumber(num) {
// 如果不是数字,直接返回
if (isNaN(num) || num === '---') {
return num;
}
// 转换为数字类型
const numericValue = Number(num);
// 获取数字的字符串表示形式
const numStr = numericValue.toString();
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 检查是否需要使用K/M格式
let useCompactFormat = false;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 方法1: 基于元素内容是否溢出判断
if (element) {
// 临时设置元素内容为完整数字
const originalContent = element.textContent;
element.textContent = numStr;
// 检查是否溢出
useCompactFormat = isContentOverflow(element);
// 恢复原始内容
element.textContent = originalContent;
}
// 方法2: 基于窗口宽度和数字长度的自适应判断
else {
// 根据窗口宽度动态调整阈值
let maxFullLength = 5;
if (window.innerWidth < 768) {
maxFullLength = 4; // 小屏幕更严格
} else if (window.innerWidth < 1024) {
maxFullLength = 5; // 中等屏幕
}
// 如果数字长度超过阈值则使用K/M格式
useCompactFormat = numStr.length > maxFullLength;
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 如果需要使用紧凑格式
if (useCompactFormat) {
if (numericValue >= 1000000) {
return (numericValue / 1000000).toFixed(1) + 'M';
} else if (numericValue >= 1000) {
return (numericValue / 1000).toFixed(1) + 'K';
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return numStr;
}
// 重新计算所有统计卡片的数字显示格式
function updateStatsCardsFormat() {
const statCardElements = document.querySelectorAll('.stat-card .stat-value');
statCardElements.forEach(element => {
// 获取原始数值可能已经是K/M格式
const text = element.textContent;
let originalNum;
// 解析K/M格式的数字
if (text.includes('M')) {
originalNum = parseFloat(text) * 1000000;
} else if (text.includes('K')) {
originalNum = parseFloat(text) * 1000;
} else {
originalNum = parseFloat(text);
}
// 重新计算显示格式
if (!isNaN(originalNum)) {
element.textContent = formatNumber(originalNum, element);
}
});
return fullNumStr;
}
// 更新运行状态
@@ -2576,7 +2696,7 @@ function showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.id = 'notification';
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform translate-y-0 opacity-100`;
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-0 opacity-0`;
// 设置样式和内容
let bgColor, textColor, icon;
@@ -2613,9 +2733,18 @@ function showNotification(message, type = 'info') {
// 添加到页面
document.body.appendChild(notification);
// 自动关闭,直接移除元素,无动画效果
// 显示通知
setTimeout(() => {
notification.remove();
notification.classList.remove('translate-y-0', 'opacity-0');
notification.classList.add('-translate-y-2', 'opacity-100');
}, 10);
// 自动关闭
setTimeout(() => {
notification.classList.add('translate-y-0', 'opacity-0');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
@@ -2794,9 +2923,6 @@ function handleResponsive() {
chart.update();
}
});
// 更新统计卡片数字格式,确保在窗口缩小时内容不溢出
updateStatsCardsFormat();
});
// 添加触摸事件支持,用于移动端
@@ -2885,9 +3011,4 @@ window.addEventListener('DOMContentLoaded', () => {
clearInterval(intervalId);
}
});
// 页面加载完成后,初始化统计卡片的数字格式,确保内容不会溢出
setTimeout(() => {
updateStatsCardsFormat();
}, 500);
});

View File

@@ -40,57 +40,52 @@ function setupNavigation() {
// 打开侧边栏函数
function openSidebar() {
console.log('打开侧边栏');
console.log('Opening sidebar...');
if (sidebar) {
sidebar.classList.remove('-translate-x-full');
sidebar.classList.add('translate-x-0');
}
if (sidebarOverlay) {
sidebarOverlay.classList.remove('hidden');
sidebarOverlay.classList.add('flex');
sidebarOverlay.classList.add('block');
}
// 防止页面滚动
document.body.style.overflow = 'hidden';
document.body.style.touchAction = 'none'; // 防止触摸滚动
console.log('Sidebar opened successfully');
}
// 关闭侧边栏函数
function closeSidebar() {
console.log('关闭侧边栏');
console.log('Closing sidebar...');
if (sidebar) {
sidebar.classList.add('-translate-x-full');
sidebar.classList.remove('translate-x-0');
}
if (sidebarOverlay) {
sidebarOverlay.classList.add('hidden');
sidebarOverlay.classList.remove('flex');
sidebarOverlay.classList.remove('block');
}
// 恢复页面滚动
document.body.style.overflow = '';
document.body.style.touchAction = '';
console.log('Sidebar closed successfully');
}
// 切换侧边栏函数
function toggleSidebarVisibility() {
console.log('切换侧边栏');
if (sidebar) {
if (sidebar.classList.contains('-translate-x-full')) {
openSidebar();
} else {
closeSidebar();
}
console.log('Toggling sidebar visibility...');
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
console.log('Sidebar is hidden, opening...');
openSidebar();
} else {
console.log('Sidebar is visible, closing...');
closeSidebar();
}
}
// 绑定切换按钮事件
if (toggleSidebar) {
// 移除可能存在的旧事件监听器
toggleSidebar.removeEventListener('click', toggleSidebarVisibility);
// 重新添加事件监听器
toggleSidebar.addEventListener('click', function(e) {
e.stopPropagation(); // 阻止事件冒泡
toggleSidebarVisibility();
});
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
}
// 绑定关闭按钮事件

View File

@@ -52,21 +52,8 @@ function displayQueryResult(result, domain) {
const statusClass = result.blocked ? 'text-danger' : 'text-success';
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
// 优先使用API返回的blacklistName字段如果没有则使用blocksource
let displaySource = '无';
if (result.blocked) {
if (result.blacklistName && result.blacklistName !== '') {
displaySource = result.blacklistName;
} else if (result.blocksource) {
displaySource = result.blocksource;
} else {
displaySource = '未知';
}
}
const timestamp = new Date(result.timestamp).toLocaleString();
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
const timestamp = new Date(result.timestamp).toLocaleString();
// 更新结果显示
document.getElementById('result-domain').textContent = domain;
@@ -114,7 +101,7 @@ function displayQueryResult(result, domain) {
// 更新屏蔽来源显示
if (blockSourceElement) {
blockSourceElement.textContent = displaySource;
blockSourceElement.textContent = blockSource;
}
document.getElementById('result-time').textContent = timestamp;
@@ -134,8 +121,7 @@ function saveQueryHistory(domain, result) {
blocked: result.blocked,
blockRuleType: result.blockRuleType,
blockRule: result.blockRule,
blocksource: result.blocksource,
blacklistName: result.blacklistName
blocksource: result.blocksource
}
};
@@ -170,19 +156,7 @@ function loadQueryHistory() {
const statusText = item.result.blocked ? '被屏蔽' : '正常';
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
const blockRule = item.result.blocked ? item.result.blockRule : '无';
// 优先显示blacklistName如果没有则显示blocksource
let sourceDisplay = '无';
if (item.result.blocked) {
if (item.result.blacklistName && item.result.blacklistName !== '') {
sourceDisplay = item.result.blacklistName;
} else if (item.result.blocksource) {
sourceDisplay = item.result.blocksource;
} else {
sourceDisplay = '未知';
}
}
const blockSource = item.result.blocked ? item.result.blocksource : '无';
const formattedTime = new Date(item.timestamp).toLocaleString();
return `
@@ -194,7 +168,7 @@ function loadQueryHistory() {
<span class="text-xs text-gray-500">${blockType}</span>
</div>
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
<div class="text-xs text-gray-500 mt-1">来源: ${sourceDisplay}</div>
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
</div>
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">