diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9a15e..6a261b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,13 +103,13 @@ ### 修改 - 在forwardDNSRequestWithCache函数中添加域名匹配逻辑,检查域名是否包含不验证DNSSEC的模式 -- 在所有查询模式(parallel、loadbalance、fastest-ip、default)中实现跳过DNSSEC验证的功能 +- 在所有查询模式(parallel、fastest-ip、default)中实现跳过DNSSEC验证的功能 ## [1.1.1] - 2025-12-19 ### 修改 - 修复NXDOMAIN响应传播逻辑,确保上游DNS服务器返回的NXDOMAIN响应能正确传递给客户端 -- 优化loadbalance、fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制 +- 优化fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制 - 确保不存在的域名能被正确识别并返回NXDOMAIN状态码 - 修复服务器绑定地址配置,确保IPv4兼容性 diff --git a/build.sh b/build.sh index 6048767..86d1f51 100755 --- a/build.sh +++ b/build.sh @@ -2,4 +2,5 @@ CGO_ENABLED=1 \ GOOS=linux \ GOARCH=amd64 \ CC=gcc \ -go build -ldflags "-linkmode external -extldflags '-static -pthread'" -o dns-server main.go +go build -o dns-server main.go && service dns-server restart + diff --git a/config.json b/config.json index f126edf..ac465a6 100644 --- a/config.json +++ b/config.json @@ -2,19 +2,16 @@ "dns": { "port": 53, "upstreamDNS": [ - "223.5.5.5" + "10.35.10.200" ], "dnssecUpstreamDNS": [ - "117.50.10.10", - "101.226.4.6", - "218.30.118.6", "208.67.220.220", "208.67.222.222" ], "saveInterval": 30, - "cacheTTL": 10, + "cacheTTL": 60, "enableDNSSEC": true, - "queryMode": "parallel", + "queryMode": "fastest-ip", "queryTimeout": 500, "enableFastReturn": true, "domainSpecificDNS": { @@ -146,7 +143,7 @@ "statsSaveInterval": 60 }, "log": { - "level": "debug", + "level": "info", "maxSize": 100, "maxBackups": 10, "maxAge": 30 diff --git a/config/config.go b/config/config.go index 21171b2..b74f9e1 100644 --- a/config/config.go +++ b/config/config.go @@ -20,7 +20,7 @@ type DNSConfig struct { SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒) CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟) EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持 - QueryMode string `json:"queryMode"` // 查询模式:"loadbalance"(负载均衡)、"parallel"(并行请求)、"fastest-ip"(最快的IP地址) + QueryMode string `json:"queryMode"` // 查询模式:"parallel"(并行请求)、"fastest-ip"(最快的IP地址) QueryTimeout int `json:"queryTimeout"` // 查询超时时间(毫秒) EnableFastReturn bool `json:"enableFastReturn"` // 是否启用快速返回机制 DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置 @@ -50,9 +50,9 @@ type BlacklistEntry struct { type ShieldConfig struct { Blacklists []BlacklistEntry `json:"blacklists"` UpdateInterval int `json:"updateInterval"` - BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP" - CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用 - StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒) + BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP" + CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用 + StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒) } // LogConfig 日志配置 diff --git a/dns-server b/dns-server deleted file mode 100755 index f4222c9..0000000 Binary files a/dns-server and /dev/null differ diff --git a/dns/cache.go b/dns/cache.go index f81a103..6b15904 100644 --- a/dns/cache.go +++ b/dns/cache.go @@ -9,9 +9,9 @@ import ( // DNSCacheItem 表示缓存中的DNS响应项 type DNSCacheItem struct { - Response *dns.Msg // DNS响应消息 - Expiry time.Time // 过期时间 - HasDNSSEC bool // 是否包含DNSSEC记录 + Response *dns.Msg // DNS响应消息 + Expiry time.Time // 过期时间 + HasDNSSEC bool // 是否包含DNSSEC记录 } // DNSCache DNS缓存结构 @@ -21,7 +21,7 @@ type DNSCache struct { defaultTTL time.Duration // 默认缓存TTL maxSize int // 最大缓存条目数 // 使用链表结构来跟踪缓存条目的访问顺序,用于LRU淘汰 - accessList []string // 记录访问顺序,最新访问的放在最后 + accessList []string // 记录访问顺序,最新访问的放在最后 } // NewDNSCache 创建新的DNS缓存实例 @@ -109,8 +109,8 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D key := cacheKey(qName, qType) item := &DNSCacheItem{ - Response: response.Copy(), // 复制响应以避免外部修改 - Expiry: time.Now().Add(ttl), + Response: response.Copy(), // 复制响应以避免外部修改 + Expiry: time.Now().Add(ttl), HasDNSSEC: hasDNSSECRecords(response), // 检查并设置DNSSEC标志 } diff --git a/dns/server.go b/dns/server.go index 857782a..3d82a7b 100644 --- a/dns/server.go +++ b/dns/server.go @@ -48,8 +48,6 @@ type ClientStats struct { LastSeen time.Time } - - // DNSAnswer DNS解析记录 type DNSAnswer struct { Type string `json:"type"` // 记录类型 @@ -131,8 +129,6 @@ type Server struct { stopped bool // 服务器是否已经停止 stoppedMutex sync.Mutex // 保护stopped标志的互斥锁 - - // DNS查询缓存 DnsCache *DNSCache // DNS响应缓存 @@ -205,7 +201,7 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie maxQueryLogs: 10000, // 最大保存10000条日志 saveDone: make(chan struct{}), stopped: false, // 初始化为未停止状态 - + // DNS查询缓存初始化 DnsCache: NewDNSCache(cacheTTL), // 初始化域名DNSSEC状态映射表 @@ -258,8 +254,6 @@ func (s *Server) Start() error { // 启动自动保存功能 go s.startAutoSave() - - // 启动UDP服务 go func() { logger.Info(fmt.Sprintf("DNS UDP服务器启动,监听端口: %d", s.config.Port)) @@ -856,6 +850,29 @@ func mergeResponses(responses []*dns.Msg) *dns.Msg { mergedResponse.Ns = []dns.RR{} mergedResponse.Extra = []dns.RR{} + // 重置Rcode为成功,除非所有响应都是NXDOMAIN + mergedResponse.Rcode = dns.RcodeSuccess + + // 检查是否所有响应都是NXDOMAIN + allNXDOMAIN := true + + // 收集所有成功响应的记录 + for _, resp := range responses { + if resp == nil { + continue + } + + // 如果有任何响应是成功的,就不是allNXDOMAIN + if resp.Rcode == dns.RcodeSuccess { + allNXDOMAIN = false + } + } + + // 如果所有响应都是NXDOMAIN,设置合并响应为NXDOMAIN + if allNXDOMAIN { + mergedResponse.Rcode = dns.RcodeNameError + } + // 使用map存储唯一记录,选择最长TTL // 预分配map容量,减少扩容开销 answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses)) @@ -867,46 +884,51 @@ func mergeResponses(responses []*dns.Msg) *dns.Msg { continue } - // 合并Answer部分 - for _, rr := range resp.Answer { - key := getRecordKey(rr) - if existing, exists := answerMap[key]; exists { - // 如果存在相同记录,选择TTL更长的 - if rr.Header().Ttl > existing.Header().Ttl { + // 只合并与最终Rcode匹配的响应记录 + if (mergedResponse.Rcode == dns.RcodeSuccess && resp.Rcode == dns.RcodeSuccess) || + (mergedResponse.Rcode == dns.RcodeNameError && resp.Rcode == dns.RcodeNameError) { + + // 合并Answer部分 + for _, rr := range resp.Answer { + key := getRecordKey(rr) + if existing, exists := answerMap[key]; exists { + // 如果存在相同记录,选择TTL更长的 + if rr.Header().Ttl > existing.Header().Ttl { + answerMap[key] = rr + } + } else { answerMap[key] = rr } - } else { - answerMap[key] = rr } - } - // 合并Ns部分 - for _, rr := range resp.Ns { - key := getRecordKey(rr) - if existing, exists := nsMap[key]; exists { - // 如果存在相同记录,选择TTL更长的 - if rr.Header().Ttl > existing.Header().Ttl { + // 合并Ns部分 + for _, rr := range resp.Ns { + key := getRecordKey(rr) + if existing, exists := nsMap[key]; exists { + // 如果存在相同记录,选择TTL更长的 + if rr.Header().Ttl > existing.Header().Ttl { + nsMap[key] = rr + } + } else { nsMap[key] = rr } - } else { - nsMap[key] = rr } - } - // 合并Extra部分 - for _, rr := range resp.Extra { - // 跳过OPT记录,避免重复 - if rr.Header().Rrtype == dns.TypeOPT { - continue - } - key := getRecordKey(rr) - if existing, exists := extraMap[key]; exists { - // 如果存在相同记录,选择TTL更长的 - if rr.Header().Ttl > existing.Header().Ttl { + // 合并Extra部分 + for _, rr := range resp.Extra { + // 跳过OPT记录,避免重复 + if rr.Header().Rrtype == dns.TypeOPT { + continue + } + key := getRecordKey(rr) + if existing, exists := extraMap[key]; exists { + // 如果存在相同记录,选择TTL更长的 + if rr.Header().Ttl > existing.Header().Ttl { + extraMap[key] = rr + } + } else { extraMap[key] = rr } - } else { - extraMap[key] = rr } } } @@ -1036,14 +1058,21 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg responses := make(chan serverResponse, len(selectedUpstreamDNS)) var wg sync.WaitGroup - // 向所有上游服务器并行发送请求 + // 向所有上游服务器并行发送请求,每个请求带有超时 for _, upstream := range selectedUpstreamDNS { wg.Add(1) go func(server string) { defer wg.Done() + // 创建带有超时的resolver + client := &dns.Client{ + Net: s.resolver.Net, + UDPSize: s.resolver.UDPSize, + Timeout: defaultTimeout, + } + // 发送请求并获取响应,确保服务器地址包含端口号 - response, rtt, err := s.resolver.Exchange(r, normalizeDNSServerAddress(server)) + response, rtt, err := client.Exchange(r, normalizeDNSServerAddress(server)) responses <- serverResponse{response, rtt, server, err} }(upstream) } @@ -1054,8 +1083,9 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg close(responses) }() - // 收集所有有效响应 - var validResponses []*dns.Msg + // 收集成功响应和NXDOMAIN响应分开 + var successResponses []*dns.Msg + var nxdomainResponses []*dns.Msg var totalRtt time.Duration var responseCount int @@ -1087,9 +1117,9 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg } } - // 收集有效响应 - if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError { - validResponses = append(validResponses, resp.response) + // 收集响应,按Rcode分类 + if resp.response.Rcode == dns.RcodeSuccess { + successResponses = append(successResponses, resp.response) totalRtt += resp.rtt responseCount++ @@ -1097,6 +1127,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg if usedDNSServer == "" { usedDNSServer = resp.server } + } else if resp.response.Rcode == dns.RcodeNameError { + nxdomainResponses = append(nxdomainResponses, resp.response) } else { // 更新备选响应,确保总有一个可用的响应 if resp.response != nil { @@ -1114,6 +1146,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg } } + // 合并响应:优先使用成功响应,只有当没有成功响应时才使用NXDOMAIN响应 + var validResponses []*dns.Msg + if len(successResponses) > 0 { + validResponses = successResponses + } else { + validResponses = nxdomainResponses + } + // 合并所有有效响应 if len(validResponses) > 0 { bestResponse = mergeResponses(validResponses) @@ -1121,11 +1161,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg bestRtt = totalRtt / time.Duration(responseCount) } hasBestResponse = true - logger.Debug("合并所有响应返回", "domain", domain, "responseCount", len(validResponses)) + // 设置日志的type字段 + logType := "success" + if len(successResponses) == 0 { + logType = "nxdomain" + } + logger.Debug("合并所有响应返回", "domain", domain, "responseCount", len(validResponses), "type", logType) } - - case "fastest-ip": // 最快的IP地址模式 - 使用TCP连接速度测量选择最快服务器 // 1. 选择最快的服务器 @@ -1279,7 +1322,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg var fastestServer string var fastestDnssecServer string var fastestHasDnssec bool - var validResponses []*dns.Msg + var successResponses []*dns.Msg + var nxdomainResponses []*dns.Msg // 等待所有请求完成或超时 timer := time.NewTimer(defaultTimeout) @@ -1301,30 +1345,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // 检查是否包含DNSSEC记录 containsDNSSEC := s.hasDNSSECRecords(resp.response) - // 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名 - // 但如果域名匹配不验证DNSSEC的模式,则跳过验证 - if s.config.EnableDNSSEC && containsDNSSEC && !noDNSSEC { - // 验证DNSSEC记录 - signatureValid := s.verifyDNSSEC(resp.response) - - // 设置AD标志(Authenticated Data) - resp.response.AuthenticatedData = signatureValid - - if signatureValid { - // 更新DNSSEC验证成功计数 - s.updateStats(func(stats *Stats) { - stats.DNSSECQueries++ - stats.DNSSECSuccess++ - }) - } else { - // 更新DNSSEC验证失败计数 - s.updateStats(func(stats *Stats) { - stats.DNSSECQueries++ - stats.DNSSECFailed++ - }) - } - } else if noDNSSEC { - // 对于不验证DNSSEC的域名,始终设置AD标志为false + // 对于不验证DNSSEC的域名,始终设置AD标志为false + if noDNSSEC { resp.response.AuthenticatedData = false } @@ -1339,8 +1361,12 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg // 如果响应成功或为NXDOMAIN if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError { - // 添加到有效响应列表,用于后续合并 - validResponses = append(validResponses, resp.response) + // 按Rcode分类添加到不同列表 + if resp.response.Rcode == dns.RcodeSuccess { + successResponses = append(successResponses, resp.response) + } else { + nxdomainResponses = append(nxdomainResponses, resp.response) + } // 快速返回逻辑:找到第一个有效响应或更快的响应 if resp.response.Rcode == dns.RcodeSuccess { @@ -1493,6 +1519,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg } doneProcessing: + // 合并响应,优先使用成功响应,只有当没有成功响应时才使用NXDOMAIN响应 + var validResponses []*dns.Msg + if len(successResponses) > 0 { + validResponses = successResponses + } else { + validResponses = nxdomainResponses + } + // 合并所有有效响应,用于缓存 if len(validResponses) > 1 { mergedResponse := mergeResponses(validResponses) @@ -2255,7 +2289,6 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in log := QueryLog{ Timestamp: time.Now(), ClientIP: clientIP, - Location: "", // 客户端IP地理位置由前端处理 Domain: domain, QueryType: queryType, ResponseTime: responseTime, @@ -2644,8 +2677,6 @@ func isPrivateIP(ip string) bool { return false } - - // loadStatsData 从文件加载统计数据 func (s *Server) loadStatsData() { // 检查文件是否存在 @@ -2908,10 +2939,6 @@ func (s *Server) startCpuUsageMonitor() { } } - - - - // getSystemCpuUsage 获取系统CPU使用率 func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) { // 读取/proc/stat文件获取CPU统计信息 diff --git a/download.sh b/download.sh deleted file mode 100755 index 9b10210..0000000 --- a/download.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e -f -u -x - -# This script syncs companies DB that we bundle with AdGuard Home. The source -# for this database is https://github.com/AdguardTeam/companiesdb. -# -trackers_url='https://raw.githubusercontent.com/AdguardTeam/companiesdb/main/dist/trackers.json' -output='./trackers.json' -readonly trackers_url output - -curl -o "$output" -v "$trackers_url" diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 8f098a2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "dns-server-console", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "detect-file-encoding-and-language": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/detect-file-encoding-and-language/-/detect-file-encoding-and-language-2.4.0.tgz", - "integrity": "sha512-moFSAumrGlLCNU5jnaHyCzRUJJu0BCZunfL08iMbnDAgvNnxZad7+WZ26U2dsrIbGChlDPLKmEyEb2tEPUJFkw==" - }, - "json-stream-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-stream-parser/-/json-stream-parser-1.3.0.tgz", - "integrity": "sha512-AF3Dm4h7lSvRRMIXJsp6pOVrVl9GeHCw5xORxaPZlJN0PdBM/dyx0qQZEK+CI4UGH9/PX3tphWdqrtvQ1txMzw==" - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - } - } -} diff --git a/static/css/style.css b/static/css/style.css index 1bdb257..cbd29e4 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -569,6 +569,71 @@ header p { -webkit-overflow-scrolling: touch; /* iOS平滑滚动 */ } +/* 列宽调节样式 */ +.resizable-table { + width: 100%; + table-layout: auto; + border-collapse: collapse; + overflow: hidden; +} + +.resizable-table th { + position: relative; + padding: 0.75rem 1rem; + background-color: #f8f9fa; + font-weight: 600; + color: #2c3e50; + border-bottom: 1px solid #e9ecef; + cursor: pointer; + user-select: none; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 50px; +} + +.resizable-table th::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 10px; + height: 100%; + cursor: col-resize; + background: transparent; + z-index: 10; + margin-right: -10px; +} + +.resizable-table th:hover::after { + background: rgba(59, 130, 246, 0.3); +} + +.resizable-table th.dragging { + cursor: col-resize; +} + +.resizable-table th.dragging::after { + background: rgba(59, 130, 246, 0.6); +} + +.resizable-table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid #e9ecef; + word-break: break-word; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 50px; +} + +/* 确保表格容器正确显示 */ +.overflow-x-auto { + overflow-x: auto; + position: relative; +} + /* 最常屏蔽和最常解析域名表格的特殊样式 */ #top-blocked-table, #top-resolved-table { font-size: 0.85rem; @@ -1154,4 +1219,75 @@ tr:hover { .tracker-tooltip a:hover { text-decoration: underline; +} + +/* 滚动条样式优化 */ +/* 基础滚动条样式 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +/* 滚动条轨道 */ +::-webkit-scrollbar-track { + background: rgba(241, 245, 249, 0.5); + border-radius: 4px; +} + +/* 滚动条滑块 */ +::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.6); + border-radius: 4px; + transition: all 0.3s ease; +} + +/* 滚动条滑块悬停效果 */ +::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.8); +} + +/* 滚动条角落 */ +::-webkit-scrollbar-corner { + background: rgba(241, 245, 249, 0.5); + border-radius: 4px; +} + +/* 为不同滚动容器添加特定样式 */ +.sidebar::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.4); +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: rgba(148, 163, 184, 0.7); +} + +/* 优化表格和卡片中的滚动条 */ +.table-wrapper::-webkit-scrollbar, +.chart-card::-webkit-scrollbar, +.stat-card::-webkit-scrollbar, +#top-blocked-table::-webkit-scrollbar, +#top-resolved-table::-webkit-scrollbar, +#top-clients-table::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.table-wrapper::-webkit-scrollbar-thumb, +.chart-card::-webkit-scrollbar-thumb, +.stat-card::-webkit-scrollbar-thumb, +#top-blocked-table::-webkit-scrollbar-thumb, +#top-resolved-table::-webkit-scrollbar-thumb, +#top-clients-table::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.5); +} + +/* Firefox滚动条样式 */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(148, 163, 184, 0.6) rgba(241, 245, 249, 0.5); +} + +/* 优化滚动行为 */ +* { + scroll-behavior: smooth; } \ No newline at end of file diff --git a/static/domain-info/domains/domain-info.json b/static/domain-info/domains/domain-info.json index f5581fd..3e3d594 100644 --- a/static/domain-info/domains/domain-info.json +++ b/static/domain-info/domains/domain-info.json @@ -64,6 +64,108 @@ }, "company": "网易股份有限公司" }, + "华为":{ + "华为全球官网": { + "name": "华为全球官网", + "categoryId": 0, + "url": "www.huawei.com", + "icon": "https://www.huawei.com/-/media/htemplate-home/1.0.1.20251205144752/components/assets/img/favicon-logo.svg" + }, + "华为中国官网": { + "name": "华为中国官网", + "categoryId": 0, + "url": "www.huawei.com.cn", + "icon": "https://www.huawei.com.cn/favicon.ico" + }, + "华为账号认证中心": { + "name": "华为账号认证中心", + "categoryId": 0, + "url": "account.cloud.huawei.com", + "icon": "https://account.cloud.huawei.com/favicon.ico" + }, + "华为云CDN根域名": { + "name": "华为云CDN根域名", + "categoryId": 2, + "url": "myhwcdn.cn", + "icon": "https://www.huaweicloud.com/favicon.ico" + }, + "华为应用市场CDN分发": { + "name": "华为应用市场CDN分发", + "categoryId": 2, + "url": "dbankcdn.com", + "icon": "https://appgallery.huawei.com/favicon.ico" + }, + "华为云静态资源CDN": { + "name": "华为云静态资源CDN", + "categoryId": 2, + "url": "huaweicloud.com", + "icon": "https://www.huaweicloud.com/favicon.ico" + }, + "华为新闻中心": { + "name": "华为新闻中心", + "categoryId": 3, + "url": "news.huawei.com", + "icon": "https://www.huawei.com/-/media/htemplate-home/1.0.1.20251205144752/components/assets/img/favicon-logo.svg" + }, + "华为商城官网": { + "name": "华为商城官网", + "categoryId": 4, + "url": "www.vmall.com", + "icon": "https://www.vmall.com/favicon.ico" + }, + "华为花粉俱乐部社区": { + "name": "华为花粉俱乐部社区", + "categoryId": 11, + "url": "club.huawei.com", + "icon": "https://club.huawei.com/favicon.ico" + }, + "华为地图服务": { + "name": "华为地图服务", + "categoryId": 12, + "url": "mapkit.huawei.com", + "icon": "https://developer.huawei.com/favicon.ico" + }, + "华为云邮箱服务": { + "name": "华为云邮箱服务", + "categoryId": 19, + "url": "mail.huaweicloud.com", + "icon": "https://www.huaweicloud.com/favicon.ico" + }, + "华为云服务平台": { + "name": "华为云服务平台", + "categoryId": 20, + "url": "www.huaweicloud.com", + "icon": "https://www.huaweicloud.com/favicon.ico" + }, + "华为开发者平台": { + "name": "华为开发者平台", + "categoryId": 24, + "url": "developer.huawei.com", + "icon": "https://developer.huawei.com/favicon.ico" + }, + "华为支付平台": { + "name": "华为支付平台", + "categoryId": 23, + "url": "pay.huawei.com", + "icon": "https://www.huawei.com/-/media/htemplate-home/1.0.1.20251205144752/components/assets/img/favicon-logo.svg" + }, + "华为鸿蒙开发者社区": { + "name": "华为鸿蒙开发者社区", + "categoryId": 11, + "url": { + "1": "harmonyos.com", + "2": "www.harmonyos.com" + }, + "icon": "https://www.harmonyos.com/assets/image/favicon.ico?v=20240307" + }, + "华为视频平台": { + "name": "华为视频平台", + "categoryId": 8, + "url": "video.huawei.com", + "icon": "https://www.huawei.com/-/media/htemplate-home/1.0.1.20251205144752/components/assets/img/favicon-logo.svg" + }, + "company": "华为技术有限公司" + }, "百度": { "百度搜索": { "name": "百度搜索", @@ -91,6 +193,64 @@ }, "company": "北京百度网讯科技有限公司" }, + "谷歌": { + "谷歌账号认证": { + "name": "谷歌账号认证", + "categoryId": 0, + "url": "accounts.google.com", + "icon": "https://accounts.google.com/favicon.ico" + }, + "谷歌搜索": { + "name": "谷歌搜索引擎", + "categoryId": 0, + "url": "www.google.com", + "icon": "https://www.google.com/favicon.ico" + }, + "谷歌CDN资源分发": { + "name": "谷歌CDN资源分发", + "categoryId": 2, + "url": "clients2.googleusercontent.com", + "icon": "https://www.google.com/favicon.ico" + }, + "谷歌CDN内容托管根域名": { + "name": "谷歌CDN内容托管根域名", + "categoryId": 2, + "url": "googleusercontent.com", + "icon": "https://www.google.com/favicon.ico" + }, + "谷歌搜索引擎": { + "name": "谷歌搜索引擎", + "categoryId": 1, + "url": "search.google.com", + "icon": "https://www.google.com/favicon.ico" + }, + "谷歌视频平台YouTube": { + "name": "谷歌视频平台YouTube", + "categoryId": 8, + "url": "www.youtube.com", + "icon": "https://www.youtube.com/favicon.ico" + }, + "谷歌云服务平台": { + "name": "谷歌云服务平台", + "categoryId": 20, + "url": "cloud.google.com", + "icon": "https://cloud.google.com/favicon.ico" + }, + "谷歌开发者平台": { + "name": "谷歌开发者平台", + "categoryId": 20, + "url": "developers.google.com", + "icon": "https://developers.google.com/favicon.ico" + }, + "谷歌Chrome浏览器更新": { + "name": "谷歌Chrome浏览器更新", + "categoryId": 2, + "url": "dl.google.com", + "icon": "https://www.google.com/favicon.ico" + }, + + "company": "谷歌 LLC" + }, "阿里云": { "阿里云": { "name": "阿里云", @@ -556,6 +716,12 @@ "url": "a-msedge.net", "icon": "https://www.microsoft.com/favicon.ico" }, + "微软 WNS(Windows 推送通知服务)的客户端通信":{ + "name": "微软 WNS(Windows 推送通知服务)的客户端通信域名", + "categoryId": 2, + "url": "client.wns.windows.com", + "icon": "https://www.microsoft.com/favicon.ico" + }, "company": "微软 Microsoft" }, "字节跳动": { @@ -565,6 +731,12 @@ "url": "https://www.douyin.com/", "icon": "https://www.douyin.com/favicon.ico" }, + "字节跳动官网": { + "name": "字节跳动官网", + "categoryId": 0, + "url": "https://www.bytedance.com/", + "icon": "data:image/vnd.microsoft.icon;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAACFlBMVEUAAACp3f8yW7U8jf8zW7U/3vJA5P8AydMzW7VPlP955908jP8AydM8jf8yWrV5590yWrV45908jf8yW7UyW7UyW7V55909jf88jf8yW7UzW7V56N49jf80XLY+j/80XbY/lf8zZr5Ilf9GXbkA2+RCY70zZsx459x5590AydI8jP8AydN4590AydM8jf8AydM8jf88jf94590AyNM8jf8AydN5590AydM8jf8AyNIAytR5594zW7V56N09jf955948jf8AydMAytN65948jf956N56594AytMAy9Q9jv976d8AytYAy9Z66N566uE/jv8AztUAy9cAytV56OSD798A0NYAydM8jP95590yW7R45twAydN45909jP8yWrU8jf945t0AydIyWrV5590AydIyW7UzWrU8jf8AydIzW7UzWrV459145908jf8yW7UAydM8jP8AydM8jf9459155909jf8zW7UAydQAytMAydN56N0AydMzXLYAydM0W7UAydQ8jv8+jv8Ay9QzW7Y9jf965t176N80W7YAydN56N156d4yW7U0XLc+jv8+kP8Ay9VAj/81XbgAzNQ1XbY5XbZAkf9+7eQ7Yrp96+FDlv+F6emI/+4AydN55t15594AydM+jf8+jf8+jv8AydQ1W7QzW7UAydR75942XbUyWrR86OCA8uYAztqA8eN45twyWrQAyNI8jP8bed+KAAAArnRSTlMAAfb9+wUD8HMJ/fr59+jb1dTLt6eamY+Jg3xXTkhHOhIRDQsJBwX59vbr4+Dc1tXRxcC7ubSqoJuak5CKiIN+enlyb21lXVFEQj09NzAnJiQdGBUPDvzx8O/s6Obl397OzsvIyMXEwcG/vbiysbGuqqeknZSTkI2Gf3ZraGViX15YWFVTUk9OS0pEQUA8MzAtLSomHx4bGhkXCwfApKCAdGdjXlxaWTY0MyEUFBKNTPVmAAAFHElEQVR42uzX105UYRSG4YUyiCgqKvbeK3ZFsaCCXUHE3kvU2FvU2EussfcSY2KiiSUDc4fuTcl/xr8/ZuZk7/e9g/UcfcuIiIiIiIiIiIiIiMhf7wFDPxlFKx1UdunBX6NoWGEHqp40GnmwXCWHlzz9YuTBck2cvWzdVyMPlmtSAJYy8mE5sJPXNhYYebBcO06tBMyP5do1v3aTkcPytedb7R8jh+Vr2sVVHyzxpaMXfEZbLNGltcqqHjba8LGWzNJyJdZUeHTws/GWvNJ61hTW5cSQkRMsWXUWqxWsX/HvJC2xTmK5dp4pfp0UsCywXL3633hrCShrLAd2653FvBxgufaV//xoMS5HWK4+5fdHW0zLJZYDu/wolmC5xXIVHlocv6GfcyxX/IZ+HrHiN/TziuWGvsWiLLCULBaBBVZLYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYAmBJQSWEFhCYOW1erCitfn2gqnNYPlruLdwenMQWJ7+rV40I3QCy9PWx1cOhkZgeRq35uqRbYEPWD6o9d+P9wxxwOq41IblcyaHMGB1XNHLFfO6hShgeXpz/WwLFFie6m+e3x1igBVlmgeB5anhV+s0B8tX2zQHK0rB8WCBZQYWWGCBBRZYYIEFFlhggQUWWGCBBRZYYIGVZKyKgcNGmYEVCSuTyXTtO2jEGLCiYYX1mLV0bXewfFiu0mPVz1NgebEc2NzqF0Vg+bBc20/XvCoAy4flmnKupg4sL5Zr74UfdWB5sVz7K++8B8theauovPsZLIflbWYw9MGKiNU+9MGKlBv6YEWvFCwhsMACCyywwAILLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLLDAAgsssMD6325d7cQVhWEY/ocWn9JSoO5GhTqlSo2Wurt72qbu3lRwgruEQCCBE+6RHQ7YR7DWP8OeWSHvewnPwZcPLLDAAgsssMACCyywwAILLLDAAgsssMACCyywwAILLLCmGtbhHclg+Vimkk4f2r4OLLEu8dS3Z2Gw7Ft1/OvjMFj2Xan48uAqWAqwY3s2g6Xo4r/dG8FSdKF/5wawFJ3/9e4WWIoaf7xJBsu+UJ139MHyszr6YKmOfncYLM3RB0sRWGDZBBZYYIEFFlhggQUWWGBFgpWQBZYd1PMFS9cIWMamP5lfvVq8wDJAPdy3eKV4gWVo1ufy5eIFlhigcv6PQoFlwLqbM9AmXmAZsDKyfzeLX6BY6U/FJjexMrJ/NolfoFjTZs9bkipWuYd143VKQ0iiyh5qy9xFK8Q6t7ASslKGfKiAsTZ9KlkmqtzB8q95DLDu7frbIXEvIiz/mscC6877Py3iRJFg+dc8cKz1b/vOiTPprdJk0hsH6tWRenEqpVTmx9JhkeCxruX31obEtRRQM+csvCTq9Fgz8g/WJImLWULl9RS3ijo9VvrW/SdSxdUsoG4XHD0rgRb3az45WDcLitRQ+uJ+zaPHuv6y6ExsRjbu1zw6rLUvvg+6ObKqgsfK3XbgZKKQESv3UWFVl5ARK+1+YWWnkBkrc2/ZZaGJG7vmZC7vQ3G7EBERERERERERERER0dRtBK5Q857p1uutAAAAAElFTkSuQmCC" + }, "今日头条": { "name": "今日头条", "categoryId": 0, @@ -712,6 +884,56 @@ }, "icon": "https://p3-pangle-empower.byteimg.com/obj/tos-cn-i-742ihxn9bs/ad/pangle/homepage/assets/favicon.ico", "company":"北京巨量引擎网络技术有限公司" + }, + "豆包":{ + "CDN":{ + "豆包CDN":{ + "name": "豆包 CDN", + "categoryId": 2, + "url": { + "1": "cdnbuild.net", + "2": "doubaocdn.com" + }, + "icon": "https://p3-pangle-empower.byteimg.com/obj/tos-cn-i-742ihxn9bs/ad/pangle/homepage/assets/favicon.ico", + "company": "抖音视界有限公司" + }, + "内部服务负载均衡":{ + "name": "内部服务负载均衡", + "categoryId": 2, + "url": { + "1": "bytelb.net" + }, + "icon": "www.doubao.com/favicon.ico", + "company": "抖音视界有限公司" + }, + "WebSocket 长连接,实时交互":{ + "name": "WebSocket 长连接,实时交互", + "categoryId": 2, + "url": { + "1": "wss100-normal.doubao.com" + }, + "icon": "www.doubao.com/favicon.ico", + "company": "抖音视界有限公司" + }, + "前端 JS/CSS/ 图片 / 安装包分发":{ + "name": "前端 JS/CSS/ 图片 / 安装包分发", + "categoryId": 2, + "url": { + "1": "lf-flow-web-cdn.doubao.com" + }, + "icon": "www.doubao.com/favicon.ico", + "company": "北京春田知韵科技有限公司" + } + }, + "豆包":{ + "name": "豆包", + "categoryId": 0, + "url": { + "1":"www.doubao.com" + }, + "icon": "www.doubao.com/favicon.ico", + "company": "北京春田知韵科技有限公司" + } }, "company": "字节跳动有限公司" }, @@ -2374,6 +2596,99 @@ "icon": "https://www.youku.com/favicon.ico" }, "company": "优酷信息技术(北京)有限公司" + }, + "Steam":{ + "Steam商店官网": { + "name": "Steam商店官网", + "categoryId": 0, + "url": "store.steampowered.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam全球官网": { + "name": "Steam全球官网", + "categoryId": 0, + "url": "www.steampowered.com", + "icon": "https://www.steampowered.com/favicon.ico" + }, + "Steam中国官网": { + "name": "Steam中国官网", + "categoryId": 0, + "url": "store.steamchina.com", + "icon": "https://store.steamchina.com/favicon.ico" + }, + "Steam社区平台": { + "name": "Steam社区平台", + "categoryId": 11, + "url": "steamcommunity.com", + "icon": "https://steamcommunity.com/favicon.ico" + }, + "Steam内容分发CDN": { + "name": "Steam内容分发CDN", + "categoryId": 2, + "url": "steamcontent.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam静态资源CDN": { + "name": "Steam静态资源CDN", + "categoryId": 2, + "url": "steamstatic.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam用户内容存储": { + "name": "Steam用户内容存储", + "categoryId": 21, + "url": "steamusercontent.com", + "icon": "https://steamcommunity.com/favicon.ico" + }, + "Steam游戏服务域名": { + "name": "Steam游戏服务域名", + "categoryId": 22, + "url": "steamgames.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam客服帮助中心": { + "name": "Steam客服帮助中心", + "categoryId": 6, + "url": "help.steampowered.com", + "icon": "https://help.steampowered.com/favicon.ico" + }, + "Steam账号认证中心": { + "name": "Steam账号认证中心", + "categoryId": 0, + "url": "account.steampowered.com", + "icon": "https://account.steampowered.com/favicon.ico" + }, + "Steam支付平台": { + "name": "Steam支付平台", + "categoryId": 23, + "url": "payment.steampowered.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam云存档服务": { + "name": "Steam云存档服务", + "categoryId": 20, + "url": "cloud.steampowered.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam开发者API平台": { + "name": "Steam开发者API平台", + "categoryId": 24, + "url": "api.steampowered.com", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam短链接跳转域名": { + "name": "Steam短链接跳转域名", + "categoryId": 6, + "url": "s.team", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "Steam发现与信令协调服务": { + "name": "Steam发现与信令协调服务", + "categoryId": 25, + "url": "discovery.steamserver.net", + "icon": "https://store.steampowered.com/favicon.ico" + }, + "company": "Valve Corporation" } } } \ No newline at end of file diff --git a/static/index.html b/static/index.html index fc0109e..73e9509 100644 --- a/static/index.html +++ b/static/index.html @@ -8,6 +8,8 @@ + + @@ -656,7 +658,7 @@
| 规则 | @@ -701,7 +703,7 @@||||||||
|---|---|---|---|---|---|---|---|---|
| 名称 | @@ -741,7 +743,7 @@|||||||
|---|---|---|---|---|---|---|---|
| IP地址 | @@ -962,7 +964,7 @@||||||
|---|---|---|---|---|---|---|
|
diff --git a/static/js/api.js b/static/js/api.js
index d8f5bbd..62725df 100644
--- a/static/js/api.js
+++ b/static/js/api.js
@@ -68,8 +68,14 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
const parsedData = JSON.parse(responseText);
// 检查解析后的数据是否有效
- if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
- console.warn('解析后的数据为空');
+ if (parsedData === null) {
+ console.warn('解析后的数据为null');
+ return null;
+ }
+
+ // 允许返回空数组,但不允许返回空对象
+ if (typeof parsedData === 'object' && !Array.isArray(parsedData) && Object.keys(parsedData).length === 0) {
+ console.warn('解析后的数据为空对象');
return null;
}
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
index 986fe31..b432370 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -165,15 +165,15 @@ function processRealTimeData(stats) {
let trendIcon = '---';
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
- const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
-
// 首次加载时初始化历史数据,不计算趋势
- if (prevResponseTime === null) {
+ if (window.dashboardHistoryData.prevResponseTime === undefined || window.dashboardHistoryData.prevResponseTime === null) {
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
responsePercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
} else {
+ const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
+
// 计算变化百分比
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
@@ -209,46 +209,15 @@ function processRealTimeData(stats) {
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
- // 计算查询类型趋势
- let queryPercent = '---';
- let trendClass = 'text-gray-400';
- let trendIcon = '---';
-
- if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
- const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount;
-
- // 首次加载时初始化历史数据,不计算趋势
- if (prevTopQueryTypeCount === null) {
- window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
- queryPercent = '0.0%';
- trendIcon = '•';
- trendClass = 'text-gray-500';
- } else {
- // 计算变化百分比
- if (prevTopQueryTypeCount > 0) {
- const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
- queryPercent = Math.abs(changePercent).toFixed(1) + '%';
-
- // 设置趋势图标和颜色
- if (changePercent > 0) {
- trendIcon = '↑';
- trendClass = 'text-primary';
- } else if (changePercent < 0) {
- trendIcon = '↓';
- trendClass = 'text-secondary';
- } else {
- trendIcon = '•';
- trendClass = 'text-gray-500';
- }
- }
-
- // 更新历史数据
- window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
- }
+ // 计算最常用查询类型的百分比
+ let queryTypePercentage = 0;
+ if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
+ const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
+ queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
}
- queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
- queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
+ queryPercentElem.textContent = `${Math.round(queryTypePercentage)}%`;
+ queryPercentElem.className = 'text-sm flex items-center text-primary';
}
}
@@ -310,6 +279,17 @@ function processRealTimeData(stats) {
// 实时更新TOP客户端和TOP域名数据
async function updateTopData() {
try {
+ // 隐藏所有加载中状态
+ const clientsLoadingElement = document.getElementById('top-clients-loading');
+ if (clientsLoadingElement) {
+ clientsLoadingElement.classList.add('hidden');
+ }
+
+ const domainsLoadingElement = document.getElementById('top-domains-loading');
+ if (domainsLoadingElement) {
+ domainsLoadingElement.classList.add('hidden');
+ }
+
// 获取最新的TOP客户端数据
let clientsData = [];
try {
@@ -618,10 +598,10 @@ async function loadDashboardData() {
updateCharts(stats, queryTypeStats);
// 更新表格数据
- updateTopBlockedTable(topBlockedDomains);
+ await updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
- updateTopClientsTable(topClients);
- updateTopDomainsTable(topDomains);
+ await updateTopClientsTable(topClients);
+ await updateTopDomainsTable(topDomains);
// 尝试从stats中获取总查询数等信息
if (stats.dns) {
@@ -650,26 +630,35 @@ async function loadDashboardData() {
let trendIcon = '---';
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
- // 存储当前值用于下次计算趋势
- const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
- window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
-
- // 计算变化百分比
- if (prevResponseTime > 0) {
- const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
- responsePercent = Math.abs(changePercent).toFixed(1) + '%';
+ // 首次加载时初始化历史数据,不计算趋势
+ if (window.dashboardHistoryData.prevResponseTime === undefined || window.dashboardHistoryData.prevResponseTime === null) {
+ window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
+ responsePercent = '0.0%';
+ trendIcon = '•';
+ trendClass = 'text-gray-500';
+ } else {
+ const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
- // 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
- if (changePercent > 0) {
- trendIcon = '↓';
- trendClass = 'text-danger';
- } else if (changePercent < 0) {
- trendIcon = '↑';
- trendClass = 'text-success';
- } else {
- trendIcon = '•';
- trendClass = 'text-gray-500';
+ // 计算变化百分比
+ if (prevResponseTime > 0) {
+ const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
+ responsePercent = Math.abs(changePercent).toFixed(1) + '%';
+
+ // 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
+ if (changePercent > 0) {
+ trendIcon = '↓';
+ trendClass = 'text-danger';
+ } else if (changePercent < 0) {
+ trendIcon = '↑';
+ trendClass = 'text-success';
+ } else {
+ trendIcon = '•';
+ trendClass = 'text-gray-500';
+ }
}
+
+ // 更新历史数据
+ window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
}
}
@@ -685,11 +674,17 @@ async function loadDashboardData() {
// 直接使用API返回的查询类型
const queryType = stats.topQueryType || '---';
- // 设置默认趋势显示
+ // 计算最常用查询类型的百分比
+ let queryTypePercentage = 0;
+ if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
+ const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
+ queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
+ }
+
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
- queryPercentElem.textContent = '• ---';
- queryPercentElem.className = 'text-sm flex items-center text-gray-500';
+ queryPercentElem.textContent = `• ${Math.round(queryTypePercentage)}%`;
+ queryPercentElem.className = 'text-sm flex items-center text-primary';
}
document.getElementById('top-query-type').textContent = queryType;
@@ -1124,7 +1119,7 @@ function updateStatsCards(stats) {
}
// 更新Top屏蔽域名表格
-function updateTopBlockedTable(domains) {
+async function updateTopBlockedTable(domains) {
console.log('更新Top屏蔽域名表格,收到数据:', domains);
const tableBody = document.getElementById('top-blocked-table');
@@ -1155,14 +1150,37 @@ function updateTopBlockedTable(domains) {
}
let html = '';
- for (let i = 0; i < tableData.length && i < 5; i++) {
+ for (let i = 0; i < tableData.length; i++) {
const domain = tableData[i];
+ // 检查域名是否是跟踪器
+ const trackerInfo = await isDomainInTrackerDatabase(domain.name);
+ const isTracker = trackerInfo !== null;
+
+ // 构建跟踪器浮窗内容
+ const trackerTooltip = isTracker ? `
+
+
+ ` : '';
+
html += `
已知跟踪器
+ 名称: ${trackerInfo.name}
+ 类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
+ ${trackerInfo.url ? `URL: ${trackerInfo.url} ` : ''}
+ ${trackerInfo.source ? `源: ${trackerInfo.source} ` : ''}
+
${i + 1}
- ${domain.name}
+
+ ${domain.name}
+ ${isTracker ? `
+
+ ` : ''}
+
${i + 1}
- ${client.ip}
+
+ ${client.ip}
+ ${location}
+
+
+ ` : '';
+
html += `
已知跟踪器
+ 名称: ${trackerInfo.name}
+ 类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
+ ${trackerInfo.url ? `URL: ${trackerInfo.url} ` : ''}
+ ${trackerInfo.source ? `源: ${trackerInfo.source} ` : ''}
+
${i + 1}
- ${domain.name}${domain.dnssec ? ' ' : ''}
+
+ ${domain.name}${domain.dnssec ? ' ' : ''}
+ ${isTracker ? `
+
+ ` : ''}
+
+
+ |
+ 暂无查询日志
+ | ||||
|---|---|---|---|---|---|