优化修复
This commit is contained in:
@@ -103,13 +103,13 @@
|
|||||||
|
|
||||||
### 修改
|
### 修改
|
||||||
- 在forwardDNSRequestWithCache函数中添加域名匹配逻辑,检查域名是否包含不验证DNSSEC的模式
|
- 在forwardDNSRequestWithCache函数中添加域名匹配逻辑,检查域名是否包含不验证DNSSEC的模式
|
||||||
- 在所有查询模式(parallel、loadbalance、fastest-ip、default)中实现跳过DNSSEC验证的功能
|
- 在所有查询模式(parallel、fastest-ip、default)中实现跳过DNSSEC验证的功能
|
||||||
|
|
||||||
## [1.1.1] - 2025-12-19
|
## [1.1.1] - 2025-12-19
|
||||||
|
|
||||||
### 修改
|
### 修改
|
||||||
- 修复NXDOMAIN响应传播逻辑,确保上游DNS服务器返回的NXDOMAIN响应能正确传递给客户端
|
- 修复NXDOMAIN响应传播逻辑,确保上游DNS服务器返回的NXDOMAIN响应能正确传递给客户端
|
||||||
- 优化loadbalance、fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制
|
- 优化fastest-ip和parallel查询模式下的NXDOMAIN响应选择机制
|
||||||
- 确保不存在的域名能被正确识别并返回NXDOMAIN状态码
|
- 确保不存在的域名能被正确识别并返回NXDOMAIN状态码
|
||||||
- 修复服务器绑定地址配置,确保IPv4兼容性
|
- 修复服务器绑定地址配置,确保IPv4兼容性
|
||||||
|
|
||||||
|
|||||||
3
build.sh
3
build.sh
@@ -2,4 +2,5 @@ CGO_ENABLED=1 \
|
|||||||
GOOS=linux \
|
GOOS=linux \
|
||||||
GOARCH=amd64 \
|
GOARCH=amd64 \
|
||||||
CC=gcc \
|
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
|
||||||
|
|
||||||
|
|||||||
11
config.json
11
config.json
@@ -2,19 +2,16 @@
|
|||||||
"dns": {
|
"dns": {
|
||||||
"port": 53,
|
"port": 53,
|
||||||
"upstreamDNS": [
|
"upstreamDNS": [
|
||||||
"223.5.5.5"
|
"10.35.10.200"
|
||||||
],
|
],
|
||||||
"dnssecUpstreamDNS": [
|
"dnssecUpstreamDNS": [
|
||||||
"117.50.10.10",
|
|
||||||
"101.226.4.6",
|
|
||||||
"218.30.118.6",
|
|
||||||
"208.67.220.220",
|
"208.67.220.220",
|
||||||
"208.67.222.222"
|
"208.67.222.222"
|
||||||
],
|
],
|
||||||
"saveInterval": 30,
|
"saveInterval": 30,
|
||||||
"cacheTTL": 10,
|
"cacheTTL": 60,
|
||||||
"enableDNSSEC": true,
|
"enableDNSSEC": true,
|
||||||
"queryMode": "parallel",
|
"queryMode": "fastest-ip",
|
||||||
"queryTimeout": 500,
|
"queryTimeout": 500,
|
||||||
"enableFastReturn": true,
|
"enableFastReturn": true,
|
||||||
"domainSpecificDNS": {
|
"domainSpecificDNS": {
|
||||||
@@ -146,7 +143,7 @@
|
|||||||
"statsSaveInterval": 60
|
"statsSaveInterval": 60
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"level": "debug",
|
"level": "info",
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
"maxAge": 30
|
"maxAge": 30
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type DNSConfig struct {
|
|||||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||||
EnableDNSSEC bool `json:"enableDNSSEC"` // 是否启用DNSSEC支持
|
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"` // 查询超时时间(毫秒)
|
QueryTimeout int `json:"queryTimeout"` // 查询超时时间(毫秒)
|
||||||
EnableFastReturn bool `json:"enableFastReturn"` // 是否启用快速返回机制
|
EnableFastReturn bool `json:"enableFastReturn"` // 是否启用快速返回机制
|
||||||
DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置
|
DomainSpecificDNS DomainSpecificDNS `json:"domainSpecificDNS"` // 域名特定DNS服务器配置
|
||||||
@@ -50,9 +50,9 @@ type BlacklistEntry struct {
|
|||||||
type ShieldConfig struct {
|
type ShieldConfig struct {
|
||||||
Blacklists []BlacklistEntry `json:"blacklists"`
|
Blacklists []BlacklistEntry `json:"blacklists"`
|
||||||
UpdateInterval int `json:"updateInterval"`
|
UpdateInterval int `json:"updateInterval"`
|
||||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogConfig 日志配置
|
// LogConfig 日志配置
|
||||||
|
|||||||
BIN
dns-server
BIN
dns-server
Binary file not shown.
12
dns/cache.go
12
dns/cache.go
@@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
// DNSCacheItem 表示缓存中的DNS响应项
|
// DNSCacheItem 表示缓存中的DNS响应项
|
||||||
type DNSCacheItem struct {
|
type DNSCacheItem struct {
|
||||||
Response *dns.Msg // DNS响应消息
|
Response *dns.Msg // DNS响应消息
|
||||||
Expiry time.Time // 过期时间
|
Expiry time.Time // 过期时间
|
||||||
HasDNSSEC bool // 是否包含DNSSEC记录
|
HasDNSSEC bool // 是否包含DNSSEC记录
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSCache DNS缓存结构
|
// DNSCache DNS缓存结构
|
||||||
@@ -21,7 +21,7 @@ type DNSCache struct {
|
|||||||
defaultTTL time.Duration // 默认缓存TTL
|
defaultTTL time.Duration // 默认缓存TTL
|
||||||
maxSize int // 最大缓存条目数
|
maxSize int // 最大缓存条目数
|
||||||
// 使用链表结构来跟踪缓存条目的访问顺序,用于LRU淘汰
|
// 使用链表结构来跟踪缓存条目的访问顺序,用于LRU淘汰
|
||||||
accessList []string // 记录访问顺序,最新访问的放在最后
|
accessList []string // 记录访问顺序,最新访问的放在最后
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSCache 创建新的DNS缓存实例
|
// NewDNSCache 创建新的DNS缓存实例
|
||||||
@@ -109,8 +109,8 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
|
|||||||
|
|
||||||
key := cacheKey(qName, qType)
|
key := cacheKey(qName, qType)
|
||||||
item := &DNSCacheItem{
|
item := &DNSCacheItem{
|
||||||
Response: response.Copy(), // 复制响应以避免外部修改
|
Response: response.Copy(), // 复制响应以避免外部修改
|
||||||
Expiry: time.Now().Add(ttl),
|
Expiry: time.Now().Add(ttl),
|
||||||
HasDNSSEC: hasDNSSECRecords(response), // 检查并设置DNSSEC标志
|
HasDNSSEC: hasDNSSECRecords(response), // 检查并设置DNSSEC标志
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
189
dns/server.go
189
dns/server.go
@@ -48,8 +48,6 @@ type ClientStats struct {
|
|||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DNSAnswer DNS解析记录
|
// DNSAnswer DNS解析记录
|
||||||
type DNSAnswer struct {
|
type DNSAnswer struct {
|
||||||
Type string `json:"type"` // 记录类型
|
Type string `json:"type"` // 记录类型
|
||||||
@@ -131,8 +129,6 @@ type Server struct {
|
|||||||
stopped bool // 服务器是否已经停止
|
stopped bool // 服务器是否已经停止
|
||||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DNS查询缓存
|
// DNS查询缓存
|
||||||
DnsCache *DNSCache // DNS响应缓存
|
DnsCache *DNSCache // DNS响应缓存
|
||||||
|
|
||||||
@@ -205,7 +201,7 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
|||||||
maxQueryLogs: 10000, // 最大保存10000条日志
|
maxQueryLogs: 10000, // 最大保存10000条日志
|
||||||
saveDone: make(chan struct{}),
|
saveDone: make(chan struct{}),
|
||||||
stopped: false, // 初始化为未停止状态
|
stopped: false, // 初始化为未停止状态
|
||||||
|
|
||||||
// DNS查询缓存初始化
|
// DNS查询缓存初始化
|
||||||
DnsCache: NewDNSCache(cacheTTL),
|
DnsCache: NewDNSCache(cacheTTL),
|
||||||
// 初始化域名DNSSEC状态映射表
|
// 初始化域名DNSSEC状态映射表
|
||||||
@@ -258,8 +254,6 @@ func (s *Server) Start() error {
|
|||||||
// 启动自动保存功能
|
// 启动自动保存功能
|
||||||
go s.startAutoSave()
|
go s.startAutoSave()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 启动UDP服务
|
// 启动UDP服务
|
||||||
go func() {
|
go func() {
|
||||||
logger.Info(fmt.Sprintf("DNS UDP服务器启动,监听端口: %d", s.config.Port))
|
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.Ns = []dns.RR{}
|
||||||
mergedResponse.Extra = []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存储唯一记录,选择最长TTL
|
||||||
// 预分配map容量,减少扩容开销
|
// 预分配map容量,减少扩容开销
|
||||||
answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses))
|
answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses))
|
||||||
@@ -867,46 +884,51 @@ func mergeResponses(responses []*dns.Msg) *dns.Msg {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并Answer部分
|
// 只合并与最终Rcode匹配的响应记录
|
||||||
for _, rr := range resp.Answer {
|
if (mergedResponse.Rcode == dns.RcodeSuccess && resp.Rcode == dns.RcodeSuccess) ||
|
||||||
key := getRecordKey(rr)
|
(mergedResponse.Rcode == dns.RcodeNameError && resp.Rcode == dns.RcodeNameError) {
|
||||||
if existing, exists := answerMap[key]; exists {
|
|
||||||
// 如果存在相同记录,选择TTL更长的
|
// 合并Answer部分
|
||||||
if rr.Header().Ttl > existing.Header().Ttl {
|
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
|
answerMap[key] = rr
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
answerMap[key] = rr
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 合并Ns部分
|
// 合并Ns部分
|
||||||
for _, rr := range resp.Ns {
|
for _, rr := range resp.Ns {
|
||||||
key := getRecordKey(rr)
|
key := getRecordKey(rr)
|
||||||
if existing, exists := nsMap[key]; exists {
|
if existing, exists := nsMap[key]; exists {
|
||||||
// 如果存在相同记录,选择TTL更长的
|
// 如果存在相同记录,选择TTL更长的
|
||||||
if rr.Header().Ttl > existing.Header().Ttl {
|
if rr.Header().Ttl > existing.Header().Ttl {
|
||||||
|
nsMap[key] = rr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
nsMap[key] = rr
|
nsMap[key] = rr
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
nsMap[key] = rr
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 合并Extra部分
|
// 合并Extra部分
|
||||||
for _, rr := range resp.Extra {
|
for _, rr := range resp.Extra {
|
||||||
// 跳过OPT记录,避免重复
|
// 跳过OPT记录,避免重复
|
||||||
if rr.Header().Rrtype == dns.TypeOPT {
|
if rr.Header().Rrtype == dns.TypeOPT {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := getRecordKey(rr)
|
key := getRecordKey(rr)
|
||||||
if existing, exists := extraMap[key]; exists {
|
if existing, exists := extraMap[key]; exists {
|
||||||
// 如果存在相同记录,选择TTL更长的
|
// 如果存在相同记录,选择TTL更长的
|
||||||
if rr.Header().Ttl > existing.Header().Ttl {
|
if rr.Header().Ttl > existing.Header().Ttl {
|
||||||
|
extraMap[key] = rr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
extraMap[key] = rr
|
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))
|
responses := make(chan serverResponse, len(selectedUpstreamDNS))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// 向所有上游服务器并行发送请求
|
// 向所有上游服务器并行发送请求,每个请求带有超时
|
||||||
for _, upstream := range selectedUpstreamDNS {
|
for _, upstream := range selectedUpstreamDNS {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(server string) {
|
go func(server string) {
|
||||||
defer wg.Done()
|
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}
|
responses <- serverResponse{response, rtt, server, err}
|
||||||
}(upstream)
|
}(upstream)
|
||||||
}
|
}
|
||||||
@@ -1054,8 +1083,9 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
close(responses)
|
close(responses)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 收集所有有效响应
|
// 收集成功响应和NXDOMAIN响应分开
|
||||||
var validResponses []*dns.Msg
|
var successResponses []*dns.Msg
|
||||||
|
var nxdomainResponses []*dns.Msg
|
||||||
var totalRtt time.Duration
|
var totalRtt time.Duration
|
||||||
var responseCount int
|
var responseCount int
|
||||||
|
|
||||||
@@ -1087,9 +1117,9 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集有效响应
|
// 收集响应,按Rcode分类
|
||||||
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
|
if resp.response.Rcode == dns.RcodeSuccess {
|
||||||
validResponses = append(validResponses, resp.response)
|
successResponses = append(successResponses, resp.response)
|
||||||
totalRtt += resp.rtt
|
totalRtt += resp.rtt
|
||||||
responseCount++
|
responseCount++
|
||||||
|
|
||||||
@@ -1097,6 +1127,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
if usedDNSServer == "" {
|
if usedDNSServer == "" {
|
||||||
usedDNSServer = resp.server
|
usedDNSServer = resp.server
|
||||||
}
|
}
|
||||||
|
} else if resp.response.Rcode == dns.RcodeNameError {
|
||||||
|
nxdomainResponses = append(nxdomainResponses, resp.response)
|
||||||
} else {
|
} else {
|
||||||
// 更新备选响应,确保总有一个可用的响应
|
// 更新备选响应,确保总有一个可用的响应
|
||||||
if resp.response != nil {
|
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 {
|
if len(validResponses) > 0 {
|
||||||
bestResponse = mergeResponses(validResponses)
|
bestResponse = mergeResponses(validResponses)
|
||||||
@@ -1121,11 +1161,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
bestRtt = totalRtt / time.Duration(responseCount)
|
bestRtt = totalRtt / time.Duration(responseCount)
|
||||||
}
|
}
|
||||||
hasBestResponse = true
|
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":
|
case "fastest-ip":
|
||||||
// 最快的IP地址模式 - 使用TCP连接速度测量选择最快服务器
|
// 最快的IP地址模式 - 使用TCP连接速度测量选择最快服务器
|
||||||
// 1. 选择最快的服务器
|
// 1. 选择最快的服务器
|
||||||
@@ -1279,7 +1322,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
var fastestServer string
|
var fastestServer string
|
||||||
var fastestDnssecServer string
|
var fastestDnssecServer string
|
||||||
var fastestHasDnssec bool
|
var fastestHasDnssec bool
|
||||||
var validResponses []*dns.Msg
|
var successResponses []*dns.Msg
|
||||||
|
var nxdomainResponses []*dns.Msg
|
||||||
|
|
||||||
// 等待所有请求完成或超时
|
// 等待所有请求完成或超时
|
||||||
timer := time.NewTimer(defaultTimeout)
|
timer := time.NewTimer(defaultTimeout)
|
||||||
@@ -1301,30 +1345,8 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
// 检查是否包含DNSSEC记录
|
// 检查是否包含DNSSEC记录
|
||||||
containsDNSSEC := s.hasDNSSECRecords(resp.response)
|
containsDNSSEC := s.hasDNSSECRecords(resp.response)
|
||||||
|
|
||||||
// 如果启用了DNSSEC且响应包含DNSSEC记录,验证DNSSEC签名
|
// 对于不验证DNSSEC的域名,始终设置AD标志为false
|
||||||
// 但如果域名匹配不验证DNSSEC的模式,则跳过验证
|
if noDNSSEC {
|
||||||
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
|
|
||||||
resp.response.AuthenticatedData = false
|
resp.response.AuthenticatedData = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,8 +1361,12 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
|
|
||||||
// 如果响应成功或为NXDOMAIN
|
// 如果响应成功或为NXDOMAIN
|
||||||
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
|
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
|
||||||
// 添加到有效响应列表,用于后续合并
|
// 按Rcode分类添加到不同列表
|
||||||
validResponses = append(validResponses, resp.response)
|
if resp.response.Rcode == dns.RcodeSuccess {
|
||||||
|
successResponses = append(successResponses, resp.response)
|
||||||
|
} else {
|
||||||
|
nxdomainResponses = append(nxdomainResponses, resp.response)
|
||||||
|
}
|
||||||
|
|
||||||
// 快速返回逻辑:找到第一个有效响应或更快的响应
|
// 快速返回逻辑:找到第一个有效响应或更快的响应
|
||||||
if resp.response.Rcode == dns.RcodeSuccess {
|
if resp.response.Rcode == dns.RcodeSuccess {
|
||||||
@@ -1493,6 +1519,14 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
}
|
}
|
||||||
|
|
||||||
doneProcessing:
|
doneProcessing:
|
||||||
|
// 合并响应,优先使用成功响应,只有当没有成功响应时才使用NXDOMAIN响应
|
||||||
|
var validResponses []*dns.Msg
|
||||||
|
if len(successResponses) > 0 {
|
||||||
|
validResponses = successResponses
|
||||||
|
} else {
|
||||||
|
validResponses = nxdomainResponses
|
||||||
|
}
|
||||||
|
|
||||||
// 合并所有有效响应,用于缓存
|
// 合并所有有效响应,用于缓存
|
||||||
if len(validResponses) > 1 {
|
if len(validResponses) > 1 {
|
||||||
mergedResponse := mergeResponses(validResponses)
|
mergedResponse := mergeResponses(validResponses)
|
||||||
@@ -2255,7 +2289,6 @@ func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime in
|
|||||||
log := QueryLog{
|
log := QueryLog{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
ClientIP: clientIP,
|
ClientIP: clientIP,
|
||||||
Location: "", // 客户端IP地理位置由前端处理
|
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
QueryType: queryType,
|
QueryType: queryType,
|
||||||
ResponseTime: responseTime,
|
ResponseTime: responseTime,
|
||||||
@@ -2644,8 +2677,6 @@ func isPrivateIP(ip string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// loadStatsData 从文件加载统计数据
|
// loadStatsData 从文件加载统计数据
|
||||||
func (s *Server) loadStatsData() {
|
func (s *Server) loadStatsData() {
|
||||||
// 检查文件是否存在
|
// 检查文件是否存在
|
||||||
@@ -2908,10 +2939,6 @@ func (s *Server) startCpuUsageMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// getSystemCpuUsage 获取系统CPU使用率
|
// getSystemCpuUsage 获取系统CPU使用率
|
||||||
func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
|
func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
|
||||||
// 读取/proc/stat文件获取CPU统计信息
|
// 读取/proc/stat文件获取CPU统计信息
|
||||||
|
|||||||
12
download.sh
12
download.sh
@@ -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"
|
|
||||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -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=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -569,6 +569,71 @@ header p {
|
|||||||
-webkit-overflow-scrolling: touch; /* iOS平滑滚动 */
|
-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 {
|
#top-blocked-table, #top-resolved-table {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -1154,4 +1219,75 @@ tr:hover {
|
|||||||
|
|
||||||
.tracker-tooltip a:hover {
|
.tracker-tooltip a:hover {
|
||||||
text-decoration: underline;
|
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;
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,108 @@
|
|||||||
},
|
},
|
||||||
"company": "网易股份有限公司"
|
"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": "百度搜索",
|
"name": "百度搜索",
|
||||||
@@ -91,6 +193,64 @@
|
|||||||
},
|
},
|
||||||
"company": "北京百度网讯科技有限公司"
|
"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": "阿里云",
|
"name": "阿里云",
|
||||||
@@ -556,6 +716,12 @@
|
|||||||
"url": "a-msedge.net",
|
"url": "a-msedge.net",
|
||||||
"icon": "https://www.microsoft.com/favicon.ico"
|
"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"
|
"company": "微软 Microsoft"
|
||||||
},
|
},
|
||||||
"字节跳动": {
|
"字节跳动": {
|
||||||
@@ -565,6 +731,12 @@
|
|||||||
"url": "https://www.douyin.com/",
|
"url": "https://www.douyin.com/",
|
||||||
"icon": "https://www.douyin.com/favicon.ico"
|
"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": "今日头条",
|
"name": "今日头条",
|
||||||
"categoryId": 0,
|
"categoryId": 0,
|
||||||
@@ -712,6 +884,56 @@
|
|||||||
},
|
},
|
||||||
"icon": "https://p3-pangle-empower.byteimg.com/obj/tos-cn-i-742ihxn9bs/ad/pangle/homepage/assets/favicon.ico",
|
"icon": "https://p3-pangle-empower.byteimg.com/obj/tos-cn-i-742ihxn9bs/ad/pangle/homepage/assets/favicon.ico",
|
||||||
"company":"北京巨量引擎网络技术有限公司"
|
"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": "字节跳动有限公司"
|
"company": "字节跳动有限公司"
|
||||||
},
|
},
|
||||||
@@ -2374,6 +2596,99 @@
|
|||||||
"icon": "https://www.youku.com/favicon.ico"
|
"icon": "https://www.youku.com/favicon.ico"
|
||||||
},
|
},
|
||||||
"company": "优酷信息技术(北京)有限公司"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
<script src="css/vendor/tailwind.css"></script>
|
<script src="css/vendor/tailwind.css"></script>
|
||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
<!-- 自定义样式 -->
|
||||||
|
<link href="css/style.css" rel="stylesheet">
|
||||||
<!-- Chart.js 本地备用 -->
|
<!-- Chart.js 本地备用 -->
|
||||||
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
|
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
|
||||||
<!-- Tailwind 配置 -->
|
<!-- Tailwind 配置 -->
|
||||||
@@ -656,7 +658,7 @@
|
|||||||
|
|
||||||
<!-- 规则列表 -->
|
<!-- 规则列表 -->
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full resizable-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr class="border-b border-gray-200">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">规则</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">规则</th>
|
||||||
@@ -701,7 +703,7 @@
|
|||||||
|
|
||||||
<!-- 黑名单列表 -->
|
<!-- 黑名单列表 -->
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full resizable-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr class="border-b border-gray-200">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
|
||||||
@@ -741,7 +743,7 @@
|
|||||||
|
|
||||||
<!-- Hosts列表 -->
|
<!-- Hosts列表 -->
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full resizable-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr class="border-b border-gray-200">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
|
||||||
@@ -962,7 +964,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full resizable-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200">
|
<tr class="border-b border-gray-200">
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="time">
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="time">
|
||||||
|
|||||||
@@ -68,8 +68,14 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
|||||||
const parsedData = JSON.parse(responseText);
|
const parsedData = JSON.parse(responseText);
|
||||||
|
|
||||||
// 检查解析后的数据是否有效
|
// 检查解析后的数据是否有效
|
||||||
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
if (parsedData === null) {
|
||||||
console.warn('解析后的数据为空');
|
console.warn('解析后的数据为null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许返回空数组,但不允许返回空对象
|
||||||
|
if (typeof parsedData === 'object' && !Array.isArray(parsedData) && Object.keys(parsedData).length === 0) {
|
||||||
|
console.warn('解析后的数据为空对象');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,15 +165,15 @@ function processRealTimeData(stats) {
|
|||||||
let trendIcon = '---';
|
let trendIcon = '---';
|
||||||
|
|
||||||
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
|
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;
|
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||||
responsePercent = '0.0%';
|
responsePercent = '0.0%';
|
||||||
trendIcon = '•';
|
trendIcon = '•';
|
||||||
trendClass = 'text-gray-500';
|
trendClass = 'text-gray-500';
|
||||||
} else {
|
} else {
|
||||||
|
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||||
|
|
||||||
// 计算变化百分比
|
// 计算变化百分比
|
||||||
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||||
@@ -209,46 +209,15 @@ function processRealTimeData(stats) {
|
|||||||
|
|
||||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||||
if (queryPercentElem) {
|
if (queryPercentElem) {
|
||||||
// 计算查询类型趋势
|
// 计算最常用查询类型的百分比
|
||||||
let queryPercent = '---';
|
let queryTypePercentage = 0;
|
||||||
let trendClass = 'text-gray-400';
|
if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
|
||||||
let trendIcon = '---';
|
const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
|
||||||
|
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
|
queryPercentElem.textContent = `${Math.round(queryTypePercentage)}%`;
|
||||||
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
|
queryPercentElem.className = 'text-sm flex items-center text-primary';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +279,17 @@ function processRealTimeData(stats) {
|
|||||||
// 实时更新TOP客户端和TOP域名数据
|
// 实时更新TOP客户端和TOP域名数据
|
||||||
async function updateTopData() {
|
async function updateTopData() {
|
||||||
try {
|
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客户端数据
|
// 获取最新的TOP客户端数据
|
||||||
let clientsData = [];
|
let clientsData = [];
|
||||||
try {
|
try {
|
||||||
@@ -618,10 +598,10 @@ async function loadDashboardData() {
|
|||||||
updateCharts(stats, queryTypeStats);
|
updateCharts(stats, queryTypeStats);
|
||||||
|
|
||||||
// 更新表格数据
|
// 更新表格数据
|
||||||
updateTopBlockedTable(topBlockedDomains);
|
await updateTopBlockedTable(topBlockedDomains);
|
||||||
updateRecentBlockedTable(recentBlockedDomains);
|
updateRecentBlockedTable(recentBlockedDomains);
|
||||||
updateTopClientsTable(topClients);
|
await updateTopClientsTable(topClients);
|
||||||
updateTopDomainsTable(topDomains);
|
await updateTopDomainsTable(topDomains);
|
||||||
|
|
||||||
// 尝试从stats中获取总查询数等信息
|
// 尝试从stats中获取总查询数等信息
|
||||||
if (stats.dns) {
|
if (stats.dns) {
|
||||||
@@ -650,26 +630,35 @@ async function loadDashboardData() {
|
|||||||
let trendIcon = '---';
|
let trendIcon = '---';
|
||||||
|
|
||||||
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
|
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
|
||||||
// 存储当前值用于下次计算趋势
|
// 首次加载时初始化历史数据,不计算趋势
|
||||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
|
if (window.dashboardHistoryData.prevResponseTime === undefined || window.dashboardHistoryData.prevResponseTime === null) {
|
||||||
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||||
|
responsePercent = '0.0%';
|
||||||
// 计算变化百分比
|
trendIcon = '•';
|
||||||
if (prevResponseTime > 0) {
|
trendClass = 'text-gray-500';
|
||||||
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
} else {
|
||||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||||
|
|
||||||
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
|
// 计算变化百分比
|
||||||
if (changePercent > 0) {
|
if (prevResponseTime > 0) {
|
||||||
trendIcon = '↓';
|
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||||
trendClass = 'text-danger';
|
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||||
} else if (changePercent < 0) {
|
|
||||||
trendIcon = '↑';
|
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
|
||||||
trendClass = 'text-success';
|
if (changePercent > 0) {
|
||||||
} else {
|
trendIcon = '↓';
|
||||||
trendIcon = '•';
|
trendClass = 'text-danger';
|
||||||
trendClass = 'text-gray-500';
|
} 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返回的查询类型
|
// 直接使用API返回的查询类型
|
||||||
const queryType = stats.topQueryType || '---';
|
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');
|
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||||
if (queryPercentElem) {
|
if (queryPercentElem) {
|
||||||
queryPercentElem.textContent = '• ---';
|
queryPercentElem.textContent = `• ${Math.round(queryTypePercentage)}%`;
|
||||||
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
|
queryPercentElem.className = 'text-sm flex items-center text-primary';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('top-query-type').textContent = queryType;
|
document.getElementById('top-query-type').textContent = queryType;
|
||||||
@@ -1124,7 +1119,7 @@ function updateStatsCards(stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新Top屏蔽域名表格
|
// 更新Top屏蔽域名表格
|
||||||
function updateTopBlockedTable(domains) {
|
async function updateTopBlockedTable(domains) {
|
||||||
console.log('更新Top屏蔽域名表格,收到数据:', domains);
|
console.log('更新Top屏蔽域名表格,收到数据:', domains);
|
||||||
const tableBody = document.getElementById('top-blocked-table');
|
const tableBody = document.getElementById('top-blocked-table');
|
||||||
|
|
||||||
@@ -1155,14 +1150,37 @@ function updateTopBlockedTable(domains) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
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 domain = tableData[i];
|
||||||
|
// 检查域名是否是跟踪器
|
||||||
|
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
|
||||||
|
const isTracker = trackerInfo !== null;
|
||||||
|
|
||||||
|
// 构建跟踪器浮窗内容
|
||||||
|
const trackerTooltip = isTracker ? `
|
||||||
|
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||||
|
<div class="font-semibold mb-1">已知跟踪器</div>
|
||||||
|
<div class="mb-1">名称: ${trackerInfo.name}</div>
|
||||||
|
<div class="mb-1">类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}</div>
|
||||||
|
${trackerInfo.url ? `<div class="mb-1">URL: <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||||
|
${trackerInfo.source ? `<div class="mb-1">源: ${trackerInfo.source}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
||||||
<span class="font-medium truncate">${domain.name}</span>
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium truncate">${domain.name}</span>
|
||||||
|
${isTracker ? `
|
||||||
|
<div class="tracker-icon-container relative ml-2">
|
||||||
|
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||||
|
${trackerTooltip}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
|
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
|
||||||
@@ -1171,6 +1189,23 @@ function updateTopBlockedTable(domains) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableBody.innerHTML = html;
|
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 = '';
|
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 domain = tableData[i];
|
||||||
const time = formatTime(domain.timestamp);
|
const time = formatTime(domain.timestamp);
|
||||||
html += `
|
html += `
|
||||||
@@ -1226,8 +1261,187 @@ function updateRecentBlockedTable(domains) {
|
|||||||
tableBody.innerHTML = html;
|
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客户端表格
|
// 更新TOP客户端表格
|
||||||
function updateTopClientsTable(clients) {
|
async function updateTopClientsTable(clients) {
|
||||||
console.log('更新TOP客户端表格,收到数据:', clients);
|
console.log('更新TOP客户端表格,收到数据:', clients);
|
||||||
const tableBody = document.getElementById('top-clients-table');
|
const tableBody = document.getElementById('top-clients-table');
|
||||||
|
|
||||||
@@ -1237,6 +1451,15 @@ function updateTopClientsTable(clients) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 隐藏加载中状态
|
||||||
|
const loadingElement = document.getElementById('top-clients-loading');
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示数据区域
|
||||||
|
tableBody.classList.remove('hidden');
|
||||||
|
|
||||||
let tableData = [];
|
let tableData = [];
|
||||||
|
|
||||||
// 适配不同的数据结构
|
// 适配不同的数据结构
|
||||||
@@ -1265,18 +1488,22 @@ function updateTopClientsTable(clients) {
|
|||||||
console.log('使用示例数据填充TOP客户端表格');
|
console.log('使用示例数据填充TOP客户端表格');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只显示前5个客户端
|
|
||||||
tableData = tableData.slice(0, 5);
|
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
const client = tableData[i];
|
const client = tableData[i];
|
||||||
|
// 获取IP地理信息
|
||||||
|
const location = await getIpGeolocation(client.ip);
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||||
<span class="font-medium truncate">${client.ip}</span>
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium truncate">${client.ip}</span>
|
||||||
|
<span class="text-xs text-gray-500">${location}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||||
@@ -1288,7 +1515,7 @@ function updateTopClientsTable(clients) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新请求域名排行表格
|
// 更新请求域名排行表格
|
||||||
function updateTopDomainsTable(domains) {
|
async function updateTopDomainsTable(domains) {
|
||||||
console.log('更新请求域名排行表格,收到数据:', domains);
|
console.log('更新请求域名排行表格,收到数据:', domains);
|
||||||
const tableBody = document.getElementById('top-domains-table');
|
const tableBody = document.getElementById('top-domains-table');
|
||||||
|
|
||||||
@@ -1326,18 +1553,40 @@ function updateTopDomainsTable(domains) {
|
|||||||
console.log('使用示例数据填充请求域名排行表格');
|
console.log('使用示例数据填充请求域名排行表格');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只显示前5个域名
|
|
||||||
tableData = tableData.slice(0, 5);
|
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
const domain = tableData[i];
|
const domain = tableData[i];
|
||||||
|
// 检查域名是否是跟踪器
|
||||||
|
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
|
||||||
|
const isTracker = trackerInfo !== null;
|
||||||
|
|
||||||
|
// 构建跟踪器浮窗内容
|
||||||
|
const trackerTooltip = isTracker ? `
|
||||||
|
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||||
|
<div class="font-semibold mb-1">已知跟踪器</div>
|
||||||
|
<div class="mb-1">名称: ${trackerInfo.name}</div>
|
||||||
|
<div class="mb-1">类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}</div>
|
||||||
|
${trackerInfo.url ? `<div class="mb-1">URL: <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||||
|
${trackerInfo.source ? `<div class="mb-1">源: ${trackerInfo.source}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
||||||
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
<div class="flex items-center">
|
||||||
|
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
||||||
|
${isTracker ? `
|
||||||
|
<div class="tracker-icon-container relative ml-2">
|
||||||
|
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||||
|
${trackerTooltip}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
<span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
||||||
@@ -1346,15 +1595,32 @@ function updateTopDomainsTable(domains) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableBody.innerHTML = html;
|
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 isMixedView = true; // 是否为混合视图 - 默认显示混合视图
|
||||||
let lastSelectedIndex = 0; // 最后选中的按钮索引
|
let lastSelectedIndex = 2; // 最后选中的按钮索引,30天是第三个按钮
|
||||||
|
|
||||||
// 详细图表专用变量
|
// 详细图表专用变量
|
||||||
let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围
|
let detailedCurrentTimeRange = '30d'; // 详细图表当前时间范围
|
||||||
let detailedIsMixedView = false; // 详细图表是否为混合视图
|
let detailedIsMixedView = false; // 详细图表是否为混合视图
|
||||||
|
|
||||||
// 初始化时间范围切换
|
// 初始化时间范围切换
|
||||||
@@ -1491,10 +1757,12 @@ function initTimeRangeToggle() {
|
|||||||
// 移除自定义鼠标悬停提示效果
|
// 移除自定义鼠标悬停提示效果
|
||||||
});
|
});
|
||||||
|
|
||||||
// 确保默认选中第一个按钮并显示混合内容
|
// 确保默认选中30天按钮并显示混合内容
|
||||||
if (timeRangeButtons.length > 0) {
|
if (timeRangeButtons.length > 0) {
|
||||||
const firstButton = timeRangeButtons[0];
|
// 选择30天按钮(索引为2),如果不存在则使用第一个按钮
|
||||||
const firstStyle = buttonStyles[0];
|
const defaultButtonIndex = 2;
|
||||||
|
const defaultButton = timeRangeButtons[defaultButtonIndex] || timeRangeButtons[0];
|
||||||
|
const defaultStyle = buttonStyles[defaultButtonIndex % buttonStyles.length] || buttonStyles[0];
|
||||||
|
|
||||||
// 先重置所有按钮
|
// 先重置所有按钮
|
||||||
timeRangeButtons.forEach((btn, index) => {
|
timeRangeButtons.forEach((btn, index) => {
|
||||||
@@ -1506,13 +1774,13 @@ function initTimeRangeToggle() {
|
|||||||
btn.classList.add(...btnStyle.hover);
|
btn.classList.add(...btnStyle.hover);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 然后设置第一个按钮为激活状态,并标记为混合视图
|
// 然后设置30天按钮为激活状态,并标记为混合视图
|
||||||
firstButton.classList.remove(...firstStyle.normal);
|
defaultButton.classList.remove(...defaultStyle.normal);
|
||||||
firstButton.classList.remove(...firstStyle.hover);
|
defaultButton.classList.remove(...defaultStyle.hover);
|
||||||
firstButton.classList.add('active', 'mixed-view-active');
|
defaultButton.classList.add('active', 'mixed-view-active');
|
||||||
firstButton.classList.add(...firstStyle.active);
|
defaultButton.classList.add(...defaultStyle.active);
|
||||||
firstButton.classList.add(...firstStyle.activeHover);
|
defaultButton.classList.add(...defaultStyle.activeHover);
|
||||||
console.log('默认选中第一个按钮并显示混合内容:', firstButton.textContent.trim());
|
console.log('默认选中30天按钮并显示混合内容:', defaultButton.textContent.trim());
|
||||||
|
|
||||||
// 设置默认显示混合内容
|
// 设置默认显示混合内容
|
||||||
isMixedView = true;
|
isMixedView = true;
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ let logsChart = null;
|
|||||||
let currentSortField = '';
|
let currentSortField = '';
|
||||||
let currentSortDirection = 'desc'; // 默认降序
|
let currentSortDirection = 'desc'; // 默认降序
|
||||||
|
|
||||||
// IP地理位置缓存
|
// IP地理位置缓存(检查是否已经存在,避免重复声明)
|
||||||
let ipGeolocationCache = {};
|
if (typeof ipGeolocationCache === 'undefined') {
|
||||||
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
var ipGeolocationCache = {};
|
||||||
|
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||||
|
}
|
||||||
|
|
||||||
// 获取IP地理位置信息
|
// 获取IP地理位置信息
|
||||||
async function getIpGeolocation(ip) {
|
async function getIpGeolocation(ip) {
|
||||||
@@ -112,10 +114,12 @@ function isPrivateIP(ip) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跟踪器数据库缓存
|
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
|
||||||
let trackersDatabase = null;
|
if (typeof trackersDatabase === 'undefined') {
|
||||||
let trackersLoaded = false;
|
var trackersDatabase = null;
|
||||||
let trackersLoading = false;
|
var trackersLoaded = false;
|
||||||
|
var trackersLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 域名信息数据库缓存
|
// 域名信息数据库缓存
|
||||||
let domainInfoDatabase = null;
|
let domainInfoDatabase = null;
|
||||||
@@ -477,6 +481,270 @@ function extractPrimaryDomain(domain) {
|
|||||||
return result;
|
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() {
|
function initLogsPage() {
|
||||||
console.log('初始化查询日志页面');
|
console.log('初始化查询日志页面');
|
||||||
@@ -499,6 +767,9 @@ function initLogsPage() {
|
|||||||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||||||
connectLogsWebSocket();
|
connectLogsWebSocket();
|
||||||
|
|
||||||
|
// 初始化列宽调节功能
|
||||||
|
initResizableColumns();
|
||||||
|
|
||||||
// 窗口大小改变时重新加载日志表格
|
// 窗口大小改变时重新加载日志表格
|
||||||
window.addEventListener('resize', handleWindowResize);
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
@@ -723,7 +994,7 @@ function loadLogsStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载日志详情
|
// 加载日志详情
|
||||||
function loadLogs() {
|
async function loadLogs() {
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
const loadingEl = document.getElementById('logs-loading');
|
const loadingEl = document.getElementById('logs-loading');
|
||||||
if (loadingEl) {
|
if (loadingEl) {
|
||||||
@@ -748,61 +1019,70 @@ function loadLogs() {
|
|||||||
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用封装的apiRequest函数进行API调用
|
try {
|
||||||
apiRequest(endpoint)
|
// 使用封装的apiRequest函数进行API调用
|
||||||
.then(data => {
|
const logsData = await apiRequest(endpoint);
|
||||||
if (data && data.error) {
|
|
||||||
console.error('加载日志详情失败:', data.error);
|
if (logsData && logsData.error) {
|
||||||
// 隐藏加载状态
|
console.error('加载日志详情失败:', logsData.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();
|
|
||||||
|
|
||||||
// 隐藏加载状态
|
// 隐藏加载状态
|
||||||
if (loadingEl) {
|
if (loadingEl) {
|
||||||
loadingEl.classList.add('hidden');
|
loadingEl.classList.add('hidden');
|
||||||
}
|
}
|
||||||
})
|
return;
|
||||||
.catch(error => {
|
}
|
||||||
console.error('加载日志详情失败:', error);
|
|
||||||
|
// 加载日志总数
|
||||||
// 隐藏加载状态
|
const [logs, countData] = await Promise.all([
|
||||||
if (loadingEl) {
|
Promise.resolve(logsData || []), // 确保logsData是数组
|
||||||
loadingEl.classList.add('hidden');
|
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 = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||||
|
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||||
|
<div>暂无查询日志</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新日志表格
|
// 更新日志表格
|
||||||
|
|||||||
@@ -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: [],
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user