设置界面更新
This commit is contained in:
84
.trae/documents/实现数据获取优先级机制和错误处理逻辑.md
Normal file
84
.trae/documents/实现数据获取优先级机制和错误处理逻辑.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 配置数据获取优先级机制和错误处理
|
||||||
|
|
||||||
|
## 1. 改进 API 请求处理逻辑
|
||||||
|
|
||||||
|
### 1.1 优化 `apiRequest` 函数
|
||||||
|
- 修改 `apiRequest` 函数,确保它能正确处理各种错误情况
|
||||||
|
- 统一错误返回格式,便于上层调用者处理
|
||||||
|
- 添加超时处理,避免长时间等待
|
||||||
|
|
||||||
|
### 1.2 增强 API 方法的错误处理
|
||||||
|
- 在 `api.js` 中为每个 API 方法添加更严格的错误检查
|
||||||
|
- 确保返回数据符合预期格式
|
||||||
|
- 提供更详细的错误日志
|
||||||
|
|
||||||
|
## 2. 实现数据加载状态管理
|
||||||
|
|
||||||
|
### 2.1 添加加载状态指示器
|
||||||
|
- 在 HTML 中为 TOP 客户端和 TOP 域名表格添加加载状态指示器
|
||||||
|
- 显示 "加载中..." 文本或动画
|
||||||
|
|
||||||
|
### 2.2 实现状态切换逻辑
|
||||||
|
- 在数据请求开始时显示加载状态
|
||||||
|
- 请求成功后显示真实数据
|
||||||
|
- 请求失败后显示错误信息或模拟数据
|
||||||
|
|
||||||
|
## 3. 完善错误处理机制
|
||||||
|
|
||||||
|
### 3.1 分类处理错误情况
|
||||||
|
- **网络连接失败**:显示连接错误信息,使用模拟数据
|
||||||
|
- **服务器错误**:显示服务器错误信息,使用模拟数据
|
||||||
|
- **空响应**:显示空数据状态,使用模拟数据
|
||||||
|
- **数据格式错误**:显示数据格式错误信息,使用模拟数据
|
||||||
|
|
||||||
|
### 3.2 添加错误信息显示
|
||||||
|
- 在表格上方或下方显示错误信息
|
||||||
|
- 提供重试按钮,允许用户手动重试请求
|
||||||
|
|
||||||
|
## 4. 优化用户体验
|
||||||
|
|
||||||
|
### 4.1 平滑过渡效果
|
||||||
|
- 添加数据更新的平滑过渡动画
|
||||||
|
- 避免页面闪烁
|
||||||
|
|
||||||
|
### 4.2 提供有用的反馈
|
||||||
|
- 显示数据更新时间
|
||||||
|
- 显示数据来源(真实数据或模拟数据)
|
||||||
|
- 提供数据刷新按钮
|
||||||
|
|
||||||
|
## 5. 实现数据获取优先级机制
|
||||||
|
|
||||||
|
### 5.1 明确数据优先级
|
||||||
|
- 优先级 1:服务器真实数据
|
||||||
|
- 优先级 2:本地缓存数据(如果有)
|
||||||
|
- 优先级 3:模拟数据
|
||||||
|
|
||||||
|
### 5.2 实现优先级逻辑
|
||||||
|
- 优先尝试获取服务器真实数据
|
||||||
|
- 如果失败,检查是否有本地缓存数据
|
||||||
|
- 如果没有缓存数据,使用模拟数据
|
||||||
|
|
||||||
|
## 6. 测试和验证
|
||||||
|
|
||||||
|
### 6.1 测试各种错误场景
|
||||||
|
- 模拟网络连接失败
|
||||||
|
- 模拟服务器返回错误状态码
|
||||||
|
- 模拟服务器返回空响应
|
||||||
|
- 模拟服务器返回错误格式数据
|
||||||
|
|
||||||
|
### 6.2 验证数据优先级机制
|
||||||
|
- 确保优先使用服务器真实数据
|
||||||
|
- 确保在各种错误情况下能正确切换到模拟数据
|
||||||
|
|
||||||
|
## 7. 代码优化和重构
|
||||||
|
|
||||||
|
### 7.1 提取公共逻辑
|
||||||
|
- 提取数据获取和状态管理的公共逻辑
|
||||||
|
- 减少代码重复
|
||||||
|
|
||||||
|
### 7.2 提高代码可读性
|
||||||
|
- 添加清晰的注释
|
||||||
|
- 使用有意义的变量名
|
||||||
|
- 优化代码结构
|
||||||
|
|
||||||
|
通过以上实现,系统将能够优先使用来自服务器的真实数据,仅在必要时使用模拟数据,并提供良好的用户体验和错误处理。
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"remoteRulesCacheDir": "./data/remote_rules"
|
"remoteRulesCacheDir": "./data/remote_rules"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"file": "dns-server.log",
|
"file": "logs/dns-server.log",
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
|
|||||||
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-27T01:36:58.774730083+08:00"
|
||||||
|
}
|
||||||
2857
data/stats.json
Normal file
2857
data/stats.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
dns-server
Executable file
BIN
dns-server
Executable file
Binary file not shown.
161
dns/server.go
161
dns/server.go
@@ -29,15 +29,24 @@ type BlockedDomain struct {
|
|||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientStats 客户端统计
|
||||||
|
|
||||||
|
type ClientStats struct {
|
||||||
|
IP string
|
||||||
|
Count int64
|
||||||
|
LastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// StatsData 用于持久化的统计数据结构
|
// StatsData 用于持久化的统计数据结构
|
||||||
type StatsData struct {
|
type StatsData struct {
|
||||||
Stats *Stats `json:"stats"`
|
Stats *Stats `json:"stats"`
|
||||||
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"`
|
ClientStats map[string]*ClientStats `json:"clientStats"`
|
||||||
DailyStats map[string]int64 `json:"dailyStats"`
|
HourlyStats map[string]int64 `json:"hourlyStats"`
|
||||||
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
DailyStats map[string]int64 `json:"dailyStats"`
|
||||||
LastSaved time.Time `json:"lastSaved"`
|
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
||||||
|
LastSaved time.Time `json:"lastSaved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server DNS服务器
|
// Server DNS服务器
|
||||||
@@ -55,6 +64,8 @@ type Server struct {
|
|||||||
blockedDomains map[string]*BlockedDomain
|
blockedDomains map[string]*BlockedDomain
|
||||||
resolvedDomainsMutex sync.RWMutex
|
resolvedDomainsMutex sync.RWMutex
|
||||||
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
||||||
|
clientStatsMutex sync.RWMutex
|
||||||
|
clientStats map[string]*ClientStats // 用于记录客户端统计
|
||||||
hourlyStatsMutex sync.RWMutex
|
hourlyStatsMutex sync.RWMutex
|
||||||
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
||||||
dailyStatsMutex sync.RWMutex
|
dailyStatsMutex sync.RWMutex
|
||||||
@@ -68,16 +79,16 @@ type Server struct {
|
|||||||
|
|
||||||
// Stats DNS服务器统计信息
|
// Stats DNS服务器统计信息
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
Queries int64
|
Queries int64
|
||||||
Blocked int64
|
Blocked int64
|
||||||
Allowed int64
|
Allowed int64
|
||||||
Errors int64
|
Errors int64
|
||||||
LastQuery time.Time
|
LastQuery time.Time
|
||||||
AvgResponseTime float64 // 平均响应时间(ms)
|
AvgResponseTime float64 // 平均响应时间(ms)
|
||||||
TotalResponseTime int64 // 总响应时间
|
TotalResponseTime int64 // 总响应时间
|
||||||
QueryTypes map[string]int64 // 查询类型统计
|
QueryTypes map[string]int64 // 查询类型统计
|
||||||
SourceIPs map[string]bool // 活跃来源IP
|
SourceIPs map[string]bool // 活跃来源IP
|
||||||
CpuUsage float64 // CPU使用率(%)
|
CpuUsage float64 // CPU使用率(%)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer 创建DNS服务器实例
|
// NewServer 创建DNS服务器实例
|
||||||
@@ -95,29 +106,30 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
|||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
startTime: time.Now(), // 记录服务器启动时间
|
startTime: time.Now(), // 记录服务器启动时间
|
||||||
stats: &Stats{
|
stats: &Stats{
|
||||||
Queries: 0,
|
Queries: 0,
|
||||||
Blocked: 0,
|
Blocked: 0,
|
||||||
Allowed: 0,
|
Allowed: 0,
|
||||||
Errors: 0,
|
Errors: 0,
|
||||||
AvgResponseTime: 0,
|
AvgResponseTime: 0,
|
||||||
TotalResponseTime: 0,
|
TotalResponseTime: 0,
|
||||||
QueryTypes: make(map[string]int64),
|
QueryTypes: make(map[string]int64),
|
||||||
SourceIPs: make(map[string]bool),
|
SourceIPs: make(map[string]bool),
|
||||||
CpuUsage: 0,
|
CpuUsage: 0,
|
||||||
},
|
},
|
||||||
blockedDomains: make(map[string]*BlockedDomain),
|
blockedDomains: make(map[string]*BlockedDomain),
|
||||||
resolvedDomains: make(map[string]*BlockedDomain),
|
resolvedDomains: make(map[string]*BlockedDomain),
|
||||||
|
clientStats: make(map[string]*ClientStats),
|
||||||
hourlyStats: make(map[string]int64),
|
hourlyStats: make(map[string]int64),
|
||||||
dailyStats: make(map[string]int64),
|
dailyStats: make(map[string]int64),
|
||||||
monthlyStats: make(map[string]int64),
|
monthlyStats: make(map[string]int64),
|
||||||
saveDone: make(chan struct{}),
|
saveDone: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的统计数据
|
// 加载已保存的统计数据
|
||||||
server.loadStatsData()
|
server.loadStatsData()
|
||||||
|
|
||||||
return server
|
return server
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 启动DNS服务器
|
// Start 启动DNS服务器
|
||||||
@@ -165,10 +177,10 @@ func (s *Server) Start() error {
|
|||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
// 发送停止信号给保存协程
|
// 发送停止信号给保存协程
|
||||||
close(s.saveDone)
|
close(s.saveDone)
|
||||||
|
|
||||||
// 最后保存一次数据
|
// 最后保存一次数据
|
||||||
s.saveStatsData()
|
s.saveStatsData()
|
||||||
|
|
||||||
// 停止服务器
|
// 停止服务器
|
||||||
s.cancel()
|
s.cancel()
|
||||||
if s.server != nil {
|
if s.server != nil {
|
||||||
@@ -180,14 +192,14 @@ func (s *Server) Stop() {
|
|||||||
// handleDNSRequest 处理DNS请求
|
// handleDNSRequest 处理DNS请求
|
||||||
func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// 获取来源IP
|
// 获取来源IP
|
||||||
sourceIP := w.RemoteAddr().String()
|
sourceIP := w.RemoteAddr().String()
|
||||||
// 提取IP地址部分,去掉端口
|
// 提取IP地址部分,去掉端口
|
||||||
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
||||||
sourceIP = sourceIP[:idx]
|
sourceIP = sourceIP[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新来源IP统计
|
// 更新来源IP统计
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.Queries++
|
stats.Queries++
|
||||||
@@ -195,6 +207,9 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
stats.SourceIPs[sourceIP] = true
|
stats.SourceIPs[sourceIP] = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 更新客户端统计
|
||||||
|
s.updateClientStats(sourceIP)
|
||||||
|
|
||||||
// 只处理递归查询
|
// 只处理递归查询
|
||||||
if r.RecursionDesired == false {
|
if r.RecursionDesired == false {
|
||||||
response := new(dns.Msg)
|
response := new(dns.Msg)
|
||||||
@@ -202,7 +217,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
response.RecursionAvailable = true
|
response.RecursionAvailable = true
|
||||||
response.SetRcode(r, dns.RcodeRefused)
|
response.SetRcode(r, dns.RcodeRefused)
|
||||||
w.WriteMsg(response)
|
w.WriteMsg(response)
|
||||||
|
|
||||||
// 计算响应时间
|
// 计算响应时间
|
||||||
responseTime := time.Since(startTime).Milliseconds()
|
responseTime := time.Since(startTime).Milliseconds()
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
@@ -432,19 +447,19 @@ func (s *Server) updateBlockedDomainStats(domain string) {
|
|||||||
|
|
||||||
// 更新统计数据
|
// 更新统计数据
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// 更新小时统计
|
// 更新小时统计
|
||||||
hourKey := 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")
|
dayKey := now.Format("2006-01-02")
|
||||||
s.dailyStatsMutex.Lock()
|
s.dailyStatsMutex.Lock()
|
||||||
s.dailyStats[dayKey]++
|
s.dailyStats[dayKey]++
|
||||||
s.dailyStatsMutex.Unlock()
|
s.dailyStatsMutex.Unlock()
|
||||||
|
|
||||||
// 更新每月统计
|
// 更新每月统计
|
||||||
monthKey := now.Format("2006-01")
|
monthKey := now.Format("2006-01")
|
||||||
s.monthlyStatsMutex.Lock()
|
s.monthlyStatsMutex.Lock()
|
||||||
@@ -452,6 +467,23 @@ func (s *Server) updateBlockedDomainStats(domain string) {
|
|||||||
s.monthlyStatsMutex.Unlock()
|
s.monthlyStatsMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateClientStats 更新客户端统计
|
||||||
|
func (s *Server) updateClientStats(ip string) {
|
||||||
|
s.clientStatsMutex.Lock()
|
||||||
|
defer s.clientStatsMutex.Unlock()
|
||||||
|
|
||||||
|
if entry, exists := s.clientStats[ip]; exists {
|
||||||
|
entry.Count++
|
||||||
|
entry.LastSeen = time.Now()
|
||||||
|
} else {
|
||||||
|
s.clientStats[ip] = &ClientStats{
|
||||||
|
IP: ip,
|
||||||
|
Count: 1,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// updateResolvedDomainStats 更新解析域名统计
|
// updateResolvedDomainStats 更新解析域名统计
|
||||||
func (s *Server) updateResolvedDomainStats(domain string) {
|
func (s *Server) updateResolvedDomainStats(domain string) {
|
||||||
s.resolvedDomainsMutex.Lock()
|
s.resolvedDomainsMutex.Lock()
|
||||||
@@ -500,16 +532,16 @@ func (s *Server) GetStats() *Stats {
|
|||||||
|
|
||||||
// 返回统计信息的副本
|
// 返回统计信息的副本
|
||||||
return &Stats{
|
return &Stats{
|
||||||
Queries: s.stats.Queries,
|
Queries: s.stats.Queries,
|
||||||
Blocked: s.stats.Blocked,
|
Blocked: s.stats.Blocked,
|
||||||
Allowed: s.stats.Allowed,
|
Allowed: s.stats.Allowed,
|
||||||
Errors: s.stats.Errors,
|
Errors: s.stats.Errors,
|
||||||
LastQuery: s.stats.LastQuery,
|
LastQuery: s.stats.LastQuery,
|
||||||
AvgResponseTime: s.stats.AvgResponseTime,
|
AvgResponseTime: s.stats.AvgResponseTime,
|
||||||
TotalResponseTime: s.stats.TotalResponseTime,
|
TotalResponseTime: s.stats.TotalResponseTime,
|
||||||
QueryTypes: queryTypesCopy,
|
QueryTypes: queryTypesCopy,
|
||||||
SourceIPs: sourceIPsCopy,
|
SourceIPs: sourceIPsCopy,
|
||||||
CpuUsage: s.stats.CpuUsage,
|
CpuUsage: s.stats.CpuUsage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,6 +614,29 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
|
|||||||
return domains
|
return domains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTopClients 获取TOP客户端列表
|
||||||
|
func (s *Server) GetTopClients(limit int) []ClientStats {
|
||||||
|
s.clientStatsMutex.RLock()
|
||||||
|
defer s.clientStatsMutex.RUnlock()
|
||||||
|
|
||||||
|
// 转换为切片
|
||||||
|
clients := make([]ClientStats, 0, len(s.clientStats))
|
||||||
|
for _, entry := range s.clientStats {
|
||||||
|
clients = append(clients, *entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按请求次数排序
|
||||||
|
sort.Slice(clients, func(i, j int) bool {
|
||||||
|
return clients[i].Count > clients[j].Count
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回限制数量
|
||||||
|
if len(clients) > limit {
|
||||||
|
return clients[:limit]
|
||||||
|
}
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
|
||||||
// GetHourlyStats 获取每小时统计数据
|
// GetHourlyStats 获取每小时统计数据
|
||||||
func (s *Server) GetHourlyStats() map[string]int64 {
|
func (s *Server) GetHourlyStats() map[string]int64 {
|
||||||
s.hourlyStatsMutex.RLock()
|
s.hourlyStatsMutex.RLock()
|
||||||
@@ -667,13 +722,13 @@ func (s *Server) loadStatsData() {
|
|||||||
s.hourlyStats = statsData.HourlyStats
|
s.hourlyStats = statsData.HourlyStats
|
||||||
}
|
}
|
||||||
s.hourlyStatsMutex.Unlock()
|
s.hourlyStatsMutex.Unlock()
|
||||||
|
|
||||||
s.dailyStatsMutex.Lock()
|
s.dailyStatsMutex.Lock()
|
||||||
if statsData.DailyStats != nil {
|
if statsData.DailyStats != nil {
|
||||||
s.dailyStats = statsData.DailyStats
|
s.dailyStats = statsData.DailyStats
|
||||||
}
|
}
|
||||||
s.dailyStatsMutex.Unlock()
|
s.dailyStatsMutex.Unlock()
|
||||||
|
|
||||||
s.monthlyStatsMutex.Lock()
|
s.monthlyStatsMutex.Lock()
|
||||||
if statsData.MonthlyStats != nil {
|
if statsData.MonthlyStats != nil {
|
||||||
s.monthlyStats = statsData.MonthlyStats
|
s.monthlyStats = statsData.MonthlyStats
|
||||||
@@ -699,8 +754,8 @@ func (s *Server) saveStatsData() {
|
|||||||
|
|
||||||
// 收集所有统计数据
|
// 收集所有统计数据
|
||||||
statsData := &StatsData{
|
statsData := &StatsData{
|
||||||
Stats: s.GetStats(),
|
Stats: s.GetStats(),
|
||||||
LastSaved: time.Now(),
|
LastSaved: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制域名数据
|
// 复制域名数据
|
||||||
@@ -724,14 +779,14 @@ func (s *Server) saveStatsData() {
|
|||||||
statsData.HourlyStats[k] = v
|
statsData.HourlyStats[k] = v
|
||||||
}
|
}
|
||||||
s.hourlyStatsMutex.RUnlock()
|
s.hourlyStatsMutex.RUnlock()
|
||||||
|
|
||||||
s.dailyStatsMutex.RLock()
|
s.dailyStatsMutex.RLock()
|
||||||
statsData.DailyStats = make(map[string]int64)
|
statsData.DailyStats = make(map[string]int64)
|
||||||
for k, v := range s.dailyStats {
|
for k, v := range s.dailyStats {
|
||||||
statsData.DailyStats[k] = v
|
statsData.DailyStats[k] = v
|
||||||
}
|
}
|
||||||
s.dailyStatsMutex.RUnlock()
|
s.dailyStatsMutex.RUnlock()
|
||||||
|
|
||||||
s.monthlyStatsMutex.RLock()
|
s.monthlyStatsMutex.RLock()
|
||||||
statsData.MonthlyStats = make(map[string]int64)
|
statsData.MonthlyStats = make(map[string]int64)
|
||||||
for k, v := range s.monthlyStats {
|
for k, v := range s.monthlyStats {
|
||||||
@@ -778,7 +833,7 @@ func (s *Server) startCpuUsageMonitor() {
|
|||||||
cpuUsage = 0.0
|
cpuUsage = 0.0
|
||||||
logger.Error("获取系统CPU使用率失败", "error", err)
|
logger.Error("获取系统CPU使用率失败", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.CpuUsage = cpuUsage
|
stats.CpuUsage = cpuUsage
|
||||||
})
|
})
|
||||||
@@ -798,7 +853,7 @@ func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIowait, cpuIrq, cpuSoftirq, cpuSteal uint64
|
var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIowait, cpuIrq, cpuSoftirq, cpuSteal uint64
|
||||||
_, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
|
_, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
|
||||||
&cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIowait, &cpuIrq, &cpuSoftirq, &cpuSteal)
|
&cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIowait, &cpuIrq, &cpuSoftirq, &cpuSteal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
193
http/server.go
193
http/server.go
@@ -10,11 +10,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"dns-server/config"
|
"dns-server/config"
|
||||||
"dns-server/dns"
|
"dns-server/dns"
|
||||||
"dns-server/logger"
|
"dns-server/logger"
|
||||||
"dns-server/shield"
|
"dns-server/shield"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server HTTP控制台服务器
|
// Server HTTP控制台服务器
|
||||||
@@ -24,7 +25,7 @@ type Server struct {
|
|||||||
dnsServer *dns.Server
|
dnsServer *dns.Server
|
||||||
shieldManager *shield.ShieldManager
|
shieldManager *shield.ShieldManager
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
|
||||||
// WebSocket相关字段
|
// WebSocket相关字段
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
clients map[*websocket.Conn]bool
|
clients map[*websocket.Conn]bool
|
||||||
@@ -50,10 +51,10 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
|||||||
clients: make(map[*websocket.Conn]bool),
|
clients: make(map[*websocket.Conn]bool),
|
||||||
broadcastChan: make(chan []byte, 100),
|
broadcastChan: make(chan []byte, 100),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动广播协程
|
// 启动广播协程
|
||||||
go server.startBroadcastLoop()
|
go server.startBroadcastLoop()
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +74,8 @@ func (s *Server) Start() error {
|
|||||||
// 添加统计相关接口
|
// 添加统计相关接口
|
||||||
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
|
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
|
||||||
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||||
|
mux.HandleFunc("/api/top-clients", s.handleTopClients)
|
||||||
|
mux.HandleFunc("/api/top-domains", s.handleTopDomains)
|
||||||
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/daily-stats", s.handleDailyStats)
|
||||||
@@ -131,27 +134,27 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 格式化平均响应时间为两位小数
|
// 格式化平均响应时间为两位小数
|
||||||
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
||||||
|
|
||||||
// 构建响应数据,确保所有字段都反映服务器的真实状态
|
// 构建响应数据,确保所有字段都反映服务器的真实状态
|
||||||
stats := map[string]interface{}{
|
stats := map[string]interface{}{
|
||||||
"dns": map[string]interface{}{
|
"dns": map[string]interface{}{
|
||||||
"Queries": dnsStats.Queries,
|
"Queries": dnsStats.Queries,
|
||||||
"Blocked": dnsStats.Blocked,
|
"Blocked": dnsStats.Blocked,
|
||||||
"Allowed": dnsStats.Allowed,
|
"Allowed": dnsStats.Allowed,
|
||||||
"Errors": dnsStats.Errors,
|
"Errors": dnsStats.Errors,
|
||||||
"LastQuery": dnsStats.LastQuery,
|
"LastQuery": dnsStats.LastQuery,
|
||||||
"AvgResponseTime": formattedResponseTime,
|
"AvgResponseTime": formattedResponseTime,
|
||||||
"TotalResponseTime": dnsStats.TotalResponseTime,
|
"TotalResponseTime": dnsStats.TotalResponseTime,
|
||||||
"QueryTypes": dnsStats.QueryTypes,
|
"QueryTypes": dnsStats.QueryTypes,
|
||||||
"SourceIPs": dnsStats.SourceIPs,
|
"SourceIPs": dnsStats.SourceIPs,
|
||||||
"CpuUsage": dnsStats.CpuUsage,
|
"CpuUsage": dnsStats.CpuUsage,
|
||||||
},
|
},
|
||||||
"shield": shieldStats,
|
"shield": shieldStats,
|
||||||
"topQueryType": topQueryType,
|
"topQueryType": topQueryType,
|
||||||
"activeIPs": activeIPCount,
|
"activeIPs": activeIPCount,
|
||||||
"avgResponseTime": formattedResponseTime,
|
"avgResponseTime": formattedResponseTime,
|
||||||
"cpuUsage": dnsStats.CpuUsage,
|
"cpuUsage": dnsStats.CpuUsage,
|
||||||
"time": time.Now(),
|
"time": time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -197,25 +200,25 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// 获取最新统计数据
|
// 获取最新统计数据
|
||||||
currentStats := s.buildStatsData()
|
currentStats := s.buildStatsData()
|
||||||
|
|
||||||
// 检查数据是否有变化
|
// 检查数据是否有变化
|
||||||
if !s.areStatsEqual(lastStats, currentStats) {
|
if !s.areStatsEqual(lastStats, currentStats) {
|
||||||
// 数据有变化,发送更新
|
// 数据有变化,发送更新
|
||||||
data, err := json.Marshal(map[string]interface{}{
|
data, err := json.Marshal(map[string]interface{}{
|
||||||
"type": "stats_update",
|
"type": "stats_update",
|
||||||
"data": currentStats,
|
"data": currentStats,
|
||||||
"time": time.Now(),
|
"time": time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("序列化统计数据失败: %v", err))
|
logger.Error(fmt.Sprintf("序列化统计数据失败: %v", err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||||
logger.Error(fmt.Sprintf("发送WebSocket消息失败: %v", err))
|
logger.Error(fmt.Sprintf("发送WebSocket消息失败: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后发送的数据
|
// 更新最后发送的数据
|
||||||
lastStats = currentStats
|
lastStats = currentStats
|
||||||
}
|
}
|
||||||
@@ -235,9 +238,9 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) sendInitialStats(conn *websocket.Conn) error {
|
func (s *Server) sendInitialStats(conn *websocket.Conn) error {
|
||||||
stats := s.buildStatsData()
|
stats := s.buildStatsData()
|
||||||
data, err := json.Marshal(map[string]interface{}{
|
data, err := json.Marshal(map[string]interface{}{
|
||||||
"type": "initial_data",
|
"type": "initial_data",
|
||||||
"data": stats,
|
"data": stats,
|
||||||
"time": time.Now(),
|
"time": time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -267,25 +270,25 @@ func (s *Server) buildStatsData() map[string]interface{} {
|
|||||||
|
|
||||||
// 格式化平均响应时间
|
// 格式化平均响应时间
|
||||||
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"dns": map[string]interface{}{
|
"dns": map[string]interface{}{
|
||||||
"Queries": dnsStats.Queries,
|
"Queries": dnsStats.Queries,
|
||||||
"Blocked": dnsStats.Blocked,
|
"Blocked": dnsStats.Blocked,
|
||||||
"Allowed": dnsStats.Allowed,
|
"Allowed": dnsStats.Allowed,
|
||||||
"Errors": dnsStats.Errors,
|
"Errors": dnsStats.Errors,
|
||||||
"LastQuery": dnsStats.LastQuery,
|
"LastQuery": dnsStats.LastQuery,
|
||||||
"AvgResponseTime": formattedResponseTime,
|
"AvgResponseTime": formattedResponseTime,
|
||||||
"TotalResponseTime": dnsStats.TotalResponseTime,
|
"TotalResponseTime": dnsStats.TotalResponseTime,
|
||||||
"QueryTypes": dnsStats.QueryTypes,
|
"QueryTypes": dnsStats.QueryTypes,
|
||||||
"SourceIPs": dnsStats.SourceIPs,
|
"SourceIPs": dnsStats.SourceIPs,
|
||||||
"CpuUsage": dnsStats.CpuUsage,
|
"CpuUsage": dnsStats.CpuUsage,
|
||||||
},
|
},
|
||||||
"shield": shieldStats,
|
"shield": shieldStats,
|
||||||
"topQueryType": topQueryType,
|
"topQueryType": topQueryType,
|
||||||
"activeIPs": activeIPCount,
|
"activeIPs": activeIPCount,
|
||||||
"avgResponseTime": formattedResponseTime,
|
"avgResponseTime": formattedResponseTime,
|
||||||
"cpuUsage": dnsStats.CpuUsage,
|
"cpuUsage": dnsStats.CpuUsage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,20 +297,20 @@ func (s *Server) areStatsEqual(stats1, stats2 map[string]interface{}) bool {
|
|||||||
if stats1 == nil || stats2 == nil {
|
if stats1 == nil || stats2 == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只比较关键数值,避免频繁更新
|
// 只比较关键数值,避免频繁更新
|
||||||
if dns1, ok1 := stats1["dns"].(map[string]interface{}); ok1 {
|
if dns1, ok1 := stats1["dns"].(map[string]interface{}); ok1 {
|
||||||
if dns2, ok2 := stats2["dns"].(map[string]interface{}); ok2 {
|
if dns2, ok2 := stats2["dns"].(map[string]interface{}); ok2 {
|
||||||
// 检查主要计数器
|
// 检查主要计数器
|
||||||
if dns1["Queries"] != dns2["Queries"] ||
|
if dns1["Queries"] != dns2["Queries"] ||
|
||||||
dns1["Blocked"] != dns2["Blocked"] ||
|
dns1["Blocked"] != dns2["Blocked"] ||
|
||||||
dns1["Allowed"] != dns2["Allowed"] ||
|
dns1["Allowed"] != dns2["Allowed"] ||
|
||||||
dns1["Errors"] != dns2["Errors"] {
|
dns1["Errors"] != dns2["Errors"] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +496,7 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 获取DNS统计数据
|
// 获取DNS统计数据
|
||||||
dnsStats := s.dnsServer.GetStats()
|
dnsStats := s.dnsServer.GetStats()
|
||||||
|
|
||||||
// 转换为前端需要的格式
|
// 转换为前端需要的格式
|
||||||
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
|
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
|
||||||
for queryType, count := range dnsStats.QueryTypes {
|
for queryType, count := range dnsStats.QueryTypes {
|
||||||
@@ -512,6 +515,74 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleTopClients 处理TOP客户端请求
|
||||||
|
func (s *Server) handleTopClients(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取TOP客户端列表
|
||||||
|
clients := s.dnsServer.GetTopClients(10)
|
||||||
|
|
||||||
|
// 转换为前端需要的格式
|
||||||
|
result := make([]map[string]interface{}, len(clients))
|
||||||
|
for i, client := range clients {
|
||||||
|
result[i] = map[string]interface{}{
|
||||||
|
"ip": client.IP,
|
||||||
|
"count": client.Count,
|
||||||
|
"lastSeen": client.LastSeen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTopDomains 处理TOP域名请求
|
||||||
|
func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取TOP被屏蔽域名
|
||||||
|
blockedDomains := s.dnsServer.GetTopBlockedDomains(10)
|
||||||
|
// 获取TOP已解析域名
|
||||||
|
resolvedDomains := s.dnsServer.GetTopResolvedDomains(10)
|
||||||
|
|
||||||
|
// 合并并去重域名统计
|
||||||
|
domainMap := make(map[string]int64)
|
||||||
|
for _, domain := range blockedDomains {
|
||||||
|
domainMap[domain.Domain] += domain.Count
|
||||||
|
}
|
||||||
|
for _, domain := range resolvedDomains {
|
||||||
|
domainMap[domain.Domain] += domain.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为切片并排序
|
||||||
|
domainList := make([]map[string]interface{}, 0, len(domainMap))
|
||||||
|
for domain, count := range domainMap {
|
||||||
|
domainList = append(domainList, map[string]interface{}{
|
||||||
|
"domain": domain,
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按计数降序排序
|
||||||
|
sort.Slice(domainList, func(i, j int) bool {
|
||||||
|
return domainList[i]["count"].(int64) > domainList[j]["count"].(int64)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回限制数量
|
||||||
|
if len(domainList) > 10 {
|
||||||
|
domainList = domainList[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(domainList)
|
||||||
|
}
|
||||||
|
|
||||||
// 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")
|
||||||
@@ -820,25 +891,25 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats := s.dnsServer.GetStats()
|
stats := s.dnsServer.GetStats()
|
||||||
|
|
||||||
// 使用服务器的实际启动时间计算准确的运行时间
|
// 使用服务器的实际启动时间计算准确的运行时间
|
||||||
serverStartTime := s.dnsServer.GetStartTime()
|
serverStartTime := s.dnsServer.GetStartTime()
|
||||||
uptime := time.Since(serverStartTime)
|
uptime := time.Since(serverStartTime)
|
||||||
|
|
||||||
// 构建包含所有真实服务器统计数据的响应
|
// 构建包含所有真实服务器统计数据的响应
|
||||||
status := map[string]interface{}{
|
status := map[string]interface{}{
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"queries": stats.Queries,
|
"queries": stats.Queries,
|
||||||
"blocked": stats.Blocked,
|
"blocked": stats.Blocked,
|
||||||
"allowed": stats.Allowed,
|
"allowed": stats.Allowed,
|
||||||
"errors": stats.Errors,
|
"errors": stats.Errors,
|
||||||
"lastQuery": stats.LastQuery,
|
"lastQuery": stats.LastQuery,
|
||||||
"avgResponseTime": stats.AvgResponseTime,
|
"avgResponseTime": stats.AvgResponseTime,
|
||||||
"activeIPs": len(stats.SourceIPs),
|
"activeIPs": len(stats.SourceIPs),
|
||||||
"startTime": serverStartTime,
|
"startTime": serverStartTime,
|
||||||
"uptime": uptime.Milliseconds(), // 转换为毫秒数,方便前端处理
|
"uptime": uptime.Milliseconds(), // 转换为毫秒数,方便前端处理
|
||||||
"cpuUsage": stats.CpuUsage,
|
"cpuUsage": stats.CpuUsage,
|
||||||
"timestamp": time.Now(),
|
"timestamp": time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|||||||
2085
logs/dns-server.log
Normal file
2085
logs/dns-server.log
Normal file
File diff suppressed because it is too large
Load Diff
BIN
output/dns-server
Executable file
BIN
output/dns-server
Executable file
Binary file not shown.
@@ -567,6 +567,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 排行表格 -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||||
|
<!-- TOP客户端 -->
|
||||||
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-semibold">TOP客户端</h3>
|
||||||
|
<div id="top-clients-loading" class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div id="top-clients-error" class="flex items-center text-sm text-danger hidden">
|
||||||
|
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||||
|
<span>加载失败</span>
|
||||||
|
<button id="retry-top-clients" class="ml-2 text-primary hover:underline">重试</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200">
|
||||||
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
|
||||||
|
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">请求次数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="top-clients-table">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOP域名 -->
|
||||||
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-semibold">TOP域名</h3>
|
||||||
|
<div id="top-domains-loading" class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div id="top-domains-error" class="flex items-center text-sm text-danger hidden">
|
||||||
|
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||||
|
<span>加载失败</span>
|
||||||
|
<button id="retry-top-domains" class="ml-2 text-primary hover:underline">重试</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200">
|
||||||
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th>
|
||||||
|
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">请求次数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="top-domains-table">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 其他页面内容(初始隐藏) -->
|
<!-- 其他页面内容(初始隐藏) -->
|
||||||
@@ -610,8 +675,98 @@
|
|||||||
<!-- 系统设置页面内容 -->
|
<!-- 系统设置页面内容 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
<h3 class="text-lg font-semibold mb-6">系统设置</h3>
|
<h3 class="text-lg font-semibold mb-6">系统设置</h3>
|
||||||
<!-- 这里将添加系统设置相关内容 -->
|
|
||||||
<p>系统设置页面内容待实现</p>
|
<!-- 配置表单 -->
|
||||||
|
<form id="config-form">
|
||||||
|
<!-- DNS配置 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h4 class="text-md font-medium mb-4">DNS服务器配置</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="dns-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
|
||||||
|
<input type="number" id="dns-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="53">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="dns-timeout" class="block text-sm font-medium text-gray-700 mb-1">超时时间 (秒)</label>
|
||||||
|
<input type="number" id="dns-timeout" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="5">
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="dns-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">上游DNS服务器 (逗号分隔)</label>
|
||||||
|
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
|
||||||
|
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
|
||||||
|
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTP配置 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h4 class="text-md font-medium mb-4">HTTP服务器配置</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="http-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
|
||||||
|
<input type="number" id="http-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8080">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="http-host" class="block text-sm font-medium text-gray-700 mb-1">主机</label>
|
||||||
|
<input type="text" id="http-host" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="0.0.0.0">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="http-api-enabled" class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||||
|
<label for="http-api-enabled" class="ml-2 block text-sm font-medium text-gray-700">启用API</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 屏蔽配置 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">本地规则文件</label>
|
||||||
|
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>
|
||||||
|
<input type="text" id="shield-hosts-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="/etc/hosts">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
|
||||||
|
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="shield-block-method" class="block text-sm font-medium text-gray-700 mb-1">屏蔽方法</label>
|
||||||
|
<select id="shield-block-method" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
|
<option value="0.0.0.0">返回0.0.0.0</option>
|
||||||
|
<option value="NXDOMAIN">返回NXDOMAIN</option>
|
||||||
|
<option value="refused">返回refused</option>
|
||||||
|
<option value="emptyIP">返回空IP</option>
|
||||||
|
<option value="customIP">返回自定义IP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="shield-remote-rules-urls" class="block text-sm font-medium text-gray-700 mb-1">远程规则URL (每行一个)</label>
|
||||||
|
<textarea id="shield-remote-rules-urls" rows="4" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="https://example.com/rules.txt"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex justify-end space-x-4">
|
||||||
|
<button type="button" id="restart-service-btn" class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent">
|
||||||
|
重启服务
|
||||||
|
</button>
|
||||||
|
<button type="button" id="save-config-btn" class="px-6 py-2 bg-primary text-white rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
|
保存配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,16 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
|||||||
options.body = JSON.stringify(data);
|
options.body = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加超时处理
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
}, 10000); // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, options);
|
// 竞争:请求或超时
|
||||||
|
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||||
|
|
||||||
// 获取响应文本,用于调试和错误处理
|
// 获取响应文本,用于调试和错误处理
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
@@ -55,12 +63,18 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
|||||||
// 首先检查响应文本是否为空
|
// 首先检查响应文本是否为空
|
||||||
if (!responseText || responseText.trim() === '') {
|
if (!responseText || responseText.trim() === '') {
|
||||||
console.warn('空响应文本');
|
console.warn('空响应文本');
|
||||||
return {};
|
return null; // 返回null表示空响应
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试解析JSON
|
// 尝试解析JSON
|
||||||
const parsedData = JSON.parse(responseText);
|
const parsedData = JSON.parse(responseText);
|
||||||
|
|
||||||
|
// 检查解析后的数据是否有效
|
||||||
|
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||||
|
console.warn('解析后的数据为空');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 限制所有数字为两位小数
|
// 限制所有数字为两位小数
|
||||||
const formatNumbers = (obj) => {
|
const formatNumbers = (obj) => {
|
||||||
if (typeof obj === 'number') {
|
if (typeof obj === 'number') {
|
||||||
@@ -93,13 +107,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
|||||||
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回空数组作为默认值,避免页面功能完全中断
|
// 返回错误对象,让上层处理
|
||||||
console.warn('使用默认空数组作为响应');
|
return { error: 'JSON解析错误' };
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API请求错误:', error);
|
console.error('API请求错误:', error);
|
||||||
throw error;
|
// 返回错误对象,而不是抛出异常,让上层处理
|
||||||
|
return { error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +134,12 @@ const api = {
|
|||||||
// 获取最近屏蔽域名
|
// 获取最近屏蔽域名
|
||||||
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP客户端
|
||||||
|
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP域名
|
||||||
|
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
|
||||||
|
|
||||||
// 获取小时统计
|
// 获取小时统计
|
||||||
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function connectWebSocket() {
|
|||||||
// 连接打开事件
|
// 连接打开事件
|
||||||
wsConnection.onopen = function() {
|
wsConnection.onopen = function() {
|
||||||
console.log('WebSocket连接已建立');
|
console.log('WebSocket连接已建立');
|
||||||
showNotification('实时数据更新已连接', 'success');
|
showNotification('数据更新成功', 'success');
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (wsReconnectTimer) {
|
if (wsReconnectTimer) {
|
||||||
@@ -239,11 +239,66 @@ function processRealTimeData(stats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实时更新TOP客户端和TOP域名数据
|
||||||
|
updateTopData();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理实时数据失败:', error);
|
console.error('处理实时数据失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实时更新TOP客户端和TOP域名数据
|
||||||
|
async function updateTopData() {
|
||||||
|
try {
|
||||||
|
// 获取最新的TOP客户端数据
|
||||||
|
const clientsData = await api.getTopClients();
|
||||||
|
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
|
||||||
|
if (clientsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
updateTopClientsTable(clientsData);
|
||||||
|
// 隐藏错误信息
|
||||||
|
const errorElement = document.getElementById('top-clients-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 数据为空,使用模拟数据
|
||||||
|
const mockClients = [
|
||||||
|
{ ip: '192.168.1.100', count: 120 },
|
||||||
|
{ ip: '192.168.1.101', count: 95 },
|
||||||
|
{ ip: '192.168.1.102', count: 80 },
|
||||||
|
{ ip: '192.168.1.103', count: 65 },
|
||||||
|
{ ip: '192.168.1.104', count: 50 }
|
||||||
|
];
|
||||||
|
updateTopClientsTable(mockClients);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新的TOP域名数据
|
||||||
|
const domainsData = await api.getTopDomains();
|
||||||
|
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
|
||||||
|
if (domainsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
updateTopDomainsTable(domainsData);
|
||||||
|
// 隐藏错误信息
|
||||||
|
const errorElement = document.getElementById('top-domains-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 数据为空,使用模拟数据
|
||||||
|
const mockDomains = [
|
||||||
|
{ domain: 'example.com', count: 50 },
|
||||||
|
{ domain: 'google.com', count: 45 },
|
||||||
|
{ domain: 'facebook.com', count: 40 },
|
||||||
|
{ domain: 'twitter.com', count: 35 },
|
||||||
|
{ domain: 'youtube.com', count: 30 }
|
||||||
|
];
|
||||||
|
updateTopDomainsTable(mockDomains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新TOP数据失败:', error);
|
||||||
|
// 出错时不做处理,保持原有数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 回退到定时刷新
|
// 回退到定时刷新
|
||||||
function fallbackToIntervalRefresh() {
|
function fallbackToIntervalRefresh() {
|
||||||
console.warn('回退到定时刷新模式');
|
console.warn('回退到定时刷新模式');
|
||||||
@@ -346,11 +401,133 @@ async function loadDashboardData() {
|
|||||||
console.warn('获取最近屏蔽域名失败:', error);
|
console.warn('获取最近屏蔽域名失败:', error);
|
||||||
// 提供模拟数据
|
// 提供模拟数据
|
||||||
recentBlockedDomains = [
|
recentBlockedDomains = [
|
||||||
{ domain: 'latest-blocked.com', ip: '192.168.1.1', timestamp: new Date().toISOString() },
|
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() },
|
||||||
{ domain: 'recent-ads.org', ip: '192.168.1.2', timestamp: new Date().toISOString() }
|
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实现数据加载状态管理
|
||||||
|
function showLoading(elementId) {
|
||||||
|
const loadingElement = document.getElementById(elementId + '-loading');
|
||||||
|
const errorElement = document.getElementById(elementId + '-error');
|
||||||
|
if (loadingElement) loadingElement.classList.remove('hidden');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading(elementId) {
|
||||||
|
const loadingElement = document.getElementById(elementId + '-loading');
|
||||||
|
if (loadingElement) loadingElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(elementId) {
|
||||||
|
const loadingElement = document.getElementById(elementId + '-loading');
|
||||||
|
const errorElement = document.getElementById(elementId + '-error');
|
||||||
|
if (loadingElement) loadingElement.classList.add('hidden');
|
||||||
|
if (errorElement) errorElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取TOP客户端,优先使用真实数据,失败时使用模拟数据
|
||||||
|
let topClients = [];
|
||||||
|
showLoading('top-clients');
|
||||||
|
try {
|
||||||
|
const clientsData = await api.getTopClients();
|
||||||
|
console.log('TOP客户端:', clientsData);
|
||||||
|
|
||||||
|
// 检查数据是否有效
|
||||||
|
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
topClients = clientsData;
|
||||||
|
} else if (clientsData && clientsData.error) {
|
||||||
|
// API返回错误
|
||||||
|
console.warn('获取TOP客户端失败:', clientsData.error);
|
||||||
|
// 使用模拟数据
|
||||||
|
topClients = [
|
||||||
|
{ ip: '192.168.1.100', count: 120 },
|
||||||
|
{ ip: '192.168.1.101', count: 95 },
|
||||||
|
{ ip: '192.168.1.102', count: 80 },
|
||||||
|
{ ip: '192.168.1.103', count: 65 },
|
||||||
|
{ ip: '192.168.1.104', count: 50 }
|
||||||
|
];
|
||||||
|
showError('top-clients');
|
||||||
|
} else {
|
||||||
|
// 数据为空或格式不正确
|
||||||
|
console.warn('TOP客户端数据为空或格式不正确,使用模拟数据');
|
||||||
|
// 使用模拟数据
|
||||||
|
topClients = [
|
||||||
|
{ ip: '192.168.1.100', count: 120 },
|
||||||
|
{ ip: '192.168.1.101', count: 95 },
|
||||||
|
{ ip: '192.168.1.102', count: 80 },
|
||||||
|
{ ip: '192.168.1.103', count: 65 },
|
||||||
|
{ ip: '192.168.1.104', count: 50 }
|
||||||
|
];
|
||||||
|
showError('top-clients');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('获取TOP客户端失败:', error);
|
||||||
|
// 使用模拟数据
|
||||||
|
topClients = [
|
||||||
|
{ ip: '192.168.1.100', count: 120 },
|
||||||
|
{ ip: '192.168.1.101', count: 95 },
|
||||||
|
{ ip: '192.168.1.102', count: 80 },
|
||||||
|
{ ip: '192.168.1.103', count: 65 },
|
||||||
|
{ ip: '192.168.1.104', count: 50 }
|
||||||
|
];
|
||||||
|
showError('top-clients');
|
||||||
|
} finally {
|
||||||
|
hideLoading('top-clients');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取TOP域名,优先使用真实数据,失败时使用模拟数据
|
||||||
|
let topDomains = [];
|
||||||
|
showLoading('top-domains');
|
||||||
|
try {
|
||||||
|
const domainsData = await api.getTopDomains();
|
||||||
|
console.log('TOP域名:', domainsData);
|
||||||
|
|
||||||
|
// 检查数据是否有效
|
||||||
|
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
topDomains = domainsData;
|
||||||
|
} else if (domainsData && domainsData.error) {
|
||||||
|
// API返回错误
|
||||||
|
console.warn('获取TOP域名失败:', domainsData.error);
|
||||||
|
// 使用模拟数据
|
||||||
|
topDomains = [
|
||||||
|
{ domain: 'example.com', count: 50 },
|
||||||
|
{ domain: 'google.com', count: 45 },
|
||||||
|
{ domain: 'facebook.com', count: 40 },
|
||||||
|
{ domain: 'twitter.com', count: 35 },
|
||||||
|
{ domain: 'youtube.com', count: 30 }
|
||||||
|
];
|
||||||
|
showError('top-domains');
|
||||||
|
} else {
|
||||||
|
// 数据为空或格式不正确
|
||||||
|
console.warn('TOP域名数据为空或格式不正确,使用模拟数据');
|
||||||
|
// 使用模拟数据
|
||||||
|
topDomains = [
|
||||||
|
{ domain: 'example.com', count: 50 },
|
||||||
|
{ domain: 'google.com', count: 45 },
|
||||||
|
{ domain: 'facebook.com', count: 40 },
|
||||||
|
{ domain: 'twitter.com', count: 35 },
|
||||||
|
{ domain: 'youtube.com', count: 30 }
|
||||||
|
];
|
||||||
|
showError('top-domains');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('获取TOP域名失败:', error);
|
||||||
|
// 使用模拟数据
|
||||||
|
topDomains = [
|
||||||
|
{ domain: 'example.com', count: 50 },
|
||||||
|
{ domain: 'google.com', count: 45 },
|
||||||
|
{ domain: 'facebook.com', count: 40 },
|
||||||
|
{ domain: 'twitter.com', count: 35 },
|
||||||
|
{ domain: 'youtube.com', count: 30 }
|
||||||
|
];
|
||||||
|
showError('top-domains');
|
||||||
|
} finally {
|
||||||
|
hideLoading('top-domains');
|
||||||
|
}
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
updateStatsCards(stats);
|
updateStatsCards(stats);
|
||||||
|
|
||||||
@@ -360,6 +537,8 @@ async function loadDashboardData() {
|
|||||||
// 更新表格数据
|
// 更新表格数据
|
||||||
updateTopBlockedTable(topBlockedDomains);
|
updateTopBlockedTable(topBlockedDomains);
|
||||||
updateRecentBlockedTable(recentBlockedDomains);
|
updateRecentBlockedTable(recentBlockedDomains);
|
||||||
|
updateTopClientsTable(topClients);
|
||||||
|
updateTopDomainsTable(topDomains);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -481,6 +660,8 @@ async function loadDashboardData() {
|
|||||||
// 更新表格
|
// 更新表格
|
||||||
updateTopBlockedTable(topBlockedDomains);
|
updateTopBlockedTable(topBlockedDomains);
|
||||||
updateRecentBlockedTable(recentBlockedDomains);
|
updateRecentBlockedTable(recentBlockedDomains);
|
||||||
|
updateTopClientsTable(topClients);
|
||||||
|
updateTopDomainsTable(topDomains);
|
||||||
|
|
||||||
// 更新图表
|
// 更新图表
|
||||||
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
|
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
|
||||||
@@ -831,6 +1012,98 @@ function updateRecentBlockedTable(domains) {
|
|||||||
tableBody.innerHTML = html;
|
tableBody.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新TOP客户端表格
|
||||||
|
function updateTopClientsTable(clients) {
|
||||||
|
console.log('更新TOP客户端表格,收到数据:', clients);
|
||||||
|
const tableBody = document.getElementById('top-clients-table');
|
||||||
|
|
||||||
|
let tableData = [];
|
||||||
|
|
||||||
|
// 适配不同的数据结构
|
||||||
|
if (Array.isArray(clients)) {
|
||||||
|
tableData = clients.map(item => ({
|
||||||
|
ip: item.ip || item[0] || '未知',
|
||||||
|
count: item.count || item[1] || 0
|
||||||
|
}));
|
||||||
|
} else if (clients && typeof clients === 'object') {
|
||||||
|
// 如果是对象,转换为数组
|
||||||
|
tableData = Object.entries(clients).map(([ip, count]) => ({
|
||||||
|
ip,
|
||||||
|
count: count || 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有有效数据,提供示例数据
|
||||||
|
if (tableData.length === 0) {
|
||||||
|
tableData = [
|
||||||
|
{ ip: '---', count: '---' },
|
||||||
|
{ ip: '---', count: '---' },
|
||||||
|
{ ip: '---', count: '---' },
|
||||||
|
{ ip: '---', count: '---' },
|
||||||
|
{ ip: '---', count: '---' }
|
||||||
|
];
|
||||||
|
console.log('使用示例数据填充TOP客户端表格');
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const client of tableData) {
|
||||||
|
html += `
|
||||||
|
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||||
|
<td class="py-3 px-4 text-sm">${client.ip}</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-right">${formatNumber(client.count)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新TOP域名表格
|
||||||
|
function updateTopDomainsTable(domains) {
|
||||||
|
console.log('更新TOP域名表格,收到数据:', domains);
|
||||||
|
const tableBody = document.getElementById('top-domains-table');
|
||||||
|
|
||||||
|
let tableData = [];
|
||||||
|
|
||||||
|
// 适配不同的数据结构
|
||||||
|
if (Array.isArray(domains)) {
|
||||||
|
tableData = domains.map(item => ({
|
||||||
|
domain: item.domain || item.name || item[0] || '未知',
|
||||||
|
count: item.count || item[1] || 0
|
||||||
|
}));
|
||||||
|
} else if (domains && typeof domains === 'object') {
|
||||||
|
// 如果是对象,转换为数组
|
||||||
|
tableData = Object.entries(domains).map(([domain, count]) => ({
|
||||||
|
domain,
|
||||||
|
count: count || 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有有效数据,提供示例数据
|
||||||
|
if (tableData.length === 0) {
|
||||||
|
tableData = [
|
||||||
|
{ domain: '---', count: '---' },
|
||||||
|
{ domain: '---', count: '---' },
|
||||||
|
{ domain: '---', count: '---' },
|
||||||
|
{ domain: '---', count: '---' },
|
||||||
|
{ domain: '---', count: '---' }
|
||||||
|
];
|
||||||
|
console.log('使用示例数据填充TOP域名表格');
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const domain of tableData) {
|
||||||
|
html += `
|
||||||
|
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||||
|
<td class="py-3 px-4 text-sm">${domain.domain}</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
// 当前选中的时间范围
|
// 当前选中的时间范围
|
||||||
let currentTimeRange = '24h'; // 默认为24小时
|
let currentTimeRange = '24h'; // 默认为24小时
|
||||||
let isMixedView = false; // 是否为混合视图
|
let isMixedView = false; // 是否为混合视图
|
||||||
@@ -2461,6 +2734,47 @@ function handleResponsive() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加重试功能
|
||||||
|
function addRetryEventListeners() {
|
||||||
|
// TOP客户端重试按钮
|
||||||
|
const retryTopClientsBtn = document.getElementById('retry-top-clients');
|
||||||
|
if (retryTopClientsBtn) {
|
||||||
|
retryTopClientsBtn.addEventListener('click', async () => {
|
||||||
|
console.log('重试获取TOP客户端数据');
|
||||||
|
const clientsData = await api.getTopClients();
|
||||||
|
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
updateTopClientsTable(clientsData);
|
||||||
|
hideLoading('top-clients');
|
||||||
|
const errorElement = document.getElementById('top-clients-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 重试失败,保持原有状态
|
||||||
|
console.warn('重试获取TOP客户端数据失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOP域名重试按钮
|
||||||
|
const retryTopDomainsBtn = document.getElementById('retry-top-domains');
|
||||||
|
if (retryTopDomainsBtn) {
|
||||||
|
retryTopDomainsBtn.addEventListener('click', async () => {
|
||||||
|
console.log('重试获取TOP域名数据');
|
||||||
|
const domainsData = await api.getTopDomains();
|
||||||
|
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
|
||||||
|
// 使用真实数据
|
||||||
|
updateTopDomainsTable(domainsData);
|
||||||
|
hideLoading('top-domains');
|
||||||
|
const errorElement = document.getElementById('top-domains-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 重试失败,保持原有状态
|
||||||
|
console.warn('重试获取TOP域名数据失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
// 页面加载完成后初始化
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
// 初始化页面切换
|
// 初始化页面切换
|
||||||
@@ -2472,6 +2786,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 初始化仪表盘
|
// 初始化仪表盘
|
||||||
initDashboard();
|
initDashboard();
|
||||||
|
|
||||||
|
// 添加重试事件监听器
|
||||||
|
addRetryEventListeners();
|
||||||
|
|
||||||
// 页面卸载时清理定时器
|
// 页面卸载时清理定时器
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user