实现日志功能

This commit is contained in:
Alex Yang
2025-11-30 02:25:36 +08:00
32 changed files with 6816 additions and 5666 deletions

488
static/api/css/style.css Normal file
View File

@@ -0,0 +1,488 @@
/* 基础样式 */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #ffffff;
color: #333333;
}
/* 默认浅色主题样式 */
.swagger-ui .topbar {
background-color: #2c3e50;
padding: 15px 0;
}
.swagger-ui .topbar .topbar-wrapper .link {
color: #ecf0f1;
font-size: 1.2rem;
}
.swagger-ui .info {
margin: 20px 0;
}
.swagger-ui .info .title {
font-size: 2rem;
margin-bottom: 10px;
color: #333;
}
.swagger-ui .info .description {
font-size: 1rem;
color: #555;
margin-bottom: 15px;
}
/* 修复服务器URL输入框样式 */
.swagger-ui .servers li input[type="text"] {
padding: 8px 12px;
width: 100%;
box-sizing: border-box;
}
/* 修复服务器选择区域的背景颜色和布局 */
.swagger-ui .servers {
padding: 16px;
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保服务器列表容器有正确的背景色和布局 */
.swagger-ui .servers-wrapper {
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保整个顶部区域颜色一致和布局正确 */
.swagger-ui .info {
margin: 0;
padding: 20px 16px;
width: 100%;
box-sizing: border-box;
}
/* 确保顶部主容器颜色一致和布局正确 */
.swagger-ui {
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 确保API信息区域颜色一致和布局正确 */
.swagger-ui .info-container {
width: 100%;
box-sizing: border-box;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 修复服务器URL输入框深色模式样式 */
body.dark-mode .swagger-ui .servers li input[type="text"] {
background-color: #1a202c !important;
color: #ffffff !important;
border-color: #4a5568 !important;
padding: 8px 12px !important;
width: 100% !important;
}
/* 修复服务器选择区域的深色模式背景颜色和布局 */
body.dark-mode .swagger-ui .servers {
background-color: #1a202c !important;
border: none !important;
padding: 16px !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保服务器列表容器在深色模式下也有正确的背景色和布局 */
body.dark-mode .swagger-ui .servers-wrapper {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保整个顶部区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info {
background-color: #1a202c !important;
margin: 0 !important;
padding: 20px 16px !important;
border-bottom: 1px solid #4a5568 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保顶部主容器在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 确保API信息区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info-container {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下内容区域的布局问题 */
body.dark-mode .swagger-ui .wrapper {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下API操作块的布局 */
body.dark-mode .swagger-ui .opblock {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下过滤器的布局 */
body.dark-mode .swagger-ui .filter {
width: 100% !important;
box-sizing: border-box !important;
padding: 16px !important;
margin: 0 !important;
}
/* 修复深色模式下顶部栏布局 */
body.dark-mode .swagger-ui .topbar {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 15px 0 !important;
}
/* 修复深色模式下顶部栏包装器布局 */
body.dark-mode .swagger-ui .topbar .topbar-wrapper {
width: 100% !important;
box-sizing: border-box !important;
padding: 0 16px !important;
}
/* 修复深色模式下响应容器布局 */
body.dark-mode .swagger-ui .responses-inner {
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下操作块摘要布局 */
body.dark-mode .swagger-ui .opblock-summary {
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保深色模式下所有容器元素都使用box-sizing */
body.dark-mode * {
box-sizing: border-box !important;
}
/* 增强标签标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
}
/* 增强标签标题h3深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag.h3 {
color: #ffffff !important;
background-color: #2d3748 !important;
}
/* 增强标签部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag-section {
background-color: #2d3748 !important;
padding: 16px !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
}
/* 增强API描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-description-wrapper {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-description-wrapper p {
color: #ffffff !important;
line-height: 1.5 !important;
}
/* 增强stats标签描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-summary-description {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 增强操作块标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-title_normal h4 {
color: #ffffff !important;
font-weight: 600 !important;
}
/* 增强参数部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-body {
background-color: #2d3748 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff !important;
font-weight: 600 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description,
body.dark-mode .swagger-ui .parameters-col_name,
body.dark-mode .swagger-ui .parameters-col_type {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description p,
body.dark-mode .swagger-ui .parameters-col_name p,
body.dark-mode .swagger-ui .parameters-col_type p {
color: #ffffff !important;
}
/* 新增适配API文档展开界面的所有文字元素 */
body.dark-mode .swagger-ui .opblock-body {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options .body-param-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner h4 {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-code {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-name {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .required {
color: #ffffff;
}
body.dark-mode .swagger-ui .scroll-to-top {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-tag-section {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
body.dark-mode .swagger-ui .auth-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-list {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item label {
color: #ffffff;
}
/* 确保代码块内的文字也清晰可见 */
body.dark-mode .swagger-ui pre {
color: #ffffff;
}
body.dark-mode .swagger-ui code {
color: #ffffff;
}
/* 确保所有表单元素的文字颜色正确 */
body.dark-mode .swagger-ui form {
color: #ffffff;
}
body.dark-mode .swagger-ui form label {
color: #ffffff;
}
body.dark-mode .swagger-ui select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
/* 适配可能的嵌套内容 */
body.dark-mode .swagger-ui .opblock-body .schema {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .title {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .required {
color: #ffffff;
}
/* 适配可能的按钮组 */
body.dark-mode .swagger-ui .btn-group {
color: #ffffff;
}
/* 适配可能的标签 */
body.dark-mode .swagger-ui .tag {
color: #ffffff;
}
/* 适配可能的警告和提示信息 */
body.dark-mode .swagger-ui .warning {
color: #ffffff;
}
body.dark-mode .swagger-ui .hint {
color: #ffffff;
}
/* 适配可能的表格内容 */
body.dark-mode .swagger-ui table {
color: #ffffff;
}
body.dark-mode .swagger-ui table th {
color: #ffffff;
}
body.dark-mode .swagger-ui table td {
color: #ffffff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.topbar-controls {
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.theme-toggle-btn {
padding: 6px 10px;
font-size: 12px;
}
.theme-toggle-btn span {
display: none;
}
}

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

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DNS Server API 文档</title>
<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/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>

1333
static/api/js/index.js Normal file

File diff suppressed because it is too large Load Diff

62
static/css/animation.css Normal file
View File

@@ -0,0 +1,62 @@
@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;
}
}
/* 服务器状态组件光晕效果 */
.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;
}

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); }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,33 +20,32 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
options.body = JSON.stringify(data);
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, 10000); // 10秒超时
});
try {
const response = await fetch(url, options);
// 竞争:请求或超时
const response = await Promise.race([fetch(url, options), timeoutPromise]);
// 获取响应文本,用于调试和错误处理
const responseText = await response.text();
if (!response.ok) {
// 尝试解析错误响应
let errorData = {};
// 优化错误响应处理
console.warn(`API请求失败: ${response.status}`);
// 尝试解析JSON但如果失败直接使用原始文本作为错误信息
try {
// 首先检查响应文本是否为空或不是有效JSON
if (!responseText || responseText.trim() === '') {
console.warn('错误响应为空');
} else {
try {
errorData = JSON.parse(responseText);
} catch (parseError) {
console.error('无法解析错误响应为JSON:', parseError);
console.error('原始错误响应文本:', responseText);
}
}
// 直接返回错误信息,而不是抛出异常,让上层处理
console.warn(`API请求失败: ${response.status}`, errorData);
return { error: errorData.error || `请求失败: ${response.status}` };
} catch (e) {
console.error('处理错误响应时出错:', e);
return { error: `请求处理失败: ${e.message}` };
const errorData = JSON.parse(responseText);
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
} catch (parseError) {
// 当响应不是有效的JSON时如中文错误信息直接使用原始文本
console.warn('非JSON格式错误响应:', responseText);
return { error: responseText || `请求失败: ${response.status}` };
}
}
@@ -55,12 +54,18 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// 首先检查响应文本是否为空
if (!responseText || responseText.trim() === '') {
console.warn('空响应文本');
return {};
return null; // 返回null表示空响应
}
// 尝试解析JSON
const parsedData = JSON.parse(responseText);
// 检查解析后的数据是否有效
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
console.warn('解析后的数据为空');
return null;
}
// 限制所有数字为两位小数
const formatNumbers = (obj) => {
if (typeof obj === 'number') {
@@ -93,13 +98,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
console.error('位置66附近的字符:', responseText.substring(60, 75));
}
// 返回空数组作为默认值,避免页面功能完全中断
console.warn('使用默认空数组作为响应');
return [];
// 返回错误对象,让上层处理
return { error: 'JSON解析错误' };
}
} catch (error) {
console.error('API请求错误:', error);
throw error;
// 返回错误对象,而不是抛出异常,让上层处理
return { error: error.message };
}
}
@@ -120,6 +125,12 @@ const api = {
// 获取最近屏蔽域名
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
// 获取TOP客户端
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
// 获取TOP域名
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
// 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),

View File

@@ -197,12 +197,25 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
// 数字格式化函数
function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
return fullNumStr;
}
// 确认对话框函数

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,93 +36,183 @@ function initConfigPage() {
// 加载系统配置
async function loadConfig() {
try {
const config = await api.getConfig();
populateConfigForm(config);
const result = await api.getConfig();
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('加载配置失败: ' + result.error);
return;
}
populateConfigForm(result);
} catch (error) {
showErrorMessage('加载配置失败: ' + error.message);
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
}
}
// 填充配置表单
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;
document.getElementById('http-host')?.value = config.HTTPServer.Host || '0.0.0.0';
document.getElementById('http-api-enabled')?.checked = config.HTTPServer.APIEnabled !== false;
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
// 屏蔽配置
document.getElementById('shield-local-rules-file')?.value = config.Shield.LocalRulesFile || './rules.txt';
document.getElementById('shield-remote-rules-urls')?.value = (config.Shield.RemoteRulesURLs || []).join('\n');
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'));
// 使用服务器端接受的屏蔽方法值默认使用NXDOMAIN
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
}
// 工具函数:安全设置元素值
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 {
await api.saveConfig(formData);
const result = await api.saveConfig(formData);
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('保存配置失败: ' + result.error);
return;
}
showSuccessMessage('配置保存成功');
} catch (error) {
showErrorMessage('保存配置失败: ' + error.message);
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
}
}
// 重启服务
async function handleRestartService() {
if (confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) {
try {
await api.restartService();
showSuccessMessage('服务重启成功');
} catch (error) {
showErrorMessage('重启服务失败: ' + error.message);
if (!confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) return;
try {
const result = await api.restartService();
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('服务重启失败: ' + result.error);
return;
}
showSuccessMessage('服务重启成功');
} catch (error) {
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
}
}
// 收集表单数据
// 收集表单数据并验证
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 || './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,
Host: document.getElementById('http-host')?.value || '0.0.0.0',
APIEnabled: document.getElementById('http-api-enabled')?.checked !== false
Port: httpPort
},
Shield: {
LocalRulesFile: document.getElementById('shield-local-rules-file')?.value || './rules.txt',
RemoteRulesURLs: document.getElementById('shield-remote-rules-urls')?.value.split('\n').map(s => s.trim()).filter(Boolean) || [],
UpdateInterval: parseInt(document.getElementById('shield-update-interval')?.value) || 3600,
HostsFile: document.getElementById('shield-hosts-file')?.value || '/etc/hosts',
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') || 'NXDOMAIN'
}
};
}
// 工具函数:安全获取元素值
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 showSuccessMessage(message) {
showNotification(message, 'success');
@@ -118,13 +235,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;
@@ -132,14 +264,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);

File diff suppressed because it is too large Load Diff

View File

@@ -2,94 +2,140 @@
// 初始化Hosts管理页面
function initHostsPage() {
loadHostsContent();
// 加载Hosts规则
loadHostsRules();
// 设置事件监听器
setupHostsEventListeners();
}
// 加载Hosts内容
async function loadHostsContent() {
// 加载Hosts规则
async function loadHostsRules() {
try {
const hostsContent = await api.getHosts();
document.getElementById('hosts-content').value = hostsContent;
const response = await fetch('/api/shield/hosts');
if (!response.ok) {
throw new Error('Failed to load hosts rules');
}
const data = await response.json();
// 处理API返回的数据格式
let hostsRules = [];
if (data && Array.isArray(data)) {
// 直接是数组格式
hostsRules = data;
} else if (data && data.hosts) {
// 包含在hosts字段中
hostsRules = data.hosts;
}
updateHostsTable(hostsRules);
} catch (error) {
showErrorMessage('加载Hosts文件失败: ' + error.message);
console.error('Error loading hosts rules:', error);
showErrorMessage('加载Hosts规则失败');
}
}
// 保存Hosts内容
async function handleSaveHosts() {
const hostsContent = document.getElementById('hosts-content').value;
// 更新Hosts表格
function updateHostsTable(hostsRules) {
const tbody = document.getElementById('hosts-table-body');
try {
await api.saveHosts(hostsContent);
showSuccessMessage('Hosts文件保存成功');
} catch (error) {
showErrorMessage('保存Hosts文件失败: ' + error.message);
}
}
// 刷新Hosts
async function handleRefreshHosts() {
try {
await api.refreshHosts();
showSuccessMessage('Hosts刷新成功');
loadHostsContent();
} catch (error) {
showErrorMessage('刷新Hosts失败: ' + error.message);
}
}
// 添加新的Hosts条目
function handleAddHostsEntry() {
const ipInput = document.getElementById('hosts-ip');
const domainInput = document.getElementById('hosts-domain');
const ip = ipInput.value.trim();
const domain = domainInput.value.trim();
if (!ip || !domain) {
showErrorMessage('IP和域名不能为空');
if (hostsRules.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
return;
}
// 简单的IP验证
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(ip)) {
showErrorMessage('请输入有效的IP地址');
return;
}
tbody.innerHTML = hostsRules.map(rule => {
// 处理对象格式的规则
const ip = rule.ip || '';
const domain = rule.domain || '';
return `
<tr class="border-b border-gray-200">
<td class="py-3 px-4">${ip}</td>
<td class="py-3 px-4">${domain}</td>
<td class="py-3 px-4 text-right">
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`;
}).join('');
const hostsTextarea = document.getElementById('hosts-content');
const newEntry = `\n${ip} ${domain}`;
hostsTextarea.value += newEntry;
// 清空输入框
ipInput.value = '';
domainInput.value = '';
// 滚动到文本区域底部
hostsTextarea.scrollTop = hostsTextarea.scrollHeight;
showSuccessMessage('Hosts条目已添加到编辑器');
// 重新绑定删除事件
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteHostsRule);
});
}
// 设置事件监听器
function setupHostsEventListeners() {
// 保存按钮
document.getElementById('save-hosts-btn')?.addEventListener('click', handleSaveHosts);
// 保存Hosts按钮
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
}
// 处理添加Hosts规则
async function handleAddHostsRule() {
const ip = document.getElementById('hosts-ip').value.trim();
const domain = document.getElementById('hosts-domain').value.trim();
// 刷新按钮
document.getElementById('refresh-hosts-btn')?.addEventListener('click', handleRefreshHosts);
if (!ip || !domain) {
showErrorMessage('IP地址和域名不能为空');
return;
}
// 添加Hosts条目按钮
document.getElementById('add-hosts-entry-btn')?.addEventListener('click', handleAddHostsEntry);
// 按回车键添加条目
document.getElementById('hosts-domain')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleAddHostsEntry();
try {
const response = await fetch('/api/shield/hosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ip, domain })
});
if (!response.ok) {
throw new Error('Failed to add hosts rule');
}
});
showSuccessMessage('Hosts规则添加成功');
// 清空输入框
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error adding hosts rule:', error);
showErrorMessage('添加Hosts规则失败');
}
}
// 处理删除Hosts规则
async function handleDeleteHostsRule(e) {
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
try {
const response = await fetch('/api/shield/hosts', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
});
if (!response.ok) {
throw new Error('Failed to delete hosts rule');
}
showSuccessMessage('Hosts规则删除成功');
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error deleting hosts rule:', error);
showErrorMessage('删除Hosts规则失败');
}
}
// 显示成功消息
@@ -102,6 +148,8 @@ function showErrorMessage(message) {
showNotification(message, 'error');
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
@@ -112,7 +160,7 @@ 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`;
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式
if (type === 'success') {
@@ -123,18 +171,22 @@ function showNotification(message, type = 'info') {
notification.classList.add('bg-blue-500', 'text-white');
}
notification.textContent = message;
notification.innerHTML = `
<div class="flex items-center space-x-2">
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.classList.remove('opacity-0');
notification.classList.add('opacity-100');
}, 10);
}, 100);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0');
setTimeout(() => {
notification.remove();
@@ -147,4 +199,4 @@ if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHostsPage);
} else {
initHostsPage();
}
}

View File

@@ -8,7 +8,6 @@ function setupNavigation() {
document.getElementById('dashboard-content'),
document.getElementById('shield-content'),
document.getElementById('hosts-content'),
document.getElementById('blacklists-content'),
document.getElementById('query-content'),
document.getElementById('config-content')
];
@@ -16,40 +15,105 @@ function setupNavigation() {
menuItems.forEach((item, index) => {
item.addEventListener('click', (e) => {
e.preventDefault();
// 允许浏览器自动更新地址栏中的hash不阻止默认行为
// 更新活跃状态
menuItems.forEach(menuItem => {
menuItem.classList.remove('sidebar-item-active');
});
item.classList.add('sidebar-item-active');
// 隐藏所有内容部分
contentSections.forEach(section => {
section.classList.add('hidden');
});
// 显示对应内容部分
const target = item.getAttribute('href').substring(1);
const activeContent = document.getElementById(`${target}-content`);
if (activeContent) {
activeContent.classList.remove('hidden');
// 移动端点击菜单项后自动关闭侧边栏
if (window.innerWidth < 768) {
closeSidebar();
}
// 更新页面标题
pageTitle.textContent = item.querySelector('span').textContent;
// 页面特定初始化 - 保留这部分逻辑因为它不会与hashchange事件处理逻辑冲突
const target = item.getAttribute('href').substring(1);
if (target === 'shield' && typeof initShieldPage === 'function') {
initShieldPage();
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
initHostsPage();
}
});
});
// 移动端侧边栏切换
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() {
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('block');
}
// 防止页面滚动
document.body.style.overflow = 'hidden';
console.log('Sidebar opened successfully');
}
// 关闭侧边栏函数
function closeSidebar() {
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('block');
}
// 恢复页面滚动
document.body.style.overflow = '';
console.log('Sidebar closed successfully');
}
// 切换侧边栏函数
function toggleSidebarVisibility() {
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.addEventListener('click', toggleSidebarVisibility);
}
// 绑定关闭按钮事件
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();
}
});
}
// 初始化函数

View File

@@ -1,314 +1,231 @@
// DNS查询工具页面功能实现
// DNS查询页面功能实现
// 初始化查询工具页面
// 初始化查询页面
function initQueryPage() {
console.log('初始化DNS查询页面...');
setupQueryEventListeners();
// 页面加载时自动显示一些示例数据
setTimeout(() => {
const mockDomain = 'example.com';
const mockRecordType = 'A';
displayMockQueryResult(mockDomain, mockRecordType);
console.log('显示示例DNS查询数据');
}, 500);
loadQueryHistory();
}
// 执行DNS查询
async function handleDNSQuery() {
// 尝试多种可能的DOM元素ID
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input');
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type');
const domainInput = document.getElementById('dns-query-domain');
const resultDiv = document.getElementById('query-result');
console.log('DOM元素查找结果:', { domainInput, recordTypeSelect, resultDiv });
if (!domainInput || !recordTypeSelect || !resultDiv) {
if (!domainInput || !resultDiv) {
console.error('找不到必要的DOM元素');
return;
}
const domain = domainInput.value.trim();
const recordType = recordTypeSelect.value;
if (!domain) {
showErrorMessage('请输入域名');
return;
}
console.log(`执行DNS查询: 域名=${domain}, 记录类型=${recordType}`);
// 清空之前的结果
resultDiv.innerHTML = '<div class="text-center py-4"><svg class="animate-spin mx-auto h-6 w-6 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> 查询中...</div>';
try {
// 检查api对象是否存在
if (!window.api || typeof window.api.queryDNS !== 'function') {
console.warn('api.queryDNS不存在使用模拟数据');
const mockResult = generateMockDNSResult(domain, recordType);
displayQueryResult(mockResult, domain, recordType);
return;
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
if (!response.ok) {
throw new Error('查询失败');
}
// 调用API适配不同的参数格式
let result;
try {
// 尝试不同的API调用方式
if (api.queryDNS.length === 1) {
result = await api.queryDNS({ domain, recordType });
} else {
result = await api.queryDNS(domain, recordType);
}
} catch (apiError) {
console.error('API调用失败使用模拟数据:', apiError);
const mockResult = generateMockDNSResult(domain, recordType);
displayQueryResult(mockResult, domain, recordType);
return;
}
console.log('DNS查询API返回结果:', result);
// 处理API返回的数据
if (!result || (Array.isArray(result) && result.length === 0) ||
(typeof result === 'object' && Object.keys(result).length === 0)) {
console.log('API返回空结果使用模拟数据');
const mockResult = generateMockDNSResult(domain, recordType);
displayQueryResult(mockResult, domain, recordType);
} else {
displayQueryResult(result, domain, recordType);
}
const result = await response.json();
displayQueryResult(result, domain);
saveQueryHistory(domain, result);
loadQueryHistory();
} catch (error) {
console.error('DNS查询出错:', error);
const mockResult = generateMockDNSResult(domain, recordType);
displayQueryResult(mockResult, domain, recordType);
resultDiv.innerHTML += `<div class="text-yellow-500 text-center py-2 text-sm">注意: 显示的是模拟数据</div>`;
showErrorMessage('查询失败,请稍后重试');
}
}
// 显示查询结果
function displayQueryResult(result, domain, recordType) {
const resultDiv = document.getElementById('query-result');
// 适配不同的数据结构
let records = [];
if (Array.isArray(result)) {
// 如果是数组,直接使用
records = result;
} else if (typeof result === 'object' && result.length === undefined) {
// 如果是对象,尝试转换为数组
if (result.records) {
records = result.records;
} else if (result.data) {
records = result.data;
} else {
// 尝试将对象转换为记录数组
records = [result];
}
}
// 创建结果表格
let html = `
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-800 mb-2">查询结果: ${domain} (${recordType})</h3>
<p class="text-sm text-gray-500 mb-3">查询时间: ${new Date().toLocaleString()}</p>
<div class="overflow-x-auto">
<table class="min-w-full bg-white rounded-lg overflow-hidden shadow-sm">
<thead class="bg-gray-50">
<tr>
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">值</th>
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">TTL</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
`;
if (records.length === 0) {
html += `
<tr>
<td colspan="3" class="py-4 text-center text-gray-500">未找到 ${domain}${recordType} 记录</td>
</tr>
`;
} else {
// 添加查询结果
records.forEach(record => {
const type = record.Type || record.type || recordType;
// 处理不同格式的值
let value;
if (record.Value) {
value = record.Value;
} else if (record.ip || record.address) {
value = record.ip || record.address;
} else if (record.target) {
value = record.target;
} else if (record.text) {
value = record.text;
} else if (record.name) {
value = record.name;
} else {
value = JSON.stringify(record);
}
// 格式化不同类型的记录值
if (type === 'MX' && (record.Preference || record.priority)) {
value = `${record.Preference || record.priority} ${value}`;
} else if (type === 'SRV') {
if (record.Priority && record.Weight && record.Port) {
value = `${record.Priority} ${record.Weight} ${record.Port} ${value}`;
}
}
const ttl = record.TTL || record.ttl || '-';
html += `
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-3 px-4 text-sm font-medium text-gray-900">${type}</td>
<td class="py-3 px-4 text-sm text-gray-900 font-mono break-all">${value}</td>
<td class="py-3 px-4 text-sm text-gray-500">${ttl}</td>
</tr>
`;
});
}
html += `
</tbody>
</table>
</div>
</div>
`;
resultDiv.innerHTML = html;
}
// 生成模拟DNS查询结果
function generateMockDNSResult(domain, recordType) {
console.log('生成模拟DNS结果:', domain, recordType);
const mockData = {
'A': [
{ Type: 'A', Value: '192.168.1.1', TTL: 300 },
{ Type: 'A', Value: '192.168.1.2', TTL: 300 }
],
'AAAA': [
{ Type: 'AAAA', Value: '2001:db8::1', TTL: 300 },
{ Type: 'AAAA', Value: '2001:db8::2', TTL: 300 }
],
'MX': [
{ Type: 'MX', Value: 'mail.' + domain, Preference: 10, TTL: 3600 },
{ Type: 'MX', Value: 'mail2.' + domain, Preference: 20, TTL: 3600 }
],
'NS': [
{ Type: 'NS', Value: 'ns1.' + domain, TTL: 86400 },
{ Type: 'NS', Value: 'ns2.' + domain, TTL: 86400 }
],
'CNAME': [
{ Type: 'CNAME', Value: 'www.' + domain, TTL: 300 }
],
'TXT': [
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + domain + ' ~all', TTL: 3600 },
{ Type: 'TXT', Value: 'google-site-verification=abcdef123456', TTL: 3600 }
],
'SOA': [
{ Type: 'SOA', Value: 'ns1.' + domain + ' admin.' + domain + ' 1 3600 1800 604800 86400', TTL: 86400 }
]
};
return mockData[recordType] || [
{ Type: recordType, Value: 'No records found', TTL: '-' }
];
}
// 显示模拟查询结果
function displayMockQueryResult(domain, recordType) {
function displayQueryResult(result, domain) {
const resultDiv = document.getElementById('query-result');
if (!resultDiv) return;
// 显示提示信息
resultDiv.innerHTML = `
<div class="p-4 bg-blue-50 border border-blue-100 rounded-lg">
<div class="flex items-start">
<svg class="h-5 w-5 text-blue-500 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<div>
<p class="text-sm text-blue-700">这是一个DNS查询工具示例。输入域名并选择记录类型然后点击查询按钮获取DNS记录信息。</p>
// 显示结果容器
resultDiv.classList.remove('hidden');
// 解析结果
const status = result.blocked ? '被屏蔽' : '正常';
const statusClass = result.blocked ? 'text-danger' : 'text-success';
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
const timestamp = new Date(result.timestamp).toLocaleString();
// 更新结果显示
document.getElementById('result-domain').textContent = domain;
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
document.getElementById('result-type').textContent = blockType;
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
let blockRuleElement = document.getElementById('result-block-rule');
if (!blockRuleElement) {
// 创建屏蔽规则显示区域
const grid = resultDiv.querySelector('.grid');
if (grid) {
const newGridItem = document.createElement('div');
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
newGridItem.innerHTML = `
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
<p class="text-lg font-semibold" id="result-block-rule">-</p>
`;
grid.appendChild(newGridItem);
blockRuleElement = document.getElementById('result-block-rule');
}
}
// 更新屏蔽规则显示
if (blockRuleElement) {
blockRuleElement.textContent = blockRule;
}
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
let blockSourceElement = document.getElementById('result-block-source');
if (!blockSourceElement) {
// 创建屏蔽来源显示区域
const grid = resultDiv.querySelector('.grid');
if (grid) {
const newGridItem = document.createElement('div');
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
newGridItem.innerHTML = `
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
<p class="text-lg font-semibold" id="result-block-source">-</p>
`;
grid.appendChild(newGridItem);
blockSourceElement = document.getElementById('result-block-source');
}
}
// 更新屏蔽来源显示
if (blockSourceElement) {
blockSourceElement.textContent = blockSource;
}
document.getElementById('result-time').textContent = timestamp;
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
}
// 保存查询历史
function saveQueryHistory(domain, result) {
// 获取现有历史记录
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
// 创建历史记录项
const historyItem = {
domain: domain,
timestamp: new Date().toISOString(),
result: {
blocked: result.blocked,
blockRuleType: result.blockRuleType,
blockRule: result.blockRule,
blocksource: result.blocksource
}
};
// 添加到历史记录开头
history.unshift(historyItem);
// 限制历史记录数量
if (history.length > 20) {
history = history.slice(0, 20);
}
// 保存到本地存储
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
}
// 加载查询历史
function loadQueryHistory() {
const historyDiv = document.getElementById('query-history');
if (!historyDiv) return;
// 获取历史记录
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
if (history.length === 0) {
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
return;
}
// 生成历史记录HTML
const historyHTML = history.map(item => {
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
const statusText = item.result.blocked ? '被屏蔽' : '正常';
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
const blockRule = item.result.blocked ? item.result.blockRule : '无';
const blockSource = item.result.blocked ? item.result.blocksource : '无';
const formattedTime = new Date(item.timestamp).toLocaleString();
return `
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-1">
<div class="flex items-center space-x-2">
<span class="font-medium">${item.domain}</span>
<span class="${statusClass} text-sm">${statusText}</span>
<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">来源: ${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}')">
<i class="fa fa-refresh mr-1"></i>重新查询
</button>
</div>
</div>
`;
`;
}).join('');
historyDiv.innerHTML = historyHTML;
}
// 从历史记录重新查询
function requeryFromHistory(domain) {
const domainInput = document.getElementById('dns-query-domain');
if (domainInput) {
domainInput.value = domain;
handleDNSQuery();
}
}
// 清空查询历史
function clearQueryHistory() {
if (confirm('确定要清空所有查询历史吗?')) {
localStorage.removeItem('dnsQueryHistory');
loadQueryHistory();
showSuccessMessage('查询历史已清空');
}
}
// 设置事件监听器
function setupQueryEventListeners() {
// 尝试多种可能的按钮ID
const queryButtons = [
document.getElementById('query-btn'),
document.getElementById('query-button'),
document.querySelector('button[type="submit"]'),
...Array.from(document.querySelectorAll('button')).filter(btn =>
btn.textContent && btn.textContent.includes('查询')
)
].filter(Boolean);
// 查询按钮事件
const queryBtn = document.getElementById('dns-query-btn');
if (queryBtn) {
queryBtn.addEventListener('click', handleDNSQuery);
}
// 绑定查询按钮事件
queryButtons.forEach(button => {
console.log('绑定查询按钮事件:', button);
button.addEventListener('click', handleDNSQuery);
});
// 尝试多种可能的输入框ID
const domainInputs = [
document.getElementById('query-domain'),
document.getElementById('domain-input'),
document.querySelector('input[id*="domain"]')
].filter(Boolean);
// 绑定回车键事件
domainInputs.forEach(input => {
console.log('绑定输入框回车事件:', input);
input.addEventListener('keypress', (e) => {
// 输入框回车键事件
const domainInput = document.getElementById('dns-query-domain');
if (domainInput) {
domainInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleDNSQuery();
}
});
});
}
// 添加示例域名按钮
const querySection = document.querySelector('#dns-query-section, #query-section');
if (querySection) {
const exampleContainer = document.createElement('div');
exampleContainer.className = 'mt-3';
exampleContainer.innerHTML = `
<p class="text-sm text-gray-500 mb-2">快速示例:</p>
<div class="flex flex-wrap gap-2">
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('example.com', 'A')">example.com (A)</button>
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('example.com', 'MX')">example.com (MX)</button>
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('google.com', 'NS')">google.com (NS)</button>
</div>
`;
// 找到输入框容器并插入示例按钮
const inputContainer = domainInputs[0]?.parentElement;
if (inputContainer && inputContainer.nextElementSibling) {
inputContainer.parentNode.insertBefore(exampleContainer, inputContainer.nextElementSibling);
} else if (querySection.lastChild) {
querySection.appendChild(exampleContainer);
}
// 清空历史按钮事件
const clearHistoryBtn = document.getElementById('clear-history-btn');
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', clearQueryHistory);
}
}
// 设置示例查询
function setExampleQuery(domain, recordType) {
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input');
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type');
if (domainInput) domainInput.value = domain;
if (recordTypeSelect) recordTypeSelect.value = recordType;
// 自动执行查询
handleDNSQuery();
}
// 显示成功消息
function showSuccessMessage(message) {
@@ -330,7 +247,7 @@ 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`;
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式
if (type === 'success') {
@@ -341,7 +258,13 @@ function showNotification(message, type = 'info') {
notification.classList.add('bg-blue-500', 'text-white');
}
notification.textContent = message;
notification.innerHTML = `
<div class="flex items-center space-x-2">
<i class="fa ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// 显示通知
@@ -365,4 +288,14 @@ if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initQueryPage);
} else {
initQueryPage();
}
}
// 当切换到DNS查询页面时重新加载数据
document.addEventListener('DOMContentLoaded', () => {
// 监听hash变化当切换到DNS查询页面时重新加载数据
window.addEventListener('hashchange', () => {
if (window.location.hash === '#query') {
initQueryPage();
}
});
});

View File

@@ -263,12 +263,25 @@ function addGlowEffect() {
// 格式化数字
function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
return fullNumStr;
}
// 在DOM加载完成后初始化

File diff suppressed because it is too large Load Diff

19
static/js/vendor/tailwind.js vendored Normal file
View File

@@ -0,0 +1,19 @@
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'],
},
},
}
}