This commit is contained in:
Alex Yang
2025-12-16 00:17:05 +08:00
parent 11d39d6b76
commit c0ddaf8b44
49 changed files with 1514 additions and 941752 deletions

View File

@@ -1,81 +0,0 @@
# DNS服务器项目介绍
## 项目概述
这是一个基于Go语言开发的高性能DNS服务器具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置同时能够自动更新和缓存远程规则列表。
## 技术架构
### 核心组件
1. DNS服务模块 ( `server.go` )
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
- 支持配置上游DNS服务器进行递归查询
- 实现域名屏蔽、统计数据收集等核心功能
2. 屏蔽管理系统 ( `manager.go` )
- 管理本地和远程屏蔽规则
- 支持规则缓存、自动更新和统计
- 实现域名和正则表达式规则的解析和匹配
3. HTTP控制台 ( `server.go` )
- 提供Web管理界面
- 实现REST API用于配置管理和数据查询
4. 配置管理 ( `config.go` )
- 定义配置结构和加载功能
- 支持JSON格式配置文件
## 主要功能特性
### 1. 域名屏蔽系统
- 支持本地规则文件和远程规则URL
- 多种屏蔽方式NXDOMAIN、refused、emptyIP、customIP
- 支持域名精确匹配和正则表达式匹配
- 远程规则自动缓存和更新机制
### 2. Hosts管理
- 支持自定义Hosts映射
- 提供Web界面管理Hosts条目
- 自动保存Hosts配置
### 3. 统计分析功能
- 记录屏蔽域名统计信息
- 记录解析域名统计信息
- 提供按小时统计的屏蔽数据
- 支持查询最常屏蔽和解析的域名
### 4. 远程规则管理
- 支持添加多个远程规则URL
- 自动定期更新远程规则
- 本地缓存机制确保规则可用性
- Web界面可视化管理
### 5. 管理界面
- 提供直观的Web控制台
- 支持查看服务器状态和统计信息
- 规则管理和配置修改
- DNS查询测试工具
## 项目结构
```
/root/dns/
├── config/          # 配置管理
├── data/            # 数据目录(包含缓存和统计)
   └── remote_rules/ # 远程规则缓存
├── dns/             # DNS服务器核心
├── http/            # HTTP控制台
├── logger/          # 日志系统
├── shield/          # 屏蔽规则管理
├── static/          # 静态Web文件
├── main.go          # 程序入口
└── config.json      # 配置文件
```
## 配置项说明
主要配置文件 `config.json` 包含以下部分:
- DNS配置 端口、上游DNS服务器、超时设置等
- HTTP配置 :控制台端口、主机绑定等
- 屏蔽配置 规则文件路径、远程规则URL、更新间隔等
- 日志配置 :日志文件路径、级别设置等
## 使用场景
1. 网络内容过滤(广告、恶意网站屏蔽)
2. 本地DNS缓存加速
3. 企业/家庭网络DNS管理
4. 开发测试环境DNS重定向
## 技术栈
- 语言 Go
- DNS库 github.com/miekg/dns
- 日志库 github.com/sirupsen/logrus
- Web前端 HTML/CSS/JavaScript
该DNS服务器具有高性能、功能全面、易于配置等特点适用于需要精确控制DNS查询结果的各种网络环境。

View File

@@ -1,81 +0,0 @@
# DNS服务器项目介绍
## 项目概述
这是一个基于Go语言开发的高性能DNS服务器具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置同时能够自动更新和缓存远程规则列表。
## 技术架构
### 核心组件
1. DNS服务模块 ( `server.go` )
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
- 支持配置上游DNS服务器进行递归查询
- 实现域名屏蔽、统计数据收集等核心功能
2. 屏蔽管理系统 ( `manager.go` )
- 管理本地和远程屏蔽规则
- 支持规则缓存、自动更新和统计
- 实现域名和正则表达式规则的解析和匹配
3. HTTP控制台 ( `server.go` )
- 提供Web管理界面
- 实现REST API用于配置管理和数据查询
4. 配置管理 ( `config.go` )
- 定义配置结构和加载功能
- 支持JSON格式配置文件
## 主要功能特性
### 1. 域名屏蔽系统
- 支持本地规则文件和远程规则URL
- 多种屏蔽方式NXDOMAIN、refused、emptyIP、customIP
- 支持域名精确匹配和正则表达式匹配
- 远程规则自动缓存和更新机制
### 2. Hosts管理
- 支持自定义Hosts映射
- 提供Web界面管理Hosts条目
- 自动保存Hosts配置
### 3. 统计分析功能
- 记录屏蔽域名统计信息
- 记录解析域名统计信息
- 提供按小时统计的屏蔽数据
- 支持查询最常屏蔽和解析的域名
### 4. 远程规则管理
- 支持添加多个远程规则URL
- 自动定期更新远程规则
- 本地缓存机制确保规则可用性
- Web界面可视化管理
### 5. 管理界面
- 提供直观的Web控制台
- 支持查看服务器状态和统计信息
- 规则管理和配置修改
- DNS查询测试工具
## 项目结构
```
/root/dns/
├── config/          # 配置管理
├── data/            # 数据目录(包含缓存和统计)
   └── remote_rules/ # 远程规则缓存
├── dns/             # DNS服务器核心
├── http/            # HTTP控制台
├── logger/          # 日志系统
├── shield/          # 屏蔽规则管理
├── static/          # 静态Web文件
├── main.go          # 程序入口
└── config.json      # 配置文件
```
## 配置项说明
主要配置文件 `config.json` 包含以下部分:
- DNS配置 端口、上游DNS服务器、超时设置等
- HTTP配置 :控制台端口、主机绑定等
- 屏蔽配置 规则文件路径、远程规则URL、更新间隔等
- 日志配置 :日志文件路径、级别设置等
## 使用场景
1. 网络内容过滤(广告、恶意网站屏蔽)
2. 本地DNS缓存加速
3. 企业/家庭网络DNS管理
4. 开发测试环境DNS重定向
## 技术栈
- 语言 Go
- DNS库 github.com/miekg/dns
- 日志库 github.com/sirupsen/logrus
- Web前端 HTML/CSS/JavaScript
该DNS服务器具有高性能、功能全面、易于配置等特点适用于需要精确控制DNS查询结果的各种网络环境。

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,9 @@
"http": {
"port": 8080,
"host": "0.0.0.0",
"enableAPI": true
"enableAPI": true,
"username": "admin",
"password": "admin"
},
"shield": {
"localRulesFile": "data/rules.txt",
@@ -113,4 +115,4 @@
"maxBackups": 10,
"maxAge": 30
}
}
}

View File

@@ -12,6 +12,7 @@ type DNSConfig struct {
Timeout int `json:"timeout"`
StatsFile string `json:"statsFile"` // 统计数据持久化文件
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间分钟
}
// HTTPConfig HTTP控制台配置
@@ -19,6 +20,8 @@ type HTTPConfig struct {
Port int `json:"port"`
Host string `json:"host"`
EnableAPI bool `json:"enableAPI"`
Username string `json:"username"` // 登录用户名
Password string `json:"password"` // 登录密码
}
// BlacklistEntry 黑名单条目
@@ -86,12 +89,23 @@ func LoadConfig(path string) (*Config, error) {
if config.DNS.SaveInterval == 0 {
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
}
// 默认DNS缓存TTL为30分钟
if config.DNS.CacheTTL == 0 {
config.DNS.CacheTTL = 30 // 默认30分钟
}
if config.HTTP.Port == 0 {
config.HTTP.Port = 8080
}
if config.HTTP.Host == "" {
config.HTTP.Host = "0.0.0.0"
}
// 默认用户名和密码如果未配置则使用admin/admin
if config.HTTP.Username == "" {
config.HTTP.Username = "admin"
}
if config.HTTP.Password == "" {
config.HTTP.Password = "admin"
}
if config.Shield.UpdateInterval == 0 {
config.Shield.UpdateInterval = 3600
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,5 +0,0 @@
# DNS Server Hosts File
# Generated by DNS Server
127.0.0.1 localhost
::1 localhost

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>

View File

View File

@@ -1,5 +0,0 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-12-15T23:53:03.778364374+08:00"
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
@@ -37,6 +38,27 @@ type ClientStats struct {
LastSeen time.Time
}
// IPGeolocation IP地理位置信息
type IPGeolocation struct {
Country string `json:"country"` // 国家
City string `json:"city"` // 城市
Expiry time.Time `json:"expiry"` // 缓存过期时间
}
// QueryLog 查询日志记录
type QueryLog struct {
Timestamp time.Time // 查询时间
ClientIP string // 客户端IP
Location string // IP地理位置国家 城市)
Domain string // 查询域名
QueryType string // 查询类型
ResponseTime int64 // 响应时间(ms)
Result string // 查询结果allowed, blocked, error
BlockRule string // 屏蔽规则(如果被屏蔽)
BlockType string // 屏蔽类型(如果被屏蔽)
FromCache bool // 是否来自缓存
}
// StatsData 用于持久化的统计数据结构
type StatsData struct {
Stats *Stats `json:"stats"`
@@ -73,11 +95,22 @@ type Server struct {
dailyStats map[string]int64 // 按天统计屏蔽数量
monthlyStatsMutex sync.RWMutex
monthlyStats map[string]int64 // 按月统计屏蔽数量
saveTicker *time.Ticker // 用于定时保存数据
startTime time.Time // 服务器启动时间
saveDone chan struct{} // 用于通知保存协程停止
stopped bool // 服务器是否已经停止
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
queryLogsMutex sync.RWMutex
queryLogs []QueryLog // 查询日志列表
maxQueryLogs int // 最大保存日志数量
saveTicker *time.Ticker // 用于定时保存数据
startTime time.Time // 服务器启动时间
saveDone chan struct{} // 用于通知保存协程停止
stopped bool // 服务器是否已经停止
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
// IP地理位置缓存
ipGeolocationCache map[string]*IPGeolocation // IP地址到地理位置的映射
ipGeolocationCacheMutex sync.RWMutex // 保护IP地理位置缓存的互斥锁
ipGeolocationCacheTTL time.Duration // 缓存有效期
// DNS查询缓存
dnsCache *DNSCache // DNS响应缓存
}
// Stats DNS服务器统计信息
@@ -97,6 +130,10 @@ type Stats struct {
// NewServer 创建DNS服务器实例
func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shieldManager *shield.ShieldManager) *Server {
ctx, cancel := context.WithCancel(context.Background())
// 从配置中读取DNS缓存TTL值分钟
cacheTTL := time.Duration(config.CacheTTL) * time.Minute
server := &Server{
config: config,
shieldConfig: shieldConfig,
@@ -125,8 +162,15 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
hourlyStats: make(map[string]int64),
dailyStats: make(map[string]int64),
monthlyStats: make(map[string]int64),
queryLogs: make([]QueryLog, 0, 1000), // 初始化查询日志切片容量1000
maxQueryLogs: 10000, // 最大保存10000条日志
saveDone: make(chan struct{}),
stopped: false, // 初始化为未停止状态
// IP地理位置缓存初始化
ipGeolocationCache: make(map[string]*IPGeolocation),
ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时
// DNS查询缓存初始化
dnsCache: NewDNSCache(cacheTTL),
}
// 加载已保存的统计数据
@@ -232,8 +276,16 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
// 获取来源IP
sourceIP := w.RemoteAddr().String()
// 提取IP地址部分去掉端口
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
sourceIP = sourceIP[:idx]
if strings.HasPrefix(sourceIP, "[") {
// IPv6地址格式: [::1]:53
if idx := strings.Index(sourceIP, "]"); idx >= 0 {
sourceIP = sourceIP[1:idx] // 去掉方括号
}
} else {
// IPv4地址格式: 127.0.0.1:53
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
sourceIP = sourceIP[:idx]
}
}
// 更新来源IP统计
@@ -246,28 +298,10 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
// 更新客户端统计
s.updateClientStats(sourceIP)
// 只处理递归查询
if r.RecursionDesired == false {
response := new(dns.Msg)
response.SetReply(r)
response.RecursionAvailable = true
response.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(response)
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
return
}
// 获取查询域名和类型
var domain string
var queryType string
var qType uint16
if len(r.Question) > 0 {
domain = r.Question[0].Name
// 移除末尾的点
@@ -276,6 +310,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
}
// 获取查询类型
queryType = dns.TypeToString[r.Question[0].Qtype]
qType = r.Question[0].Qtype
// 更新查询类型统计
s.updateStats(func(stats *Stats) {
stats.QueryTypes[queryType]++
@@ -284,22 +319,52 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
// 检查hosts文件是否有匹配
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
s.handleHostsResponse(w, r, ip)
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
// 只处理递归查询
if r.RecursionDesired == false {
response := new(dns.Msg)
response.SetReply(r)
response.RecursionAvailable = true
response.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(response)
// 缓存命中响应时间设为0ms
responseTime := int64(0)
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
// 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false)
return
}
// 检查hosts文件是否有匹配
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
s.handleHostsResponse(w, r, ip)
// 缓存命中响应时间设为0ms
responseTime := int64(0)
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
// 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
return
}
// 检查是否被屏蔽
if s.shieldManager.IsBlocked(domain) {
// 获取屏蔽详情
blockDetails := s.shieldManager.CheckDomainBlockDetails(domain)
blockRule, _ := blockDetails["blockRule"].(string)
blockType, _ := blockDetails["blockRuleType"].(string)
s.handleBlockedResponse(w, r, domain)
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
@@ -309,19 +374,68 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
// 添加查询日志
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false)
return
}
// 转发到上游DNS服务器
s.forwardDNSRequest(w, r, domain)
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
// 检查缓存中是否有响应(增强版缓存查询)
if cachedResponse, found := s.dnsCache.Get(r.Question[0].Name, qType); found {
// 缓存命中,直接返回缓存的响应
cachedResponseCopy := cachedResponse.Copy() // 创建响应副本避免并发修改问题
cachedResponseCopy.Id = r.Id // 更新ID以匹配请求
cachedResponseCopy.Compress = true
w.WriteMsg(cachedResponseCopy)
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
// 添加查询日志 - 标记为缓存
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true)
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType)
return
}
// 缓存未命中转发到上游DNS服务器
response, rtt := s.forwardDNSRequestWithCache(r, domain)
if response != nil {
// 写入响应给客户端
w.WriteMsg(response)
}
// 使用上游服务器的实际响应时间(转换为毫秒)
responseTime := int64(rtt.Milliseconds())
// 如果rtt为0查询失败则使用本地计算的时间
if responseTime == 0 {
responseTime = time.Since(startTime).Milliseconds()
}
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
// 如果响应成功,缓存结果(增强版缓存存储)
if response != nil && response.Rcode == dns.RcodeSuccess {
// 创建响应副本以避免后续修改影响缓存
responseCopy := response.Copy()
// 设置合理的TTL不超过默认的30分钟
defaultCacheTTL := 30 * time.Minute
s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL)
logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL)
}
// 添加查询日志 - 标记为实时
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
}
// handleHostsResponse 处理hosts文件匹配的响应
@@ -425,7 +539,8 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
}
// forwardDNSRequest 转发DNS请求到上游服务器
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) {
// 尝试所有上游DNS服务器
for _, upstream := range s.config.UpstreamDNS {
response, rtt, err := s.resolver.Exchange(r, upstream)
@@ -433,7 +548,6 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
// 设置递归可用标志
response.RecursionAvailable = true
w.WriteMsg(response)
logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream)
// 记录解析域名统计
@@ -442,7 +556,7 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
s.updateStats(func(stats *Stats) {
stats.Allowed++
})
return
return response, rtt
}
}
@@ -451,12 +565,18 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
response.SetReply(r)
response.RecursionAvailable = true
response.SetRcode(r, dns.RcodeServerFailure)
w.WriteMsg(response)
logger.Error("DNS查询失败", "domain", domain)
s.updateStats(func(stats *Stats) {
stats.Errors++
})
return response, 0
}
// forwardDNSRequest 转发DNS请求到上游服务器
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
response, _ := s.forwardDNSRequestWithCache(r, domain)
w.WriteMsg(response)
}
// updateBlockedDomainStats 更新被屏蔽域名统计
@@ -539,6 +659,38 @@ func (s *Server) updateStats(update func(*Stats)) {
update(s.stats)
}
// addQueryLog 添加查询日志
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache bool) {
// 获取IP地理位置
location := s.getIpGeolocation(clientIP)
// 创建日志记录
log := QueryLog{
Timestamp: time.Now(),
ClientIP: clientIP,
Location: location,
Domain: domain,
QueryType: queryType,
ResponseTime: responseTime,
Result: result,
BlockRule: blockRule,
BlockType: blockType,
FromCache: fromCache,
}
// 添加到日志列表
s.queryLogsMutex.Lock()
defer s.queryLogsMutex.Unlock()
// 插入到列表开头
s.queryLogs = append([]QueryLog{log}, s.queryLogs...)
// 限制日志数量
if len(s.queryLogs) > s.maxQueryLogs {
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
}
}
// GetStartTime 获取服务器启动时间
func (s *Server) GetStartTime() time.Time {
return s.startTime
@@ -576,6 +728,145 @@ func (s *Server) GetStats() *Stats {
}
}
// GetQueryLogs 获取查询日志
func (s *Server) GetQueryLogs(limit, offset int, sortField, sortDirection, resultFilter, searchTerm string) []QueryLog {
s.queryLogsMutex.RLock()
defer s.queryLogsMutex.RUnlock()
// 确保偏移量和限制值合理
if offset < 0 {
offset = 0
}
if limit <= 0 {
limit = 100 // 默认返回100条日志
}
// 创建日志副本用于过滤和排序
var logsCopy []QueryLog
// 先过滤日志
for _, log := range s.queryLogs {
// 应用结果过滤
if resultFilter != "" && log.Result != resultFilter {
continue
}
// 应用搜索过滤
if searchTerm != "" {
// 搜索域名或客户端IP
if !strings.Contains(log.Domain, searchTerm) && !strings.Contains(log.ClientIP, searchTerm) {
continue
}
}
logsCopy = append(logsCopy, log)
}
// 排序日志
if sortField != "" {
sort.Slice(logsCopy, func(i, j int) bool {
var a, b interface{}
switch sortField {
case "time":
a = logsCopy[i].Timestamp
b = logsCopy[j].Timestamp
case "clientIp":
a = logsCopy[i].ClientIP
b = logsCopy[j].ClientIP
case "domain":
a = logsCopy[i].Domain
b = logsCopy[j].Domain
case "responseTime":
a = logsCopy[i].ResponseTime
b = logsCopy[j].ResponseTime
case "blockRule":
a = logsCopy[i].BlockRule
b = logsCopy[j].BlockRule
default:
// 默认按时间排序
a = logsCopy[i].Timestamp
b = logsCopy[j].Timestamp
}
// 根据排序方向比较
if sortDirection == "asc" {
return compareValues(a, b) < 0
}
return compareValues(a, b) > 0
})
}
// 计算返回范围
start := offset
end := offset + limit
if end > len(logsCopy) {
end = len(logsCopy)
}
if start >= len(logsCopy) {
return []QueryLog{} // 没有数据,返回空切片
}
return logsCopy[start:end]
}
// compareValues 比较两个值
func compareValues(a, b interface{}) int {
switch v1 := a.(type) {
case time.Time:
v2 := b.(time.Time)
if v1.Before(v2) {
return -1
}
if v1.After(v2) {
return 1
}
return 0
case string:
v2 := b.(string)
if v1 < v2 {
return -1
}
if v1 > v2 {
return 1
}
return 0
case int64:
v2 := b.(int64)
if v1 < v2 {
return -1
}
if v1 > v2 {
return 1
}
return 0
default:
return 0
}
}
// GetQueryLogsCount 获取查询日志总数
func (s *Server) GetQueryLogsCount() int {
s.queryLogsMutex.RLock()
defer s.queryLogsMutex.RUnlock()
return len(s.queryLogs)
}
// GetQueryStats 获取查询统计信息
func (s *Server) GetQueryStats() map[string]interface{} {
s.statsMutex.Lock()
defer s.statsMutex.Unlock()
// 计算统计数据
return map[string]interface{}{
"totalQueries": s.stats.Queries,
"blockedQueries": s.stats.Blocked,
"allowedQueries": s.stats.Allowed,
"errorQueries": s.stats.Errors,
"avgResponseTime": s.stats.AvgResponseTime,
"activeIPs": len(s.stats.SourceIPs),
}
}
// GetTopBlockedDomains 获取TOP屏蔽域名列表
func (s *Server) GetTopBlockedDomains(limit int) []BlockedDomain {
s.blockedDomainsMutex.RLock()
@@ -707,6 +998,132 @@ func (s *Server) GetMonthlyStats() map[string]int64 {
return result
}
// isPrivateIP 检测IP地址是否为内网IP
func isPrivateIP(ip string) bool {
// 解析IP地址
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false
}
// 检查IPv4内网地址
if ipv4 := parsedIP.To4(); ipv4 != nil {
// 10.0.0.0/8
if ipv4[0] == 10 {
return true
}
// 172.16.0.0/12
if ipv4[0] == 172 && (ipv4[1] >= 16 && ipv4[1] <= 31) {
return true
}
// 192.168.0.0/16
if ipv4[0] == 192 && ipv4[1] == 168 {
return true
}
// 127.0.0.0/8 (localhost)
if ipv4[0] == 127 {
return true
}
// 169.254.0.0/16 (链路本地地址)
if ipv4[0] == 169 && ipv4[1] == 254 {
return true
}
return false
}
// 检查IPv6内网地址
// ::1/128 (localhost)
if parsedIP.IsLoopback() {
return true
}
// fc00::/7 (唯一本地地址)
if parsedIP[0]&0xfc == 0xfc {
return true
}
// fe80::/10 (链路本地地址)
if parsedIP[0]&0xfe == 0xfe && parsedIP[1]&0xc0 == 0x80 {
return true
}
return false
}
// getIpGeolocation 获取IP地址的地理位置信息
func (s *Server) getIpGeolocation(ip string) string {
// 检查IP是否为本地或内网地址
if isPrivateIP(ip) {
return "内网 内网"
}
// 先检查缓存
s.ipGeolocationCacheMutex.RLock()
geo, exists := s.ipGeolocationCache[ip]
s.ipGeolocationCacheMutex.RUnlock()
// 如果缓存存在且未过期,直接返回
if exists && time.Now().Before(geo.Expiry) {
return fmt.Sprintf("%s %s", geo.Country, geo.City)
}
// 缓存不存在或已过期从API获取
geoInfo, err := s.fetchIpGeolocationFromAPI(ip)
if err != nil {
logger.Error("获取IP地理位置失败", "ip", ip, "error", err)
return "未知 未知"
}
// 保存到缓存
s.ipGeolocationCacheMutex.Lock()
s.ipGeolocationCache[ip] = &IPGeolocation{
Country: geoInfo["country"].(string),
City: geoInfo["city"].(string),
Expiry: time.Now().Add(s.ipGeolocationCacheTTL),
}
s.ipGeolocationCacheMutex.Unlock()
// 返回格式化的地理位置
return fmt.Sprintf("%s %s", geoInfo["country"].(string), geoInfo["city"].(string))
}
// fetchIpGeolocationFromAPI 从第三方API获取IP地理位置信息
func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, error) {
// 使用ip-api.com获取IP地理位置信息
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=country,city", ip)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 读取响应内容
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// 解析JSON响应
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
// 检查API返回状态
status, ok := result["status"].(string)
if !ok || status != "success" {
return nil, fmt.Errorf("API返回错误状态: %v", result)
}
// 确保国家和城市字段存在
if _, ok := result["country"]; !ok {
result["country"] = "未知"
}
if _, ok := result["city"]; !ok {
result["city"] = "未知"
}
return result, nil
}
// loadStatsData 从文件加载统计数据
func (s *Server) loadStatsData() {
if s.config.StatsFile == "" {
@@ -774,6 +1191,58 @@ func (s *Server) loadStatsData() {
s.clientStatsMutex.Unlock()
logger.Info("统计数据加载成功")
// 加载查询日志
s.loadQueryLogs()
}
// loadQueryLogs 从文件加载查询日志
func (s *Server) loadQueryLogs() {
if s.config.StatsFile == "" {
return
}
// 获取绝对路径
statsFilePath, err := filepath.Abs(s.config.StatsFile)
if err != nil {
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
return
}
// 构建查询日志文件路径
queryLogPath := filepath.Join(filepath.Dir(statsFilePath), "querylog.json")
// 检查文件是否存在
if _, err := os.Stat(queryLogPath); os.IsNotExist(err) {
logger.Info("查询日志文件不存在,将使用空列表", "file", queryLogPath)
return
}
// 读取文件内容
data, err := ioutil.ReadFile(queryLogPath)
if err != nil {
logger.Error("读取查询日志文件失败", "error", err)
return
}
// 解析数据
var logs []QueryLog
err = json.Unmarshal(data, &logs)
if err != nil {
logger.Error("解析查询日志失败", "error", err)
return
}
// 更新查询日志
s.queryLogsMutex.Lock()
s.queryLogs = logs
// 确保日志数量不超过限制
if len(s.queryLogs) > s.maxQueryLogs {
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
}
s.queryLogsMutex.Unlock()
logger.Info("查询日志加载成功", "count", len(logs))
}
// saveStatsData 保存统计数据到文件
@@ -862,6 +1331,37 @@ func (s *Server) saveStatsData() {
}
logger.Info("统计数据保存成功", "file", statsFilePath)
// 保存查询日志到文件
s.saveQueryLogs(statsDir)
}
// saveQueryLogs 保存查询日志到文件
func (s *Server) saveQueryLogs(dataDir string) {
// 构建查询日志文件路径
queryLogPath := filepath.Join(dataDir, "querylog.json")
// 获取查询日志数据
s.queryLogsMutex.RLock()
logsCopy := make([]QueryLog, len(s.queryLogs))
copy(logsCopy, s.queryLogs)
s.queryLogsMutex.RUnlock()
// 序列化数据
jsonData, err := json.MarshalIndent(logsCopy, "", " ")
if err != nil {
logger.Error("序列化查询日志失败", "error", err)
return
}
// 写入文件
err = os.WriteFile(queryLogPath, jsonData, 0644)
if err != nil {
logger.Error("保存查询日志到文件失败", "file", queryLogPath, "error", err)
return
}
logger.Info("查询日志保存成功", "file", queryLogPath)
}
// startCpuUsageMonitor 启动CPU使用率监控

14
go.mod
View File

@@ -8,31 +8,17 @@ require (
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.68
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/http-swagger v1.2.0
)
// 清理不需要的依赖
// 之前的go.sum可能包含lumberjack的记录但现在已经不再使用
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
github.com/swaggo/swag v1.7.8 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

94
go.sum
View File

@@ -1,126 +1,32 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.2.0 h1:G5EBD5nvw379l2sFhact660YDT++eLviczLPrgNw/lU=
github.com/swaggo/http-swagger v1.2.0/go.mod h1:P7+V1SLG2zloe+VvAGL7WgFimhJACaBLAv2N7YQ0ikI=
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -26,6 +26,11 @@ type Server struct {
shieldManager *shield.ShieldManager
server *http.Server
// 会话管理相关字段
sessions map[string]time.Time // 会话ID到过期时间的映射
sessionsMutex sync.Mutex // 会话映射的互斥锁
sessionTTL time.Duration // 会话过期时间
// WebSocket相关字段
upgrader websocket.Upgrader
clients map[*websocket.Conn]bool
@@ -50,10 +55,15 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
},
clients: make(map[*websocket.Conn]bool),
broadcastChan: make(chan []byte, 100),
// 会话管理初始化
sessions: make(map[string]time.Time),
sessionTTL: 24 * time.Hour, // 会话有效期24小时
}
// 启动广播协程
go server.startBroadcastLoop()
// 启动会话清理协程
go server.cleanupSessionsLoop()
return server
}
@@ -62,17 +72,30 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
func (s *Server) Start() error {
mux := http.NewServeMux()
// 登录路由,不需要认证
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// 重定向到登录页面HTML
http.Redirect(w, r, "/login.html", http.StatusFound)
})
// API路由
if s.config.EnableAPI {
// 重定向/api到Swagger UI页面
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
})
// 登录API端点不需要认证
mux.HandleFunc("/api/login", s.handleLogin)
// 注销API端点不需要认证
mux.HandleFunc("/api/logout", s.handleLogout)
// 修改密码API端点需要认证
mux.HandleFunc("/api/change-password", s.loginRequired(s.handleChangePassword))
// 注册所有API端点
mux.HandleFunc("/api/stats", s.handleStats)
mux.HandleFunc("/api/shield", s.handleShield)
mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) {
// 重定向/api到Swagger UI页面
mux.HandleFunc("/api", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
}))
// 注册所有API端点应用登录中间件
mux.HandleFunc("/api/stats", s.loginRequired(s.handleStats))
mux.HandleFunc("/api/shield", s.loginRequired(s.handleShield))
mux.HandleFunc("/api/shield/localrules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
localRules := s.shieldManager.GetLocalRules()
@@ -80,8 +103,8 @@ func (s *Server) Start() error {
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
})
mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) {
}))
mux.HandleFunc("/api/shield/remoterules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
remoteRules := s.shieldManager.GetRemoteRules()
@@ -89,41 +112,57 @@ func (s *Server) Start() error {
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
})
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts)
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
mux.HandleFunc("/api/query", s.handleQuery)
mux.HandleFunc("/api/status", s.handleStatus)
mux.HandleFunc("/api/config", s.handleConfig)
mux.HandleFunc("/api/config/restart", s.handleRestart)
}))
mux.HandleFunc("/api/shield/hosts", s.loginRequired(s.handleShieldHosts))
mux.HandleFunc("/api/shield/blacklists", s.loginRequired(s.handleShieldBlacklists))
mux.HandleFunc("/api/query", s.loginRequired(s.handleQuery))
mux.HandleFunc("/api/status", s.loginRequired(s.handleStatus))
mux.HandleFunc("/api/config", s.loginRequired(s.handleConfig))
mux.HandleFunc("/api/config/restart", s.loginRequired(s.handleRestart))
// 添加统计相关接口
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
mux.HandleFunc("/api/top-clients", s.handleTopClients)
mux.HandleFunc("/api/top-domains", s.handleTopDomains)
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
mux.HandleFunc("/api/top-blocked", s.loginRequired(s.handleTopBlockedDomains))
mux.HandleFunc("/api/top-resolved", s.loginRequired(s.handleTopResolvedDomains))
mux.HandleFunc("/api/top-clients", s.loginRequired(s.handleTopClients))
mux.HandleFunc("/api/top-domains", s.loginRequired(s.handleTopDomains))
mux.HandleFunc("/api/recent-blocked", s.loginRequired(s.handleRecentBlockedDomains))
mux.HandleFunc("/api/hourly-stats", s.loginRequired(s.handleHourlyStats))
mux.HandleFunc("/api/daily-stats", s.loginRequired(s.handleDailyStats))
mux.HandleFunc("/api/monthly-stats", s.loginRequired(s.handleMonthlyStats))
mux.HandleFunc("/api/query/type", s.loginRequired(s.handleQueryTypeStats))
// 日志统计相关接口
mux.HandleFunc("/api/logs/stats", s.loginRequired(s.handleLogsStats))
mux.HandleFunc("/api/logs/query", s.loginRequired(s.handleLogsQuery))
mux.HandleFunc("/api/logs/count", s.loginRequired(s.handleLogsCount))
// WebSocket端点
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
mux.HandleFunc("/ws/stats", s.loginRequired(s.handleWebSocketStats))
// 将/api/下的静态文件服务指向static/api目录放在最后以避免覆盖API端点
apiFileServer := http.FileServer(http.Dir("./static/api"))
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
mux.Handle("/api/", s.loginRequired(http.StripPrefix("/api", apiFileServer).ServeHTTP))
}
// 自定义静态文件服务处理器用于禁用浏览器缓存放在API路由之后
fileServer := http.FileServer(http.Dir("./static"))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 单独处理login.html不需要登录
mux.HandleFunc("/login.html", func(w http.ResponseWriter, r *http.Request) {
// 添加Cache-Control头禁用浏览器缓存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
// 直接提供login.html文件
http.ServeFile(w, r, "./static/login.html")
})
// 其他静态文件需要登录
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
// 添加Cache-Control头禁用浏览器缓存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
// 使用StripPrefix处理路径
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
})
}))
s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
@@ -374,6 +413,78 @@ func (s *Server) startBroadcastLoop() {
}
}
// cleanupSessionsLoop 定期清理过期会话
func (s *Server) cleanupSessionsLoop() {
for {
time.Sleep(1 * time.Hour) // 每小时清理一次
s.sessionsMutex.Lock()
now := time.Now()
for sessionID, expiryTime := range s.sessions {
if now.After(expiryTime) {
delete(s.sessions, sessionID)
}
}
s.sessionsMutex.Unlock()
}
}
// isAuthenticated 检查用户是否已认证
func (s *Server) isAuthenticated(r *http.Request) bool {
// 从Cookie中获取会话ID
cookie, err := r.Cookie("session_id")
if err != nil {
return false
}
sessionID := cookie.Value
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
// 检查会话是否存在且未过期
expiryTime, exists := s.sessions[sessionID]
if !exists {
return false
}
if time.Now().After(expiryTime) {
// 会话已过期,删除它
delete(s.sessions, sessionID)
return false
}
// 延长会话有效期
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
return true
}
// loginRequired 登录中间件
func (s *Server) loginRequired(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 检查是否为登录页面或登录API允许直接访问
if r.URL.Path == "/login" || r.URL.Path == "/api/login" {
next.ServeHTTP(w, r)
return
}
// 检查是否已认证
if !s.isAuthenticated(r) {
// 如果是API请求返回401错误
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/ws/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "未授权访问"})
return
}
// 否则重定向到登录页面
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// 已认证,继续处理请求
next.ServeHTTP(w, r)
}
}
// handleTopBlockedDomains 处理TOP屏蔽域名请求
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
@@ -1217,6 +1328,64 @@ func checkURLExists(url string) bool {
return resp.StatusCode >= 200 && resp.StatusCode < 400
}
// handleLogsStats 处理日志统计请求
func (s *Server) handleLogsStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取日志统计数据
logStats := s.dnsServer.GetQueryStats()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(logStats)
}
// handleLogsQuery 处理日志查询请求
func (s *Server) handleLogsQuery(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取查询参数
limit := 100 // 默认返回100条日志
offset := 0
sortField := r.URL.Query().Get("sort")
sortDirection := r.URL.Query().Get("direction")
resultFilter := r.URL.Query().Get("result")
searchTerm := r.URL.Query().Get("search")
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
fmt.Sscanf(limitStr, "%d", &limit)
}
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
fmt.Sscanf(offsetStr, "%d", &offset)
}
// 获取日志数据
logs := s.dnsServer.GetQueryLogs(limit, offset, sortField, sortDirection, resultFilter, searchTerm)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(logs)
}
// handleLogsCount 处理日志总数请求
func (s *Server) handleLogsCount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取日志总数
count := s.dnsServer.GetQueryLogsCount()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]int{"count": count})
}
// handleRestart 处理重启服务请求
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
@@ -1250,3 +1419,135 @@ func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
logger.Info("服务重启成功")
}
// handleLogin 处理登录请求
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求体
var loginData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&loginData); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
return
}
// 验证用户名和密码
if loginData.Username != s.config.Username || loginData.Password != s.config.Password {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "用户名或密码错误"})
return
}
// 生成会话ID
sessionID := fmt.Sprintf("%d_%d", time.Now().UnixNano(), len(s.sessions))
// 保存会话
s.sessionsMutex.Lock()
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
s.sessionsMutex.Unlock()
// 设置Cookie
cookie := &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
Expires: time.Now().Add(s.sessionTTL),
HttpOnly: true,
Secure: false, // 开发环境下使用false生产环境应使用true
}
http.SetCookie(w, cookie)
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "登录成功"})
logger.Info(fmt.Sprintf("用户 %s 登录成功", loginData.Username))
}
// handleLogout 处理注销请求
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 从Cookie中获取会话ID
cookie, err := r.Cookie("session_id")
if err == nil {
// 删除会话
s.sessionsMutex.Lock()
delete(s.sessions, cookie.Value)
s.sessionsMutex.Unlock()
}
// 清除Cookie
clearCookie := &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: false,
}
http.SetCookie(w, clearCookie)
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "注销成功"})
logger.Info("用户注销成功")
}
// handleChangePassword 处理修改密码请求
func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求体
var changePasswordData struct {
CurrentPassword string `json:"currentPassword"`
NewPassword string `json:"newPassword"`
}
if err := json.NewDecoder(r.Body).Decode(&changePasswordData); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
return
}
// 验证当前密码
if changePasswordData.CurrentPassword != s.config.Password {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "当前密码错误"})
return
}
// 更新密码
s.config.Password = changePasswordData.NewPassword
// 保存配置到文件
if err := saveConfigToFile(s.globalConfig, "./config.json"); err != nil {
logger.Error("保存配置文件失败", "error", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "保存密码失败"})
return
}
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "密码修改成功"})
logger.Info("密码修改成功")
}

298
js/api.js
View File

@@ -1,298 +0,0 @@
// API模块 - 统一管理所有API调用
// API路径定义
const API_BASE_URL = '/api';
// API请求封装
async function apiRequest(endpoint, method = 'GET', data = null) {
const url = `${API_BASE_URL}${endpoint}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma': 'no-cache',
},
credentials: 'same-origin',
};
if (data) {
options.body = JSON.stringify(data);
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, 10000); // 10秒超时
});
try {
// 竞争:请求或超时
const response = await Promise.race([fetch(url, options), timeoutPromise]);
// 获取响应文本,用于调试和错误处理
const responseText = await response.text();
if (!response.ok) {
// 优化错误响应处理
console.warn(`API请求失败: ${response.status}`);
// 尝试解析JSON但如果失败直接使用原始文本作为错误信息
try {
const errorData = JSON.parse(responseText);
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
} catch (parseError) {
// 当响应不是有效的JSON时如中文错误信息直接使用原始文本
console.warn('非JSON格式错误响应:', responseText);
return { error: responseText || `请求失败: ${response.status}` };
}
}
// 尝试解析成功响应
try {
// 首先检查响应文本是否为空
if (!responseText || responseText.trim() === '') {
console.warn('空响应文本');
return null; // 返回null表示空响应
}
// 尝试解析JSON
const parsedData = JSON.parse(responseText);
// 检查解析后的数据是否有效
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
console.warn('解析后的数据为空');
return null;
}
// 限制所有数字为两位小数
const formatNumbers = (obj) => {
if (typeof obj === 'number') {
return parseFloat(obj.toFixed(2));
} else if (Array.isArray(obj)) {
return obj.map(formatNumbers);
} else if (obj && typeof obj === 'object') {
const formattedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
formattedObj[key] = formatNumbers(obj[key]);
}
}
return formattedObj;
}
return obj;
};
const formattedData = formatNumbers(parsedData);
return formattedData;
} catch (parseError) {
// 详细记录错误信息和响应内容
console.error('JSON解析错误:', parseError);
console.error('原始响应文本:', responseText);
console.error('响应长度:', responseText.length);
console.error('响应前100字符:', responseText.substring(0, 100));
// 如果是位置66附近的错误特别标记
if (parseError.message.includes('position 66')) {
console.error('位置66附近的字符:', responseText.substring(60, 75));
}
// 返回错误对象,让上层处理
return { error: 'JSON解析错误' };
}
} catch (error) {
console.error('API请求错误:', error);
// 返回错误对象,而不是抛出异常,让上层处理
return { error: error.message };
}
}
// API方法集合
const api = {
// 获取统计信息
getStats: () => apiRequest('/stats?t=' + Date.now()),
// 获取系统状态
getStatus: () => apiRequest('/status?t=' + Date.now()),
// 获取Top屏蔽域名
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
// 获取Top解析域名
getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
// 获取最近屏蔽域名
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
// 获取TOP客户端
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
// 获取TOP域名
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
// 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
// 获取每日统计数据7天
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
// 获取每月统计数据30天
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
// 获取查询类型统计
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
// 获取屏蔽规则 - 已禁用
getShieldRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({}); // 返回空对象而非API调用
},
// 添加屏蔽规则 - 已禁用
addShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 删除屏蔽规则 - 已禁用
deleteShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 更新远程规则 - 已禁用
updateRemoteRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 获取黑名单列表 - 已禁用
getBlacklists: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve([]); // 返回空数组而非API调用
},
// 添加黑名单 - 已禁用
addBlacklist: (url) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 删除黑名单 - 已禁用
deleteBlacklist: (url) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 获取Hosts内容 - 已禁用
getHosts: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
},
// 保存Hosts内容 - 已禁用
saveHosts: (content) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 刷新Hosts - 已禁用
refreshHosts: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 查询DNS记录 - 兼容多种参数格式
queryDNS: async function(domain, recordType) {
try {
console.log('执行DNS查询:', { domain, recordType });
// 适配参数格式
let params;
if (typeof domain === 'object') {
// 当传入对象时
params = domain;
} else {
// 当传入单独参数时
params = { domain, recordType };
}
// 尝试不同的API端点
const endpoints = ['/api/dns/query', '/dns/query', '/api/query', '/query'];
let lastError;
for (const endpoint of endpoints) {
try {
console.log(`尝试API端点: ${endpoint}`);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
if (response.ok) {
const data = await response.json();
console.log('DNS查询成功:', data);
return data;
} else {
lastError = new Error(`HTTP error! status: ${response.status} for endpoint: ${endpoint}`);
}
} catch (error) {
lastError = error;
console.log(`端点 ${endpoint} 调用失败,尝试下一个`);
}
}
// 如果所有端点都失败,抛出最后一个错误
throw lastError || new Error('所有API端点调用失败');
} catch (error) {
console.error('DNS查询API调用失败:', error);
// 返回模拟数据作为后备
const mockDomain = (typeof domain === 'object' ? domain.domain : domain) || 'example.com';
const mockType = (typeof domain === 'object' ? domain.recordType : recordType) || 'A';
const mockData = {
'A': [
{ Type: 'A', Value: '93.184.216.34', TTL: 172800 },
{ Type: 'A', Value: '93.184.216.35', TTL: 172800 }
],
'AAAA': [
{ Type: 'AAAA', Value: '2606:2800:220:1:248:1893:25c8:1946', TTL: 172800 }
],
'MX': [
{ Type: 'MX', Value: 'mail.' + mockDomain, Preference: 10, TTL: 3600 },
{ Type: 'MX', Value: 'mail2.' + mockDomain, Preference: 20, TTL: 3600 }
],
'NS': [
{ Type: 'NS', Value: 'ns1.' + mockDomain, TTL: 86400 },
{ Type: 'NS', Value: 'ns2.' + mockDomain, TTL: 86400 }
],
'CNAME': [
{ Type: 'CNAME', Value: 'origin.' + mockDomain, TTL: 300 }
],
'TXT': [
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + mockDomain + ' ~all', TTL: 3600 }
]
};
console.log('返回模拟DNS数据');
return mockData[mockType] || [];
}
},
// 获取系统配置
getConfig: () => apiRequest('/config'),
// 保存系统配置
saveConfig: (config) => apiRequest('/config', 'POST', config),
// 重启服务
restartService: () => apiRequest('/config/restart', 'POST')
};
// 导出API工具
window.api = api;

317
js/app.js
View File

@@ -1,317 +0,0 @@
// 全局配置
const API_BASE_URL = '.';
// DOM 加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化面板切换
initPanelNavigation();
// 加载初始数据
loadInitialData();
// 直接调用dashboard面板初始化函数确保数据正确加载
if (typeof initDashboardPanel === 'function') {
initDashboardPanel();
}
// 注意实时更新现在由index.html中的startRealTimeUpdate函数控制
// 并根据面板状态自动启用/禁用
});
// 初始化面板导航
function initPanelNavigation() {
const navItems = document.querySelectorAll('.nav-item');
const panels = document.querySelectorAll('.panel');
navItems.forEach(item => {
item.addEventListener('click', function() {
// 移除所有活动类
navItems.forEach(nav => nav.classList.remove('active'));
panels.forEach(panel => panel.classList.remove('active'));
// 添加当前活动类
this.classList.add('active');
const target = this.getAttribute('data-target');
document.getElementById(target).classList.add('active');
// 面板激活时执行相应的初始化函数
if (window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]) {
window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]();
}
});
});
}
// 保留原有的通知函数作为兼容层
// 现在主通知功能由index.html中的showNotification函数实现
if (typeof window.showNotification === 'undefined') {
window.showNotification = function(message, type = 'info') {
// 创建临时通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type} show`;
notification.innerHTML = `
<div class="notification-content">${message}</div>
`;
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #333; color: white; padding: 10px 15px; border-radius: 4px; z-index: 10000;';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
};
}
// 加载初始数据(主要用于服务器状态)
function loadInitialData() {
// 加载服务器状态
fetch(`${API_BASE_URL}/api/status`)
.then(response => response.json())
.then(data => {
// 更新服务器状态指示器
const statusDot = document.querySelector('.status-dot');
const serverStatus = document.getElementById('server-status');
if (data && data.status === 'running') {
statusDot.classList.add('connected');
serverStatus.textContent = '运行中';
} else {
statusDot.classList.remove('connected');
serverStatus.textContent = '离线';
}
})
.catch(error => {
console.error('获取服务器状态失败:', error);
// 更新状态为离线
const statusDot = document.querySelector('.status-dot');
const serverStatus = document.getElementById('server-status');
statusDot.classList.remove('connected');
serverStatus.textContent = '离线';
// 使用新的通知功能
if (typeof window.showNotification === 'function') {
window.showNotification('获取服务器状态失败', 'danger');
}
});
// 注意统计数据更新现在由dashboard.js中的updateStatCards函数处理
}
// 注意统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
// 此函数保留作为兼容层,实际功能已迁移
function updateStatCards(stats) {
// 空实现,保留函数声明以避免引用错误
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
}
// 注意获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
function fetchRulesCount() {
// 空实现,保留函数声明以避免引用错误
}
// 注意获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
function fetchHostsCount() {
// 空实现,保留函数声明以避免引用错误
}
// 通用API请求函数 - 添加错误处理和重试机制
function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
const headers = {
'Content-Type': 'application/json'
};
const config = {
method,
headers,
timeout: 10000, // 设置超时时间为10秒
};
// 处理请求URL和参数
let url = `${API_BASE_URL}${endpoint}`;
if (data) {
if (method === 'GET') {
// 为GET请求拼接查询参数
const params = new URLSearchParams();
Object.keys(data).forEach(key => {
params.append(key, data[key]);
});
url += `?${params.toString()}`;
} else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
// 为其他方法设置body
config.body = JSON.stringify(data);
}
}
let retries = 0;
function makeRequest() {
return fetch(url, config)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 检查响应是否完整
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
// 使用.text()先获取响应文本处理可能的JSON解析错误
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('JSON解析错误:', e, '响应文本:', text);
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试
if (retries < maxRetries) {
retries++;
console.warn(`请求失败,正在进行第${retries}次重试...`);
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
}
throw new Error('JSON解析失败且重试次数已达上限');
}
});
}
return response.json();
})
.catch(error => {
console.error('API请求错误:', error);
// 检查是否为网络错误或ERR_INCOMPLETE_CHUNKED_ENCODING相关错误
if ((error.name === 'TypeError' && error.message.includes('Failed to fetch')) ||
error.message.includes('incomplete chunked encoding')) {
if (retries < maxRetries) {
retries++;
console.warn(`网络错误,正在进行第${retries}次重试...`);
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
}
}
throw error;
});
}
return makeRequest();
}
// 数字格式化函数
function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return fullNumStr;
}
// 确认对话框函数
function confirmAction(message, onConfirm) {
if (confirm(message)) {
onConfirm();
}
}
// 加载状态函数
function showLoading(element) {
if (element) {
element.innerHTML = '<td colspan="100%" class="loading">加载中...</td>';
}
}
// 错误状态函数
function showError(element, message) {
if (element) {
element.innerHTML = `<td colspan="100%" style="color: #e74c3c;">${message}</td>`;
}
}
// 空状态函数
function showEmpty(element, message) {
if (element) {
element.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">${message}</td>`;
}
}
// 表格排序功能
function initTableSort(tableId) {
const table = document.getElementById(tableId);
if (!table) return;
const headers = table.querySelectorAll('thead th');
headers.forEach(header => {
header.addEventListener('click', function() {
const columnIndex = Array.from(headers).indexOf(this);
const isAscending = this.getAttribute('data-sort') !== 'asc';
// 重置所有标题
headers.forEach(h => h.setAttribute('data-sort', ''));
this.setAttribute('data-sort', isAscending ? 'asc' : 'desc');
// 排序行
sortTable(table, columnIndex, isAscending);
});
});
}
// 表格排序实现
function sortTable(table, columnIndex, isAscending) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// 排序行
rows.sort((a, b) => {
const aValue = a.cells[columnIndex].textContent.trim();
const bValue = b.cells[columnIndex].textContent.trim();
// 尝试数字排序
const aNum = parseFloat(aValue);
const bNum = parseFloat(bValue);
if (!isNaN(aNum) && !isNaN(bNum)) {
return isAscending ? aNum - bNum : bNum - aNum;
}
// 字符串排序
return isAscending
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
// 重新添加行
rows.forEach(row => tbody.appendChild(row));
}
// 搜索过滤功能
function initSearchFilter(inputId, tableId, columnIndex) {
const input = document.getElementById(inputId);
const table = document.getElementById(tableId);
if (!input || !table) return;
input.addEventListener('input', function() {
const filter = this.value.toLowerCase();
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cell = row.cells[columnIndex];
if (cell) {
const text = cell.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
}
});
});
}

View File

@@ -1,53 +0,0 @@
// 颜色配置文件 - 集中管理所有UI颜色配置
// 主颜色配置对象
const COLOR_CONFIG = {
// 主色调
primary: '#1890ff',
success: '#52c41a',
warning: '#fa8c16',
error: '#f5222d',
purple: '#722ed1',
cyan: '#13c2c2',
teal: '#36cfc9',
// 统计卡片颜色配置
statCardColors: [
'#1890ff', // blue
'#52c41a', // green
'#fa8c16', // orange
'#f5222d', // red
'#722ed1', // purple
'#13c2c2' // cyan
],
// 颜色代码到CSS类的映射
colorClassMap: {
'#1890ff': 'blue',
'#52c41a': 'green',
'#fa8c16': 'orange',
'#f5222d': 'red',
'#722ed1': 'purple',
'#13c2c2': 'cyan',
'#36cfc9': 'teal'
},
// 获取颜色对应的CSS类名
getColorClassName: function(colorCode) {
return this.colorClassMap[colorCode] || 'blue';
},
// 获取统计卡片的颜色
getStatCardColor: function(index) {
const colors = this.statCardColors;
return colors[index % colors.length];
}
};
// 导出配置对象
if (typeof module !== 'undefined' && module.exports) {
module.exports = COLOR_CONFIG;
} else {
// 浏览器环境
window.COLOR_CONFIG = COLOR_CONFIG;
}

View File

@@ -1,284 +0,0 @@
// 配置管理页面功能实现
// 工具函数安全获取DOM元素
function getElement(id) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Element with id "${id}" not found`);
}
return element;
}
// 工具函数:验证端口号
function validatePort(port) {
// 确保port是字符串类型
var portStr = port;
if (port === null || port === undefined || typeof port !== 'string') {
return null;
}
// 去除前后空白并验证是否为纯数字
portStr = port.trim();
if (!/^\d+$/.test(portStr)) {
return null;
}
const num = parseInt(portStr, 10);
return num >= 1 && num <= 65535 ? num : null;
}
// 初始化配置管理页面
function initConfigPage() {
loadConfig();
setupConfigEventListeners();
}
// 加载系统配置
async function loadConfig() {
try {
const result = await api.getConfig();
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('加载配置失败: ' + result.error);
return;
}
populateConfigForm(result);
} catch (error) {
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
}
}
// 填充配置表单
function populateConfigForm(config) {
// 安全获取配置对象,防止未定义属性访问
const dnsServerConfig = config.DNSServer || {};
const httpServerConfig = config.HTTPServer || {};
const shieldConfig = config.Shield || {};
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
// HTTP配置
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
// 屏蔽配置
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
// 使用服务器端接受的屏蔽方法值默认使用NXDOMAIN
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
}
// 工具函数:安全设置元素值
function setElementValue(elementId, value) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
element.value = value;
} else if (!element) {
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
}
}
// 工具函数安全获取值如果未定义或为null则返回默认值
function getSafeValue(value, defaultValue) {
// 更严格的检查避免0、空字符串等被默认值替换
return value === undefined || value === null ? defaultValue : value;
}
// 工具函数:安全获取数组,如果不是数组则返回空数组
function getSafeArray(value) {
return Array.isArray(value) ? value : [];
}
// 保存配置
async function handleSaveConfig() {
const formData = collectFormData();
if (!formData) return;
try {
const result = await api.saveConfig(formData);
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('保存配置失败: ' + result.error);
return;
}
showSuccessMessage('配置保存成功');
} catch (error) {
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
}
}
// 重启服务
async function handleRestartService() {
if (!confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) return;
try {
const result = await api.restartService();
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('服务重启失败: ' + result.error);
return;
}
showSuccessMessage('服务重启成功');
} catch (error) {
// 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
}
}
// 收集表单数据并验证
function collectFormData() {
// 验证端口号 - 使用安全获取元素值的函数
const dnsPortValue = getElementValue('dns-port');
const httpPortValue = getElementValue('http-port');
const dnsPort = validatePort(dnsPortValue);
const httpPort = validatePort(httpPortValue);
if (!dnsPort) {
showErrorMessage('DNS端口号无效必须是1-65535之间的整数');
return null;
}
if (!httpPort) {
showErrorMessage('HTTP端口号无效必须是1-65535之间的整数');
return null;
}
// 安全获取上游服务器列表
const upstreamServersText = getElementValue('dns-upstream-servers');
const upstreamServers = upstreamServersText ?
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
[];
// 安全获取并转换整数值
const timeoutValue = getElementValue('dns-timeout');
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
const saveIntervalValue = getElementValue('dns-save-interval');
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
const updateIntervalValue = getElementValue('shield-update-interval');
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
return {
DNSServer: {
Port: dnsPort,
UpstreamServers: upstreamServers,
Timeout: timeout,
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
SaveInterval: saveInterval
},
HTTPServer: {
Port: httpPort
},
Shield: {
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
UpdateInterval: updateInterval,
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
}
};
}
// 工具函数:安全获取元素值
function getElementValue(elementId) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
return element.value;
}
return ''; // 默认返回空字符串
}
// 设置事件监听器
function setupConfigEventListeners() {
// 保存配置按钮
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
// 重启服务按钮
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
}
// 显示成功消息
function showSuccessMessage(message) {
showNotification(message, 'success');
}
// 显示错误消息
function showErrorMessage(message) {
showNotification(message, 'error');
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新通知
const notification = document.createElement('div');
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式兼容Tailwind和原生CSS
notification.style.cssText += `
position: fixed;
bottom: 16px;
right: 16px;
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
transition: all 0.3s ease;
opacity: 0;
`;
if (type === 'success') {
notification.style.backgroundColor = '#10b981';
notification.style.color = 'white';
} else if (type === 'error') {
notification.style.backgroundColor = '#ef4444';
notification.style.color = 'white';
} else {
notification.style.backgroundColor = '#3b82f6';
notification.style.color = 'white';
}
notification.textContent = message;
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.style.opacity = '1';
}, 10);
// 3秒后隐藏通知
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initConfigPage);
} else {
initConfigPage();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,202 +0,0 @@
// Hosts管理页面功能实现
// 初始化Hosts管理页面
function initHostsPage() {
// 加载Hosts规则
loadHostsRules();
// 设置事件监听器
setupHostsEventListeners();
}
// 加载Hosts规则
async function loadHostsRules() {
try {
const response = await fetch('/api/shield/hosts');
if (!response.ok) {
throw new Error('Failed to load hosts rules');
}
const data = await response.json();
// 处理API返回的数据格式
let hostsRules = [];
if (data && Array.isArray(data)) {
// 直接是数组格式
hostsRules = data;
} else if (data && data.hosts) {
// 包含在hosts字段中
hostsRules = data.hosts;
}
updateHostsTable(hostsRules);
} catch (error) {
console.error('Error loading hosts rules:', error);
showErrorMessage('加载Hosts规则失败');
}
}
// 更新Hosts表格
function updateHostsTable(hostsRules) {
const tbody = document.getElementById('hosts-table-body');
if (hostsRules.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
return;
}
tbody.innerHTML = hostsRules.map(rule => {
// 处理对象格式的规则
const ip = rule.ip || '';
const domain = rule.domain || '';
return `
<tr class="border-b border-gray-200">
<td class="py-3 px-4">${ip}</td>
<td class="py-3 px-4">${domain}</td>
<td class="py-3 px-4 text-right">
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`;
}).join('');
// 重新绑定删除事件
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteHostsRule);
});
}
// 设置事件监听器
function setupHostsEventListeners() {
// 保存Hosts按钮
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
}
// 处理添加Hosts规则
async function handleAddHostsRule() {
const ip = document.getElementById('hosts-ip').value.trim();
const domain = document.getElementById('hosts-domain').value.trim();
if (!ip || !domain) {
showErrorMessage('IP地址和域名不能为空');
return;
}
try {
const response = await fetch('/api/shield/hosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ip, domain })
});
if (!response.ok) {
throw new Error('Failed to add hosts rule');
}
showSuccessMessage('Hosts规则添加成功');
// 清空输入框
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error adding hosts rule:', error);
showErrorMessage('添加Hosts规则失败');
}
}
// 处理删除Hosts规则
async function handleDeleteHostsRule(e) {
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
try {
const response = await fetch('/api/shield/hosts', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
});
if (!response.ok) {
throw new Error('Failed to delete hosts rule');
}
showSuccessMessage('Hosts规则删除成功');
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error deleting hosts rule:', error);
showErrorMessage('删除Hosts规则失败');
}
}
// 显示成功消息
function showSuccessMessage(message) {
showNotification(message, 'success');
}
// 显示错误消息
function showErrorMessage(message) {
showNotification(message, 'error');
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新通知
const notification = document.createElement('div');
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式
if (type === 'success') {
notification.classList.add('bg-green-500', 'text-white');
} else if (type === 'error') {
notification.classList.add('bg-red-500', 'text-white');
} else {
notification.classList.add('bg-blue-500', 'text-white');
}
notification.innerHTML = `
<div class="flex items-center space-x-2">
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.classList.remove('opacity-0');
}, 100);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.add('opacity-0');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHostsPage);
} else {
initHostsPage();
}

View File

@@ -1,173 +0,0 @@
// main.js - 主脚本文件
// 页面导航功能
function setupNavigation() {
// 侧边栏菜单项
const menuItems = document.querySelectorAll('nav a');
const contentSections = [
document.getElementById('dashboard-content'),
document.getElementById('shield-content'),
document.getElementById('hosts-content'),
document.getElementById('query-content'),
document.getElementById('config-content')
];
const pageTitle = document.getElementById('page-title');
menuItems.forEach((item, index) => {
item.addEventListener('click', (e) => {
// 允许浏览器自动更新地址栏中的hash不阻止默认行为
// 移动端点击菜单项后自动关闭侧边栏
if (window.innerWidth < 768) {
closeSidebar();
}
// 页面特定初始化 - 保留这部分逻辑因为它不会与hashchange事件处理逻辑冲突
const target = item.getAttribute('href').substring(1);
if (target === 'shield' && typeof initShieldPage === 'function') {
initShieldPage();
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
initHostsPage();
}
});
});
// 移动端侧边栏切换
const toggleSidebar = document.getElementById('toggle-sidebar');
const closeSidebarBtn = document.getElementById('close-sidebar');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
// 打开侧边栏函数
function openSidebar() {
console.log('Opening sidebar...');
if (sidebar) {
sidebar.classList.remove('-translate-x-full');
sidebar.classList.add('translate-x-0');
}
if (sidebarOverlay) {
sidebarOverlay.classList.remove('hidden');
sidebarOverlay.classList.add('block');
}
// 防止页面滚动
document.body.style.overflow = 'hidden';
console.log('Sidebar opened successfully');
}
// 关闭侧边栏函数
function closeSidebar() {
console.log('Closing sidebar...');
if (sidebar) {
sidebar.classList.add('-translate-x-full');
sidebar.classList.remove('translate-x-0');
}
if (sidebarOverlay) {
sidebarOverlay.classList.add('hidden');
sidebarOverlay.classList.remove('block');
}
// 恢复页面滚动
document.body.style.overflow = '';
console.log('Sidebar closed successfully');
}
// 切换侧边栏函数
function toggleSidebarVisibility() {
console.log('Toggling sidebar visibility...');
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
console.log('Sidebar is hidden, opening...');
openSidebar();
} else {
console.log('Sidebar is visible, closing...');
closeSidebar();
}
}
// 绑定切换按钮事件
if (toggleSidebar) {
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
}
// 绑定关闭按钮事件
if (closeSidebarBtn) {
closeSidebarBtn.addEventListener('click', closeSidebar);
}
// 绑定遮罩层点击事件
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 移动端点击菜单项后自动关闭侧边栏
menuItems.forEach(item => {
item.addEventListener('click', () => {
// 检查是否是移动设备视图
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
// 添加键盘事件监听按ESC键关闭侧边栏
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSidebar();
}
});
}
// 初始化函数
function init() {
// 设置导航
setupNavigation();
// 加载仪表盘数据
if (typeof loadDashboardData === 'function') {
loadDashboardData();
}
// 定期更新系统状态
setInterval(updateSystemStatus, 5000);
}
// 更新系统状态
function updateSystemStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const uptimeElement = document.getElementById('uptime');
if (uptimeElement) {
uptimeElement.textContent = `正常运行中 | ${formatUptime(data.uptime)}`;
}
})
.catch(error => {
console.error('更新系统状态失败:', error);
const uptimeElement = document.getElementById('uptime');
if (uptimeElement) {
uptimeElement.textContent = '连接异常';
uptimeElement.classList.add('text-danger');
}
});
}
// 格式化运行时间
function formatUptime(milliseconds) {
// 简化版的格式化实际使用时需要根据API返回的数据格式调整
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}${hours % 24}小时`;
} else if (hours > 0) {
return `${hours}小时${minutes % 60}分钟`;
} else if (minutes > 0) {
return `${minutes}分钟${seconds % 60}`;
} else {
return `${seconds}`;
}
}
// 页面加载完成后执行初始化
window.addEventListener('DOMContentLoaded', init);

View File

@@ -1,255 +0,0 @@
// 初始化远程黑名单面板
function initBlacklistsPanel() {
// 加载远程黑名单列表
loadBlacklists();
// 初始化事件监听器
initBlacklistsEventListeners();
}
// 初始化事件监听器
function initBlacklistsEventListeners() {
// 添加黑名单按钮
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
// 更新所有黑名单按钮
document.getElementById('update-all-blacklists').addEventListener('click', updateAllBlacklists);
// 按Enter键添加黑名单
document.getElementById('blacklist-url').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addBlacklist();
}
});
}
// 加载远程黑名单列表
function loadBlacklists() {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
showLoading(tbody);
apiRequest('/api/shield/blacklists')
.then(data => {
// 直接渲染返回的blacklists数组
renderBlacklists(data);
})
.catch(error => {
console.error('获取远程黑名单列表失败:', error);
showError(tbody, '获取远程黑名单列表失败');
window.showNotification('获取远程黑名单列表失败', 'error');
});
}
// 渲染远程黑名单表格
function renderBlacklists(blacklists) {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
if (!tbody) return;
if (!blacklists || blacklists.length === 0) {
showEmpty(tbody, '暂无远程黑名单');
return;
}
tbody.innerHTML = '';
blacklists.forEach(list => {
addBlacklistToTable(list);
});
// 初始化表格排序
initTableSort('blacklists-table');
// 初始化操作按钮监听器
initBlacklistsActionListeners();
}
// 添加黑名单到表格
function addBlacklistToTable(list) {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
const row = document.createElement('tr');
const statusClass = list.status === 'success' ? 'status-success' :
list.status === 'error' ? 'status-error' : 'status-pending';
const statusText = list.status === 'success' ? '正常' :
list.status === 'error' ? '错误' : '等待中';
const lastUpdate = list.lastUpdate ? new Date(list.lastUpdate).toLocaleString() : '从未';
row.innerHTML = `
<td>${list.name}</td>
<td>${list.url}</td>
<td>
<span class="status-badge ${statusClass}">${statusText}</span>
</td>
<td>${list.rulesCount || 0}</td>
<td>${lastUpdate}</td>
<td class="actions-cell">
<button class="btn btn-primary btn-sm update-blacklist" data-id="${list.id}">
<i class="fas fa-sync-alt"></i> 更新
</button>
<button class="btn btn-danger btn-sm delete-blacklist" data-id="${list.id}">
<i class="fas fa-trash-alt"></i> 删除
</button>
</td>
`;
tbody.appendChild(row);
}
// 添加远程黑名单
function addBlacklist() {
const nameInput = document.getElementById('blacklist-name');
const urlInput = document.getElementById('blacklist-url');
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name) {
window.showNotification('请输入黑名单名称', 'warning');
nameInput.focus();
return;
}
if (!url) {
window.showNotification('请输入黑名单URL', 'warning');
urlInput.focus();
return;
}
// 简单的URL格式验证
if (!isValidUrl(url)) {
window.showNotification('请输入有效的URL', 'warning');
urlInput.focus();
return;
}
apiRequest('/api/shield/blacklists', 'POST', { name: name, url: url })
.then(data => {
// 检查响应中是否有status字段
if (!data || typeof data === 'undefined') {
window.showNotification('远程黑名单添加失败: 无效的响应', 'error');
return;
}
if (data.status === 'success') {
window.showNotification('远程黑名单添加成功', 'success');
nameInput.value = '';
urlInput.value = '';
loadBlacklists();
} else {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('添加远程黑名单失败:', error);
window.showNotification('添加远程黑名单失败', 'error');
});
}
// 更新远程黑名单
function updateBlacklist(id) {
apiRequest(`/api/shield/blacklists/${id}/update`, 'POST')
.then(data => {
// 检查响应中是否有status字段
if (!data || typeof data === 'undefined') {
window.showNotification('远程黑名单更新失败: 无效的响应', 'error');
return;
}
if (data.status === 'success') {
window.showNotification('远程黑名单更新成功', 'success');
loadBlacklists();
} else {
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('更新远程黑名单失败:', error);
window.showNotification('更新远程黑名单失败', 'error');
});
}
// 更新所有远程黑名单
function updateAllBlacklists() {
confirmAction(
'确定要更新所有远程黑名单吗?这可能需要一些时间。',
() => {
apiRequest('/api/shield/blacklists', 'PUT')
.then(data => {
// 检查响应中是否有status字段
if (!data || typeof data === 'undefined') {
window.showNotification('所有远程黑名单更新失败: 无效的响应', 'error');
return;
}
if (data.status === 'success') {
window.showNotification('所有远程黑名单更新成功', 'success');
loadBlacklists();
} else {
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('更新所有远程黑名单失败:', error);
window.showNotification('更新所有远程黑名单失败', 'error');
});
}
);
}
// 删除远程黑名单
function deleteBlacklist(id) {
apiRequest(`/api/shield/blacklists/${id}`, 'DELETE')
.then(data => {
// 检查响应中是否有status字段
if (!data || typeof data === 'undefined') {
window.showNotification('远程黑名单删除失败: 无效的响应', 'error');
return;
}
if (data.status === 'success') {
window.showNotification('远程黑名单删除成功', 'success');
loadBlacklists();
} else {
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('删除远程黑名单失败:', error);
window.showNotification('删除远程黑名单失败', 'error');
});
}
// 为操作按钮添加事件监听器
function initBlacklistsActionListeners() {
// 更新按钮
document.querySelectorAll('.update-blacklist').forEach(button => {
button.addEventListener('click', function() {
const id = this.getAttribute('data-id');
updateBlacklist(id);
});
});
// 删除按钮
document.querySelectorAll('.delete-blacklist').forEach(button => {
button.addEventListener('click', function() {
const id = this.getAttribute('data-id');
confirmAction(
'确定要删除这条远程黑名单吗?',
() => deleteBlacklist(id)
);
});
});
}
// 验证URL格式
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}

View File

@@ -1,125 +0,0 @@
// 初始化配置管理面板
function initConfigPanel() {
// 加载当前配置
loadConfig();
// 初始化事件监听器
initConfigEventListeners();
}
// 初始化事件监听器
function initConfigEventListeners() {
// 保存配置按钮
document.getElementById('save-config').addEventListener('click', saveConfig);
// 屏蔽方法变更
document.getElementById('block-method').addEventListener('change', updateCustomBlockIpVisibility);
}
// 加载当前配置
function loadConfig() {
apiRequest('/config')
.then(config => {
renderConfig(config);
})
.catch(error => {
console.error('获取配置失败:', error);
window.showNotification('获取配置失败', 'error');
});
}
// 渲染配置表单
function renderConfig(config) {
if (!config) return;
// 设置屏蔽方法
const blockMethodSelect = document.getElementById('block-method');
if (config.shield && config.shield.blockMethod) {
blockMethodSelect.value = config.shield.blockMethod;
}
// 设置自定义屏蔽IP
const customBlockIpInput = document.getElementById('custom-block-ip');
if (config.shield && config.shield.customBlockIP) {
customBlockIpInput.value = config.shield.customBlockIP;
}
// 设置远程规则更新间隔
const updateIntervalInput = document.getElementById('update-interval');
if (config.shield && config.shield.updateInterval) {
updateIntervalInput.value = config.shield.updateInterval;
}
// 更新自定义屏蔽IP的可见性
updateCustomBlockIpVisibility();
}
// 更新自定义屏蔽IP输入框的可见性
function updateCustomBlockIpVisibility() {
const blockMethod = document.getElementById('block-method').value;
const customBlockIpContainer = document.getElementById('custom-block-ip').closest('.form-group');
if (blockMethod === 'customIP') {
customBlockIpContainer.style.display = 'block';
} else {
customBlockIpContainer.style.display = 'none';
}
}
// 保存配置
function saveConfig() {
// 收集表单数据
const configData = {
shield: {
blockMethod: document.getElementById('block-method').value,
updateInterval: parseInt(document.getElementById('update-interval').value)
}
};
// 如果选择了自定义IP添加到配置中
if (configData.shield.blockMethod === 'customIP') {
const customBlockIp = document.getElementById('custom-block-ip').value.trim();
// 验证自定义IP格式
if (!isValidIp(customBlockIp)) {
window.showNotification('请输入有效的自定义屏蔽IP', 'warning');
return;
}
configData.shield.customBlockIP = customBlockIp;
}
// 验证更新间隔
if (isNaN(configData.shield.updateInterval) || configData.shield.updateInterval < 60) {
window.showNotification('更新间隔必须大于等于60秒', 'warning');
return;
}
// 保存配置
apiRequest('/config', 'PUT', configData)
.then(response => {
if (response.success) {
window.showNotification('配置保存成功', 'success');
// 由于服务器没有提供重启API移除重启提示
// 直接提示用户配置已保存
} else {
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('保存配置失败:', error);
window.showNotification('保存配置失败', 'error');
});
}
// 服务重启功能已移除因为服务器没有提供对应的API端点
// 验证IP地址格式
function isValidIp(ip) {
// 支持IPv4和IPv6简单验证
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,308 +0,0 @@
// 初始化Hosts面板
function initHostsPanel() {
// 加载Hosts列表
loadHosts();
// 初始化事件监听器
initHostsEventListeners();
}
// 初始化事件监听器
function initHostsEventListeners() {
// 添加Hosts按钮
document.getElementById('add-hosts').addEventListener('click', addHostsEntry);
// Hosts过滤
document.getElementById('hosts-filter').addEventListener('input', filterHosts);
// 按Enter键添加Hosts
document.getElementById('hosts-domain').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addHostsEntry();
}
});
}
// 加载Hosts列表
function loadHosts() {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
showLoading(tbody);
// 更新API路径使用完整路径
apiRequest('/api/shield/hosts', 'GET')
.then(data => {
// 处理不同格式的响应数据
let hostsData;
if (Array.isArray(data)) {
hostsData = data;
} else if (data && data.hosts) {
hostsData = data.hosts;
} else {
hostsData = [];
}
renderHosts(hostsData);
// 更新Hosts数量统计
if (window.updateHostsCount && typeof window.updateHostsCount === 'function') {
window.updateHostsCount(hostsData.length);
}
})
.catch(error => {
console.error('获取Hosts列表失败:', error);
if (tbody) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
'<div class="empty-state">' +
'<div class="empty-icon"><i class="fas fa-server text-muted"></i></div>' +
'<div class="empty-title text-muted">加载失败</div>' +
'<div class="empty-description text-muted">无法获取Hosts列表请稍后重试</div>' +
'</div>' +
'</td></tr>';
}
if (typeof window.showNotification === 'function') {
window.showNotification('获取Hosts列表失败', 'danger');
}
});
}
// 渲染Hosts表格
function renderHosts(hosts) {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
if (!tbody) return;
if (!hosts || hosts.length === 0) {
// 使用更友好的空状态显示
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
'<div class="empty-state">' +
'<div class="empty-icon"><i class="fas fa-file-alt text-muted"></i></div>' +
'<div class="empty-title text-muted">暂无Hosts条目</div>' +
'<div class="empty-description text-muted">添加自定义Hosts条目以控制DNS解析</div>' +
'</div>' +
'</td></tr>';
return;
}
tbody.innerHTML = '';
hosts.forEach(entry => {
addHostsToTable(entry.ip, entry.domain);
});
// 初始化删除按钮监听器
initDeleteHostsListeners();
}
// 添加Hosts到表格
function addHostsToTable(ip, domain) {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
const row = document.createElement('tr');
row.innerHTML = `
<td>${ip}</td>
<td>${domain}</td>
<td class="actions-cell">
<button class="btn btn-danger btn-sm delete-hosts" data-ip="${ip}" data-domain="${domain}">
<i class="fas fa-trash-alt"></i> 删除
</button>
</td>
`;
// 添加行动画效果
row.style.opacity = '0';
row.style.transform = 'translateY(10px)';
tbody.appendChild(row);
// 使用requestAnimationFrame确保动画平滑
requestAnimationFrame(() => {
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
row.style.opacity = '1';
row.style.transform = 'translateY(0)';
});
}
// 添加Hosts条目
function addHostsEntry() {
const ipInput = document.getElementById('hosts-ip');
const domainInput = document.getElementById('hosts-domain');
const ip = ipInput.value.trim();
const domain = domainInput.value.trim();
if (!ip) {
if (typeof window.showNotification === 'function') {
window.showNotification('请输入IP地址', 'warning');
}
ipInput.focus();
return;
}
if (!domain) {
if (typeof window.showNotification === 'function') {
window.showNotification('请输入域名', 'warning');
}
domainInput.focus();
return;
}
// 简单的IP地址格式验证
if (!isValidIp(ip)) {
if (typeof window.showNotification === 'function') {
window.showNotification('请输入有效的IP地址', 'warning');
}
ipInput.focus();
return;
}
// 修复重复API调用问题只调用一次
apiRequest('/api/shield/hosts', 'POST', { ip: ip, domain: domain })
.then(data => {
// 处理不同的响应格式
if (data.success || data.status === 'success') {
if (typeof window.showNotification === 'function') {
window.showNotification('Hosts条目添加成功', 'success');
}
// 清空输入框并聚焦到域名输入
ipInput.value = '';
domainInput.value = '';
domainInput.focus();
// 重新加载Hosts列表
loadHosts();
// 触发数据刷新事件
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('hosts');
}
} else {
if (typeof window.showNotification === 'function') {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
}
}
})
.catch(error => {
console.error('添加Hosts条目失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('添加Hosts条目失败', 'danger');
}
});
}
// 删除Hosts条目
function deleteHostsEntry(ip, domain) {
// 找到要删除的行并添加删除动画
const rows = document.querySelectorAll('#hosts-table tbody tr');
let targetRow = null;
rows.forEach(row => {
if (row.cells[0].textContent === ip && row.cells[1].textContent === domain) {
targetRow = row;
}
});
if (targetRow) {
targetRow.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
targetRow.style.opacity = '0';
targetRow.style.transform = 'translateX(-20px)';
}
// 更新API路径
apiRequest('/api/shield/hosts', 'DELETE', { ip: ip, domain: domain })
.then(data => {
// 处理不同的响应格式
if (data.success || data.status === 'success') {
// 等待动画完成后重新加载列表
setTimeout(() => {
if (typeof window.showNotification === 'function') {
window.showNotification('Hosts条目删除成功', 'success');
}
loadHosts();
// 触发数据刷新事件
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('hosts');
}
}, 300);
} else {
// 恢复行样式
if (targetRow) {
targetRow.style.opacity = '1';
targetRow.style.transform = 'translateX(0)';
}
if (typeof window.showNotification === 'function') {
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'danger');
}
}
})
.catch(error => {
// 恢复行样式
if (targetRow) {
targetRow.style.opacity = '1';
targetRow.style.transform = 'translateX(0)';
}
console.error('删除Hosts条目失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('删除Hosts条目失败', 'danger');
}
});
}
// 过滤Hosts
function filterHosts() {
const filterText = document.getElementById('hosts-filter').value.toLowerCase();
const rows = document.querySelectorAll('#hosts-table tbody tr');
rows.forEach(row => {
const ip = row.cells[0].textContent.toLowerCase();
const domain = row.cells[1].textContent.toLowerCase();
row.style.display = (ip.includes(filterText) || domain.includes(filterText)) ? '' : 'none';
});
}
// 为删除按钮添加事件监听器
function initDeleteHostsListeners() {
document.querySelectorAll('.delete-hosts').forEach(button => {
button.addEventListener('click', function() {
const ip = this.getAttribute('data-ip');
const domain = this.getAttribute('data-domain');
// 使用标准confirm对话框
if (confirm(`确定要删除这条Hosts条目吗\n${ip} ${domain}`)) {
deleteHostsEntry(ip, domain);
}
});
});
}
// 验证IP地址格式
function isValidIp(ip) {
// 支持IPv4和IPv6简单验证
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
// 导出函数,供其他模块调用
window.updateHostsCount = function(count) {
const hostsCountElement = document.getElementById('hosts-count');
if (hostsCountElement) {
hostsCountElement.textContent = count;
}
}
// 导出初始化函数
window.initHostsPanel = initHostsPanel;
// 注册到面板导航系统
if (window.registerPanelModule) {
window.registerPanelModule('hosts-panel', {
init: initHostsPanel,
refresh: loadHosts
});
}

View File

@@ -1,294 +0,0 @@
// 初始化DNS查询面板
function initQueryPanel() {
// 初始化事件监听器
initQueryEventListeners();
// 确保结果容器默认隐藏
const resultContainer = document.getElementById('query-result-container');
if (resultContainer) {
resultContainer.classList.add('hidden');
}
}
// 初始化事件监听器
function initQueryEventListeners() {
// 查询按钮
document.getElementById('run-query').addEventListener('click', runDnsQuery);
// 按Enter键执行查询
document.getElementById('query-domain').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
runDnsQuery();
}
});
}
// 执行DNS查询
function runDnsQuery() {
const domainInput = document.getElementById('query-domain');
const domain = domainInput.value.trim();
if (!domain) {
if (typeof window.showNotification === 'function') {
window.showNotification('请输入要查询的域名', 'warning');
}
domainInput.focus();
return;
}
// 显示查询中状态
showQueryLoading();
// 更新API路径使用完整路径
apiRequest('/api/query', 'GET', { domain: domain })
.then(data => {
// 处理可能的不同响应格式
renderQueryResult(data);
// 触发数据刷新事件
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('query');
}
})
.catch(error => {
console.error('DNS查询失败:', error);
showQueryError('查询失败,请稍后重试');
if (typeof window.showNotification === 'function') {
window.showNotification('DNS查询失败', 'danger');
}
});
}
// 显示查询加载状态
function showQueryLoading() {
const resultContainer = document.getElementById('query-result-container');
if (!resultContainer) return;
// 添加加载动画类
resultContainer.classList.add('loading-animation');
resultContainer.classList.remove('hidden', 'error-animation', 'success-animation');
// 清空之前的结果
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
if (resultHeader) resultHeader.textContent = '查询中...';
if (resultContent) {
resultContent.innerHTML = '<div class="loading">' +
'<div class="spinner"></div><span>正在查询...</span>' +
'</div>';
}
}
// 显示查询错误
function showQueryError(message) {
const resultContainer = document.getElementById('query-result-container');
if (!resultContainer) return;
// 添加错误动画类
resultContainer.classList.add('error-animation');
resultContainer.classList.remove('hidden', 'loading-animation', 'success-animation');
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
if (resultHeader) resultHeader.textContent = '查询错误';
if (resultContent) {
resultContent.innerHTML = `<div class="result-item error-message">
<i class="fas fa-exclamation-circle"></i>
<span>${message}</span>
</div>`;
}
}
// 渲染查询结果
function renderQueryResult(result) {
const resultContainer = document.getElementById('query-result-container');
if (!resultContainer) return;
// 添加成功动画类
resultContainer.classList.add('success-animation');
resultContainer.classList.remove('hidden', 'loading-animation', 'error-animation');
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
if (resultHeader) resultHeader.textContent = '查询结果';
if (!resultContent) return;
// 安全的HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
// 根据查询结果构建内容
let content = '<div class="result-grid">';
// 域名
const safeDomain = escapeHtml(result.domain || '');
content += `<div class="result-item domain-item">
<div class="result-label"><i class="fas fa-globe"></i> 域名</div>
<div class="result-value" id="result-domain">${safeDomain}</div>
</div>`;
// 状态 - 映射API字段
const isBlocked = result.blocked || false;
const isExcluded = result.excluded || false;
const isAllowed = !isBlocked || isExcluded;
const statusText = isBlocked ? '被屏蔽' : isAllowed ? '允许访问' : '未知';
const statusClass = isBlocked ? 'status-error' : isAllowed ? 'status-success' : '';
const statusIcon = isBlocked ? 'fa-ban' : isAllowed ? 'fa-check-circle' : 'fa-question-circle';
content += `<div class="result-item status-item">
<div class="result-label"><i class="fas fa-shield-alt"></i> 状态</div>
<div class="result-value" id="result-status" class="${statusClass}">
<i class="fas ${statusIcon}"></i> ${statusText}
</div>
</div>`;
// 规则类型 - 映射API字段
let ruleType = '';
if (isBlocked) {
if (result.blockRuleType && result.blockRuleType.toLowerCase().includes('regex')) {
ruleType = '正则表达式规则';
} else {
ruleType = result.blockRuleType || '域名规则';
}
} else {
if (isExcluded) {
ruleType = '白名单规则';
} else if (result.hasHosts) {
ruleType = 'Hosts记录';
} else {
ruleType = '未匹配任何规则';
}
}
content += `<div class="result-item rule-type-item">
<div class="result-label"><i class="fas fa-list-alt"></i> 规则类型</div>
<div class="result-value" id="result-rule-type">${escapeHtml(ruleType)}</div>
</div>`;
// 匹配规则 - 映射API字段
let matchedRule = '';
if (isBlocked) {
matchedRule = result.blockRule || '无';
} else if (isExcluded) {
matchedRule = result.excludeRule || '无';
} else {
matchedRule = '无';
}
content += `<div class="result-item matched-rule-item">
<div class="result-label"><i class="fas fa-sitemap"></i> 匹配规则</div>
<div class="result-value rule-code" id="result-rule">${escapeHtml(matchedRule)}</div>
</div>`;
// Hosts记录 - 映射API字段
const hostsRecord = result.hasHosts && result.hostsIP ?
escapeHtml(`${result.hostsIP} ${result.domain}`) : '无';
content += `<div class="result-item hosts-item">
<div class="result-label"><i class="fas fa-file-alt"></i> Hosts记录</div>
<div class="result-value" id="result-hosts">${hostsRecord}</div>
</div>`;
// 查询时间 - API没有提供计算当前时间
const queryTime = `${Date.now() % 100} ms`;
content += `<div class="result-item time-item">
<div class="result-label"><i class="fas fa-clock"></i> 查询时间</div>
<div class="result-value" id="result-time">${queryTime}</div>
</div>`;
content += '</div>'; // 结束result-grid
// DNS响应如果有
if (result.dnsResponse) {
content += '<div class="dns-response-section">';
content += '<h4><i class="fas fa-exchange-alt"></i> DNS响应</h4>';
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
content += '<div class="dns-answers">';
result.dnsResponse.answers.forEach((answer, index) => {
content += `<div class="dns-answer-item">
<span class="answer-index">#${index + 1}</span>
<span class="answer-name">${escapeHtml(answer.name)}</span>
<span class="answer-type">${escapeHtml(answer.type)}</span>
<span class="answer-value">${escapeHtml(answer.value)}</span>
</div>`;
});
content += '</div>';
} else {
content += '<div class="empty-dns"><i class="fas fa-info-circle"></i> 无DNS响应记录</div>';
}
content += '</div>';
}
// 添加复制功能
content += `<div class="result-actions">
<button class="btn btn-sm btn-secondary" onclick="copyQueryResult()">
<i class="fas fa-copy"></i> 复制结果
</button>
</div>`;
resultContent.innerHTML = content;
// 通知用户查询成功
if (typeof window.showNotification === 'function') {
const statusMsg = isBlocked ? '查询完成,该域名被屏蔽' :
isAllowed ? '查询完成,该域名允许访问' : '查询完成';
window.showNotification(statusMsg, 'info');
}
}
// 复制查询结果到剪贴板
function copyQueryResult() {
const resultContainer = document.getElementById('query-result-container');
if (!resultContainer) return;
// 收集关键信息
const domain = document.getElementById('result-domain')?.textContent || '未知域名';
const status = document.getElementById('result-status')?.textContent || '未知状态';
const ruleType = document.getElementById('result-rule-type')?.textContent || '无规则类型';
const matchedRule = document.getElementById('result-rule')?.textContent || '无匹配规则';
const queryTime = document.getElementById('result-time')?.textContent || '未知时间';
// 构建要复制的文本
const textToCopy = `DNS查询结果:\n` +
`域名: ${domain}\n` +
`状态: ${status}\n` +
`规则类型: ${ruleType}\n` +
`匹配规则: ${matchedRule}\n` +
`查询时间: ${queryTime}`;
// 复制到剪贴板
navigator.clipboard.writeText(textToCopy)
.then(() => {
if (typeof window.showNotification === 'function') {
window.showNotification('查询结果已复制到剪贴板', 'success');
}
})
.catch(err => {
console.error('复制失败:', err);
if (typeof window.showNotification === 'function') {
window.showNotification('复制失败,请手动复制', 'warning');
}
});
}
// 导出函数,供其他模块调用
window.initQueryPanel = initQueryPanel;
window.runDnsQuery = runDnsQuery;
// 注册到面板导航系统
if (window.registerPanelModule) {
window.registerPanelModule('query-panel', {
init: initQueryPanel,
refresh: function() {
// 清除当前查询结果
const resultContainer = document.getElementById('query-result-container');
if (resultContainer) {
resultContainer.classList.add('hidden');
}
}
});
}

View File

@@ -1,422 +0,0 @@
// 屏蔽规则管理模块
// 全局变量
let rules = [];
let currentPage = 1;
let itemsPerPage = 50; // 默认每页显示50条规则
let filteredRules = [];
// 初始化屏蔽规则面板
function initRulesPanel() {
// 加载规则列表
loadRules();
// 绑定添加规则按钮事件
document.getElementById('add-rule-btn').addEventListener('click', addNewRule);
// 绑定刷新规则按钮事件
document.getElementById('reload-rules-btn').addEventListener('click', reloadRules);
// 绑定搜索框事件
document.getElementById('rule-search').addEventListener('input', filterRules);
// 绑定每页显示数量变更事件
document.getElementById('items-per-page').addEventListener('change', () => {
itemsPerPage = parseInt(document.getElementById('items-per-page').value);
currentPage = 1; // 重置为第一页
renderRulesList();
});
// 绑定分页按钮事件
document.getElementById('prev-page-btn').addEventListener('click', goToPreviousPage);
document.getElementById('next-page-btn').addEventListener('click', goToNextPage);
document.getElementById('first-page-btn').addEventListener('click', goToFirstPage);
document.getElementById('last-page-btn').addEventListener('click', goToLastPage);
}
// 加载规则列表
async function loadRules() {
try {
const rulesPanel = document.getElementById('rules-panel');
showLoading(rulesPanel);
// 更新API路径使用正确的API路径
const data = await apiRequest('/api/shield', 'GET');
// 处理后端返回的复杂对象数据格式
let allRules = [];
if (data && typeof data === 'object') {
// 合并所有类型的规则到一个数组
if (Array.isArray(data.domainRules)) allRules = allRules.concat(data.domainRules);
if (Array.isArray(data.domainExceptions)) allRules = allRules.concat(data.domainExceptions);
if (Array.isArray(data.regexRules)) allRules = allRules.concat(data.regexRules);
if (Array.isArray(data.regexExceptions)) allRules = allRules.concat(data.regexExceptions);
}
rules = allRules;
filteredRules = [...rules];
currentPage = 1; // 重置为第一页
renderRulesList();
// 更新规则数量统计卡片
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
} catch (error) {
console.error('加载规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('加载规则失败', 'danger');
}
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
}
}
// 渲染规则列表
function renderRulesList() {
const rulesList = document.getElementById('rules-list');
const paginationInfo = document.getElementById('pagination-info');
// 清空列表
rulesList.innerHTML = '';
if (filteredRules.length === 0) {
// 使用更友好的空状态显示
rulesList.innerHTML = '<tr><td colspan="4" class="text-center py-4">' +
'<div class="empty-state">' +
'<div class="empty-icon"><i class="fas fa-shield-alt text-muted"></i></div>' +
'<div class="empty-title text-muted">暂无规则</div>' +
'<div class="empty-description text-muted">点击添加按钮或刷新规则来获取规则列表</div>' +
'</div>' +
'</td></tr>';
paginationInfo.textContent = '共0条规则';
updatePaginationButtons();
return;
}
// 计算分页数据
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredRules.length);
const currentRules = filteredRules.slice(startIndex, endIndex);
// 渲染当前页的规则
currentRules.forEach((rule, index) => {
const row = document.createElement('tr');
const globalIndex = startIndex + index;
// 根据规则类型添加不同的样式
const ruleTypeClass = getRuleTypeClass(rule);
row.innerHTML = `
<td class="rule-id">${globalIndex + 1}</td>
<td class="rule-content ${ruleTypeClass}"><pre>${escapeHtml(rule)}</pre></td>
<td class="rule-actions">
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
<i class="fas fa-trash"></i> 删除
</button>
</td>
`;
// 添加行动画效果
row.style.opacity = '0';
row.style.transform = 'translateY(10px)';
rulesList.appendChild(row);
// 使用requestAnimationFrame确保动画平滑
requestAnimationFrame(() => {
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
row.style.opacity = '1';
row.style.transform = 'translateY(0)';
});
});
// 绑定删除按钮事件
document.querySelectorAll('.delete-rule').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
deleteRule(index);
});
});
// 更新分页信息
paginationInfo.textContent = `显示 ${startIndex + 1}-${endIndex} 条,共 ${filteredRules.length} 条规则,第 ${currentPage}/${totalPages}`;
// 更新分页按钮状态
updatePaginationButtons();
}
// 更新分页按钮状态
function updatePaginationButtons() {
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
const firstBtn = document.getElementById('first-page-btn');
const lastBtn = document.getElementById('last-page-btn');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
firstBtn.disabled = currentPage === 1;
lastBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// 上一页
function goToPreviousPage() {
if (currentPage > 1) {
currentPage--;
renderRulesList();
}
}
// 下一页
function goToNextPage() {
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderRulesList();
}
}
// 第一页
function goToFirstPage() {
currentPage = 1;
renderRulesList();
}
// 最后一页
function goToLastPage() {
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
renderRulesList();
}
// 添加新规则
async function addNewRule() {
const ruleInput = document.getElementById('rule-input');
const rule = ruleInput.value.trim();
if (!rule) {
if (typeof window.showNotification === 'function') {
window.showNotification('请输入规则内容', 'warning');
}
return;
}
try {
// 预处理规则支持AdGuardHome格式
const processedRule = preprocessRule(rule);
// 使用正确的API路径
const response = await apiRequest('/api/shield', 'POST', { rule: processedRule });
// 处理不同的响应格式
if (response.success || response.status === 'success') {
rules.push(processedRule);
filteredRules = [...rules];
ruleInput.value = '';
// 添加后跳转到最后一页,显示新添加的规则
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
renderRulesList();
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则添加成功', 'success');
}
} else {
if (typeof window.showNotification === 'function') {
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
}
}
} catch (error) {
console.error('添加规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('添加规则失败', 'danger');
}
}
}
// 删除规则
async function deleteRule(index) {
if (!confirm('确定要删除这条规则吗?')) {
return;
}
try {
const rule = filteredRules[index];
const rowElement = document.querySelectorAll('#rules-list tr')[index];
// 添加删除动画
if (rowElement) {
rowElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
rowElement.style.opacity = '0';
rowElement.style.transform = 'translateX(-20px)';
}
// 使用正确的API路径
const response = await apiRequest('/api/shield', 'DELETE', { rule });
// 处理不同的响应格式
if (response.success || response.status === 'success') {
// 在原规则列表中找到并删除
const originalIndex = rules.indexOf(rule);
if (originalIndex !== -1) {
rules.splice(originalIndex, 1);
}
// 在过滤后的列表中删除
filteredRules.splice(index, 1);
// 如果当前页没有数据了,回到上一页
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
}
// 等待动画完成后重新渲染列表
setTimeout(() => {
renderRulesList();
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则删除成功', 'success');
}
}, 300);
} else {
// 恢复行样式
if (rowElement) {
rowElement.style.opacity = '1';
rowElement.style.transform = 'translateX(0)';
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则删除失败:' + (response.message || '未知错误'), 'danger');
}
}
} catch (error) {
console.error('删除规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('删除规则失败', 'danger');
}
}
}
// 重新加载规则
async function reloadRules() {
if (!confirm('确定要重新加载所有规则吗?这将覆盖当前内存中的规则。')) {
return;
}
try {
const rulesPanel = document.getElementById('rules-panel');
showLoading(rulesPanel);
// 使用正确的API路径和方法 - PUT请求到/api/shield
await apiRequest('/api/shield', 'PUT');
// 重新加载规则列表
await loadRules();
// 触发数据刷新事件,通知其他模块数据已更新
if (typeof window.triggerDataRefresh === 'function') {
window.triggerDataRefresh('rules');
}
if (typeof window.showNotification === 'function') {
window.showNotification('规则重新加载成功', 'success');
}
} catch (error) {
console.error('重新加载规则失败:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('重新加载规则失败', 'danger');
}
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
}
}
// 过滤规则
function filterRules() {
const searchTerm = document.getElementById('rule-search').value.toLowerCase();
if (searchTerm) {
filteredRules = rules.filter(rule => rule.toLowerCase().includes(searchTerm));
} else {
filteredRules = [...rules];
}
currentPage = 1; // 重置为第一页
renderRulesList();
}
// HTML转义防止XSS攻击
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>'"]/g, m => map[m]);
}
// 根据规则类型返回对应的CSS类名
function getRuleTypeClass(rule) {
// 简单的规则类型判断
if (rule.startsWith('||') || rule.startsWith('|http')) {
return 'rule-type-url';
} else if (rule.startsWith('@@')) {
return 'rule-type-exception';
} else if (rule.startsWith('#')) {
return 'rule-type-comment';
} else if (rule.includes('$')) {
return 'rule-type-filter';
}
return 'rule-type-standard';
}
// 预处理规则,支持多种规则格式
function preprocessRule(rule) {
// 移除首尾空白字符
let processed = rule.trim();
// 处理AdGuardHome格式的规则
if (processed.startsWith('0.0.0.0 ') || processed.startsWith('127.0.0.1 ')) {
const parts = processed.split(' ');
if (parts.length >= 2) {
// 转换为AdBlock Plus格式
processed = '||' + parts[1] + '^';
}
}
return processed;
}
// 导出函数,供其他模块调用
window.updateRulesCount = function(count) {
const rulesCountElement = document.getElementById('rules-count');
if (rulesCountElement) {
rulesCountElement.textContent = count;
}
}
// 导出初始化函数
window.initRulesPanel = initRulesPanel;
// 注册到面板导航系统
if (window.registerPanelModule) {
window.registerPanelModule('rules-panel', {
init: initRulesPanel,
refresh: loadRules
});
}

View File

@@ -1,301 +0,0 @@
// DNS查询页面功能实现
// 初始化查询页面
function initQueryPage() {
console.log('初始化DNS查询页面...');
setupQueryEventListeners();
loadQueryHistory();
}
// 执行DNS查询
async function handleDNSQuery() {
const domainInput = document.getElementById('dns-query-domain');
const resultDiv = document.getElementById('query-result');
if (!domainInput || !resultDiv) {
console.error('找不到必要的DOM元素');
return;
}
const domain = domainInput.value.trim();
if (!domain) {
showErrorMessage('请输入域名');
return;
}
try {
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
if (!response.ok) {
throw new Error('查询失败');
}
const result = await response.json();
displayQueryResult(result, domain);
saveQueryHistory(domain, result);
loadQueryHistory();
} catch (error) {
console.error('DNS查询出错:', error);
showErrorMessage('查询失败,请稍后重试');
}
}
// 显示查询结果
function displayQueryResult(result, domain) {
const resultDiv = document.getElementById('query-result');
if (!resultDiv) return;
// 显示结果容器
resultDiv.classList.remove('hidden');
// 解析结果
const status = result.blocked ? '被屏蔽' : '正常';
const statusClass = result.blocked ? 'text-danger' : 'text-success';
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
const timestamp = new Date(result.timestamp).toLocaleString();
// 更新结果显示
document.getElementById('result-domain').textContent = domain;
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
document.getElementById('result-type').textContent = blockType;
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
let blockRuleElement = document.getElementById('result-block-rule');
if (!blockRuleElement) {
// 创建屏蔽规则显示区域
const grid = resultDiv.querySelector('.grid');
if (grid) {
const newGridItem = document.createElement('div');
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
newGridItem.innerHTML = `
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
<p class="text-lg font-semibold" id="result-block-rule">-</p>
`;
grid.appendChild(newGridItem);
blockRuleElement = document.getElementById('result-block-rule');
}
}
// 更新屏蔽规则显示
if (blockRuleElement) {
blockRuleElement.textContent = blockRule;
}
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
let blockSourceElement = document.getElementById('result-block-source');
if (!blockSourceElement) {
// 创建屏蔽来源显示区域
const grid = resultDiv.querySelector('.grid');
if (grid) {
const newGridItem = document.createElement('div');
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
newGridItem.innerHTML = `
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
<p class="text-lg font-semibold" id="result-block-source">-</p>
`;
grid.appendChild(newGridItem);
blockSourceElement = document.getElementById('result-block-source');
}
}
// 更新屏蔽来源显示
if (blockSourceElement) {
blockSourceElement.textContent = blockSource;
}
document.getElementById('result-time').textContent = timestamp;
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
}
// 保存查询历史
function saveQueryHistory(domain, result) {
// 获取现有历史记录
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
// 创建历史记录项
const historyItem = {
domain: domain,
timestamp: new Date().toISOString(),
result: {
blocked: result.blocked,
blockRuleType: result.blockRuleType,
blockRule: result.blockRule,
blocksource: result.blocksource
}
};
// 添加到历史记录开头
history.unshift(historyItem);
// 限制历史记录数量
if (history.length > 20) {
history = history.slice(0, 20);
}
// 保存到本地存储
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
}
// 加载查询历史
function loadQueryHistory() {
const historyDiv = document.getElementById('query-history');
if (!historyDiv) return;
// 获取历史记录
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
if (history.length === 0) {
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
return;
}
// 生成历史记录HTML
const historyHTML = history.map(item => {
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
const statusText = item.result.blocked ? '被屏蔽' : '正常';
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
const blockRule = item.result.blocked ? item.result.blockRule : '无';
const blockSource = item.result.blocked ? item.result.blocksource : '无';
const formattedTime = new Date(item.timestamp).toLocaleString();
return `
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-1">
<div class="flex items-center space-x-2">
<span class="font-medium">${item.domain}</span>
<span class="${statusClass} text-sm">${statusText}</span>
<span class="text-xs text-gray-500">${blockType}</span>
</div>
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
</div>
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
<i class="fa fa-refresh mr-1"></i>重新查询
</button>
</div>
`;
}).join('');
historyDiv.innerHTML = historyHTML;
}
// 从历史记录重新查询
function requeryFromHistory(domain) {
const domainInput = document.getElementById('dns-query-domain');
if (domainInput) {
domainInput.value = domain;
handleDNSQuery();
}
}
// 清空查询历史
function clearQueryHistory() {
if (confirm('确定要清空所有查询历史吗?')) {
localStorage.removeItem('dnsQueryHistory');
loadQueryHistory();
showSuccessMessage('查询历史已清空');
}
}
// 设置事件监听器
function setupQueryEventListeners() {
// 查询按钮事件
const queryBtn = document.getElementById('dns-query-btn');
if (queryBtn) {
queryBtn.addEventListener('click', handleDNSQuery);
}
// 输入框回车键事件
const domainInput = document.getElementById('dns-query-domain');
if (domainInput) {
domainInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleDNSQuery();
}
});
}
// 清空历史按钮事件
const clearHistoryBtn = document.getElementById('clear-history-btn');
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', clearQueryHistory);
}
}
// 显示成功消息
function showSuccessMessage(message) {
showNotification(message, 'success');
}
// 显示错误消息
function showErrorMessage(message) {
showNotification(message, 'error');
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新通知
const notification = document.createElement('div');
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
// 设置通知样式
if (type === 'success') {
notification.classList.add('bg-green-500', 'text-white');
} else if (type === 'error') {
notification.classList.add('bg-red-500', 'text-white');
} else {
notification.classList.add('bg-blue-500', 'text-white');
}
notification.innerHTML = `
<div class="flex items-center space-x-2">
<i class="fa ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.classList.remove('opacity-0');
notification.classList.add('opacity-100');
}, 10);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initQueryPage);
} else {
initQueryPage();
}
// 当切换到DNS查询页面时重新加载数据
document.addEventListener('DOMContentLoaded', () => {
// 监听hash变化当切换到DNS查询页面时重新加载数据
window.addEventListener('hashchange', () => {
if (window.location.hash === '#query') {
initQueryPage();
}
});
});

View File

@@ -1,305 +0,0 @@
// 服务器状态组件 - 显示CPU使用率和查询统计
// 全局变量
let serverStatusUpdateTimer = null;
let previousServerData = {
cpu: 0,
queries: 0
};
// 初始化服务器状态组件
function initServerStatusWidget() {
// 确保DOM元素存在
const widget = document.getElementById('server-status-widget');
if (!widget) return;
// 初始化页面类型检测
updateWidgetDisplayByPageType();
// 设置页面切换事件监听
handlePageSwitchEvents();
// 设置WebSocket监听如果可用
setupWebSocketListeners();
// 立即加载一次数据
loadServerStatusData();
// 设置定时更新每5秒更新一次
serverStatusUpdateTimer = setInterval(loadServerStatusData, 5000);
}
// 判断当前页面是否为仪表盘
function isCurrentPageDashboard() {
// 方法1检查侧边栏激活状态
const dashboardLink = document.querySelector('.sidebar a[href="#dashboard"]');
if (dashboardLink && dashboardLink.classList.contains('active')) {
return true;
}
// 方法2检查仪表盘特有元素
const dashboardElements = [
'#dashboard-container',
'.dashboard-summary',
'#dashboard-stats'
];
for (const selector of dashboardElements) {
if (document.querySelector(selector)) {
return true;
}
}
// 方法3检查URL哈希值
if (window.location.hash === '#dashboard' || window.location.hash === '') {
return true;
}
return false;
}
// 根据页面类型更新组件显示
function updateWidgetDisplayByPageType() {
const additionalStats = document.getElementById('server-additional-stats');
if (!additionalStats) return;
// 如果当前页面是仪表盘,隐藏额外统计指标
if (isCurrentPageDashboard()) {
additionalStats.classList.add('hidden');
} else {
// 非仪表盘页面,显示额外统计指标
additionalStats.classList.remove('hidden');
}
}
// 处理页面切换事件
function handlePageSwitchEvents() {
// 监听哈希变化(导航切换)
window.addEventListener('hashchange', updateWidgetDisplayByPageType);
// 监听侧边栏点击事件
const sidebarLinks = document.querySelectorAll('.sidebar a');
sidebarLinks.forEach(link => {
link.addEventListener('click', function() {
// 延迟检查,确保页面已切换
setTimeout(updateWidgetDisplayByPageType, 100);
});
});
// 监听导航菜单点击事件
const navLinks = document.querySelectorAll('nav a');
navLinks.forEach(link => {
link.addEventListener('click', function() {
setTimeout(updateWidgetDisplayByPageType, 100);
});
});
}
// 监控WebSocket连接状态
function monitorWebSocketConnection() {
// 如果存在WebSocket连接监听消息
if (window.socket) {
window.socket.addEventListener('message', function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'status_update') {
updateServerStatusWidget(data.payload);
}
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
});
}
}
// 设置WebSocket监听器
function setupWebSocketListeners() {
// 如果WebSocket已经存在
if (window.socket) {
monitorWebSocketConnection();
} else {
// 监听socket初始化事件
window.addEventListener('socketInitialized', function() {
monitorWebSocketConnection();
});
}
}
// 加载服务器状态数据
async function loadServerStatusData() {
try {
// 使用现有的API获取系统状态
const api = window.api || {};
const getStatusFn = api.getStatus || function() { return Promise.resolve({}); };
const statusData = await getStatusFn();
if (statusData && !statusData.error) {
updateServerStatusWidget(statusData);
}
} catch (error) {
console.error('加载服务器状态数据失败:', error);
}
}
// 更新服务器状态组件
function updateServerStatusWidget(stats) {
// 确保组件存在
const widget = document.getElementById('server-status-widget');
if (!widget) return;
// 确保stats存在
stats = stats || {};
// 提取CPU使用率
let cpuUsage = 0;
if (stats.system && typeof stats.system.cpu === 'number') {
cpuUsage = stats.system.cpu;
} else if (typeof stats.cpuUsage === 'number') {
cpuUsage = stats.cpuUsage;
}
// 提取查询统计数据
let totalQueries = 0;
let blockedQueries = 0;
let allowedQueries = 0;
if (stats.dns) {
const allowed = typeof stats.dns.Allowed === 'number' ? stats.dns.Allowed : 0;
const blocked = typeof stats.dns.Blocked === 'number' ? stats.dns.Blocked : 0;
const errors = typeof stats.dns.Errors === 'number' ? stats.dns.Errors : 0;
totalQueries = allowed + blocked + errors;
blockedQueries = blocked;
allowedQueries = allowed;
} else {
totalQueries = typeof stats.totalQueries === 'number' ? stats.totalQueries : 0;
blockedQueries = typeof stats.blockedQueries === 'number' ? stats.blockedQueries : 0;
allowedQueries = typeof stats.allowedQueries === 'number' ? stats.allowedQueries : 0;
}
// 更新CPU使用率
const cpuValueElement = document.getElementById('server-cpu-value');
if (cpuValueElement) {
cpuValueElement.textContent = cpuUsage.toFixed(1) + '%';
}
const cpuBarElement = document.getElementById('server-cpu-bar');
if (cpuBarElement) {
cpuBarElement.style.width = Math.min(cpuUsage, 100) + '%';
// 根据CPU使用率改变颜色
if (cpuUsage > 80) {
cpuBarElement.className = 'h-full bg-danger rounded-full';
} else if (cpuUsage > 50) {
cpuBarElement.className = 'h-full bg-warning rounded-full';
} else {
cpuBarElement.className = 'h-full bg-success rounded-full';
}
}
// 更新查询量
const queriesValueElement = document.getElementById('server-queries-value');
if (queriesValueElement) {
queriesValueElement.textContent = formatNumber(totalQueries);
}
// 计算查询量百分比假设最大查询量为10000
const queryPercentage = Math.min((totalQueries / 10000) * 100, 100);
const queriesBarElement = document.getElementById('server-queries-bar');
if (queriesBarElement) {
queriesBarElement.style.width = queryPercentage + '%';
}
// 更新额外统计指标
const totalQueriesElement = document.getElementById('server-total-queries');
if (totalQueriesElement) {
totalQueriesElement.textContent = formatNumber(totalQueries);
}
const blockedQueriesElement = document.getElementById('server-blocked-queries');
if (blockedQueriesElement) {
blockedQueriesElement.textContent = formatNumber(blockedQueries);
}
const allowedQueriesElement = document.getElementById('server-allowed-queries');
if (allowedQueriesElement) {
allowedQueriesElement.textContent = formatNumber(allowedQueries);
}
// 添加光晕提示效果
if (previousServerData.cpu !== cpuUsage || previousServerData.queries !== totalQueries) {
addGlowEffect();
}
// 更新服务器状态指示器
const statusIndicator = document.getElementById('server-status-indicator');
if (statusIndicator) {
// 检查系统状态
if (stats.system && stats.system.status === 'error') {
statusIndicator.className = 'inline-block w-2 h-2 bg-danger rounded-full';
} else {
statusIndicator.className = 'inline-block w-2 h-2 bg-success rounded-full';
}
}
// 保存当前数据用于下次比较
previousServerData = {
cpu: cpuUsage,
queries: totalQueries
};
}
// 添加光晕提示效果
function addGlowEffect() {
const widget = document.getElementById('server-status-widget');
if (!widget) return;
// 添加光晕类
widget.classList.add('glow-effect');
// 2秒后移除光晕
setTimeout(function() {
widget.classList.remove('glow-effect');
}, 2000);
}
// 格式化数字
function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return fullNumStr;
}
// 在DOM加载完成后初始化
window.addEventListener('DOMContentLoaded', function() {
// 延迟初始化,确保页面完全加载
setTimeout(initServerStatusWidget, 500);
});
// 在页面卸载时清理资源
window.addEventListener('beforeunload', function() {
if (serverStatusUpdateTimer) {
clearInterval(serverStatusUpdateTimer);
serverStatusUpdateTimer = null;
}
});
// 导出函数供其他模块使用
window.serverStatusWidget = {
init: initServerStatusWidget,
update: updateServerStatusWidget
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,10 @@
package logger
import (
"fmt"
"io"
"os"
"path/filepath"
"sync"
"github.com/sirupsen/logrus"
@@ -15,7 +17,7 @@ var (
)
// InitLogger 初始化日志系统
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool) error {
logMutex.Lock()
defer logMutex.Unlock()
@@ -30,21 +32,42 @@ func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
FullTimestamp: true,
})
// 创建日志目录
if logFile != "" {
logDir := filepath.Dir(logFile)
if logDir != "." {
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("创建日志目录失败: %w", err)
}
}
}
// 设置输出目标
outputTargets := []io.Writer{}
if logFile != "" {
// 使用标准库打开文件,支持追加写入
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
logrus.Warn("无法打开日志文件,将使用标准输出", "error", err)
log.SetOutput(os.Stdout)
// 无法打开日志文件时,回退到标准输出
log.Println("无法打开日志文件,将使用标准输出:", err)
} else {
// 同时输出到文件和标准输出
log.SetOutput(io.MultiWriter(file, os.Stdout))
outputTargets = append(outputTargets, file)
defer file.Sync() // 确保日志内容被写入磁盘
}
} else {
log.SetOutput(os.Stdout)
}
// 无论是否指定日志文件,都同时输出到标准输出
if len(outputTargets) > 0 {
outputTargets = append(outputTargets, os.Stdout)
} else {
// 如果没有指定日志文件,仅使用标准输出
outputTargets = append(outputTargets, os.Stdout)
}
// 设置日志输出
log.SetOutput(io.MultiWriter(outputTargets...))
// 设置日志级别
logLevel, err := logrus.ParseLevel(level)
if err != nil {
@@ -61,13 +84,20 @@ func Close() {
logMutex.Lock()
defer logMutex.Unlock()
if !initialized {
if !initialized || log == nil {
return
}
// 执行日志刷新
log.Warn("日志系统已关闭")
// 确保日志被写入磁盘
if loggerOutput, ok := log.Out.(*os.File); ok {
loggerOutput.Sync()
}
initialized = false
log = nil
}
// Info 记录信息级别日志
@@ -122,6 +152,20 @@ func Warn(msg string, fields ...interface{}) {
}
}
// Fatal 记录致命级别日志并退出程序
func Fatal(msg string, fields ...interface{}) {
if !initialized {
// 如果日志系统未初始化使用标准库log
log.Fatal(msg)
}
if len(fields) > 0 {
log.WithFields(toFields(fields)).Fatal(msg)
} else {
log.Fatal(msg)
}
}
// toFields 将键值对转换为logrus字段
func toFields(keyValues []interface{}) logrus.Fields {
fields := make(logrus.Fields)

File diff suppressed because it is too large Load Diff

58
main.go
View File

@@ -42,9 +42,11 @@ func createDefaultConfig(configFile string) error {
"saveInterval": 300
},
"http": {
"port": 8081,
"port": 8080,
"host": "0.0.0.0",
"enableAPI": true
"enableAPI": true,
"username": "admin",
"password": "admin"
},
"shield": {
"localRulesFile": "data/rules.txt",
@@ -146,20 +148,9 @@ func createRequiredFiles(cfg *config.Config) error {
func main() {
// 命令行参数解析
var configFile string
var daemonMode bool
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
flag.BoolVar(&daemonMode, "daemon", false, "以守护进程模式运行")
flag.Parse()
// 如果是守护进程模式,创建守护进程
if daemonMode {
if err := daemonize(); err != nil {
log.Fatalf("创建守护进程失败: %v", err)
}
// 父进程退出
os.Exit(0)
}
// 检查配置文件是否存在,如果不存在则创建默认配置文件
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
@@ -185,7 +176,7 @@ func main() {
log.Println("所需文件和文件夹创建成功")
// 初始化日志系统
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil {
log.Fatalf("初始化日志系统失败: %v", err)
}
defer logger.Close()
@@ -225,45 +216,10 @@ func main() {
<-sigCh
// 清理资源
log.Println("正在关闭服务...")
logger.Info("正在关闭服务...")
dnsServer.Stop()
httpServer.Stop()
shieldManager.StopAutoUpdate()
// 守护进程模式下不需要删除PID文件
log.Println("服务已关闭")
}
// daemonize 创建守护进程
func daemonize() error {
// 使用更简单的方式创建守护进程:直接在当前进程中进行守护化处理
// 1. 重定向标准输入、输出、错误
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("打开/dev/null失败: %w", err)
}
defer nullFile.Close()
// 重定向文件描述符
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("重定向stdin失败: %w", err)
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdout.Fd()))
if err != nil {
return fmt.Errorf("重定向stdout失败: %w", err)
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stderr.Fd()))
if err != nil {
return fmt.Errorf("重定向stderr失败: %w", err)
}
// 2. 创建新的会话和进程组
_, err = syscall.Setsid()
if err != nil {
return fmt.Errorf("创建新会话失败: %w", err)
}
fmt.Println("守护进程已启动")
return nil
logger.Info("服务已关闭")
}

Binary file not shown.

View File

@@ -59,6 +59,12 @@
<span>DNS屏蔽查询</span>
</a>
</li>
<li>
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-file-text-o mr-3 text-lg"></i>
<span>查询日志</span>
</a>
</li>
<li>
<a href="#config" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-cog mr-3 text-lg"></i>
@@ -143,9 +149,22 @@
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<i class="fa fa-bell text-lg"></i>
</button>
<div class="flex items-center">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
<span class="ml-2 hidden md:block">管理员</span>
<!-- 账户下拉菜单 -->
<div class="relative group" id="account-dropdown">
<button class="flex items-center p-2 rounded-full hover:bg-gray-100 transition-colors focus:outline-none">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
<span class="ml-2 hidden md:block">管理员</span>
<i class="fa fa-caret-down ml-1 text-xs hidden md:block"></i>
</button>
<!-- 下拉菜单 -->
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-2 z-50 hidden group-hover:block" id="account-menu">
<button id="change-password-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
<i class="fa fa-key mr-2"></i>修改密码
</button>
<button id="logout-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
<i class="fa fa-sign-out mr-2"></i>注销
</button>
</div>
</div>
</div>
</header>
@@ -762,6 +781,195 @@
</div>
</div>
<!-- 查询日志页面 -->
<div id="logs-content" class="hidden space-y-6">
<!-- 日志统计概览 -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 gap-6">
<!-- 总查询数 -->
<div class="bg-blue-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">总查询数</h3>
<div class="p-2 rounded-full bg-primary/10 text-primary">
<i class="fa fa-refresh"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="logs-total-queries">0</p>
</div>
</div>
</div>
</div>
<!-- 平均响应时间 -->
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">平均响应时间</h3>
<div class="p-2 rounded-full bg-info/10 text-info">
<i class="fa fa-clock-o"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="logs-avg-response-time">0ms</p>
</div>
</div>
</div>
</div>
<!-- 活跃来源IP -->
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">活跃来源IP</h3>
<div class="p-2 rounded-full bg-success/10 text-success">
<i class="fa fa-globe"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="logs-active-ips">0</p>
</div>
</div>
</div>
</div>
<!-- 屏蔽率 -->
<div class="bg-red-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">屏蔽率</h3>
<div class="p-2 rounded-full bg-danger/10 text-danger">
<i class="fa fa-ban"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="logs-block-rate">0%</p>
</div>
</div>
</div>
</div>
</div>
<!-- 日志搜索和过滤 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div class="flex-1">
<input type="text" id="logs-search" placeholder="搜索域名或客户端IP" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="w-32">
<select id="logs-result-filter" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="">全部结果</option>
<option value="allowed">允许</option>
<option value="blocked">屏蔽</option>
<option value="error">错误</option>
</select>
</div>
<div class="w-32">
<select id="logs-per-page" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="30" selected>30条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
<button id="logs-search-btn" class="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
<i class="fa fa-search mr-2"></i>搜索
</button>
</div>
</div>
<!-- 日志趋势图表 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">查询趋势</h3>
<div class="flex space-x-2">
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
</div>
</div>
<div class="h-64">
<canvas id="logs-trend-chart"></canvas>
</div>
</div>
<!-- 日志详情表格 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<h3 class="text-lg font-semibold">查询日志详情</h3>
<button id="logs-refresh-btn" class="ml-3 p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="刷新日志">
<i class="fa fa-refresh"></i>
</button>
</div>
<div id="logs-loading" class="flex items-center text-sm text-gray-500 hidden">
<i class="fa fa-spinner fa-spin mr-2"></i>
<span>加载中...</span>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="time">
<div class="flex items-center">
时间
<i class="fa fa-sort ml-1 text-xs"></i>
</div>
</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="clientIp">
<div class="flex items-center">
客户端IP
<i class="fa fa-sort ml-1 text-xs"></i>
</div>
</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="domain">
<div class="flex items-center">
请求
<i class="fa fa-sort ml-1 text-xs"></i>
</div>
</th>
<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>
</tr>
</thead>
<tbody id="logs-table-body">
<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>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="flex items-center justify-between mt-6">
<div class="text-sm text-gray-500">
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span>
</div>
<div class="flex space-x-2">
<button id="logs-prev-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
<i class="fa fa-chevron-left"></i>
</button>
<button id="logs-next-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
<div id="config-content" class="hidden">
<!-- 系统设置页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow">
@@ -852,6 +1060,41 @@
</main>
</div>
<!-- 修改密码模态框 -->
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">修改密码</h3>
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<form id="change-password-form">
<div class="mb-4">
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
<input type="password" id="current-password" name="currentPassword" placeholder="请输入当前密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
</div>
<div class="mb-4">
<label for="new-password" class="block text-sm font-medium text-gray-700 mb-1">新密码</label>
<input type="password" id="new-password" name="newPassword" placeholder="请输入新密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
</div>
<div class="mb-6">
<label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
<input type="password" id="confirm-password" name="confirmPassword" placeholder="请再次输入新密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
<div id="password-mismatch" class="text-danger text-sm mt-1 hidden">新密码和确认密码不匹配</div>
</div>
<div class="flex items-center justify-end space-x-3">
<button type="button" id="cancel-change-password" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors">取消</button>
<button type="submit" id="save-password-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">保存</button>
</div>
</form>
</div>
</div>
<!-- 脚本 -->
<script src="js/main.js"></script>
<script src="js/api.js"></script>
@@ -860,6 +1103,7 @@
<script src="js/shield.js"></script>
<script src="js/hosts.js"></script>
<script src="js/query.js"></script>
<script src="js/logs.js"></script>
<script src="js/config.js"></script>
<!-- 直接渲染滚动列表的静态HTML内容 -->

View File

@@ -38,6 +38,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// 优化错误响应处理
console.warn(`API请求失败: ${response.status}`);
// 处理401未授权错误重定向到登录页面
if (response.status === 401) {
console.warn('未授权访问,重定向到登录页面');
window.location.href = '/login';
return { error: '未授权访问' };
}
// 尝试解析JSON但如果失败直接使用原始文本作为错误信息
try {
const errorData = JSON.parse(responseText);

View File

@@ -6,12 +6,18 @@ let dnsRequestsChart = null;
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
let wsConnection = null;
let wsReconnectTimer = null;
let dashboardWsConnection = null;
let dashboardWsReconnectTimer = null;
// 存储统计卡片图表实例
let statCardCharts = {};
// 存储统计卡片历史数据
let statCardHistoryData = {};
// 存储仪表盘历史数据,用于计算趋势
window.dashboardHistoryData = window.dashboardHistoryData || {
prevResponseTime: null,
prevActiveIPs: null,
prevTopQueryTypeCount: null
};
// 引入颜色配置文件
const COLOR_CONFIG = window.COLOR_CONFIG || {};
@@ -53,22 +59,22 @@ function connectWebSocket() {
console.log('正在连接WebSocket:', wsUrl);
// 创建WebSocket连接
wsConnection = new WebSocket(wsUrl);
dashboardWsConnection = new WebSocket(wsUrl);
// 连接打开事件
wsConnection.onopen = function() {
dashboardWsConnection.onopen = function() {
console.log('WebSocket连接已建立');
showNotification('数据更新成功', 'success');
// 清除重连计时器
if (wsReconnectTimer) {
clearTimeout(wsReconnectTimer);
wsReconnectTimer = null;
if (dashboardWsReconnectTimer) {
clearTimeout(dashboardWsReconnectTimer);
dashboardWsReconnectTimer = null;
}
};
// 接收消息事件
wsConnection.onmessage = function(event) {
dashboardWsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
@@ -82,16 +88,16 @@ function connectWebSocket() {
};
// 连接关闭事件
wsConnection.onclose = function(event) {
dashboardWsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭代码:', event.code);
wsConnection = null;
dashboardWsConnection = null;
// 设置重连
setupReconnect();
};
// 连接错误事件
wsConnection.onerror = function(error) {
dashboardWsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
@@ -104,14 +110,14 @@ function connectWebSocket() {
// 设置重连逻辑
function setupReconnect() {
if (wsReconnectTimer) {
if (dashboardWsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
wsReconnectTimer = setTimeout(() => {
dashboardWsReconnectTimer = setTimeout(() => {
connectWebSocket();
}, reconnectDelay);
}
@@ -192,14 +198,42 @@ function processRealTimeData(stats) {
if (document.getElementById('top-query-type')) {
const queryType = stats.topQueryType || '---';
document.getElementById('top-query-type').textContent = queryType;
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
queryPercentElem.textContent = '• ---';
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
// 计算查询类型趋势
let queryPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
// 存储当前值用于下次计算趋势
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount || stats.topQueryTypeCount;
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
// 计算变化百分比
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';
}
}
}
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
}
document.getElementById('top-query-type').textContent = queryType;
}
if (document.getElementById('active-ips')) {
@@ -362,15 +396,15 @@ function fallbackToIntervalRefresh() {
// 清理资源
function cleanupResources() {
// 清除WebSocket连接
if (wsConnection) {
wsConnection.close();
wsConnection = null;
if (dashboardWsConnection) {
dashboardWsConnection.close();
dashboardWsConnection = null;
}
// 清除重连计时器
if (wsReconnectTimer) {
clearTimeout(wsReconnectTimer);
wsReconnectTimer = null;
if (dashboardWsReconnectTimer) {
clearTimeout(dashboardWsReconnectTimer);
dashboardWsReconnectTimer = null;
}
// 清除定时刷新
@@ -797,17 +831,20 @@ function updateStatsCards(stats) {
const originalStyle = element.getAttribute('style') || '';
try {
// 复制原始元素的样式到新元素,确保大小完全一致
const computedStyle = getComputedStyle(element);
// 配置翻页容器样式,确保与原始元素大小完全一致
const containerStyle =
'position: relative; '
+ 'display: ' + computedStyle.display + '; '
+ 'overflow: hidden; '
+ 'height: ' + element.offsetHeight + 'px; '
+ 'width: ' + element.offsetWidth + 'px; '
+ 'margin: ' + computedStyle.margin + '; '
+ 'padding: ' + computedStyle.padding + '; '
+ 'box-sizing: ' + computedStyle.boxSizing + '; '
+ 'line-height: ' + computedStyle.lineHeight + ';';
'position: relative; ' +
'display: ' + computedStyle.display + '; ' +
'overflow: hidden; ' +
'height: ' + element.offsetHeight + 'px; ' +
'width: ' + element.offsetWidth + 'px; ' +
'margin: ' + computedStyle.margin + '; ' +
'padding: ' + computedStyle.padding + '; ' +
'box-sizing: ' + computedStyle.boxSizing + '; ' +
'line-height: ' + computedStyle.lineHeight + ';';
// 创建翻页容器
const flipContainer = document.createElement('div');
@@ -844,9 +881,6 @@ function updateStatsCards(stats) {
'transition: transform 400ms ease-in-out; ' +
'transform-origin: center; ' +
'transform: translateY(100%);';
// 复制原始元素的样式到新元素,确保大小完全一致
const computedStyle = getComputedStyle(element);
[oldValueElement, newValueElement].forEach(el => {
el.style.fontSize = computedStyle.fontSize;
el.style.fontWeight = computedStyle.fontWeight;

View File

@@ -9,6 +9,7 @@ function setupNavigation() {
document.getElementById('shield-content'),
document.getElementById('hosts-content'),
document.getElementById('query-content'),
document.getElementById('logs-content'),
document.getElementById('config-content')
];
const pageTitle = document.getElementById('page-title');
@@ -21,14 +22,6 @@ function setupNavigation() {
if (window.innerWidth < 768) {
closeSidebar();
}
// 页面特定初始化 - 保留这部分逻辑因为它不会与hashchange事件处理逻辑冲突
const target = item.getAttribute('href').substring(1);
if (target === 'shield' && typeof initShieldPage === 'function') {
initShieldPage();
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
initHostsPage();
}
});
});
@@ -116,15 +109,84 @@ function setupNavigation() {
});
}
// 页面初始化函数 - 根据当前hash值初始化对应页面
function initPageByHash() {
const hash = window.location.hash.substring(1);
// 隐藏所有内容区域
const contentSections = [
document.getElementById('dashboard-content'),
document.getElementById('shield-content'),
document.getElementById('hosts-content'),
document.getElementById('query-content'),
document.getElementById('logs-content'),
document.getElementById('config-content')
];
contentSections.forEach(section => {
if (section) {
section.classList.add('hidden');
}
});
// 显示当前页面内容
const currentSection = document.getElementById(`${hash}-content`);
if (currentSection) {
currentSection.classList.remove('hidden');
}
// 更新页面标题
const pageTitle = document.getElementById('page-title');
if (pageTitle) {
const titles = {
'dashboard': '仪表盘',
'shield': '屏蔽管理',
'hosts': 'Hosts管理',
'query': 'DNS屏蔽查询',
'logs': '查询日志',
'config': '系统设置'
};
pageTitle.textContent = titles[hash] || '仪表盘';
}
// 页面特定初始化 - 使用setTimeout延迟调用确保所有脚本文件都已加载完成
if (hash === 'shield') {
setTimeout(() => {
if (typeof initShieldPage === 'function') {
initShieldPage();
}
}, 0);
} else if (hash === 'hosts') {
setTimeout(() => {
if (typeof initHostsPage === 'function') {
initHostsPage();
}
}, 0);
} else if (hash === 'logs') {
setTimeout(() => {
if (typeof initLogsPage === 'function') {
initLogsPage();
}
}, 0);
} else if (hash === 'dashboard') {
setTimeout(() => {
if (typeof loadDashboardData === 'function') {
loadDashboardData();
}
}, 0);
}
}
// 初始化函数
function init() {
// 设置导航
setupNavigation();
// 加载仪表盘数据
if (typeof loadDashboardData === 'function') {
loadDashboardData();
}
// 初始化页面
initPageByHash();
// 添加hashchange事件监听处理浏览器前进/后退按钮
window.addEventListener('hashchange', initPageByHash);
// 定期更新系统状态
setInterval(updateSystemStatus, 5000);
@@ -169,5 +231,175 @@ function formatUptime(milliseconds) {
}
}
// 账户功能 - 下拉菜单、注销和修改密码
function setupAccountFeatures() {
// 下拉菜单功能
const accountDropdown = document.getElementById('account-dropdown');
const accountMenu = document.getElementById('account-menu');
const changePasswordBtn = document.getElementById('change-password-btn');
const logoutBtn = document.getElementById('logout-btn');
const changePasswordModal = document.getElementById('change-password-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const cancelChangePasswordBtn = document.getElementById('cancel-change-password');
const changePasswordForm = document.getElementById('change-password-form');
const passwordMismatch = document.getElementById('password-mismatch');
const newPassword = document.getElementById('new-password');
const confirmPassword = document.getElementById('confirm-password');
// 点击外部关闭下拉菜单
document.addEventListener('click', (e) => {
if (accountDropdown && !accountDropdown.contains(e.target)) {
accountMenu.classList.add('hidden');
}
});
// 点击账户区域切换下拉菜单
if (accountDropdown) {
accountDropdown.addEventListener('click', (e) => {
e.stopPropagation();
accountMenu.classList.toggle('hidden');
});
}
// 打开修改密码模态框
if (changePasswordBtn) {
changePasswordBtn.addEventListener('click', () => {
accountMenu.classList.add('hidden');
changePasswordModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
});
}
// 关闭修改密码模态框
function closeModal() {
changePasswordModal.classList.add('hidden');
document.body.style.overflow = '';
changePasswordForm.reset();
passwordMismatch.classList.add('hidden');
}
// 绑定关闭模态框事件
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeModal);
}
if (cancelChangePasswordBtn) {
cancelChangePasswordBtn.addEventListener('click', closeModal);
}
// 点击模态框外部关闭模态框
if (changePasswordModal) {
changePasswordModal.addEventListener('click', (e) => {
if (e.target === changePasswordModal) {
closeModal();
}
});
}
// 按ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !changePasswordModal.classList.contains('hidden')) {
closeModal();
}
});
// 密码匹配验证
if (newPassword && confirmPassword) {
confirmPassword.addEventListener('input', () => {
if (newPassword.value !== confirmPassword.value) {
passwordMismatch.classList.remove('hidden');
} else {
passwordMismatch.classList.add('hidden');
}
});
newPassword.addEventListener('input', () => {
if (newPassword.value !== confirmPassword.value) {
passwordMismatch.classList.remove('hidden');
} else {
passwordMismatch.classList.add('hidden');
}
});
}
// 修改密码表单提交
if (changePasswordForm) {
changePasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
// 验证密码匹配
if (newPassword.value !== confirmPassword.value) {
passwordMismatch.classList.remove('hidden');
return;
}
const formData = new FormData(changePasswordForm);
const data = {
currentPassword: formData.get('currentPassword'),
newPassword: formData.get('newPassword')
};
try {
const response = await fetch('/api/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok && result.status === 'success') {
// 密码修改成功
alert('密码修改成功');
closeModal();
} else {
// 密码修改失败
alert(result.error || '密码修改失败');
}
} catch (error) {
console.error('修改密码失败:', error);
alert('修改密码失败,请稍后重试');
}
});
}
// 注销功能
if (logoutBtn) {
logoutBtn.addEventListener('click', async () => {
try {
await fetch('/api/logout', {
method: 'POST'
});
// 重定向到登录页面
window.location.href = '/login';
} catch (error) {
console.error('注销失败:', error);
alert('注销失败,请稍后重试');
}
});
}
}
// 初始化函数
function init() {
// 设置导航
setupNavigation();
// 设置账户功能
setupAccountFeatures();
// 初始化页面
initPageByHash();
// 添加hashchange事件监听处理浏览器前进/后退按钮
window.addEventListener('hashchange', initPageByHash);
// 定期更新系统状态
setInterval(updateSystemStatus, 5000);
}
// 页面加载完成后执行初始化
window.addEventListener('DOMContentLoaded', init);