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:46:31.42769342+08:00"
|
||||
}}
|
||||
88
data/stats.json
Normal file
88
data/stats.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"stats": {
|
||||
"Queries": 126,
|
||||
"Blocked": 4,
|
||||
"Allowed": 115,
|
||||
"Errors": 9,
|
||||
"LastQuery": "2025-11-25T16:45:53.348580932+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"
|
||||
},
|
||||
"exmail.qq.com": {
|
||||
"Domain": "exmail.qq.com",
|
||||
"Count": 6,
|
||||
"LastSeen": "2025-11-25T16:45:51.452852503+08:00"
|
||||
},
|
||||
"exmail.qq.com.amazehome.xyz": {
|
||||
"Domain": "exmail.qq.com.amazehome.xyz",
|
||||
"Count": 49,
|
||||
"LastSeen": "2025-11-25T16:45:53.360508736+08:00"
|
||||
},
|
||||
"mail.qq.com": {
|
||||
"Domain": "mail.qq.com",
|
||||
"Count": 2,
|
||||
"LastSeen": "2025-11-25T16:45:12.664586136+08:00"
|
||||
},
|
||||
"mail.qq.com.amazehome.xyz": {
|
||||
"Domain": "mail.qq.com.amazehome.xyz",
|
||||
"Count": 2,
|
||||
"LastSeen": "2025-11-25T16:45:12.587554115+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": 10,
|
||||
"LastSeen": "2025-11-25T16:45:01.46181203+08:00"
|
||||
},
|
||||
"so.com.amazehome.xyz": {
|
||||
"Domain": "so.com.amazehome.xyz",
|
||||
"Count": 9,
|
||||
"LastSeen": "2025-11-25T16:45:01.361909763+08:00"
|
||||
},
|
||||
"type=mx.amazehome.xyz": {
|
||||
"Domain": "type=mx.amazehome.xyz",
|
||||
"Count": 1,
|
||||
"LastSeen": "2025-11-25T16:44:58.39597173+08:00"
|
||||
}
|
||||
},
|
||||
"hourlyStats": {
|
||||
"2025-11-25-16": 2
|
||||
},
|
||||
"dailyStats": {
|
||||
"2025-11-25": 2
|
||||
},
|
||||
"monthlyStats": {
|
||||
"2025-11": 2
|
||||
},
|
||||
"lastSaved": "2025-11-25T16:46:31.4255773+08:00"
|
||||
}
|
||||
BIN
dns-server
Executable file
BIN
dns-server
Executable file
Binary file not shown.
6768
dns-server.log
Normal file
6768
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"`
|
||||
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, "", " ")
|
||||
|
||||
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/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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 从文件加载计数数据
|
||||
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 启动计数数据自动保存功能
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -138,93 +140,194 @@
|
||||
<!-- 统计卡片 -->
|
||||
<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="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 class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<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 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 class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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 class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<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 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 class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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 class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<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 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 class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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 class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<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 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 class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">平均响应时间</h3>
|
||||
<div class="p-2 rounded-full bg-info/10 text-info">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="avg-response-time">0ms</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-down mr-1"></i>
|
||||
<span id="response-time-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最常用查询类型卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-secondary opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">最常用查询类型</h3>
|
||||
<div class="p-2 rounded-full bg-secondary/10 text-secondary">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="top-query-type">A</p>
|
||||
<span class="text-primary text-sm flex items-center">
|
||||
<i class="fa fa-circle text-xs mr-1"></i>
|
||||
<span id="query-type-percentage">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活跃来源IP数卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">活跃来源IP</h3>
|
||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||
<i class="fa fa-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="active-ips">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="active-ips-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU使用率卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">CPU使用率</h3>
|
||||
<div class="p-2 rounded-full bg-warning/10 text-warning">
|
||||
<i class="fa fa-microchip"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="cpu-usage">0%</p>
|
||||
<span class="text-warning text-sm flex items-center">
|
||||
<i class="fa fa-bolt mr-1"></i>
|
||||
<span id="cpu-status">正常</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表和数据表格 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 查询趋势图表 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-2">
|
||||
<!-- DNS请求趋势图表 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
|
||||
<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">
|
||||
<button class="px-3 py-1 text-sm rounded-full bg-primary/10 text-primary">24小时</button>
|
||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">7天</button>
|
||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">30天</button>
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</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="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 class="h-80">
|
||||
<canvas id="query-trend-chart"></canvas>
|
||||
<canvas id="dns-requests-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 解析与屏蔽比例 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
|
||||
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
|
||||
<div class="h-80 flex items-center justify-center">
|
||||
<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,
|
||||
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) {
|
||||
|
||||
@@ -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,85 @@ 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')) {
|
||||
document.getElementById('avg-response-time').textContent = formatNumber(stats.avgResponseTime || 0) + 'ms';
|
||||
document.getElementById('response-time-percent').textContent = (stats.responseTimePercent || 0) + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('top-query-type')) {
|
||||
document.getElementById('top-query-type').textContent = stats.topQueryType || 'A';
|
||||
document.getElementById('query-type-percentage').textContent = (stats.queryTypePercentage || 0) + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('active-ips')) {
|
||||
document.getElementById('active-ips').textContent = formatNumber(stats.activeIPs || 0);
|
||||
document.getElementById('active-ips-percent').textContent = (stats.activeIPsPercent || 0) + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('cpu-usage')) {
|
||||
document.getElementById('cpu-usage').textContent = (stats.cpuUsage || 0) + '%';
|
||||
|
||||
// 设置CPU状态颜色
|
||||
const cpuStatusElem = document.getElementById('cpu-status');
|
||||
if (cpuStatusElem) {
|
||||
const cpuUsage = stats.cpuUsage || 0;
|
||||
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 +141,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 +149,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 +187,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 +228,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 +251,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 +303,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 +413,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();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新运行状态
|
||||
|
||||
@@ -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 = '<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);
|
||||
});
|
||||
// 不再更新表格,避免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('屏蔽规则相关事件监听器已设置,但功能已禁用');
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
|
||||
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