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 @@
-
名称
+
@@ -962,7 +964,7 @@
-
IP地址
+
+ + + `; + } + } } // 更新日志表格 diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 74c293f..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./static/**/*.{html,js}", - ], - theme: { - extend: { - colors: { - primary: '#165DFF', - secondary: '#36CFFB', - success: '#00B42A', - warning: '#FF7D00', - danger: '#F53F3F', - info: '#86909C', - dark: '#1D2129', - light: '#F2F3F5', - }, - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], - }, - }, - }, - plugins: [], -} \ No newline at end of file
diff --git a/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 ? ` +
+
已知跟踪器
+
名称: ${trackerInfo.name}
+
类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
+ ${trackerInfo.url ? `` : ''} + ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''} +
+ ` : ''; + html += `
${i + 1} - ${domain.name} +
+ ${domain.name} + ${isTracker ? ` +
+ + ${trackerTooltip} +
+ ` : ''} +
${formatNumber(domain.count)} @@ -1171,6 +1189,23 @@ function updateTopBlockedTable(domains) { } tableBody.innerHTML = html; + + // 添加跟踪器图标悬停事件 + const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container'); + trackerIconContainers.forEach(container => { + const tooltip = container.querySelector('.tracker-tooltip'); + if (tooltip) { + tooltip.style.display = 'none'; + + container.addEventListener('mouseenter', () => { + tooltip.style.display = 'block'; + }); + + container.addEventListener('mouseleave', () => { + tooltip.style.display = 'none'; + }); + } + }); } // 更新最近屏蔽域名表格 @@ -1209,7 +1244,7 @@ function updateRecentBlockedTable(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 time = formatTime(domain.timestamp); html += ` @@ -1226,8 +1261,187 @@ function updateRecentBlockedTable(domains) { tableBody.innerHTML = html; } +// IP地理位置缓存(检查是否已经存在,避免重复声明) +if (typeof ipGeolocationCache === 'undefined') { + var ipGeolocationCache = {}; + var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 +} + +// 跟踪器数据库缓存(检查是否已经存在,避免重复声明) +if (typeof trackersDatabase === 'undefined') { + var trackersDatabase = null; + var trackersLoaded = false; + var trackersLoading = false; +} + +// 加载跟踪器数据库 +async function loadTrackersDatabase() { + if (trackersLoaded) return trackersDatabase; + if (trackersLoading) { + // 等待正在进行的加载完成 + while (trackersLoading) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return trackersDatabase; + } + + trackersLoading = true; + + try { + const response = await fetch('domain-info/tracker/trackers.json'); + if (!response.ok) { + console.error('加载跟踪器数据库失败:', response.statusText); + trackersDatabase = { trackers: {} }; + return trackersDatabase; + } + + trackersDatabase = await response.json(); + trackersLoaded = true; + return trackersDatabase; + } catch (error) { + console.error('加载跟踪器数据库失败:', error); + trackersDatabase = { trackers: {} }; + return trackersDatabase; + } finally { + trackersLoading = false; + } +} + +// 检查域名是否在跟踪器数据库中 +async function isDomainInTrackerDatabase(domain) { + if (!trackersDatabase || !trackersLoaded) { + await loadTrackersDatabase(); + } + + if (!trackersDatabase || !trackersDatabase.trackers) { + return null; + } + + // 检查域名是否直接作为跟踪器键存在 + if (trackersDatabase.trackers.hasOwnProperty(domain)) { + return trackersDatabase.trackers[domain]; + } + + // 检查域名是否在跟踪器URL中 + for (const trackerKey in trackersDatabase.trackers) { + if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) { + const tracker = trackersDatabase.trackers[trackerKey]; + if (tracker && tracker.url) { + try { + const trackerUrl = new URL(tracker.url); + if (trackerUrl.hostname === domain) { + return tracker; + } + } catch (e) { + // 忽略无效URL + } + } + } + } + + return null; +} + +// 获取IP地理位置信息 +async function getIpGeolocation(ip) { + // 检查是否为内网IP + if (isPrivateIP(ip)) { + return "内网 内网"; + } + + // 检查缓存 + const now = Date.now(); + if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) { + return ipGeolocationCache[ip].location; + } + + try { + // 使用whois.pconline.com.cn API获取IP地理位置 + const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`; + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // 解析响应数据 + const data = await response.json(); + let location = "未知 未知"; + + if (data && data.addr) { + // 直接使用addr字段作为完整的地理位置信息 + location = data.addr; + } + + // 保存到缓存 + ipGeolocationCache[ip] = { + location: location, + timestamp: now + }; + + return location; + } catch (error) { + console.error('获取IP地理位置失败:', error); + return "未知 未知"; + } +} + +// 检查是否为内网IP +function isPrivateIP(ip) { + const parts = ip.split('.'); + + // 检查IPv4内网地址 + if (parts.length === 4) { + const first = parseInt(parts[0]); + const second = parseInt(parts[1]); + + // 10.0.0.0/8 + if (first === 10) { + return true; + } + // 172.16.0.0/12 + if (first === 172 && second >= 16 && second <= 31) { + return true; + } + // 192.168.0.0/16 + if (first === 192 && second === 168) { + return true; + } + // 127.0.0.0/8 (localhost) + if (first === 127) { + return true; + } + // 169.254.0.0/16 (link-local) + if (first === 169 && second === 254) { + return true; + } + } + + // 检查IPv6内网地址 + if (ip.includes(':')) { + // ::1/128 (localhost) + if (ip === '::1' || ip.startsWith('0:0:0:0:0:0:0:1')) { + return true; + } + // fc00::/7 (unique local address) + if (ip.startsWith('fc') || ip.startsWith('fd')) { + return true; + } + // fe80::/10 (link-local) + if (ip.startsWith('fe80:')) { + return true; + } + } + + return false; +} + // 更新TOP客户端表格 -function updateTopClientsTable(clients) { +async function updateTopClientsTable(clients) { console.log('更新TOP客户端表格,收到数据:', clients); const tableBody = document.getElementById('top-clients-table'); @@ -1237,6 +1451,15 @@ function updateTopClientsTable(clients) { return; } + // 隐藏加载中状态 + const loadingElement = document.getElementById('top-clients-loading'); + if (loadingElement) { + loadingElement.classList.add('hidden'); + } + + // 显示数据区域 + tableBody.classList.remove('hidden'); + let tableData = []; // 适配不同的数据结构 @@ -1265,18 +1488,22 @@ function updateTopClientsTable(clients) { console.log('使用示例数据填充TOP客户端表格'); } - // 只显示前5个客户端 - tableData = tableData.slice(0, 5); + let html = ''; for (let i = 0; i < tableData.length; i++) { const client = tableData[i]; + // 获取IP地理信息 + const location = await getIpGeolocation(client.ip); html += `
${i + 1} - ${client.ip} +
+ ${client.ip} + ${location} +
${formatNumber(client.count)} @@ -1288,7 +1515,7 @@ function updateTopClientsTable(clients) { } // 更新请求域名排行表格 -function updateTopDomainsTable(domains) { +async function updateTopDomainsTable(domains) { console.log('更新请求域名排行表格,收到数据:', domains); const tableBody = document.getElementById('top-domains-table'); @@ -1326,18 +1553,40 @@ function updateTopDomainsTable(domains) { console.log('使用示例数据填充请求域名排行表格'); } - // 只显示前5个域名 - tableData = tableData.slice(0, 5); + let html = ''; 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 ? ` +
+
已知跟踪器
+
名称: ${trackerInfo.name}
+
类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
+ ${trackerInfo.url ? `` : ''} + ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''} +
+ ` : ''; + html += `
${i + 1} - ${domain.name}${domain.dnssec ? ' ' : ''} +
+ ${domain.name}${domain.dnssec ? ' ' : ''} + ${isTracker ? ` +
+ + ${trackerTooltip} +
+ ` : ''} +
${formatNumber(domain.count)} @@ -1346,15 +1595,32 @@ function updateTopDomainsTable(domains) { } tableBody.innerHTML = html; + + // 添加跟踪器图标悬停事件 + const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container'); + trackerIconContainers.forEach(container => { + const tooltip = container.querySelector('.tracker-tooltip'); + if (tooltip) { + tooltip.style.display = 'none'; + + container.addEventListener('mouseenter', () => { + tooltip.style.display = 'block'; + }); + + container.addEventListener('mouseleave', () => { + tooltip.style.display = 'none'; + }); + } + }); } // 当前选中的时间范围 -let currentTimeRange = '24h'; // 默认为24小时 +let currentTimeRange = '30d'; // 默认为30天 let isMixedView = true; // 是否为混合视图 - 默认显示混合视图 -let lastSelectedIndex = 0; // 最后选中的按钮索引 +let lastSelectedIndex = 2; // 最后选中的按钮索引,30天是第三个按钮 // 详细图表专用变量 -let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围 +let detailedCurrentTimeRange = '30d'; // 详细图表当前时间范围 let detailedIsMixedView = false; // 详细图表是否为混合视图 // 初始化时间范围切换 @@ -1491,10 +1757,12 @@ function initTimeRangeToggle() { // 移除自定义鼠标悬停提示效果 }); - // 确保默认选中第一个按钮并显示混合内容 + // 确保默认选中30天按钮并显示混合内容 if (timeRangeButtons.length > 0) { - const firstButton = timeRangeButtons[0]; - const firstStyle = buttonStyles[0]; + // 选择30天按钮(索引为2),如果不存在则使用第一个按钮 + const defaultButtonIndex = 2; + const defaultButton = timeRangeButtons[defaultButtonIndex] || timeRangeButtons[0]; + const defaultStyle = buttonStyles[defaultButtonIndex % buttonStyles.length] || buttonStyles[0]; // 先重置所有按钮 timeRangeButtons.forEach((btn, index) => { @@ -1506,13 +1774,13 @@ function initTimeRangeToggle() { btn.classList.add(...btnStyle.hover); }); - // 然后设置第一个按钮为激活状态,并标记为混合视图 - firstButton.classList.remove(...firstStyle.normal); - firstButton.classList.remove(...firstStyle.hover); - firstButton.classList.add('active', 'mixed-view-active'); - firstButton.classList.add(...firstStyle.active); - firstButton.classList.add(...firstStyle.activeHover); - console.log('默认选中第一个按钮并显示混合内容:', firstButton.textContent.trim()); + // 然后设置30天按钮为激活状态,并标记为混合视图 + defaultButton.classList.remove(...defaultStyle.normal); + defaultButton.classList.remove(...defaultStyle.hover); + defaultButton.classList.add('active', 'mixed-view-active'); + defaultButton.classList.add(...defaultStyle.active); + defaultButton.classList.add(...defaultStyle.activeHover); + console.log('默认选中30天按钮并显示混合内容:', defaultButton.textContent.trim()); // 设置默认显示混合内容 isMixedView = true; diff --git a/static/js/logs.js b/static/js/logs.js index bcbf0e0..bfef799 100644 --- a/static/js/logs.js +++ b/static/js/logs.js @@ -10,9 +10,11 @@ let logsChart = null; let currentSortField = ''; let currentSortDirection = 'desc'; // 默认降序 -// IP地理位置缓存 -let ipGeolocationCache = {}; -const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 +// IP地理位置缓存(检查是否已经存在,避免重复声明) +if (typeof ipGeolocationCache === 'undefined') { + var ipGeolocationCache = {}; + var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 +} // 获取IP地理位置信息 async function getIpGeolocation(ip) { @@ -112,10 +114,12 @@ function isPrivateIP(ip) { return false; } -// 跟踪器数据库缓存 -let trackersDatabase = null; -let trackersLoaded = false; -let trackersLoading = false; +// 跟踪器数据库缓存(检查是否已经存在,避免重复声明) +if (typeof trackersDatabase === 'undefined') { + var trackersDatabase = null; + var trackersLoaded = false; + var trackersLoading = false; +} // 域名信息数据库缓存 let domainInfoDatabase = null; @@ -477,6 +481,270 @@ function extractPrimaryDomain(domain) { return result; } +// 初始化列宽调节功能 +function initResizableColumns() { + const table = document.querySelector('.resizable-table'); + if (!table) return; + + // 为每个表头添加调整手柄元素 + function addResizeHandles() { + const headers = table.querySelectorAll('th'); + headers.forEach(header => { + // 移除已存在的手柄 + const existingHandle = header.querySelector('.resize-handle'); + if (existingHandle) { + existingHandle.remove(); + } + + // 创建新的调整手柄 + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'resize-handle'; + resizeHandle.style.cssText = ` + position: absolute; + top: 0; + right: 0; + width: 10px; + height: 100%; + cursor: col-resize; + background: rgba(59, 130, 246, 0.1); + z-index: 10; + transition: background-color 0.2s ease; + `; + + // 添加悬停效果 + resizeHandle.addEventListener('mouseenter', () => { + resizeHandle.style.background = 'rgba(59, 130, 246, 0.3)'; + }); + + resizeHandle.addEventListener('mouseleave', () => { + if (!resizeHandle.classList.contains('dragging')) { + resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)'; + } + }); + + header.style.position = 'relative'; + header.appendChild(resizeHandle); + }); + } + + // 计算列宽并设置固定宽度 + function calculateAndSetColumnWidths() { + // 确保表格可见 + table.style.visibility = 'visible'; + + // 保存当前表格布局 + const originalLayout = table.style.tableLayout; + table.style.tableLayout = 'auto'; + + // 获取所有表头和数据行 + const headers = table.querySelectorAll('th'); + const rows = table.querySelectorAll('tbody tr'); + + // 计算每列的最大宽度 + const columnWidths = []; + headers.forEach((header, index) => { + // 获取表头宽度 + let maxWidth = header.offsetWidth; + + // 遍历所有数据行,找到该列的最大宽度 + rows.forEach(row => { + const cell = row.children[index]; + if (cell) { + maxWidth = Math.max(maxWidth, cell.offsetWidth); + } + }); + + // 添加一些 padding + maxWidth += 20; + + // 保存最大宽度 + columnWidths[index] = maxWidth; + }); + + // 设置每列的固定宽度 + headers.forEach((header, index) => { + const width = `${columnWidths[index]}px`; + header.style.width = width; + header.style.minWidth = width; + header.style.maxWidth = width; + + // 找到对应的数据列并设置宽度 + rows.forEach(row => { + const cell = row.children[index]; + if (cell) { + cell.style.width = width; + cell.style.minWidth = width; + cell.style.maxWidth = width; + } + }); + }); + + // 恢复表格布局 + table.style.tableLayout = 'fixed'; + } + + // 保存列宽设置的函数 + function saveColumnWidths() { + const headers = table.querySelectorAll('th'); + const columnWidths = {}; + headers.forEach((header, index) => { + columnWidths[index] = header.style.width; + }); + localStorage.setItem('logsTableColumnWidths', JSON.stringify(columnWidths)); + } + + // 恢复列宽设置的函数 + function restoreColumnWidths() { + const headers = table.querySelectorAll('th'); + const savedWidths = localStorage.getItem('logsTableColumnWidths'); + + if (savedWidths) { + const columnWidths = JSON.parse(savedWidths); + + // 设置表格布局为fixed + table.style.tableLayout = 'fixed'; + + headers.forEach((header, index) => { + if (columnWidths[index]) { + const width = columnWidths[index]; + header.style.width = width; + header.style.minWidth = width; + header.style.maxWidth = width; + + // 找到对应的数据列并设置宽度 + const rows = table.querySelectorAll('tbody tr'); + rows.forEach(row => { + const cell = row.children[index]; + if (cell) { + cell.style.width = width; + cell.style.minWidth = width; + cell.style.maxWidth = width; + } + }); + } + }); + } else { + // 没有保存的宽度,计算并设置列宽 + calculateAndSetColumnWidths(); + } + } + + // 恢复保存的列宽设置或计算初始列宽 + restoreColumnWidths(); + + // 添加调整手柄 + addResizeHandles(); + + // 拖拽状态变量 + let currentHeader = null; + let startX = 0; + let startWidth = 0; + let isDragging = false; + + // 鼠标按下事件 + table.addEventListener('mousedown', (e) => { + const resizeHandle = e.target.closest('.resize-handle'); + if (resizeHandle) { + currentHeader = resizeHandle.parentElement; + startX = e.clientX; + startWidth = currentHeader.offsetWidth; + isDragging = true; + + // 添加拖拽状态类 + currentHeader.classList.add('dragging'); + resizeHandle.classList.add('dragging'); + + // 改变拖拽手柄样式 + resizeHandle.style.background = 'rgba(59, 130, 246, 0.6)'; + + // 阻止默认事件和冒泡 + e.preventDefault(); + e.stopPropagation(); + + // 阻止文本选择 + document.addEventListener('selectstart', preventSelect, { capture: true }); + document.addEventListener('copy', preventCopy, { capture: true }); + + // 添加全局事件监听器 + document.addEventListener('mousemove', onMouseMove, { capture: true }); + document.addEventListener('mouseup', onMouseUp, { capture: true }); + } + }); + + // 鼠标移动事件处理函数 + function onMouseMove(e) { + if (!currentHeader) return; + + // 阻止默认事件 + e.preventDefault(); + e.stopPropagation(); + + // 计算新宽度 + const deltaX = e.clientX - startX; + const newWidth = Math.max(50, startWidth + deltaX); + + // 设置新宽度 + const width = `${newWidth}px`; + currentHeader.style.width = width; + currentHeader.style.minWidth = width; + currentHeader.style.maxWidth = width; + + // 找到对应的数据列并设置宽度 + const headers = table.querySelectorAll('th'); + const index = Array.from(headers).indexOf(currentHeader); + const rows = table.querySelectorAll('tbody tr'); + rows.forEach(row => { + const cell = row.children[index]; + if (cell) { + cell.style.width = width; + cell.style.minWidth = width; + cell.style.maxWidth = width; + } + }); + } + + // 鼠标释放事件处理函数 + function onMouseUp(e) { + if (!currentHeader) return; + + // 阻止默认事件 + e.preventDefault(); + e.stopPropagation(); + + // 获取调整手柄 + const resizeHandle = currentHeader.querySelector('.resize-handle'); + + // 移除拖拽状态类 + currentHeader.classList.remove('dragging'); + resizeHandle.classList.remove('dragging'); + + // 恢复拖拽手柄样式 + resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)'; + + // 保存列宽设置 + saveColumnWidths(); + + // 重置状态 + currentHeader = null; + isDragging = false; + + // 移除事件监听器 + document.removeEventListener('selectstart', preventSelect, { capture: true }); + document.removeEventListener('copy', preventCopy, { capture: true }); + document.removeEventListener('mousemove', onMouseMove, { capture: true }); + document.removeEventListener('mouseup', onMouseUp, { capture: true }); + } + + // 阻止文本选择和复制 + function preventSelect(e) { + e.preventDefault(); + } + + function preventCopy(e) { + e.preventDefault(); + } +} + // 初始化查询日志页面 function initLogsPage() { console.log('初始化查询日志页面'); @@ -499,6 +767,9 @@ function initLogsPage() { // 建立WebSocket连接,用于实时更新统计数据和图表 connectLogsWebSocket(); + // 初始化列宽调节功能 + initResizableColumns(); + // 窗口大小改变时重新加载日志表格 window.addEventListener('resize', handleWindowResize); @@ -723,7 +994,7 @@ function loadLogsStats() { } // 加载日志详情 -function loadLogs() { +async function loadLogs() { // 显示加载状态 const loadingEl = document.getElementById('logs-loading'); if (loadingEl) { @@ -748,61 +1019,70 @@ function loadLogs() { endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`; } - // 使用封装的apiRequest函数进行API调用 - apiRequest(endpoint) - .then(data => { - if (data && data.error) { - console.error('加载日志详情失败:', data.error); - // 隐藏加载状态 - if (loadingEl) { - loadingEl.classList.add('hidden'); - } - return; - } - - // 加载日志总数 - return apiRequest('/logs/count').then(countData => { - return { logs: data, count: countData.count }; - }); - }) - .then(result => { - if (!result || !result.logs) { - console.error('加载日志详情失败: 无效的响应数据'); - // 隐藏加载状态 - if (loadingEl) { - loadingEl.classList.add('hidden'); - } - return; - } - - const logs = result.logs; - const totalLogs = result.count; - - // 计算总页数 - totalPages = Math.ceil(totalLogs / logsPerPage); - - // 更新日志表格 - updateLogsTable(logs); - - // 绑定操作按钮事件 - bindActionButtonsEvents(); - - // 更新分页信息 - updateLogsPagination(); - + try { + // 使用封装的apiRequest函数进行API调用 + const logsData = await apiRequest(endpoint); + + if (logsData && logsData.error) { + console.error('加载日志详情失败:', logsData.error); // 隐藏加载状态 if (loadingEl) { loadingEl.classList.add('hidden'); } - }) - .catch(error => { - console.error('加载日志详情失败:', error); - - // 隐藏加载状态 - if (loadingEl) { - loadingEl.classList.add('hidden'); - } - }); + return; + } + + // 加载日志总数 + const [logs, countData] = await Promise.all([ + Promise.resolve(logsData || []), // 确保logsData是数组 + apiRequest('/logs/count') + ]); + + // 确保logs是数组 + const logsArray = Array.isArray(logs) ? logs : []; + // 确保countData是有效的 + const totalLogs = countData && countData.count ? countData.count : logsArray.length; + + // 计算总页数 + totalPages = Math.ceil(totalLogs / logsPerPage); + + // 更新日志表格 + await updateLogsTable(logsArray); + + // 绑定操作按钮事件 + bindActionButtonsEvents(); + + // 更新分页信息 + updateLogsPagination(); + + // 重新初始化列宽调节功能,确保新添加的行也能继承列宽设置 + initResizableColumns(); + + // 隐藏加载状态 + if (loadingEl) { + loadingEl.classList.add('hidden'); + } + } catch (error) { + console.error('加载日志详情失败:', error); + + // 隐藏加载状态 + if (loadingEl) { + loadingEl.classList.add('hidden'); + } + + // 显示空状态 + const tableBody = document.getElementById('logs-table-body'); + if (tableBody) { + tableBody.innerHTML = ` +
+ +
暂无查询日志
+