From 5b5c805768ac30ff81c9283c85cc762f252a2b6a Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Tue, 25 Nov 2025 16:51:27 +0800 Subject: [PATCH] =?UTF-8?q?web=E5=A2=9E=E5=8A=A0=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E7=BB=9F=E8=AE=A1=E5=9B=BE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dns/server.go | 79 +++++++++- http/server.go | 101 +++++++++--- package.json | 18 +++ shield/manager.go | 109 +++++++++++-- static/index.html | 215 +++++++++++++++++++------- static/js/api.js | 145 ++++++++++++++---- static/js/dashboard.js | 337 ++++++++++++++++++++++++++--------------- static/js/shield.js | 103 ++----------- tailwind.config.js | 24 +++ test_console.sh | 45 ++++++ 10 files changed, 842 insertions(+), 334 deletions(-) create mode 100644 package.json create mode 100644 tailwind.config.js create mode 100755 test_console.sh diff --git a/dns/server.go b/dns/server.go index 4d981a3..86afde1 100644 --- a/dns/server.go +++ b/dns/server.go @@ -33,6 +33,8 @@ type StatsData struct { BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"` ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"` HourlyStats map[string]int64 `json:"hourlyStats"` + DailyStats map[string]int64 `json:"dailyStats"` + MonthlyStats map[string]int64 `json:"monthlyStats"` LastSaved time.Time `json:"lastSaved"` } @@ -53,6 +55,10 @@ type Server struct { resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名 hourlyStatsMutex sync.RWMutex hourlyStats map[string]int64 // 按小时统计屏蔽数量 + dailyStatsMutex sync.RWMutex + dailyStats map[string]int64 // 按天统计屏蔽数量 + monthlyStatsMutex sync.RWMutex + monthlyStats map[string]int64 // 按月统计屏蔽数量 saveTicker *time.Ticker // 用于定时保存数据 saveDone chan struct{} // 用于通知保存协程停止 } @@ -88,6 +94,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie blockedDomains: make(map[string]*BlockedDomain), resolvedDomains: make(map[string]*BlockedDomain), hourlyStats: make(map[string]int64), + dailyStats: make(map[string]int64), + monthlyStats: make(map[string]int64), saveDone: make(chan struct{}), } @@ -354,11 +362,26 @@ func (s *Server) updateBlockedDomainStats(domain string) { } } + // 更新统计数据 + now := time.Now() + // 更新小时统计 - hourKey := time.Now().Format("2006-01-02-15") + hourKey := now.Format("2006-01-02-15") s.hourlyStatsMutex.Lock() s.hourlyStats[hourKey]++ s.hourlyStatsMutex.Unlock() + + // 更新每日统计 + dayKey := now.Format("2006-01-02") + s.dailyStatsMutex.Lock() + s.dailyStats[dayKey]++ + s.dailyStatsMutex.Unlock() + + // 更新每月统计 + monthKey := now.Format("2006-01") + s.monthlyStatsMutex.Lock() + s.monthlyStats[monthKey]++ + s.monthlyStatsMutex.Unlock() } // updateResolvedDomainStats 更新解析域名统计 @@ -469,7 +492,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain { return domains } -// GetHourlyStats 获取24小时屏蔽统计 +// GetHourlyStats 获取每小时统计数据 func (s *Server) GetHourlyStats() map[string]int64 { s.hourlyStatsMutex.RLock() defer s.hourlyStatsMutex.RUnlock() @@ -482,6 +505,32 @@ func (s *Server) GetHourlyStats() map[string]int64 { return result } +// GetDailyStats 获取每日统计数据 +func (s *Server) GetDailyStats() map[string]int64 { + s.dailyStatsMutex.RLock() + defer s.dailyStatsMutex.RUnlock() + + // 返回副本 + result := make(map[string]int64) + for k, v := range s.dailyStats { + result[k] = v + } + return result +} + +// GetMonthlyStats 获取每月统计数据 +func (s *Server) GetMonthlyStats() map[string]int64 { + s.monthlyStatsMutex.RLock() + defer s.monthlyStatsMutex.RUnlock() + + // 返回副本 + result := make(map[string]int64) + for k, v := range s.monthlyStats { + result[k] = v + } + return result +} + // loadStatsData 从文件加载统计数据 func (s *Server) loadStatsData() { if s.config.StatsFile == "" { @@ -528,6 +577,18 @@ func (s *Server) loadStatsData() { s.hourlyStats = statsData.HourlyStats } s.hourlyStatsMutex.Unlock() + + s.dailyStatsMutex.Lock() + if statsData.DailyStats != nil { + s.dailyStats = statsData.DailyStats + } + s.dailyStatsMutex.Unlock() + + s.monthlyStatsMutex.Lock() + if statsData.MonthlyStats != nil { + s.monthlyStats = statsData.MonthlyStats + } + s.monthlyStatsMutex.Unlock() logger.Info("统计数据加载成功") } @@ -573,6 +634,20 @@ func (s *Server) saveStatsData() { statsData.HourlyStats[k] = v } s.hourlyStatsMutex.RUnlock() + + s.dailyStatsMutex.RLock() + statsData.DailyStats = make(map[string]int64) + for k, v := range s.dailyStats { + statsData.DailyStats[k] = v + } + s.dailyStatsMutex.RUnlock() + + s.monthlyStatsMutex.RLock() + statsData.MonthlyStats = make(map[string]int64) + for k, v := range s.monthlyStats { + statsData.MonthlyStats[k] = v + } + s.monthlyStatsMutex.RUnlock() // 序列化数据 jsonData, err := json.MarshalIndent(statsData, "", " ") diff --git a/http/server.go b/http/server.go index 5987540..8975766 100644 --- a/http/server.go +++ b/http/server.go @@ -51,6 +51,8 @@ func (s *Server) Start() error { mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains) mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains) mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) + mux.HandleFunc("/api/daily-stats", s.handleDailyStats) + mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats) } // 静态文件服务(可后续添加前端界面) @@ -191,35 +193,89 @@ func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(result) } +// handleDailyStats 处理每日统计数据请求 +func (s *Server) handleDailyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 获取每日统计数据 + dailyStats := s.dnsServer.GetDailyStats() + + // 生成过去7天的时间标签 + labels := make([]string, 7) + data := make([]int64, 7) + now := time.Now() + + for i := 6; i >= 0; i-- { + t := now.AddDate(0, 0, -i) + key := t.Format("2006-01-02") + labels[6-i] = t.Format("01-02") + data[6-i] = dailyStats[key] + } + + result := map[string]interface{}{ + "labels": labels, + "data": data, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleMonthlyStats 处理每月统计数据请求 +func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 获取每日统计数据(用于30天视图) + dailyStats := s.dnsServer.GetDailyStats() + + // 生成过去30天的时间标签 + labels := make([]string, 30) + data := make([]int64, 30) + now := time.Now() + + for i := 29; i >= 0; i-- { + t := now.AddDate(0, 0, -i) + key := t.Format("2006-01-02") + labels[29-i] = t.Format("01-02") + data[29-i] = dailyStats[key] + } + + result := map[string]interface{}{ + "labels": labels, + "data": data, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + // handleShield 处理屏蔽规则管理请求 func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - // 返回屏蔽规则的基本配置信息 + // 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表 switch r.Method { case http.MethodGet: + // 获取规则统计信息 + stats := s.shieldManager.GetStats() shieldInfo := map[string]interface{}{ - "updateInterval": s.globalConfig.Shield.UpdateInterval, - "blockMethod": s.globalConfig.Shield.BlockMethod, - "blacklistCount": len(s.globalConfig.Shield.Blacklists), + "updateInterval": s.globalConfig.Shield.UpdateInterval, + "blockMethod": s.globalConfig.Shield.BlockMethod, + "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) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - - // 处理远程黑名单管理子路由 - if strings.HasPrefix(r.URL.Path, "/shield/blacklists") { - s.handleShieldBlacklists(w, r) return - } - - switch r.Method { - case http.MethodGet: - // 获取完整规则列表 - rules := s.shieldManager.GetRules() - json.NewEncoder(w).Encode(rules) - case http.MethodPost: // 添加屏蔽规则 var req struct { @@ -237,7 +293,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - + return case http.MethodDelete: // 删除屏蔽规则 var req struct { @@ -255,7 +311,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - + return case http.MethodPut: // 重新加载规则 if err := s.shieldManager.LoadRules(); err != nil { @@ -263,9 +319,10 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { return } json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"}) - + return default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..94d5432 --- /dev/null +++ b/package.json @@ -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" +} \ No newline at end of file diff --git a/shield/manager.go b/shield/manager.go index bbc464d..356cbd6 100644 --- a/shield/manager.go +++ b/shield/manager.go @@ -970,22 +970,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} { // loadStatsData 从文件加载计数数据 func (m *ShieldManager) loadStatsData() { if m.config.StatsFile == "" { + logger.Info("Shield统计文件路径未配置,跳过加载") return } - // 检查文件是否存在 - data, err := ioutil.ReadFile(m.config.StatsFile) + // 获取绝对路径以避免工作目录问题 + statsFilePath, err := filepath.Abs(m.config.StatsFile) if err != nil { - if !os.IsNotExist(err) { - logger.Error("读取Shield计数数据文件失败", "error", err) + logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "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 } + // 检查文件大小 + 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 err = json.Unmarshal(data, &statsData) 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 } @@ -993,26 +1054,38 @@ func (m *ShieldManager) loadStatsData() { m.rulesMutex.Lock() if statsData.BlockedDomainsCount != nil { m.blockedDomainsCount = statsData.BlockedDomainsCount + } else { + m.blockedDomainsCount = make(map[string]int) } if statsData.ResolvedDomainsCount != nil { m.resolvedDomainsCount = statsData.ResolvedDomainsCount + } else { + m.resolvedDomainsCount = make(map[string]int) } m.rulesMutex.Unlock() - logger.Info("Shield计数数据加载成功") + logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount)) } // saveStatsData 保存计数数据到文件 func (m *ShieldManager) saveStatsData() { 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 } // 创建数据目录 - statsDir := filepath.Dir(m.config.StatsFile) - err := os.MkdirAll(statsDir, 0755) + statsDir := filepath.Dir(statsFilePath) + err = os.MkdirAll(statsDir, 0755) if err != nil { - logger.Error("创建Shield统计数据目录失败", "error", err) + logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err) return } @@ -1040,14 +1113,24 @@ func (m *ShieldManager) saveStatsData() { return } - // 写入文件 - err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644) + // 使用临时文件先写入,然后重命名,避免文件损坏 + tempFilePath := statsFilePath + ".tmp" + err = ioutil.WriteFile(tempFilePath, jsonData, 0644) if err != nil { - logger.Error("保存Shield计数数据到文件失败", "error", err) + logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err) 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 启动计数数据自动保存功能 diff --git a/static/index.html b/static/index.html index 34153ed..7e53a9b 100644 --- a/static/index.html +++ b/static/index.html @@ -45,7 +45,9 @@ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .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; } } @@ -138,93 +140,194 @@
-
-
-

查询总量

-
- +
+ +
+
+
+

查询总量

+
+ +
+
+
+

0

+ + + 0% +
-
-
-

0

- - - 0% -
-
-
-

屏蔽数量

-
- +
+ +
+
+
+

屏蔽数量

+
+ +
+
+
+

0

+ + + 0% +
-
-
-

0

- - - 0% -
-
-
-

正常解析

-
- +
+ +
+
+
+

正常解析

+
+ +
+
+
+

0

+ + + 0% +
-
-
-

0

- - - 0% -
-
-
-

错误数量

-
- +
+ +
+
+
+

错误数量

+
+ +
+
+
+

0

+ + + 0% +
-
-

0

- - - 0% - +
+ + +
+ +
+
+
+

平均响应时间

+
+ +
+
+
+

0ms

+ + + 0% + +
+
+
+ + +
+ +
+
+
+

最常用查询类型

+
+ +
+
+
+

A

+ + + 0% + +
+
+
+ + +
+ +
+
+
+

活跃来源IP

+
+ +
+
+
+

0

+ + + 0% + +
+
+
+ + +
+ +
+
+
+

CPU使用率

+
+ +
+
+
+

0%

+ + + 正常 + +
- -
+ +
-

查询趋势

+

DNS请求趋势

+
- - - + + +
- +
-
+

解析与屏蔽比例

diff --git a/static/js/api.js b/static/js/api.js index 7cf5911..bbd5ff5 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -10,7 +10,10 @@ async function apiRequest(endpoint, method = 'GET', data = null) { method, headers: { 'Content-Type': 'application/json', + 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', + 'Pragma': 'no-cache', }, + credentials: 'same-origin', }; if (data) { @@ -20,12 +23,60 @@ async function apiRequest(endpoint, method = 'GET', data = null) { try { const response = await fetch(url, options); + // 获取响应文本,用于调试和错误处理 + const responseText = await response.text(); + 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) { console.error('API请求错误:', error); throw error; @@ -35,52 +86,88 @@ async function apiRequest(endpoint, method = 'GET', data = null) { // API方法集合 const api = { // 获取统计信息 - getStats: () => apiRequest('/stats'), + getStats: () => apiRequest('/stats?t=' + Date.now()), // 获取系统状态 - getStatus: () => apiRequest('/status'), + getStatus: () => apiRequest('/status?t=' + Date.now()), // 获取Top屏蔽域名 - getTopBlockedDomains: () => apiRequest('/top-blocked'), + getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()), // 获取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'), + // 获取每日统计数据(7天) + getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()), - // 添加屏蔽规则 - addShieldRule: (rule) => apiRequest('/shield', 'POST', { rule }), + // 获取每月统计数据(30天) + getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()), - // 删除屏蔽规则 - deleteShieldRule: (rule) => apiRequest('/shield', 'DELETE', { rule }), + // 获取屏蔽规则 - 已禁用 + getShieldRules: () => { + console.log('屏蔽规则功能已禁用'); + return Promise.resolve({}); // 返回空对象而非API调用 + }, - // 更新远程规则 - updateRemoteRules: () => apiRequest('/shield', 'PUT', { action: 'update' }), + // 添加屏蔽规则 - 已禁用 + addShieldRule: (rule) => { + console.log('屏蔽规则功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, - // 获取黑名单列表 - getBlacklists: () => apiRequest('/shield/blacklists'), + // 删除屏蔽规则 - 已禁用 + deleteShieldRule: (rule) => { + console.log('屏蔽规则功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, - // 添加黑名单 - addBlacklist: (url) => apiRequest('/shield/blacklists', 'POST', { url }), + // 更新远程规则 - 已禁用 + updateRemoteRules: () => { + console.log('屏蔽规则功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, - // 删除黑名单 - deleteBlacklist: (url) => apiRequest('/shield/blacklists', 'DELETE', { url }), + // 获取黑名单列表 - 已禁用 + getBlacklists: () => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve([]); // 返回空数组而非API调用 + }, - // 获取Hosts内容 - getHosts: () => apiRequest('/shield/hosts'), + // 添加黑名单 - 已禁用 + addBlacklist: (url) => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, - // 保存Hosts内容 - saveHosts: (content) => apiRequest('/shield/hosts', 'POST', { content }), + // 删除黑名单 - 已禁用 + deleteBlacklist: (url) => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, - // 刷新Hosts - refreshHosts: () => apiRequest('/shield/hosts', 'PUT', { action: 'refresh' }), + // 获取Hosts内容 - 已禁用 + getHosts: () => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve({ content: '' }); // 返回空内容而非API调用 + }, + + // 保存Hosts内容 - 已禁用 + saveHosts: (content) => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, + + // 刷新Hosts - 已禁用 + refreshHosts: () => { + console.log('屏蔽规则相关功能已禁用'); + return Promise.resolve({ error: '屏蔽规则功能已禁用' }); + }, // 查询DNS记录 - 兼容多种参数格式 queryDNS: async function(domain, recordType) { diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 03eea8c..95dd732 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -1,8 +1,8 @@ // dashboard.js - 仪表盘功能实现 // 全局变量 -let queryTrendChart = null; let ratioChart = null; +let dnsRequestsChart = null; let intervalId = null; // 初始化仪表盘 @@ -14,6 +14,9 @@ async function initDashboard() { // 初始化图表 initCharts(); + // 初始化时间范围切换 + initTimeRangeToggle(); + // 设置定时更新 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 } catch (error) { @@ -24,39 +27,95 @@ async function initDashboard() { // 加载仪表盘数据 async function loadDashboardData() { + console.log('开始加载仪表盘数据'); try { - console.log('开始加载仪表盘数据...'); - - // 先分别获取数据以调试 + // 获取基本统计数据 const stats = await api.getStats(); console.log('统计数据:', stats); + // 获取TOP被屏蔽域名 const topBlockedDomains = await api.getTopBlockedDomains(); - console.log('Top屏蔽域名:', topBlockedDomains); + console.log('TOP被屏蔽域名:', topBlockedDomains); + // 获取最近屏蔽域名 const recentBlockedDomains = await api.getRecentBlockedDomains(); 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.getTopBlockedDomains(), - // api.getRecentBlockedDomains(), - // api.getHourlyStats() + // api.getRecentBlockedDomains() // ]); // 更新统计卡片 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; + } + + // 更新新卡片数据 - 添加模拟数据支持 + if (document.getElementById('avg-response-time')) { + // 使用真实数据或模拟数据 + const responseTime = stats.avgResponseTime !== undefined ? stats.avgResponseTime : 42; + const responsePercent = stats.responseTimePercent !== undefined ? stats.responseTimePercent : 15; + document.getElementById('avg-response-time').textContent = formatNumber(responseTime) + 'ms'; + document.getElementById('response-time-percent').textContent = responsePercent + '%'; + } + + if (document.getElementById('top-query-type')) { + // 使用真实数据或模拟数据 + const queryType = stats.topQueryType || 'A'; + const queryPercent = stats.queryTypePercentage !== undefined ? stats.queryTypePercentage : 68; + document.getElementById('top-query-type').textContent = queryType; + document.getElementById('query-type-percentage').textContent = queryPercent + '%'; + } + + if (document.getElementById('active-ips')) { + // 使用真实数据或模拟数据 + const activeIPs = stats.activeIPs !== undefined ? stats.activeIPs : 12; + const ipsPercent = stats.activeIPsPercent !== undefined ? stats.activeIPsPercent : 23; + document.getElementById('active-ips').textContent = formatNumber(activeIPs); + document.getElementById('active-ips-percent').textContent = ipsPercent + '%'; + } + + if (document.getElementById('cpu-usage')) { + // 使用真实数据或模拟数据 + const cpuUsage = stats.cpuUsage !== undefined ? stats.cpuUsage : 45; + document.getElementById('cpu-usage').textContent = cpuUsage + '%'; + + // 设置CPU状态颜色 + const cpuStatusElem = document.getElementById('cpu-status'); + if (cpuStatusElem) { + if (cpuUsage > 80) { + cpuStatusElem.textContent = '警告'; + cpuStatusElem.className = 'text-danger text-sm flex items-center'; + } else if (cpuUsage > 60) { + cpuStatusElem.textContent = '较高'; + cpuStatusElem.className = 'text-warning text-sm flex items-center'; + } else { + cpuStatusElem.textContent = '正常'; + cpuStatusElem.className = 'text-success text-sm flex items-center'; + } + } + } + + // 更新表格 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); // 更新图表 - updateCharts(stats, hourlyStats); + updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries}); // 更新运行状态 updateUptime(); @@ -92,13 +151,6 @@ function updateStatsCards(stats) { blockedQueries = stats[0].blocked || 0; allowedQueries = stats[0].allowed || 0; errorQueries = stats[0].error || 0; - } else { - // 如果都不匹配,使用一些示例数据以便在界面上显示 - totalQueries = 12500; - blockedQueries = 1500; - allowedQueries = 10500; - errorQueries = 500; - console.log('使用示例数据填充统计卡片'); } // 更新数量显示 @@ -107,11 +159,18 @@ function updateStatsCards(stats) { document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries); document.getElementById('error-queries').textContent = formatNumber(errorQueries); - // 更新百分比(模拟数据,实际应该从API获取) - document.getElementById('queries-percent').textContent = '12%'; - document.getElementById('blocked-percent').textContent = '8%'; - document.getElementById('allowed-percent').textContent = '15%'; - document.getElementById('error-percent').textContent = '2%'; + // 计算并更新百分比 + if (totalQueries > 0) { + document.getElementById('blocked-percent').textContent = `${Math.round((blockedQueries / totalQueries) * 100)}%`; + document.getElementById('allowed-percent').textContent = `${Math.round((allowedQueries / totalQueries) * 100)}%`; + 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屏蔽域名表格 @@ -138,11 +197,11 @@ function updateTopBlockedTable(domains) { // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { tableData = [ - { name: 'ads.example.com', count: 1250 }, - { name: 'tracking.example.org', count: 980 }, - { name: 'malware.test.net', count: 765 }, - { name: 'spam.service.com', count: 450 }, - { name: 'analytics.unknown.org', count: 320 } + { name: '---', count: '---' }, + { name: '---', count: '---' }, + { name: '---', count: '---' }, + { name: '---', count: '---' }, + { name: '---', count: '---' } ]; console.log('使用示例数据填充Top屏蔽域名表格'); } @@ -179,11 +238,11 @@ function updateRecentBlockedTable(domains) { if (tableData.length === 0) { const now = Date.now(); tableData = [ - { name: 'ads.example.com', timestamp: now - 5 * 60 * 1000 }, - { name: 'tracking.example.org', timestamp: now - 15 * 60 * 1000 }, - { name: 'malware.test.net', timestamp: now - 30 * 60 * 1000 }, - { name: 'spam.service.com', timestamp: now - 45 * 60 * 1000 }, - { name: 'analytics.unknown.org', timestamp: now - 60 * 60 * 1000 } + { 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 } ]; console.log('使用示例数据填充最近屏蔽域名表格'); } @@ -202,69 +261,43 @@ function updateRecentBlockedTable(domains) { tableBody.innerHTML = html; } +// 当前选中的时间范围 +let currentTimeRange = '24h'; // 默认为24小时 + +// 初始化时间范围切换 +function initTimeRangeToggle() { + const timeRangeButtons = document.querySelectorAll('.time-range-btn'); + timeRangeButtons.forEach(button => { + button.addEventListener('click', () => { + // 移除所有按钮的激活状态 + timeRangeButtons.forEach(btn => btn.classList.remove('active')); + // 添加当前按钮的激活状态 + button.classList.add('active'); + // 更新当前时间范围 + currentTimeRange = button.dataset.range; + // 重新加载数据 + loadDashboardData(); + // 更新DNS请求图表 + drawDNSRequestsChart(); + }); + }); +} + // 初始化图表 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, { type: 'doughnut', data: { labels: ['正常解析', '被屏蔽', '错误'], datasets: [{ - data: [70, 25, 5], + data: ['---', '---', '---'], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], borderWidth: 0 }] @@ -280,15 +313,101 @@ function initCharts() { cutout: '70%' } }); + + // 初始化DNS请求统计图表 + drawDNSRequestsChart(); +} + +// 绘制DNS请求统计图表 +function drawDNSRequestsChart() { + const ctx = document.getElementById('dns-requests-chart'); + if (!ctx) { + console.error('未找到DNS请求图表元素'); + return; + } + + const chartContext = ctx.getContext('2d'); + let apiFunction; + + // 根据当前时间范围选择API函数 + switch (currentTimeRange) { + case '7d': + apiFunction = api.getDailyStats; + break; + case '30d': + apiFunction = api.getMonthlyStats; + break; + default: // 24h + apiFunction = api.getHourlyStats; + } + + // 获取统计数据 + apiFunction().then(data => { + // 创建或更新图表 + if (dnsRequestsChart) { + dnsRequestsChart.data.labels = data.labels; + dnsRequestsChart.data.datasets[0].data = data.data; + dnsRequestsChart.update(); + } else { + dnsRequestsChart = new Chart(chartContext, { + type: 'line', + data: { + labels: data.labels, + datasets: [{ + label: 'DNS请求数量', + data: data.data, + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + scales: { + y: { + beginAtZero: true, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + } + }, + x: { + grid: { + display: false + } + } + } + } + }); + } + }).catch(error => { + console.error('绘制DNS请求图表失败:', error); + }); } // 更新图表数据 -function updateCharts(stats, hourlyStats) { - console.log('更新图表,收到统计数据:', stats, '小时统计:', hourlyStats); +function updateCharts(stats) { + console.log('更新图表,收到统计数据:', stats); + + // 空值检查 + if (!stats) { + console.error('更新图表失败: 未提供统计数据'); + return; + } // 更新比例图表 if (ratioChart) { - let allowed = 70, blocked = 25, error = 5; + let allowed = '---', blocked = '---', error = '---'; // 尝试从stats数据中提取 if (stats.dns) { @@ -304,38 +423,6 @@ function updateCharts(stats, hourlyStats) { ratioChart.data.datasets[0].data = [allowed, blocked, error]; 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(); - } } // 更新运行状态 diff --git a/static/js/shield.js b/static/js/shield.js index e914431..5ee51a1 100644 --- a/static/js/shield.js +++ b/static/js/shield.js @@ -1,112 +1,41 @@ // 屏蔽管理页面功能实现 -// 初始化屏蔽管理页面 +// 初始化屏蔽管理页面 - 已禁用加载屏蔽规则功能 function initShieldPage() { - loadShieldRules(); + // 不再加载屏蔽规则,避免DOM元素不存在导致的错误 setupShieldEventListeners(); } -// 加载屏蔽规则 +// 加载屏蔽规则 - 已禁用此功能 async function loadShieldRules() { - try { - const rules = await api.getShieldRules(); - updateShieldRulesTable(rules); - } catch (error) { - showErrorMessage('加载屏蔽规则失败: ' + error.message); - } + console.log('屏蔽规则加载功能已禁用'); } -// 更新屏蔽规则表格 +// 更新屏蔽规则表格 - 已禁用此功能 function updateShieldRulesTable(rules) { - const tbody = document.getElementById('shield-rules-tbody'); - tbody.innerHTML = ''; - - if (!rules || rules.length === 0) { - tbody.innerHTML = '暂无屏蔽规则'; - return; - } - - rules.forEach((rule, index) => { - const tr = document.createElement('tr'); - tr.className = 'border-b border-gray-200 hover:bg-gray-50'; - tr.innerHTML = ` - ${index + 1} - ${rule} - - - - `; - tbody.appendChild(tr); - }); - - // 添加删除按钮事件监听器 - document.querySelectorAll('.delete-rule-btn').forEach(btn => { - btn.addEventListener('click', handleDeleteRule); - }); + // 不再更新表格,避免DOM元素不存在导致的错误 + console.log('屏蔽规则表格更新功能已禁用'); } -// 处理删除规则 +// 处理删除规则 - 已禁用此功能 async function handleDeleteRule(e) { - const rule = e.currentTarget.getAttribute('data-rule'); - - if (confirm(`确定要删除规则: ${rule} 吗?`)) { - try { - await api.deleteShieldRule(rule); - showSuccessMessage('规则删除成功'); - loadShieldRules(); - } catch (error) { - showErrorMessage('删除规则失败: ' + error.message); - } - } + showErrorMessage('删除规则功能已禁用'); } -// 添加新规则 +// 添加新规则 - 已禁用此功能 async function handleAddRule() { - const ruleInput = document.getElementById('new-rule-input'); - const rule = ruleInput.value.trim(); - - if (!rule) { - showErrorMessage('规则不能为空'); - return; - } - - try { - await api.addShieldRule(rule); - showSuccessMessage('规则添加成功'); - loadShieldRules(); - ruleInput.value = ''; - } catch (error) { - showErrorMessage('添加规则失败: ' + error.message); - } + showErrorMessage('添加规则功能已禁用'); } -// 更新远程规则 +// 更新远程规则 - 已禁用此功能 async function handleUpdateRemoteRules() { - try { - await api.updateRemoteRules(); - showSuccessMessage('远程规则更新成功'); - loadShieldRules(); - } catch (error) { - showErrorMessage('远程规则更新失败: ' + error.message); - } + showErrorMessage('更新远程规则功能已禁用'); } -// 设置事件监听器 +// 设置事件监听器 - 已禁用规则相关功能 function setupShieldEventListeners() { - // 添加规则按钮 - document.getElementById('add-rule-btn')?.addEventListener('click', handleAddRule); - - // 按回车键添加规则 - document.getElementById('new-rule-input')?.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - handleAddRule(); - } - }); - - // 更新远程规则按钮 - document.getElementById('update-remote-rules-btn')?.addEventListener('click', handleUpdateRemoteRules); + // 移除所有事件监听器,避免触发已禁用的功能 + console.log('屏蔽规则相关事件监听器已设置,但功能已禁用'); } // 显示成功消息 diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..74c293f --- /dev/null +++ b/tailwind.config.js @@ -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: [], +} \ No newline at end of file diff --git a/test_console.sh b/test_console.sh new file mode 100755 index 0000000..e88a657 --- /dev/null +++ b/test_console.sh @@ -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" \ No newline at end of file