添加了Swagger API文档以及诸多优化

This commit is contained in:
Alex Yang
2025-11-28 02:15:42 +08:00
parent 67c651c804
commit 2e7d5fb1ce
9 changed files with 1080 additions and 125 deletions

464
static/api/index.html Normal file
View File

@@ -0,0 +1,464 @@
<!DOCTYPE html>
<html lang="zh-CN">
<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>
</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>
</body>
</html>

View File

@@ -171,7 +171,13 @@
<body class="bg-gray-50 text-dark font-sans">
<div class="flex h-screen overflow-hidden">
<!-- 侧边栏 -->
<aside id="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col transition-all duration-300 z-10">
<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">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<!-- Logo -->
<div class="flex items-center justify-center h-16 border-b border-gray-200">
<i class="fa fa-server text-3xl text-primary mr-3"></i>
@@ -227,12 +233,15 @@
</div>
</aside>
<!-- 侧边栏遮罩层 -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
<!-- 主内容区 -->
<main class="flex-1 overflow-y-auto">
<!-- 顶部导航栏 -->
<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="lg:hidden text-gray-500 hover:text-gray-700">
<button id="toggle-sidebar" class="md:block lg: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>
@@ -529,41 +538,99 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 最常屏蔽域名 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">最常屏蔽域名</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left 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>
<tbody id="top-blocked-table">
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
</tr>
</tbody>
</table>
<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 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>
<span class="font-medium truncate">example1.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">150</span>
</div>
<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">2</span>
<span class="font-medium truncate">example2.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">130</span>
</div>
<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">3</span>
<span class="font-medium truncate">example3.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">120</span>
</div>
<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">4</span>
<span class="font-medium truncate">example4.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">110</span>
</div>
<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">5</span>
<span class="font-medium truncate">example5.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">100</span>
</div>
</div>
</div>
</div>
<!-- 最近屏蔽域名 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">最近屏蔽域名</h3>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left 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>
<tbody id="recent-blocked-table">
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
</tr>
</tbody>
</table>
<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="recent-blocked-table">
<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">recent1.com</div>
<div class="text-sm text-gray-500 mt-1">2024-01-01 10:00:00</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">广告</span>
</div>
<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">recent2.com</div>
<div class="text-sm text-gray-500 mt-1">2024-01-01 10:01:00</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">恶意</span>
</div>
<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">recent3.com</div>
<div class="text-sm text-gray-500 mt-1">2024-01-01 10:02:00</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">广告</span>
</div>
<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">recent4.com</div>
<div class="text-sm text-gray-500 mt-1">2024-01-01 10:03:00</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">追踪</span>
</div>
<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">recent5.com</div>
<div class="text-sm text-gray-500 mt-1">2024-01-01 10:04:00</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">恶意</span>
</div>
</div>
</div>
</div>
</div>
@@ -572,7 +639,7 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<!-- TOP客户端 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">TOP客户端</h3>
<div id="top-clients-loading" class="flex items-center text-sm text-gray-500">
<i class="fa fa-spinner fa-spin mr-2"></i>
@@ -584,20 +651,54 @@
<button id="retry-top-clients" class="ml-2 text-primary hover:underline">重试</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">请求次数</th>
</tr>
</thead>
<tbody id="top-clients-table">
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
</tr>
</tbody>
</table>
<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 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>
<span class="font-medium truncate">192.168.1.1</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">500</span>
</div>
<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">2</span>
<span class="font-medium truncate">192.168.1.2</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">450</span>
</div>
<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">3</span>
<span class="font-medium truncate">192.168.1.3</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">400</span>
</div>
<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">4</span>
<span class="font-medium truncate">192.168.1.4</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">350</span>
</div>
<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">5</span>
<span class="font-medium truncate">192.168.1.5</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">300</span>
</div>
</div>
</div>
</div>
@@ -972,5 +1073,7 @@
<script src="js/hosts.js"></script>
<script src="js/query.js"></script>
<script src="js/config.js"></script>
<!-- 直接渲染滚动列表的静态HTML内容 -->
</body>
</html>

View File

@@ -1,5 +1,32 @@
// 配置管理页面功能实现
// 工具函数安全获取DOM元素
function getElement(id) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Element with id "${id}" not found`);
}
return element;
}
// 工具函数:验证端口号
function validatePort(port) {
// 确保port是字符串类型
var portStr = port;
if (port === null || port === undefined || typeof port !== 'string') {
return null;
}
// 去除前后空白并验证是否为纯数字
portStr = port.trim();
if (!/^\d+$/.test(portStr)) {
return null;
}
const num = parseInt(portStr, 10);
return num >= 1 && num <= 65535 ? num : null;
}
// 初始化配置管理页面
function initConfigPage() {
loadConfig();
@@ -9,85 +36,192 @@ function initConfigPage() {
// 加载系统配置
async function loadConfig() {
try {
showLoading(true);
const config = await api.getConfig();
populateConfigForm(config);
} catch (error) {
showErrorMessage('加载配置失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 填充配置表单
function populateConfigForm(config) {
// DNS配置
document.getElementById('dns-port')?.value = config.DNSServer.Port || 53;
document.getElementById('dns-upstream-servers')?.value = (config.DNSServer.UpstreamServers || []).join(', ');
document.getElementById('dns-timeout')?.value = config.DNSServer.Timeout || 5;
document.getElementById('dns-stats-file')?.value = config.DNSServer.StatsFile || './stats.json';
document.getElementById('dns-save-interval')?.value = config.DNSServer.SaveInterval || 300;
// 安全获取配置对象,防止未定义属性访问
const dnsServerConfig = config.DNSServer || {};
const httpServerConfig = config.HTTPServer || {};
const shieldConfig = config.Shield || {};
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
// HTTP配置
document.getElementById('http-port')?.value = config.HTTPServer.Port || 8080;
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
// 屏蔽配置
document.getElementById('shield-local-rules-file')?.value = config.Shield.LocalRulesFile || './rules.txt';
document.getElementById('shield-update-interval')?.value = config.Shield.UpdateInterval || 3600;
document.getElementById('shield-hosts-file')?.value = config.Shield.HostsFile || '/etc/hosts';
document.getElementById('shield-block-method')?.value = config.Shield.BlockMethod || '0.0.0.0';
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, '0.0.0.0'));
}
// 工具函数:安全设置元素值
function setElementValue(elementId, value) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
element.value = value;
} else if (!element) {
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
}
}
// 工具函数安全获取值如果未定义或为null则返回默认值
function getSafeValue(value, defaultValue) {
// 更严格的检查避免0、空字符串等被默认值替换
return value === undefined || value === null ? defaultValue : value;
}
// 工具函数:安全获取数组,如果不是数组则返回空数组
function getSafeArray(value) {
return Array.isArray(value) ? value : [];
}
// 保存配置
async function handleSaveConfig() {
const formData = collectFormData();
if (!formData) return;
try {
showLoading(true);
await api.saveConfig(formData);
showSuccessMessage('配置保存成功');
} catch (error) {
showErrorMessage('保存配置失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 重启服务
async function handleRestartService() {
if (confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) {
try {
await api.restartService();
showSuccessMessage('服务重启成功');
} catch (error) {
showErrorMessage('重启服务失败: ' + error.message);
}
if (!confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) return;
try {
showLoading(true);
await api.restartService();
showSuccessMessage('服务重启成功');
} catch (error) {
showErrorMessage('重启服务失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 收集表单数据
// 收集表单数据并验证
function collectFormData() {
// 验证端口号 - 使用安全获取元素值的函数
const dnsPortValue = getElementValue('dns-port');
const httpPortValue = getElementValue('http-port');
const dnsPort = validatePort(dnsPortValue);
const httpPort = validatePort(httpPortValue);
if (!dnsPort) {
showErrorMessage('DNS端口号无效必须是1-65535之间的整数');
return null;
}
if (!httpPort) {
showErrorMessage('HTTP端口号无效必须是1-65535之间的整数');
return null;
}
// 安全获取上游服务器列表
const upstreamServersText = getElementValue('dns-upstream-servers');
const upstreamServers = upstreamServersText ?
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
[];
// 安全获取并转换整数值
const timeoutValue = getElementValue('dns-timeout');
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
const saveIntervalValue = getElementValue('dns-save-interval');
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
const updateIntervalValue = getElementValue('shield-update-interval');
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
return {
DNSServer: {
Port: parseInt(document.getElementById('dns-port')?.value) || 53,
UpstreamServers: document.getElementById('dns-upstream-servers')?.value.split(',').map(s => s.trim()).filter(Boolean) || [],
Timeout: parseInt(document.getElementById('dns-timeout')?.value) || 5,
StatsFile: document.getElementById('dns-stats-file')?.value || './data/stats.json',
SaveInterval: parseInt(document.getElementById('dns-save-interval')?.value) || 300
Port: dnsPort,
UpstreamServers: upstreamServers,
Timeout: timeout,
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
SaveInterval: saveInterval
},
HTTPServer: {
Port: parseInt(document.getElementById('http-port')?.value) || 8080
Port: httpPort
},
Shield: {
LocalRulesFile: document.getElementById('shield-local-rules-file')?.value || './data/rules.txt',
UpdateInterval: parseInt(document.getElementById('shield-update-interval')?.value) || 3600,
HostsFile: document.getElementById('shield-hosts-file')?.value || './data/hosts.txt',
BlockMethod: document.getElementById('shield-block-method')?.value || '0.0.0.0'
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
UpdateInterval: updateInterval,
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
BlockMethod: getElementValue('shield-block-method') || '0.0.0.0'
}
};
}
// 工具函数:安全获取元素值
function getElementValue(elementId) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
return element.value;
}
return ''; // 默认返回空字符串
}
// 设置事件监听器
function setupConfigEventListeners() {
// 保存配置按钮
document.getElementById('save-config-btn')?.addEventListener('click', handleSaveConfig);
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
// 重启服务按钮
document.getElementById('restart-service-btn')?.addEventListener('click', handleRestartService);
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
}
// 显示加载状态
function showLoading(show) {
const loadingElement = document.getElementById('loading-overlay');
if (show) {
if (!loadingElement) {
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
color: white;
font-size: 18px;
`;
overlay.innerHTML = '<div>处理中...</div>';
document.body.appendChild(overlay);
}
} else {
loadingElement?.remove();
}
}
// 显示成功消息
@@ -112,13 +246,28 @@ function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式
// 设置通知样式兼容Tailwind和原生CSS
notification.style.cssText += `
position: fixed;
bottom: 16px;
right: 16px;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
transition: all 0.3s ease;
opacity: 0;
`;
if (type === 'success') {
notification.classList.add('bg-green-500', 'text-white');
notification.style.backgroundColor = '#10b981';
notification.style.color = 'white';
} else if (type === 'error') {
notification.classList.add('bg-red-500', 'text-white');
notification.style.backgroundColor = '#ef4444';
notification.style.color = 'white';
} else {
notification.classList.add('bg-blue-500', 'text-white');
notification.style.backgroundColor = '#3b82f6';
notification.style.color = 'white';
}
notification.textContent = message;
@@ -126,14 +275,12 @@ function showNotification(message, type = 'info') {
// 显示通知
setTimeout(() => {
notification.classList.remove('opacity-0');
notification.classList.add('opacity-100');
notification.style.opacity = '1';
}, 10);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0');
notification.style.opacity = '0';
setTimeout(() => {
notification.remove();
}, 300);

View File

@@ -948,22 +948,28 @@ function updateTopBlockedTable(domains) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ name: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' }
{ 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 }
];
console.log('使用示例数据填充Top屏蔽域名表格');
}
let html = '';
for (const domain of tableData) {
for (let i = 0; i < tableData.length && i < 5; i++) {
const domain = tableData[i];
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name}</td>
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
</tr>
<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>
<span class="font-medium truncate">${domain.name}</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
</div>
`;
}
@@ -981,7 +987,8 @@ function updateRecentBlockedTable(domains) {
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.name || item.domain || item[0] || '未知',
timestamp: item.timestamp || item.time || Date.now()
timestamp: item.timestamp || item.time || Date.now(),
type: item.type || '广告'
}));
}
@@ -989,23 +996,27 @@ function updateRecentBlockedTable(domains) {
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ name: '---', timestamp: now - 5 * 60 * 1000 },
{ name: '---', timestamp: now - 15 * 60 * 1000 },
{ name: '---', timestamp: now - 30 * 60 * 1000 },
{ name: '---', timestamp: now - 45 * 60 * 1000 },
{ name: '---', timestamp: now - 60 * 60 * 1000 }
{ 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: '恶意' }
];
console.log('使用示例数据填充最近屏蔽域名表格');
}
let html = '';
for (const domain of tableData) {
for (let i = 0; i < tableData.length && i < 5; i++) {
const domain = tableData[i];
const time = formatTime(domain.timestamp);
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name}</td>
<td class="py-3 px-4 text-sm text-right text-gray-500">${time}</td>
</tr>
<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>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">${domain.type}</span>
</div>
`;
}
@@ -1036,22 +1047,28 @@ function updateTopClientsTable(clients) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ ip: '---', count: '---' },
{ ip: '---', count: '---' },
{ ip: '---', count: '---' },
{ ip: '---', count: '---' },
{ ip: '---', count: '---' }
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' }
];
console.log('使用示例数据填充TOP客户端表格');
}
let html = '';
for (const client of tableData) {
for (let i = 0; i < tableData.length && i < 5; i++) {
const client = tableData[i];
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${client.ip}</td>
<td class="py-3 px-4 text-sm text-right">${formatNumber(client.count)}</td>
</tr>
<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>
<span class="font-medium truncate">${client.ip}</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
</div>
`;
}

View File

@@ -50,13 +50,74 @@ function setupNavigation() {
// 移动端侧边栏切换
const toggleSidebar = document.getElementById('toggle-sidebar');
const closeSidebarBtn = document.getElementById('close-sidebar');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
if (toggleSidebar && sidebar) {
toggleSidebar.addEventListener('click', () => {
sidebar.classList.toggle('-translate-x-full');
});
// 打开侧边栏函数
function openSidebar() {
if (sidebar) {
sidebar.classList.remove('-translate-x-full');
}
if (sidebarOverlay) {
sidebarOverlay.classList.remove('hidden');
}
// 防止页面滚动
document.body.style.overflow = 'hidden';
}
// 关闭侧边栏函数
function closeSidebar() {
if (sidebar) {
sidebar.classList.add('-translate-x-full');
}
if (sidebarOverlay) {
sidebarOverlay.classList.add('hidden');
}
// 恢复页面滚动
document.body.style.overflow = '';
}
// 切换侧边栏函数
function toggleSidebarVisibility() {
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
openSidebar();
} else {
closeSidebar();
}
}
// 绑定切换按钮事件
if (toggleSidebar) {
toggleSidebar.addEventListener('click', openSidebar);
}
// 绑定关闭按钮事件
if (closeSidebarBtn) {
closeSidebarBtn.addEventListener('click', closeSidebar);
}
// 绑定遮罩层点击事件
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 移动端点击菜单项后自动关闭侧边栏
menuItems.forEach(item => {
item.addEventListener('click', () => {
// 检查是否是移动设备视图
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
// 添加键盘事件监听按ESC键关闭侧边栏
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSidebar();
}
});
}
// 初始化函数