web异常修复

This commit is contained in:
Alex Yang
2025-11-25 15:17:45 +08:00
parent aea162a616
commit e86c3db45f
10 changed files with 385 additions and 290 deletions

View File

@@ -195,31 +195,23 @@ func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// 返回屏蔽规则的基本配置信息 // 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
// 获取规则统计信息
stats := s.shieldManager.GetStats()
shieldInfo := map[string]interface{}{ shieldInfo := map[string]interface{}{
"updateInterval": s.globalConfig.Shield.UpdateInterval, "updateInterval": s.globalConfig.Shield.UpdateInterval,
"blockMethod": s.globalConfig.Shield.BlockMethod, "blockMethod": s.globalConfig.Shield.BlockMethod,
"blacklistCount": len(s.globalConfig.Shield.Blacklists), "blacklistCount": len(s.globalConfig.Shield.Blacklists),
"domainRulesCount": stats["domainRules"],
"domainExceptionsCount": stats["domainExceptions"],
"regexRulesCount": stats["regexRules"],
"regexExceptionsCount": stats["regexExceptions"],
"hostsRulesCount": stats["hostsRules"],
} }
json.NewEncoder(w).Encode(shieldInfo) json.NewEncoder(w).Encode(shieldInfo)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// 处理远程黑名单管理子路由
if strings.HasPrefix(r.URL.Path, "/shield/blacklists") {
s.handleShieldBlacklists(w, r)
return return
}
switch r.Method {
case http.MethodGet:
// 获取完整规则列表
rules := s.shieldManager.GetRules()
json.NewEncoder(w).Encode(rules)
case http.MethodPost: case http.MethodPost:
// 添加屏蔽规则 // 添加屏蔽规则
var req struct { var req struct {
@@ -237,7 +229,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
} }
json.NewEncoder(w).Encode(map[string]string{"status": "success"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
case http.MethodDelete: case http.MethodDelete:
// 删除屏蔽规则 // 删除屏蔽规则
var req struct { var req struct {
@@ -255,7 +247,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
} }
json.NewEncoder(w).Encode(map[string]string{"status": "success"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
case http.MethodPut: case http.MethodPut:
// 重新加载规则 // 重新加载规则
if err := s.shieldManager.LoadRules(); err != nil { if err := s.shieldManager.LoadRules(); err != nil {
@@ -263,9 +255,10 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
return return
} }
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"}) json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"})
return
default: default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
} }
} }

BIN
output/dns-server Executable file

Binary file not shown.

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "dns-server-console",
"version": "1.0.0",
"description": "DNS服务器Web控制台",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"tailwindcss": "^3.3.3",
"font-awesome": "^4.7.0",
"chart.js": "^4.4.8"
},
"devDependencies": {},
"keywords": ["dns", "server", "console", "web"],
"author": "",
"license": "ISC"
}

View File

@@ -970,22 +970,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
// loadStatsData 从文件加载计数数据 // loadStatsData 从文件加载计数数据
func (m *ShieldManager) loadStatsData() { func (m *ShieldManager) loadStatsData() {
if m.config.StatsFile == "" { if m.config.StatsFile == "" {
logger.Info("Shield统计文件路径未配置跳过加载")
return return
} }
// 检查文件是否存在 // 获取绝对路径以避免工作目录问题
data, err := ioutil.ReadFile(m.config.StatsFile) statsFilePath, err := filepath.Abs(m.config.StatsFile)
if err != nil { if err != nil {
if !os.IsNotExist(err) { logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
logger.Error("读取Shield计数数据文件失败", "error", err) return
}
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
// 检查文件是否存在
fileInfo, err := os.Stat(statsFilePath)
if err != nil {
if os.IsNotExist(err) {
logger.Info("Shield统计文件不存在将创建新文件", "file", statsFilePath)
// 初始化空的计数数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试立即保存一个有效的空文件
m.saveStatsData()
} else {
logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err)
} }
return return
} }
// 检查文件大小
if fileInfo.Size() == 0 {
logger.Warn("Shield统计文件为空将重新初始化", "file", statsFilePath)
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
m.saveStatsData()
return
}
// 读取文件内容
data, err := ioutil.ReadFile(statsFilePath)
if err != nil {
logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err)
return
}
// 检查数据长度
if len(data) == 0 {
logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath)
return
}
// 尝试解析JSON
var statsData ShieldStatsData var statsData ShieldStatsData
err = json.Unmarshal(data, &statsData) err = json.Unmarshal(data, &statsData)
if err != nil { if err != nil {
logger.Error("解析Shield计数数据失败", "error", err) // 记录更详细的错误信息包括数据前50个字符
dataSample := string(data)
if len(dataSample) > 50 {
dataSample = dataSample[:50] + "..."
}
logger.Error("解析Shield计数数据失败",
"file", statsFilePath,
"error", err,
"data_length", len(data),
"data_sample", dataSample)
// 重置为默认空数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试保存一个有效的空文件
m.saveStatsData()
return return
} }
@@ -993,26 +1054,38 @@ func (m *ShieldManager) loadStatsData() {
m.rulesMutex.Lock() m.rulesMutex.Lock()
if statsData.BlockedDomainsCount != nil { if statsData.BlockedDomainsCount != nil {
m.blockedDomainsCount = statsData.BlockedDomainsCount m.blockedDomainsCount = statsData.BlockedDomainsCount
} else {
m.blockedDomainsCount = make(map[string]int)
} }
if statsData.ResolvedDomainsCount != nil { if statsData.ResolvedDomainsCount != nil {
m.resolvedDomainsCount = statsData.ResolvedDomainsCount m.resolvedDomainsCount = statsData.ResolvedDomainsCount
} else {
m.resolvedDomainsCount = make(map[string]int)
} }
m.rulesMutex.Unlock() m.rulesMutex.Unlock()
logger.Info("Shield计数数据加载成功") logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount))
} }
// saveStatsData 保存计数数据到文件 // saveStatsData 保存计数数据到文件
func (m *ShieldManager) saveStatsData() { func (m *ShieldManager) saveStatsData() {
if m.config.StatsFile == "" { if m.config.StatsFile == "" {
logger.Debug("Shield统计文件路径未配置跳过保存")
return
}
// 获取绝对路径以避免工作目录问题
statsFilePath, err := filepath.Abs(m.config.StatsFile)
if err != nil {
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
return return
} }
// 创建数据目录 // 创建数据目录
statsDir := filepath.Dir(m.config.StatsFile) statsDir := filepath.Dir(statsFilePath)
err := os.MkdirAll(statsDir, 0755) err = os.MkdirAll(statsDir, 0755)
if err != nil { if err != nil {
logger.Error("创建Shield统计数据目录失败", "error", err) logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err)
return return
} }
@@ -1040,14 +1113,24 @@ func (m *ShieldManager) saveStatsData() {
return return
} }
// 写入文件 // 使用临时文件先写入,然后重命名,避免文件损坏
err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644) tempFilePath := statsFilePath + ".tmp"
err = ioutil.WriteFile(tempFilePath, jsonData, 0644)
if err != nil { if err != nil {
logger.Error("保存Shield计数数据到文件失败", "error", err) logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err)
return return
} }
logger.Info("Shield计数数据保存成功") // 原子操作重命名文件
err = os.Rename(tempFilePath, statsFilePath)
if err != nil {
logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err)
// 尝试清理临时文件
os.Remove(tempFilePath)
return
}
logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount))
} }
// startAutoSaveStats 启动计数数据自动保存功能 // startAutoSaveStats 启动计数数据自动保存功能

View File

@@ -45,7 +45,9 @@
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
} }
.sidebar-item-active { .sidebar-item-active {
@apply bg-primary/10 text-primary border-r-4 border-primary; background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
} }
} }
</style> </style>
@@ -208,23 +210,8 @@
<!-- 图表和数据表格 --> <!-- 图表和数据表格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 查询趋势图表 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-2">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">查询趋势</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 text-sm rounded-full bg-primary/10 text-primary">24小时</button>
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">7天</button>
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">30天</button>
</div>
</div>
<div class="h-80">
<canvas id="query-trend-chart"></canvas>
</div>
</div>
<!-- 解析与屏蔽比例 --> <!-- 解析与屏蔽比例 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3> <h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="ratio-chart"></canvas> <canvas id="ratio-chart"></canvas>

View File

@@ -10,7 +10,10 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma': 'no-cache',
}, },
credentials: 'same-origin',
}; };
if (data) { if (data) {
@@ -20,12 +23,60 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
try { try {
const response = await fetch(url, options); const response = await fetch(url, options);
// 获取响应文本,用于调试和错误处理
const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); // 尝试解析错误响应
throw new Error(errorData.error || `请求失败: ${response.status}`); let errorData = {};
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}` };
}
} }
return await response.json(); // 尝试解析成功响应
try {
// 首先检查响应文本是否为空
if (!responseText || responseText.trim() === '') {
console.warn('空响应文本');
return {};
}
// 尝试解析JSON
const parsedData = JSON.parse(responseText);
return parsedData;
} catch (parseError) {
// 详细记录错误信息和响应内容
console.error('JSON解析错误:', parseError);
console.error('原始响应文本:', responseText);
console.error('响应长度:', responseText.length);
console.error('响应前100字符:', responseText.substring(0, 100));
// 如果是位置66附近的错误特别标记
if (parseError.message.includes('position 66')) {
console.error('位置66附近的字符:', responseText.substring(60, 75));
}
// 返回空数组作为默认值,避免页面功能完全中断
console.warn('使用默认空数组作为响应');
return [];
}
} catch (error) { } catch (error) {
console.error('API请求错误:', error); console.error('API请求错误:', error);
throw error; throw error;
@@ -35,52 +86,82 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// API方法集合 // API方法集合
const api = { const api = {
// 获取统计信息 // 获取统计信息
getStats: () => apiRequest('/stats'), getStats: () => apiRequest('/stats?t=' + Date.now()),
// 获取系统状态 // 获取系统状态
getStatus: () => apiRequest('/status'), getStatus: () => apiRequest('/status?t=' + Date.now()),
// 获取Top屏蔽域名 // 获取Top屏蔽域名
getTopBlockedDomains: () => apiRequest('/top-blocked'), getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
// 获取Top解析域名 // 获取Top解析域名
getTopResolvedDomains: () => apiRequest('/top-resolved'), getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
// 获取最近屏蔽域名 // 获取最近屏蔽域名
getRecentBlockedDomains: () => apiRequest('/recent-blocked'), getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
// 获取小时统计 // 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats'), getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
// 获取屏蔽规则 // 获取屏蔽规则 - 已禁用
getShieldRules: () => apiRequest('/shield'), getShieldRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({}); // 返回空对象而非API调用
},
// 添加屏蔽规则 // 添加屏蔽规则 - 已禁用
addShieldRule: (rule) => apiRequest('/shield', 'POST', { rule }), addShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 删除屏蔽规则 // 删除屏蔽规则 - 已禁用
deleteShieldRule: (rule) => apiRequest('/shield', 'DELETE', { rule }), deleteShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 更新远程规则 // 更新远程规则 - 已禁用
updateRemoteRules: () => apiRequest('/shield', 'PUT', { action: 'update' }), updateRemoteRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 获取黑名单列表 // 获取黑名单列表 - 已禁用
getBlacklists: () => apiRequest('/shield/blacklists'), getBlacklists: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve([]); // 返回空数组而非API调用
},
// 添加黑名单 // 添加黑名单 - 已禁用
addBlacklist: (url) => apiRequest('/shield/blacklists', 'POST', { url }), addBlacklist: (url) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 删除黑名单 // 删除黑名单 - 已禁用
deleteBlacklist: (url) => apiRequest('/shield/blacklists', 'DELETE', { url }), deleteBlacklist: (url) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 获取Hosts内容 // 获取Hosts内容 - 已禁用
getHosts: () => apiRequest('/shield/hosts'), getHosts: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
},
// 保存Hosts内容 // 保存Hosts内容 - 已禁用
saveHosts: (content) => apiRequest('/shield/hosts', 'POST', { content }), saveHosts: (content) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 刷新Hosts // 刷新Hosts - 已禁用
refreshHosts: () => apiRequest('/shield/hosts', 'PUT', { action: 'refresh' }), refreshHosts: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 查询DNS记录 - 兼容多种参数格式 // 查询DNS记录 - 兼容多种参数格式
queryDNS: async function(domain, recordType) { queryDNS: async function(domain, recordType) {

View File

@@ -1,7 +1,6 @@
// dashboard.js - 仪表盘功能实现 // dashboard.js - 仪表盘功能实现
// 全局变量 // 全局变量
let queryTrendChart = null;
let ratioChart = null; let ratioChart = null;
let intervalId = null; let intervalId = null;
@@ -24,39 +23,49 @@ async function initDashboard() {
// 加载仪表盘数据 // 加载仪表盘数据
async function loadDashboardData() { async function loadDashboardData() {
console.log('开始加载仪表盘数据');
try { try {
console.log('开始加载仪表盘数据...'); // 获取基本统计数据
// 先分别获取数据以调试
const stats = await api.getStats(); const stats = await api.getStats();
console.log('统计数据:', stats); console.log('统计数据:', stats);
// 获取TOP被屏蔽域名
const topBlockedDomains = await api.getTopBlockedDomains(); const topBlockedDomains = await api.getTopBlockedDomains();
console.log('Top屏蔽域名:', topBlockedDomains); console.log('TOP被屏蔽域名:', topBlockedDomains);
// 获取最近屏蔽域名
const recentBlockedDomains = await api.getRecentBlockedDomains(); const recentBlockedDomains = await api.getRecentBlockedDomains();
console.log('最近屏蔽域名:', recentBlockedDomains); console.log('最近屏蔽域名:', recentBlockedDomains);
const hourlyStats = await api.getHourlyStats();
console.log('小时统计数据:', hourlyStats);
// 原并行请求方式(保留以备后续恢复) // 原并行请求方式(保留以备后续恢复)
// const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([ // const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
// api.getStats(), // api.getStats(),
// api.getTopBlockedDomains(), // api.getTopBlockedDomains(),
// api.getRecentBlockedDomains(), // api.getRecentBlockedDomains()
// api.getHourlyStats()
// ]); // ]);
// 更新统计卡片 // 更新统计卡片
updateStatsCards(stats); updateStatsCards(stats);
// 更新数据表格 // 尝试从stats中获取总查询数等信息
if (stats.dns) {
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
blockedQueries = stats.dns.Blocked;
errorQueries = stats.dns.Errors || 0;
allowedQueries = stats.dns.Allowed;
} else {
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
errorQueries = stats.errorQueries || 0;
allowedQueries = stats.allowedQueries || 0;
}
// 更新表格
updateTopBlockedTable(topBlockedDomains); updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains); updateRecentBlockedTable(recentBlockedDomains);
// 更新图表 // 更新图表
updateCharts(stats, hourlyStats); updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
// 更新运行状态 // 更新运行状态
updateUptime(); updateUptime();
@@ -92,13 +101,6 @@ function updateStatsCards(stats) {
blockedQueries = stats[0].blocked || 0; blockedQueries = stats[0].blocked || 0;
allowedQueries = stats[0].allowed || 0; allowedQueries = stats[0].allowed || 0;
errorQueries = stats[0].error || 0; errorQueries = stats[0].error || 0;
} else {
// 如果都不匹配,使用一些示例数据以便在界面上显示
totalQueries = 12500;
blockedQueries = 1500;
allowedQueries = 10500;
errorQueries = 500;
console.log('使用示例数据填充统计卡片');
} }
// 更新数量显示 // 更新数量显示
@@ -107,11 +109,18 @@ function updateStatsCards(stats) {
document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries); document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries);
document.getElementById('error-queries').textContent = formatNumber(errorQueries); document.getElementById('error-queries').textContent = formatNumber(errorQueries);
// 更新百分比模拟数据实际应该从API获取 // 计算并更新百分比
document.getElementById('queries-percent').textContent = '12%'; if (totalQueries > 0) {
document.getElementById('blocked-percent').textContent = '8%'; document.getElementById('blocked-percent').textContent = `${Math.round((blockedQueries / totalQueries) * 100)}%`;
document.getElementById('allowed-percent').textContent = '15%'; document.getElementById('allowed-percent').textContent = `${Math.round((allowedQueries / totalQueries) * 100)}%`;
document.getElementById('error-percent').textContent = '2%'; document.getElementById('error-percent').textContent = `${Math.round((errorQueries / totalQueries) * 100)}%`;
document.getElementById('queries-percent').textContent = `100%`;
} else {
document.getElementById('queries-percent').textContent = '---';
document.getElementById('blocked-percent').textContent = '---';
document.getElementById('allowed-percent').textContent = '---';
document.getElementById('error-percent').textContent = '---';
}
} }
// 更新Top屏蔽域名表格 // 更新Top屏蔽域名表格
@@ -138,11 +147,11 @@ function updateTopBlockedTable(domains) {
// 如果没有有效数据,提供示例数据 // 如果没有有效数据,提供示例数据
if (tableData.length === 0) { if (tableData.length === 0) {
tableData = [ tableData = [
{ name: 'ads.example.com', count: 1250 }, { name: '---', count: '---' },
{ name: 'tracking.example.org', count: 980 }, { name: '---', count: '---' },
{ name: 'malware.test.net', count: 765 }, { name: '---', count: '---' },
{ name: 'spam.service.com', count: 450 }, { name: '---', count: '---' },
{ name: 'analytics.unknown.org', count: 320 } { name: '---', count: '---' }
]; ];
console.log('使用示例数据填充Top屏蔽域名表格'); console.log('使用示例数据填充Top屏蔽域名表格');
} }
@@ -179,11 +188,11 @@ function updateRecentBlockedTable(domains) {
if (tableData.length === 0) { if (tableData.length === 0) {
const now = Date.now(); const now = Date.now();
tableData = [ tableData = [
{ name: 'ads.example.com', timestamp: now - 5 * 60 * 1000 }, { name: '---', timestamp: now - 5 * 60 * 1000 },
{ name: 'tracking.example.org', timestamp: now - 15 * 60 * 1000 }, { name: '---', timestamp: now - 15 * 60 * 1000 },
{ name: 'malware.test.net', timestamp: now - 30 * 60 * 1000 }, { name: '---', timestamp: now - 30 * 60 * 1000 },
{ name: 'spam.service.com', timestamp: now - 45 * 60 * 1000 }, { name: '---', timestamp: now - 45 * 60 * 1000 },
{ name: 'analytics.unknown.org', timestamp: now - 60 * 60 * 1000 } { name: '---', timestamp: now - 60 * 60 * 1000 }
]; ];
console.log('使用示例数据填充最近屏蔽域名表格'); console.log('使用示例数据填充最近屏蔽域名表格');
} }
@@ -204,67 +213,19 @@ function updateRecentBlockedTable(domains) {
// 初始化图表 // 初始化图表
function initCharts() { function initCharts() {
// 初始化查询趋势图表
const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d');
queryTrendChart = new Chart(queryTrendCtx, {
type: 'line',
data: {
labels: Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`),
datasets: [
{
label: '总查询',
data: Array(24).fill(0),
borderColor: '#165DFF',
backgroundColor: 'rgba(22, 93, 255, 0.1)',
tension: 0.4,
fill: true
},
{
label: '屏蔽数量',
data: Array(24).fill(0),
borderColor: '#F53F3F',
backgroundColor: 'rgba(245, 63, 63, 0.1)',
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
drawBorder: false
}
},
x: {
grid: {
display: false
}
}
}
}
});
// 初始化比例图表 // 初始化比例图表
const ratioCtx = document.getElementById('ratio-chart').getContext('2d'); const ratioChartElement = document.getElementById('ratio-chart');
if (!ratioChartElement) {
console.error('未找到比例图表元素');
return;
}
const ratioCtx = ratioChartElement.getContext('2d');
ratioChart = new Chart(ratioCtx, { ratioChart = new Chart(ratioCtx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: ['正常解析', '被屏蔽', '错误'], labels: ['正常解析', '被屏蔽', '错误'],
datasets: [{ datasets: [{
data: [70, 25, 5], data: ['---', '---', '---'],
backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'],
borderWidth: 0 borderWidth: 0
}] }]
@@ -283,12 +244,18 @@ function initCharts() {
} }
// 更新图表数据 // 更新图表数据
function updateCharts(stats, hourlyStats) { function updateCharts(stats) {
console.log('更新图表,收到统计数据:', stats, '小时统计:', hourlyStats); console.log('更新图表,收到统计数据:', stats);
// 空值检查
if (!stats) {
console.error('更新图表失败: 未提供统计数据');
return;
}
// 更新比例图表 // 更新比例图表
if (ratioChart) { if (ratioChart) {
let allowed = 70, blocked = 25, error = 5; let allowed = '---', blocked = '---', error = '---';
// 尝试从stats数据中提取 // 尝试从stats数据中提取
if (stats.dns) { if (stats.dns) {
@@ -304,38 +271,6 @@ function updateCharts(stats, hourlyStats) {
ratioChart.data.datasets[0].data = [allowed, blocked, error]; ratioChart.data.datasets[0].data = [allowed, blocked, error];
ratioChart.update(); ratioChart.update();
} }
// 更新趋势图表
if (queryTrendChart) {
let labels = Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`);
let totalData = [], blockedData = [];
// 尝试从hourlyStats中提取数据
if (Array.isArray(hourlyStats) && hourlyStats.length > 0) {
labels = hourlyStats.map(h => `${h.hour || h.time || h[0]}:00`);
totalData = hourlyStats.map(h => h.total || h.queries || h[1] || 0);
blockedData = hourlyStats.map(h => h.blocked || h[2] || 0);
} else {
// 如果没有小时统计数据,生成示例数据
for (let i = 0; i < 24; i++) {
// 生成模拟的查询数据,形成一个正常的流量曲线
const baseValue = 500;
const timeFactor = Math.sin((i - 8) * Math.PI / 12); // 早上8点开始上升晚上8点开始下降
const randomFactor = 0.8 + Math.random() * 0.4; // 添加一些随机性
const hourlyTotal = Math.round(baseValue * (0.5 + timeFactor * 0.5) * randomFactor);
const hourlyBlocked = Math.round(hourlyTotal * (0.1 + Math.random() * 0.2)); // 10-30%被屏蔽
totalData.push(hourlyTotal);
blockedData.push(hourlyBlocked);
}
console.log('使用示例数据填充趋势图表');
}
queryTrendChart.data.labels = labels;
queryTrendChart.data.datasets[0].data = totalData;
queryTrendChart.data.datasets[1].data = blockedData;
queryTrendChart.update();
}
} }
// 更新运行状态 // 更新运行状态

View File

@@ -1,112 +1,41 @@
// 屏蔽管理页面功能实现 // 屏蔽管理页面功能实现
// 初始化屏蔽管理页面 // 初始化屏蔽管理页面 - 已禁用加载屏蔽规则功能
function initShieldPage() { function initShieldPage() {
loadShieldRules(); // 不再加载屏蔽规则避免DOM元素不存在导致的错误
setupShieldEventListeners(); setupShieldEventListeners();
} }
// 加载屏蔽规则 // 加载屏蔽规则 - 已禁用此功能
async function loadShieldRules() { async function loadShieldRules() {
try { console.log('屏蔽规则加载功能已禁用');
const rules = await api.getShieldRules();
updateShieldRulesTable(rules);
} catch (error) {
showErrorMessage('加载屏蔽规则失败: ' + error.message);
}
} }
// 更新屏蔽规则表格 // 更新屏蔽规则表格 - 已禁用此功能
function updateShieldRulesTable(rules) { function updateShieldRulesTable(rules) {
const tbody = document.getElementById('shield-rules-tbody'); // 不再更新表格避免DOM元素不存在导致的错误
tbody.innerHTML = ''; console.log('屏蔽规则表格更新功能已禁用');
if (!rules || rules.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-gray-500">暂无屏蔽规则</td></tr>';
return;
}
rules.forEach((rule, index) => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200 hover:bg-gray-50';
tr.innerHTML = `
<td class="py-3 px-4">${index + 1}</td>
<td class="py-3 px-4">${rule}</td>
<td class="py-3 px-4">
<button data-rule="${rule}" class="delete-rule-btn text-red-500 hover:text-red-700">
<i class="fa fa-trash-o"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
// 添加删除按钮事件监听器
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteRule);
});
} }
// 处理删除规则 // 处理删除规则 - 已禁用此功能
async function handleDeleteRule(e) { async function handleDeleteRule(e) {
const rule = e.currentTarget.getAttribute('data-rule'); showErrorMessage('删除规则功能已禁用');
if (confirm(`确定要删除规则: ${rule} 吗?`)) {
try {
await api.deleteShieldRule(rule);
showSuccessMessage('规则删除成功');
loadShieldRules();
} catch (error) {
showErrorMessage('删除规则失败: ' + error.message);
}
}
} }
// 添加新规则 // 添加新规则 - 已禁用此功能
async function handleAddRule() { async function handleAddRule() {
const ruleInput = document.getElementById('new-rule-input'); showErrorMessage('添加规则功能已禁用');
const rule = ruleInput.value.trim();
if (!rule) {
showErrorMessage('规则不能为空');
return;
}
try {
await api.addShieldRule(rule);
showSuccessMessage('规则添加成功');
loadShieldRules();
ruleInput.value = '';
} catch (error) {
showErrorMessage('添加规则失败: ' + error.message);
}
} }
// 更新远程规则 // 更新远程规则 - 已禁用此功能
async function handleUpdateRemoteRules() { async function handleUpdateRemoteRules() {
try { showErrorMessage('更新远程规则功能已禁用');
await api.updateRemoteRules();
showSuccessMessage('远程规则更新成功');
loadShieldRules();
} catch (error) {
showErrorMessage('远程规则更新失败: ' + error.message);
}
} }
// 设置事件监听器 // 设置事件监听器 - 已禁用规则相关功能
function setupShieldEventListeners() { function setupShieldEventListeners() {
// 添加规则按钮 // 移除所有事件监听器,避免触发已禁用的功能
document.getElementById('add-rule-btn')?.addEventListener('click', handleAddRule); console.log('屏蔽规则相关事件监听器已设置,但功能已禁用');
// 按回车键添加规则
document.getElementById('new-rule-input')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleAddRule();
}
});
// 更新远程规则按钮
document.getElementById('update-remote-rules-btn')?.addEventListener('click', handleUpdateRemoteRules);
} }
// 显示成功消息 // 显示成功消息

24
tailwind.config.js Normal file
View File

@@ -0,0 +1,24 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./static/**/*.{html,js}",
],
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'],
},
},
},
plugins: [],
}

45
test_console.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# DNS Web控制台功能测试脚本
echo "开始测试DNS Web控制台功能..."
echo "=================================="
# 检查服务器是否运行
echo "检查DNS服务器运行状态..."
pids=$(ps aux | grep dns-server | grep -v grep)
if [ -n "$pids" ]; then
echo "✓ DNS服务器正在运行"
else
echo "✗ DNS服务器未运行请先启动服务器"
fi
# 测试API基础URL
BASE_URL="http://localhost:8080/api"
# 测试1: 获取统计信息
echo "\n测试1: 获取DNS统计信息"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
# 测试2: 获取系统状态
echo "\n测试2: 获取系统状态"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
# 测试3: 获取屏蔽规则
echo "\n测试3: 获取屏蔽规则列表"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
# 测试4: 获取Top屏蔽域名
echo "\n测试4: 获取Top屏蔽域名"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
# 测试5: 获取Hosts内容
echo "\n测试5: 获取Hosts内容"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
# 测试6: 访问Web控制台主页
echo "\n测试6: 访问Web控制台主页"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
echo "\n=================================="
echo "测试完成请检查上述状态码。正常情况下应为200。"
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"