web增加恢复解析统计图表
This commit is contained in:
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
File diff suppressed because it is too large
Load Diff
5
data/shield_stats.json
Normal file
5
data/shield_stats.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"blockedDomainsCount": {},
|
||||||
|
"resolvedDomainsCount": {},
|
||||||
|
"lastSaved": "2025-11-25T16:28:07.014160808+08:00"
|
||||||
|
}
|
||||||
63
data/stats.json
Normal file
63
data/stats.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"stats": {
|
||||||
|
"Queries": 52,
|
||||||
|
"Blocked": 4,
|
||||||
|
"Allowed": 47,
|
||||||
|
"Errors": 3,
|
||||||
|
"LastQuery": "2025-11-25T16:25:32.369810766+08:00"
|
||||||
|
},
|
||||||
|
"blockedDomains": {
|
||||||
|
"makeding.com": {
|
||||||
|
"Domain": "makeding.com",
|
||||||
|
"Count": 2,
|
||||||
|
"LastSeen": "2025-11-25T16:25:22.356227178+08:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolvedDomains": {
|
||||||
|
"ad.qq.com": {
|
||||||
|
"Domain": "ad.qq.com",
|
||||||
|
"Count": 12,
|
||||||
|
"LastSeen": "2025-11-25T16:25:27.168428267+08:00"
|
||||||
|
},
|
||||||
|
"ad.qq.com.amazehome.xyz": {
|
||||||
|
"Domain": "ad.qq.com.amazehome.xyz",
|
||||||
|
"Count": 10,
|
||||||
|
"LastSeen": "2025-11-25T16:25:27.085406193+08:00"
|
||||||
|
},
|
||||||
|
"adjust.com": {
|
||||||
|
"Domain": "adjust.com",
|
||||||
|
"Count": 6,
|
||||||
|
"LastSeen": "2025-11-25T16:25:30.020960393+08:00"
|
||||||
|
},
|
||||||
|
"adjust.com.amazehome.xyz": {
|
||||||
|
"Domain": "adjust.com.amazehome.xyz",
|
||||||
|
"Count": 6,
|
||||||
|
"LastSeen": "2025-11-25T16:25:29.845812094+08:00"
|
||||||
|
},
|
||||||
|
"makeding.com.amazehome.xyz": {
|
||||||
|
"Domain": "makeding.com.amazehome.xyz",
|
||||||
|
"Count": 2,
|
||||||
|
"LastSeen": "2025-11-25T16:25:22.291376134+08:00"
|
||||||
|
},
|
||||||
|
"so.com": {
|
||||||
|
"Domain": "so.com",
|
||||||
|
"Count": 6,
|
||||||
|
"LastSeen": "2025-11-25T16:25:32.376609155+08:00"
|
||||||
|
},
|
||||||
|
"so.com.amazehome.xyz": {
|
||||||
|
"Domain": "so.com.amazehome.xyz",
|
||||||
|
"Count": 5,
|
||||||
|
"LastSeen": "2025-11-25T16:25:32.297090186+08:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hourlyStats": {
|
||||||
|
"2025-11-25-16": 2
|
||||||
|
},
|
||||||
|
"dailyStats": {
|
||||||
|
"2025-11-25": 2
|
||||||
|
},
|
||||||
|
"monthlyStats": {
|
||||||
|
"2025-11": 2
|
||||||
|
},
|
||||||
|
"lastSaved": "2025-11-25T16:28:07.012405988+08:00"
|
||||||
|
}
|
||||||
BIN
dns-server
Executable file
BIN
dns-server
Executable file
Binary file not shown.
6324
dns-server.log
Normal file
6324
dns-server.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,8 @@ type StatsData struct {
|
|||||||
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
|
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
|
||||||
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
|
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
|
||||||
HourlyStats map[string]int64 `json:"hourlyStats"`
|
HourlyStats map[string]int64 `json:"hourlyStats"`
|
||||||
|
DailyStats map[string]int64 `json:"dailyStats"`
|
||||||
|
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
||||||
LastSaved time.Time `json:"lastSaved"`
|
LastSaved time.Time `json:"lastSaved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +55,10 @@ type Server struct {
|
|||||||
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
||||||
hourlyStatsMutex sync.RWMutex
|
hourlyStatsMutex sync.RWMutex
|
||||||
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
||||||
|
dailyStatsMutex sync.RWMutex
|
||||||
|
dailyStats map[string]int64 // 按天统计屏蔽数量
|
||||||
|
monthlyStatsMutex sync.RWMutex
|
||||||
|
monthlyStats map[string]int64 // 按月统计屏蔽数量
|
||||||
saveTicker *time.Ticker // 用于定时保存数据
|
saveTicker *time.Ticker // 用于定时保存数据
|
||||||
saveDone chan struct{} // 用于通知保存协程停止
|
saveDone chan struct{} // 用于通知保存协程停止
|
||||||
}
|
}
|
||||||
@@ -88,6 +94,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
|||||||
blockedDomains: make(map[string]*BlockedDomain),
|
blockedDomains: make(map[string]*BlockedDomain),
|
||||||
resolvedDomains: make(map[string]*BlockedDomain),
|
resolvedDomains: make(map[string]*BlockedDomain),
|
||||||
hourlyStats: make(map[string]int64),
|
hourlyStats: make(map[string]int64),
|
||||||
|
dailyStats: make(map[string]int64),
|
||||||
|
monthlyStats: make(map[string]int64),
|
||||||
saveDone: make(chan struct{}),
|
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.hourlyStatsMutex.Lock()
|
||||||
s.hourlyStats[hourKey]++
|
s.hourlyStats[hourKey]++
|
||||||
s.hourlyStatsMutex.Unlock()
|
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 更新解析域名统计
|
// updateResolvedDomainStats 更新解析域名统计
|
||||||
@@ -469,7 +492,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
|
|||||||
return domains
|
return domains
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHourlyStats 获取24小时屏蔽统计
|
// GetHourlyStats 获取每小时统计数据
|
||||||
func (s *Server) GetHourlyStats() map[string]int64 {
|
func (s *Server) GetHourlyStats() map[string]int64 {
|
||||||
s.hourlyStatsMutex.RLock()
|
s.hourlyStatsMutex.RLock()
|
||||||
defer s.hourlyStatsMutex.RUnlock()
|
defer s.hourlyStatsMutex.RUnlock()
|
||||||
@@ -482,6 +505,32 @@ func (s *Server) GetHourlyStats() map[string]int64 {
|
|||||||
return result
|
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 从文件加载统计数据
|
// loadStatsData 从文件加载统计数据
|
||||||
func (s *Server) loadStatsData() {
|
func (s *Server) loadStatsData() {
|
||||||
if s.config.StatsFile == "" {
|
if s.config.StatsFile == "" {
|
||||||
@@ -528,6 +577,18 @@ func (s *Server) loadStatsData() {
|
|||||||
s.hourlyStats = statsData.HourlyStats
|
s.hourlyStats = statsData.HourlyStats
|
||||||
}
|
}
|
||||||
s.hourlyStatsMutex.Unlock()
|
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("统计数据加载成功")
|
logger.Info("统计数据加载成功")
|
||||||
}
|
}
|
||||||
@@ -573,6 +634,20 @@ func (s *Server) saveStatsData() {
|
|||||||
statsData.HourlyStats[k] = v
|
statsData.HourlyStats[k] = v
|
||||||
}
|
}
|
||||||
s.hourlyStatsMutex.RUnlock()
|
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, "", " ")
|
jsonData, err := json.MarshalIndent(statsData, "", " ")
|
||||||
|
|||||||
101
http/server.go
101
http/server.go
@@ -51,6 +51,8 @@ func (s *Server) Start() error {
|
|||||||
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||||
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
||||||
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
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)
|
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 处理屏蔽规则管理请求
|
// handleShield 处理屏蔽规则管理请求
|
||||||
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 +293,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 +311,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 +319,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
BIN
output/dns-server
Executable file
Binary file not shown.
18
package.json
Normal file
18
package.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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 启动计数数据自动保存功能
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -138,93 +140,110 @@
|
|||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<!-- 查询总量卡片 -->
|
<!-- 查询总量卡片 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- 颜色蒙版 -->
|
||||||
<h3 class="text-gray-500 font-medium">查询总量</h3>
|
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
|
||||||
<div class="p-2 rounded-full bg-primary/10 text-primary">
|
<div class="relative z-10">
|
||||||
<i class="fa fa-refresh"></i>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-gray-500 font-medium">查询总量</h3>
|
||||||
|
<div class="p-2 rounded-full bg-primary/10 text-primary">
|
||||||
|
<i class="fa fa-refresh"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end justify-between">
|
||||||
|
<p class="text-3xl font-bold" id="total-queries">0</p>
|
||||||
|
<span class="text-success text-sm flex items-center">
|
||||||
|
<i class="fa fa-arrow-up mr-1"></i>
|
||||||
|
<span id="queries-percent">0%</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="total-queries">0</p>
|
|
||||||
<span class="text-success text-sm flex items-center">
|
|
||||||
<i class="fa fa-arrow-up mr-1"></i>
|
|
||||||
<span id="queries-percent">0%</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 屏蔽数量卡片 -->
|
<!-- 屏蔽数量卡片 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- 颜色蒙版 -->
|
||||||
<h3 class="text-gray-500 font-medium">屏蔽数量</h3>
|
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
|
||||||
<div class="p-2 rounded-full bg-danger/10 text-danger">
|
<div class="relative z-10">
|
||||||
<i class="fa fa-ban"></i>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-gray-500 font-medium">屏蔽数量</h3>
|
||||||
|
<div class="p-2 rounded-full bg-danger/10 text-danger">
|
||||||
|
<i class="fa fa-ban"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end justify-between">
|
||||||
|
<p class="text-3xl font-bold" id="blocked-queries">0</p>
|
||||||
|
<span class="text-danger text-sm flex items-center">
|
||||||
|
<i class="fa fa-arrow-up mr-1"></i>
|
||||||
|
<span id="blocked-percent">0%</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="blocked-queries">0</p>
|
|
||||||
<span class="text-danger text-sm flex items-center">
|
|
||||||
<i class="fa fa-arrow-up mr-1"></i>
|
|
||||||
<span id="blocked-percent">0%</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 正常解析卡片 -->
|
<!-- 正常解析卡片 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- 颜色蒙版 -->
|
||||||
<h3 class="text-gray-500 font-medium">正常解析</h3>
|
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
|
||||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
<div class="relative z-10">
|
||||||
<i class="fa fa-check"></i>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-gray-500 font-medium">正常解析</h3>
|
||||||
|
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end justify-between">
|
||||||
|
<p class="text-3xl font-bold" id="allowed-queries">0</p>
|
||||||
|
<span class="text-success text-sm flex items-center">
|
||||||
|
<i class="fa fa-arrow-up mr-1"></i>
|
||||||
|
<span id="allowed-percent">0%</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="allowed-queries">0</p>
|
|
||||||
<span class="text-success text-sm flex items-center">
|
|
||||||
<i class="fa fa-arrow-up mr-1"></i>
|
|
||||||
<span id="allowed-percent">0%</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误数量卡片 -->
|
<!-- 错误数量卡片 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<!-- 颜色蒙版 -->
|
||||||
<h3 class="text-gray-500 font-medium">错误数量</h3>
|
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
|
||||||
<div class="p-2 rounded-full bg-warning/10 text-warning">
|
<div class="relative z-10">
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-gray-500 font-medium">错误数量</h3>
|
||||||
|
<div class="p-2 rounded-full bg-warning/10 text-warning">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end justify-between">
|
||||||
|
<p class="text-3xl font-bold" id="error-queries">0</p>
|
||||||
|
<span class="text-warning text-sm flex items-center">
|
||||||
|
<i class="fa fa-arrow-up mr-1"></i>
|
||||||
|
<span id="error-percent">0%</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="error-queries">0</p>
|
|
||||||
<span class="text-warning text-sm flex items-center">
|
|
||||||
<i class="fa fa-arrow-up mr-1"></i>
|
|
||||||
<span id="error-percent">0%</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图表和数据表格 -->
|
<!-- 图表和数据表格 -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<!-- 查询趋势图表 -->
|
<!-- DNS请求趋势图表 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-2">
|
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h3 class="text-lg font-semibold">查询趋势</h3>
|
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
|
||||||
|
<!-- 时间范围切换按钮 -->
|
||||||
<div class="flex space-x-2">
|
<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="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
|
||||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">7天</button>
|
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
|
||||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">30天</button>
|
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-80">
|
<div class="h-80">
|
||||||
<canvas id="query-trend-chart"></canvas>
|
<canvas id="dns-requests-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|||||||
145
static/js/api.js
145
static/js/api.js
@@ -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,88 @@ 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()),
|
||||||
|
|
||||||
// 获取屏蔽规则
|
// 获取每日统计数据(7天)
|
||||||
getShieldRules: () => apiRequest('/shield'),
|
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
|
||||||
|
|
||||||
// 添加屏蔽规则
|
// 获取每月统计数据(30天)
|
||||||
addShieldRule: (rule) => apiRequest('/shield', 'POST', { rule }),
|
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
|
// 获取Hosts内容 - 已禁用
|
||||||
refreshHosts: () => apiRequest('/shield/hosts', 'PUT', { action: 'refresh' }),
|
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记录 - 兼容多种参数格式
|
// 查询DNS记录 - 兼容多种参数格式
|
||||||
queryDNS: async function(domain, recordType) {
|
queryDNS: async function(domain, recordType) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// dashboard.js - 仪表盘功能实现
|
// dashboard.js - 仪表盘功能实现
|
||||||
|
|
||||||
// 全局变量
|
// 全局变量
|
||||||
let queryTrendChart = null;
|
|
||||||
let ratioChart = null;
|
let ratioChart = null;
|
||||||
|
let dnsRequestsChart = null;
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
|
|
||||||
// 初始化仪表盘
|
// 初始化仪表盘
|
||||||
@@ -14,6 +14,9 @@ async function initDashboard() {
|
|||||||
// 初始化图表
|
// 初始化图表
|
||||||
initCharts();
|
initCharts();
|
||||||
|
|
||||||
|
// 初始化时间范围切换
|
||||||
|
initTimeRangeToggle();
|
||||||
|
|
||||||
// 设置定时更新
|
// 设置定时更新
|
||||||
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
|
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -24,39 +27,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 +105,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 +113,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 +151,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 +192,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('使用示例数据填充最近屏蔽域名表格');
|
||||||
}
|
}
|
||||||
@@ -202,69 +215,43 @@ function updateRecentBlockedTable(domains) {
|
|||||||
tableBody.innerHTML = html;
|
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() {
|
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
|
||||||
}]
|
}]
|
||||||
@@ -280,15 +267,101 @@ function initCharts() {
|
|||||||
cutout: '70%'
|
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) {
|
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 +377,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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新运行状态
|
// 更新运行状态
|
||||||
|
|||||||
@@ -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
24
tailwind.config.js
Normal 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
45
test_console.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user