更新Swagger API文档

This commit is contained in:
Alex Yang
2025-11-29 19:30:47 +08:00
parent aea162a616
commit ca876a8951
39 changed files with 16612 additions and 2003 deletions

1767
api/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"dns": {
"port": 53,
"upstreamDNS": [
"223.5.5.5:53",
"223.6.6.6:53"
],
"timeout": 5000,
"statsFile": "./data/stats.json",
"saveInterval": 300
},
"http": {
"port": 8080,
"host": "0.0.0.0",
"enableAPI": true
},
"shield": {
"localRulesFile": "data/rules.txt",
"remoteRules": [
"https://example.com/rules.txt",
"https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt"
],
"updateInterval": 60,
"hostsFile": "data/hosts.txt",
"blockMethod": "NXDOMAIN",
"customBlockIP": "",
"statsFile": "./data/shield_stats.json",
"statsSaveInterval": 60,
"remoteRulesCacheDir": "./data/remote_rules"
},
"log": {
"file": "dns-server.log",
"level": "debug",
"maxSize": 100,
"maxBackups": 10,
"maxAge": 30
}
}

1078
css/style.css Normal file

File diff suppressed because it is too large Load Diff

9
css/vendor/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -8,7 +8,9 @@ import (
"net"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
@@ -33,6 +35,8 @@ type StatsData struct {
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
HourlyStats map[string]int64 `json:"hourlyStats"`
DailyStats map[string]int64 `json:"dailyStats"`
MonthlyStats map[string]int64 `json:"monthlyStats"`
LastSaved time.Time `json:"lastSaved"`
}
@@ -53,17 +57,27 @@ type Server struct {
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
hourlyStatsMutex sync.RWMutex
hourlyStats map[string]int64 // 按小时统计屏蔽数量
dailyStatsMutex sync.RWMutex
dailyStats map[string]int64 // 按天统计屏蔽数量
monthlyStatsMutex sync.RWMutex
monthlyStats map[string]int64 // 按月统计屏蔽数量
saveTicker *time.Ticker // 用于定时保存数据
startTime time.Time // 服务器启动时间
saveDone chan struct{} // 用于通知保存协程停止
}
// Stats DNS服务器统计信息
type Stats struct {
Queries int64
Blocked int64
Allowed int64
Errors int64
LastQuery time.Time
Queries int64
Blocked int64
Allowed int64
Errors int64
LastQuery time.Time
AvgResponseTime float64 // 平均响应时间(ms)
TotalResponseTime int64 // 总响应时间
QueryTypes map[string]int64 // 查询类型统计
SourceIPs map[string]bool // 活跃来源IP
CpuUsage float64 // CPU使用率(%)
}
// NewServer 创建DNS服务器实例
@@ -77,17 +91,25 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
Net: "udp",
Timeout: time.Duration(config.Timeout) * time.Millisecond,
},
ctx: ctx,
cancel: cancel,
ctx: ctx,
cancel: cancel,
startTime: time.Now(), // 记录服务器启动时间
stats: &Stats{
Queries: 0,
Blocked: 0,
Allowed: 0,
Errors: 0,
Queries: 0,
Blocked: 0,
Allowed: 0,
Errors: 0,
AvgResponseTime: 0,
TotalResponseTime: 0,
QueryTypes: make(map[string]int64),
SourceIPs: make(map[string]bool),
CpuUsage: 0,
},
blockedDomains: make(map[string]*BlockedDomain),
resolvedDomains: make(map[string]*BlockedDomain),
hourlyStats: make(map[string]int64),
dailyStats: make(map[string]int64),
monthlyStats: make(map[string]int64),
saveDone: make(chan struct{}),
}
@@ -113,6 +135,9 @@ func (s *Server) Start() error {
Handler: dns.HandlerFunc(s.handleDNSRequest),
}
// 启动CPU使用率监控
go s.startCpuUsageMonitor()
// 启动UDP服务
go func() {
logger.Info(fmt.Sprintf("DNS UDP服务器启动监听端口: %d", s.config.Port))
@@ -154,9 +179,20 @@ func (s *Server) Stop() {
// handleDNSRequest 处理DNS请求
func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
startTime := time.Now()
// 获取来源IP
sourceIP := w.RemoteAddr().String()
// 提取IP地址部分去掉端口
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
sourceIP = sourceIP[:idx]
}
// 更新来源IP统计
s.updateStats(func(stats *Stats) {
stats.Queries++
stats.LastQuery = time.Now()
stats.SourceIPs[sourceIP] = true
})
// 只处理递归查询
@@ -166,35 +202,75 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
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
if len(r.Question) > 0 {
domain = r.Question[0].Name
// 移除末尾的点
if len(domain) > 0 && domain[len(domain)-1] == '.' {
domain = domain[:len(domain)-1]
}
// 获取查询类型
queryType = dns.TypeToString[r.Question[0].Qtype]
// 更新查询类型统计
s.updateStats(func(stats *Stats) {
stats.QueryTypes[queryType]++
})
}
logger.Debug("接收到DNS查询", "domain", domain, "type", r.Question[0].Qtype, "client", w.RemoteAddr())
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()
s.updateStats(func(stats *Stats) {
stats.TotalResponseTime += responseTime
if stats.Queries > 0 {
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
}
})
return
}
// 检查是否被屏蔽
if s.shieldManager.IsBlocked(domain) {
s.handleBlockedResponse(w, r, domain)
// 计算响应时间
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
}
// 转发到上游DNS服务器
s.forwardDNSRequest(w, r, domain)
// 计算响应时间
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)
}
})
}
// handleHostsResponse 处理hosts文件匹配的响应
@@ -354,11 +430,26 @@ func (s *Server) updateBlockedDomainStats(domain string) {
}
}
// 更新统计数据
now := time.Now()
// 更新小时统计
hourKey := time.Now().Format("2006-01-02-15")
hourKey := now.Format("2006-01-02-15")
s.hourlyStatsMutex.Lock()
s.hourlyStats[hourKey]++
s.hourlyStatsMutex.Unlock()
// 更新每日统计
dayKey := now.Format("2006-01-02")
s.dailyStatsMutex.Lock()
s.dailyStats[dayKey]++
s.dailyStatsMutex.Unlock()
// 更新每月统计
monthKey := now.Format("2006-01")
s.monthlyStatsMutex.Lock()
s.monthlyStats[monthKey]++
s.monthlyStatsMutex.Unlock()
}
// updateResolvedDomainStats 更新解析域名统计
@@ -385,18 +476,40 @@ func (s *Server) updateStats(update func(*Stats)) {
update(s.stats)
}
// GetStartTime 获取服务器启动时间
func (s *Server) GetStartTime() time.Time {
return s.startTime
}
// GetStats 获取DNS服务器统计信息
func (s *Server) GetStats() *Stats {
s.statsMutex.Lock()
defer s.statsMutex.Unlock()
// 复制查询类型统计
queryTypesCopy := make(map[string]int64)
for k, v := range s.stats.QueryTypes {
queryTypesCopy[k] = v
}
// 复制来源IP统计
sourceIPsCopy := make(map[string]bool)
for ip := range s.stats.SourceIPs {
sourceIPsCopy[ip] = true
}
// 返回统计信息的副本
return &Stats{
Queries: s.stats.Queries,
Blocked: s.stats.Blocked,
Allowed: s.stats.Allowed,
Errors: s.stats.Errors,
LastQuery: s.stats.LastQuery,
Queries: s.stats.Queries,
Blocked: s.stats.Blocked,
Allowed: s.stats.Allowed,
Errors: s.stats.Errors,
LastQuery: s.stats.LastQuery,
AvgResponseTime: s.stats.AvgResponseTime,
TotalResponseTime: s.stats.TotalResponseTime,
QueryTypes: queryTypesCopy,
SourceIPs: sourceIPsCopy,
CpuUsage: s.stats.CpuUsage,
}
}
@@ -469,7 +582,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
return domains
}
// GetHourlyStats 获取24小时屏蔽统计
// GetHourlyStats 获取每小时统计数据
func (s *Server) GetHourlyStats() map[string]int64 {
s.hourlyStatsMutex.RLock()
defer s.hourlyStatsMutex.RUnlock()
@@ -482,6 +595,32 @@ func (s *Server) GetHourlyStats() map[string]int64 {
return result
}
// GetDailyStats 获取每日统计数据
func (s *Server) GetDailyStats() map[string]int64 {
s.dailyStatsMutex.RLock()
defer s.dailyStatsMutex.RUnlock()
// 返回副本
result := make(map[string]int64)
for k, v := range s.dailyStats {
result[k] = v
}
return result
}
// GetMonthlyStats 获取每月统计数据
func (s *Server) GetMonthlyStats() map[string]int64 {
s.monthlyStatsMutex.RLock()
defer s.monthlyStatsMutex.RUnlock()
// 返回副本
result := make(map[string]int64)
for k, v := range s.monthlyStats {
result[k] = v
}
return result
}
// loadStatsData 从文件加载统计数据
func (s *Server) loadStatsData() {
if s.config.StatsFile == "" {
@@ -528,6 +667,18 @@ func (s *Server) loadStatsData() {
s.hourlyStats = statsData.HourlyStats
}
s.hourlyStatsMutex.Unlock()
s.dailyStatsMutex.Lock()
if statsData.DailyStats != nil {
s.dailyStats = statsData.DailyStats
}
s.dailyStatsMutex.Unlock()
s.monthlyStatsMutex.Lock()
if statsData.MonthlyStats != nil {
s.monthlyStats = statsData.MonthlyStats
}
s.monthlyStatsMutex.Unlock()
logger.Info("统计数据加载成功")
}
@@ -573,6 +724,20 @@ func (s *Server) saveStatsData() {
statsData.HourlyStats[k] = v
}
s.hourlyStatsMutex.RUnlock()
s.dailyStatsMutex.RLock()
statsData.DailyStats = make(map[string]int64)
for k, v := range s.dailyStats {
statsData.DailyStats[k] = v
}
s.dailyStatsMutex.RUnlock()
s.monthlyStatsMutex.RLock()
statsData.MonthlyStats = make(map[string]int64)
for k, v := range s.monthlyStats {
statsData.MonthlyStats[k] = v
}
s.monthlyStatsMutex.RUnlock()
// 序列化数据
jsonData, err := json.MarshalIndent(statsData, "", " ")
@@ -591,6 +756,77 @@ func (s *Server) saveStatsData() {
logger.Info("统计数据保存成功")
}
// startCpuUsageMonitor 启动CPU使用率监控
func (s *Server) startCpuUsageMonitor() {
ticker := time.NewTicker(time.Second * 5) // 每5秒更新一次CPU使用率
defer ticker.Stop()
// 初始化
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// 存储上一次的CPU时间统计
var prevIdle, prevTotal uint64
for {
select {
case <-ticker.C:
// 获取真实的系统级CPU使用率
cpuUsage, err := getSystemCpuUsage(&prevIdle, &prevTotal)
if err != nil {
// 如果获取失败,使用默认值
cpuUsage = 0.0
logger.Error("获取系统CPU使用率失败", "error", err)
}
s.updateStats(func(stats *Stats) {
stats.CpuUsage = cpuUsage
})
case <-s.ctx.Done():
return
}
}
}
// getSystemCpuUsage 获取系统CPU使用率
func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
// 读取/proc/stat文件获取CPU统计信息
file, err := os.Open("/proc/stat")
if err != nil {
return 0, err
}
defer file.Close()
var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIowait, cpuIrq, cpuSoftirq, cpuSteal uint64
_, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
&cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIowait, &cpuIrq, &cpuSoftirq, &cpuSteal)
if err != nil {
return 0, err
}
// 计算总的CPU时间
total := cpuUser + cpuNice + cpuSystem + cpuIdle + cpuIowait + cpuIrq + cpuSoftirq + cpuSteal
idle := cpuIdle + cpuIowait
// 第一次调用时,只初始化值,不计算使用率
if *prevTotal == 0 || *prevIdle == 0 {
*prevIdle = idle
*prevTotal = total
return 0, nil
}
// 计算CPU使用率
idleDelta := idle - *prevIdle
totalDelta := total - *prevTotal
utilization := float64(totalDelta-idleDelta) / float64(totalDelta) * 100
// 更新上一次的值
*prevIdle = idle
*prevTotal = total
return utilization, nil
}
// startAutoSave 启动自动保存功能
func (s *Server) startAutoSave() {
if s.config.StatsFile == "" || s.config.SaveInterval <= 0 {

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.23.0
toolchain go1.24.10
require (
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.68
github.com/sirupsen/logrus v1.9.3
)

2
go.sum
View File

@@ -3,6 +3,8 @@ 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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@@ -5,9 +5,12 @@ import (
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"dns-server/config"
"dns-server/dns"
"dns-server/logger"
@@ -21,16 +24,37 @@ type Server struct {
dnsServer *dns.Server
shieldManager *shield.ShieldManager
server *http.Server
// WebSocket相关字段
upgrader websocket.Upgrader
clients map[*websocket.Conn]bool
clientsMutex sync.Mutex
broadcastChan chan []byte
}
// NewServer 创建HTTP服务器实例
func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager *shield.ShieldManager) *Server {
return &Server{
server := &Server{
globalConfig: globalConfig,
config: &globalConfig.HTTP,
dnsServer: dnsServer,
shieldManager: shieldManager,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 允许所有CORS请求
CheckOrigin: func(r *http.Request) bool {
return true
},
},
clients: make(map[*websocket.Conn]bool),
broadcastChan: make(chan []byte, 100),
}
// 启动广播协程
go server.startBroadcastLoop()
return server
}
// Start 启动HTTP服务器
@@ -51,6 +75,11 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
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)
// WebSocket端点
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
}
// 静态文件服务(可后续添加前端界面)
@@ -85,16 +114,218 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
dnsStats := s.dnsServer.GetStats()
shieldStats := s.shieldManager.GetStats()
// 获取最常用查询类型(如果有)
topQueryType := "-"
maxCount := int64(0)
if len(dnsStats.QueryTypes) > 0 {
for queryType, count := range dnsStats.QueryTypes {
if count > maxCount {
maxCount = count
topQueryType = queryType
}
}
}
// 获取活跃来源IP数量
activeIPCount := len(dnsStats.SourceIPs)
// 格式化平均响应时间为两位小数
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
// 构建响应数据,确保所有字段都反映服务器的真实状态
stats := map[string]interface{}{
"dns": dnsStats,
"shield": shieldStats,
"time": time.Now(),
"dns": map[string]interface{}{
"Queries": dnsStats.Queries,
"Blocked": dnsStats.Blocked,
"Allowed": dnsStats.Allowed,
"Errors": dnsStats.Errors,
"LastQuery": dnsStats.LastQuery,
"AvgResponseTime": formattedResponseTime,
"TotalResponseTime": dnsStats.TotalResponseTime,
"QueryTypes": dnsStats.QueryTypes,
"SourceIPs": dnsStats.SourceIPs,
"CpuUsage": dnsStats.CpuUsage,
},
"shield": shieldStats,
"topQueryType": topQueryType,
"activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime,
"cpuUsage": dnsStats.CpuUsage,
"time": time.Now(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stats)
}
// WebSocket相关方法
// handleWebSocketStats 处理WebSocket连接用于实时推送统计数据
func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
// 升级HTTP连接为WebSocket
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error(fmt.Sprintf("WebSocket升级失败: %v", err))
return
}
defer conn.Close()
// 将新客户端添加到客户端列表
s.clientsMutex.Lock()
s.clients[conn] = true
clientCount := len(s.clients)
s.clientsMutex.Unlock()
logger.Info(fmt.Sprintf("新WebSocket客户端连接当前连接数: %d", clientCount))
// 发送初始数据
if err := s.sendInitialStats(conn); err != nil {
logger.Error(fmt.Sprintf("发送初始数据失败: %v", err))
return
}
// 定期发送更新数据
ticker := time.NewTicker(500 * time.Millisecond) // 每500ms检查一次数据变化
defer ticker.Stop()
// 最后一次发送的数据快照,用于检测变化
var lastStats map[string]interface{}
// 保持连接并定期发送数据
for {
select {
case <-ticker.C:
// 获取最新统计数据
currentStats := s.buildStatsData()
// 检查数据是否有变化
if !s.areStatsEqual(lastStats, currentStats) {
// 数据有变化,发送更新
data, err := json.Marshal(map[string]interface{}{
"type": "stats_update",
"data": currentStats,
"time": time.Now(),
})
if err != nil {
logger.Error(fmt.Sprintf("序列化统计数据失败: %v", err))
continue
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
logger.Error(fmt.Sprintf("发送WebSocket消息失败: %v", err))
return
}
// 更新最后发送的数据
lastStats = currentStats
}
case <-r.Context().Done():
// 客户端断开连接
s.clientsMutex.Lock()
delete(s.clients, conn)
clientCount := len(s.clients)
s.clientsMutex.Unlock()
logger.Info(fmt.Sprintf("WebSocket客户端断开连接当前连接数: %d", clientCount))
return
}
}
}
// sendInitialStats 发送初始统计数据
func (s *Server) sendInitialStats(conn *websocket.Conn) error {
stats := s.buildStatsData()
data, err := json.Marshal(map[string]interface{}{
"type": "initial_data",
"data": stats,
"time": time.Now(),
})
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, data)
}
// buildStatsData 构建统计数据
func (s *Server) buildStatsData() map[string]interface{} {
dnsStats := s.dnsServer.GetStats()
shieldStats := s.shieldManager.GetStats()
// 获取最常用查询类型
topQueryType := "-"
maxCount := int64(0)
if len(dnsStats.QueryTypes) > 0 {
for queryType, count := range dnsStats.QueryTypes {
if count > maxCount {
maxCount = count
topQueryType = queryType
}
}
}
// 获取活跃来源IP数量
activeIPCount := len(dnsStats.SourceIPs)
// 格式化平均响应时间
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
return map[string]interface{}{
"dns": map[string]interface{}{
"Queries": dnsStats.Queries,
"Blocked": dnsStats.Blocked,
"Allowed": dnsStats.Allowed,
"Errors": dnsStats.Errors,
"LastQuery": dnsStats.LastQuery,
"AvgResponseTime": formattedResponseTime,
"TotalResponseTime": dnsStats.TotalResponseTime,
"QueryTypes": dnsStats.QueryTypes,
"SourceIPs": dnsStats.SourceIPs,
"CpuUsage": dnsStats.CpuUsage,
},
"shield": shieldStats,
"topQueryType": topQueryType,
"activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime,
"cpuUsage": dnsStats.CpuUsage,
}
}
// areStatsEqual 检查两次统计数据是否相等(用于检测变化)
func (s *Server) areStatsEqual(stats1, stats2 map[string]interface{}) bool {
if stats1 == nil || stats2 == nil {
return false
}
// 只比较关键数值,避免频繁更新
if dns1, ok1 := stats1["dns"].(map[string]interface{}); ok1 {
if dns2, ok2 := stats2["dns"].(map[string]interface{}); ok2 {
// 检查主要计数器
if dns1["Queries"] != dns2["Queries"] ||
dns1["Blocked"] != dns2["Blocked"] ||
dns1["Allowed"] != dns2["Allowed"] ||
dns1["Errors"] != dns2["Errors"] {
return false
}
}
}
return true
}
// startBroadcastLoop 启动广播循环
func (s *Server) startBroadcastLoop() {
for message := range s.broadcastChan {
s.clientsMutex.Lock()
for client := range s.clients {
if err := client.WriteMessage(websocket.TextMessage, message); err != nil {
logger.Error(fmt.Sprintf("广播消息失败: %v", err))
client.Close()
delete(s.clients, client)
}
}
s.clientsMutex.Unlock()
}
}
// handleTopBlockedDomains 处理TOP屏蔽域名请求
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
@@ -191,35 +422,117 @@ func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(result)
}
// handleDailyStats 处理每日统计数据请求
func (s *Server) handleDailyStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取每日统计数据
dailyStats := s.dnsServer.GetDailyStats()
// 生成过去7天的时间标签
labels := make([]string, 7)
data := make([]int64, 7)
now := time.Now()
for i := 6; i >= 0; i-- {
t := now.AddDate(0, 0, -i)
key := t.Format("2006-01-02")
labels[6-i] = t.Format("01-02")
data[6-i] = dailyStats[key]
}
result := map[string]interface{}{
"labels": labels,
"data": data,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// handleMonthlyStats 处理每月统计数据请求
func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取每日统计数据用于30天视图
dailyStats := s.dnsServer.GetDailyStats()
// 生成过去30天的时间标签
labels := make([]string, 30)
data := make([]int64, 30)
now := time.Now()
for i := 29; i >= 0; i-- {
t := now.AddDate(0, 0, -i)
key := t.Format("2006-01-02")
labels[29-i] = t.Format("01-02")
data[29-i] = dailyStats[key]
}
result := map[string]interface{}{
"labels": labels,
"data": data,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// handleQueryTypeStats 处理查询类型统计请求
func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取DNS统计数据
dnsStats := s.dnsServer.GetStats()
// 转换为前端需要的格式
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
for queryType, count := range dnsStats.QueryTypes {
result = append(result, map[string]interface{}{
"type": queryType,
"count": count,
})
}
// 按计数降序排序
sort.Slice(result, func(i, j int) bool {
return result[i]["count"].(int64) > result[j]["count"].(int64)
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// handleShield 处理屏蔽规则管理请求
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 返回屏蔽规则的基本配置信息
// 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
switch r.Method {
case http.MethodGet:
// 获取规则统计信息
stats := s.shieldManager.GetStats()
shieldInfo := map[string]interface{}{
"updateInterval": s.globalConfig.Shield.UpdateInterval,
"blockMethod": s.globalConfig.Shield.BlockMethod,
"blacklistCount": len(s.globalConfig.Shield.Blacklists),
"updateInterval": s.globalConfig.Shield.UpdateInterval,
"blockMethod": s.globalConfig.Shield.BlockMethod,
"blacklistCount": len(s.globalConfig.Shield.Blacklists),
"domainRulesCount": stats["domainRules"],
"domainExceptionsCount": stats["domainExceptions"],
"regexRulesCount": stats["regexRules"],
"regexExceptionsCount": stats["regexExceptions"],
"hostsRulesCount": stats["hostsRules"],
}
json.NewEncoder(w).Encode(shieldInfo)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// 处理远程黑名单管理子路由
if strings.HasPrefix(r.URL.Path, "/shield/blacklists") {
s.handleShieldBlacklists(w, r)
return
}
switch r.Method {
case http.MethodGet:
// 获取完整规则列表
rules := s.shieldManager.GetRules()
json.NewEncoder(w).Encode(rules)
case http.MethodPost:
// 添加屏蔽规则
var req struct {
@@ -237,7 +550,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
case http.MethodDelete:
// 删除屏蔽规则
var req struct {
@@ -255,7 +568,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
case http.MethodPut:
// 重新加载规则
if err := s.shieldManager.LoadRules(); err != nil {
@@ -263,9 +576,10 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"})
return
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
@@ -506,12 +820,25 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
}
stats := s.dnsServer.GetStats()
// 使用服务器的实际启动时间计算准确的运行时间
serverStartTime := s.dnsServer.GetStartTime()
uptime := time.Since(serverStartTime)
// 构建包含所有真实服务器统计数据的响应
status := map[string]interface{}{
"status": "running",
"queries": stats.Queries,
"lastQuery": stats.LastQuery,
"uptime": time.Since(stats.LastQuery),
"timestamp": time.Now(),
"status": "running",
"queries": stats.Queries,
"blocked": stats.Blocked,
"allowed": stats.Allowed,
"errors": stats.Errors,
"lastQuery": stats.LastQuery,
"avgResponseTime": stats.AvgResponseTime,
"activeIPs": len(stats.SourceIPs),
"startTime": serverStartTime,
"uptime": uptime,
"cpuUsage": stats.CpuUsage,
"timestamp": time.Now(),
}
w.Header().Set("Content-Type", "application/json")

2633
index.html

File diff suppressed because it is too large Load Diff

298
js/api.js Normal file
View File

@@ -0,0 +1,298 @@
// 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 Normal file
View File

@@ -0,0 +1,317 @@
// 全局配置
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';
}
});
});
}

53
js/colors.config.js Normal file
View File

@@ -0,0 +1,53 @@
// 颜色配置文件 - 集中管理所有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;
}

284
js/config.js Normal file
View File

@@ -0,0 +1,284 @@
// 配置管理页面功能实现
// 工具函数安全获取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();
}

3012
js/dashboard.js Normal file

File diff suppressed because it is too large Load Diff

202
js/hosts.js Normal file
View File

@@ -0,0 +1,202 @@
// 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();
}

173
js/main.js Normal file
View File

@@ -0,0 +1,173 @@
// 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);

255
js/modules/blacklists.js Normal file
View File

@@ -0,0 +1,255 @@
// 初始化远程黑名单面板
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;
}
}

125
js/modules/config.js Normal file
View File

@@ -0,0 +1,125 @@
// 初始化配置管理面板
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);
}

1220
js/modules/dashboard.js Normal file

File diff suppressed because it is too large Load Diff

308
js/modules/hosts.js Normal file
View File

@@ -0,0 +1,308 @@
// 初始化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
});
}

294
js/modules/query.js Normal file
View File

@@ -0,0 +1,294 @@
// 初始化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');
}
}
});
}

422
js/modules/rules.js Normal file
View File

@@ -0,0 +1,422 @@
// 屏蔽规则管理模块
// 全局变量
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
});
}

301
js/query.js Normal file
View File

@@ -0,0 +1,301 @@
// 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();
}
});
});

305
js/server-status.js Normal file
View File

@@ -0,0 +1,305 @@
// 服务器状态组件 - 显示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
};

1302
js/shield.js Normal file

File diff suppressed because it is too large Load Diff

1
js/vendor/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

69
main.go
View File

@@ -16,12 +16,26 @@ import (
)
func main() {
// 解析命令行参数
configPath := flag.String("config", "config.json", "配置文件路径")
// 命令行参数解析
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)
}
// 初始化配置
cfg, err := config.LoadConfig(*configPath)
var cfg *config.Config
var err error
cfg, err = config.LoadConfig(configFile)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
@@ -61,14 +75,51 @@ func main() {
logger.Info(fmt.Sprintf("DNS服务器已启动监听端口: %d", cfg.DNS.Port))
logger.Info(fmt.Sprintf("HTTP控制台已启动监听端口: %d", cfg.HTTP.Port))
// 等待退出信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// 监听信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
logger.Info("正在关闭服务...")
// 清理资源
log.Println("正在关闭服务...")
dnsServer.Stop()
httpServer.Stop()
shieldManager.StopAutoUpdate()
logger.Info("所有服务已关闭")
// 守护进程模式下不需要删除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
}

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "dns-server-console",
"version": "1.0.0",
"description": "DNS服务器Web控制台",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"tailwindcss": "^3.3.3",
"font-awesome": "^4.7.0",
"chart.js": "^4.4.8"
},
"devDependencies": {},
"keywords": ["dns", "server", "console", "web"],
"author": "",
"license": "ISC"
}

View File

@@ -970,22 +970,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
// loadStatsData 从文件加载计数数据
func (m *ShieldManager) loadStatsData() {
if m.config.StatsFile == "" {
logger.Info("Shield统计文件路径未配置跳过加载")
return
}
// 检查文件是否存在
data, err := ioutil.ReadFile(m.config.StatsFile)
// 获取绝对路径以避免工作目录问题
statsFilePath, err := filepath.Abs(m.config.StatsFile)
if err != nil {
if !os.IsNotExist(err) {
logger.Error("读取Shield计数数据文件失败", "error", err)
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
return
}
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
// 检查文件是否存在
fileInfo, err := os.Stat(statsFilePath)
if err != nil {
if os.IsNotExist(err) {
logger.Info("Shield统计文件不存在将创建新文件", "file", statsFilePath)
// 初始化空的计数数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试立即保存一个有效的空文件
m.saveStatsData()
} else {
logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err)
}
return
}
// 检查文件大小
if fileInfo.Size() == 0 {
logger.Warn("Shield统计文件为空将重新初始化", "file", statsFilePath)
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
m.saveStatsData()
return
}
// 读取文件内容
data, err := ioutil.ReadFile(statsFilePath)
if err != nil {
logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err)
return
}
// 检查数据长度
if len(data) == 0 {
logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath)
return
}
// 尝试解析JSON
var statsData ShieldStatsData
err = json.Unmarshal(data, &statsData)
if err != nil {
logger.Error("解析Shield计数数据失败", "error", err)
// 记录更详细的错误信息包括数据前50个字符
dataSample := string(data)
if len(dataSample) > 50 {
dataSample = dataSample[:50] + "..."
}
logger.Error("解析Shield计数数据失败",
"file", statsFilePath,
"error", err,
"data_length", len(data),
"data_sample", dataSample)
// 重置为默认空数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
// 尝试保存一个有效的空文件
m.saveStatsData()
return
}
@@ -993,26 +1054,38 @@ func (m *ShieldManager) loadStatsData() {
m.rulesMutex.Lock()
if statsData.BlockedDomainsCount != nil {
m.blockedDomainsCount = statsData.BlockedDomainsCount
} else {
m.blockedDomainsCount = make(map[string]int)
}
if statsData.ResolvedDomainsCount != nil {
m.resolvedDomainsCount = statsData.ResolvedDomainsCount
} else {
m.resolvedDomainsCount = make(map[string]int)
}
m.rulesMutex.Unlock()
logger.Info("Shield计数数据加载成功")
logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount))
}
// saveStatsData 保存计数数据到文件
func (m *ShieldManager) saveStatsData() {
if m.config.StatsFile == "" {
logger.Debug("Shield统计文件路径未配置跳过保存")
return
}
// 获取绝对路径以避免工作目录问题
statsFilePath, err := filepath.Abs(m.config.StatsFile)
if err != nil {
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
return
}
// 创建数据目录
statsDir := filepath.Dir(m.config.StatsFile)
err := os.MkdirAll(statsDir, 0755)
statsDir := filepath.Dir(statsFilePath)
err = os.MkdirAll(statsDir, 0755)
if err != nil {
logger.Error("创建Shield统计数据目录失败", "error", err)
logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err)
return
}
@@ -1040,14 +1113,24 @@ func (m *ShieldManager) saveStatsData() {
return
}
// 写入文件
err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644)
// 使用临时文件先写入,然后重命名,避免文件损坏
tempFilePath := statsFilePath + ".tmp"
err = ioutil.WriteFile(tempFilePath, jsonData, 0644)
if err != nil {
logger.Error("保存Shield计数数据到文件失败", "error", err)
logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err)
return
}
logger.Info("Shield计数数据保存成功")
// 原子操作重命名文件
err = os.Rename(tempFilePath, statsFilePath)
if err != nil {
logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err)
// 尝试清理临时文件
os.Remove(tempFilePath)
return
}
logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount))
}
// startAutoSaveStats 启动计数数据自动保存功能

View File

@@ -193,6 +193,40 @@ header p {
transition: padding-left 0.3s ease;
}
/* Tooltip趋势信息颜色类 - 替代内联style */
.tooltip-trend {
font-weight: 500;
}
/* 注意这些颜色值与colors.config.js中的COLOR_CONFIG.colorClassMap保持同步 */
.tooltip-trend.blue {
color: #1890ff;
}
.tooltip-trend.green {
color: #52c41a;
}
.tooltip-trend.orange {
color: #fa8c16;
}
.tooltip-trend.red {
color: #f5222d;
}
.tooltip-trend.purple {
color: #722ed1;
}
.tooltip-trend.cyan {
color: #13c2c2;
}
.tooltip-trend.teal {
color: #36cfc9;
}
/* 平板设备适配 - 侧边栏折叠时调整内容区域 */
@media (max-width: 992px) {
.content {

View File

@@ -45,11 +45,129 @@
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.sidebar-item-active {
@apply bg-primary/10 text-primary border-r-4 border-primary;
background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
}
}
</style>
</head>
<!-- 数字光晕效果样式 -->
<style>
/* 数字光晕效果基础样式 */
.number-glow {
animation: glow-pulse 2s ease-in-out;
}
/* 服务器状态组件光晕效果 */
.glow-effect {
animation: pulse 2s ease-in-out;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
}
}
/* 服务器状态组件样式优化 */
.server-status-widget {
min-width: 170px;
transition: all 0.3s ease;
}
.server-status-widget:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 蓝色光晕效果 */
.number-glow-blue {
animation: glow-blue 2s ease-in-out;
}
/* 红色光晕效果 */
.number-glow-red {
animation: glow-red 2s ease-in-out;
}
/* 绿色光晕效果 */
.number-glow-green {
animation: glow-green 2s ease-in-out;
}
/* 黄色光晕效果 */
.number-glow-yellow {
animation: glow-yellow 2s ease-in-out;
}
/* 光晕动画定义 */
@keyframes glow-pulse {
0% {
text-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(0, 0, 0, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
}
@keyframes glow-blue {
0% {
text-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(59, 130, 246, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
}
}
@keyframes glow-red {
0% {
text-shadow: 0 0 5px rgba(239, 68, 68, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(239, 68, 68, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(239, 68, 68, 0.3);
}
}
@keyframes glow-green {
0% {
text-shadow: 0 0 5px rgba(16, 185, 129, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(16, 185, 129, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(16, 185, 129, 0.3);
}
}
@keyframes glow-yellow {
0% {
text-shadow: 0 0 5px rgba(250, 204, 21, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(250, 204, 21, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(250, 204, 21, 0.3);
}
}
</style>
</head>
<body class="bg-gray-50 text-dark font-sans">
<div class="flex h-screen overflow-hidden">
<!-- 侧边栏 -->
@@ -121,6 +239,56 @@
</div>
<div class="flex items-center space-x-4">
<!-- 服务器状态组件 -->
<div class="relative bg-white rounded-lg shadow-md px-3 py-2 flex items-center space-x-2 server-status-widget" id="server-status-widget">
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">CPU</span>
<span id="server-cpu-value" class="ml-2 text-sm font-semibold">0%</span>
</div>
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
<div id="server-cpu-bar" class="h-full bg-warning rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">查询</span>
<span id="server-queries-value" class="ml-2 text-sm font-semibold">0</span>
</div>
<div class="w-16 h-1 bg-gray-100 rounded-full mt-1">
<div id="server-queries-bar" class="h-full bg-primary rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- 额外指标区域 - 初始隐藏,只在非首页显示 -->
<div id="server-additional-stats" class="hidden flex items-center">
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">总量</span>
<span id="server-total-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">屏蔽</span>
<span id="server-blocked-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col">
<div class="flex items-center">
<span class="text-xs font-medium text-gray-500">正常</span>
<span id="server-allowed-queries" class="ml-2 text-sm font-semibold">0</span>
</div>
</div>
</div>
<div class="absolute top-1 right-1">
<span id="server-status-indicator" class="inline-block w-2 h-2 bg-success rounded-full"></span>
</div>
</div>
<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>
@@ -138,96 +306,263 @@
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- 查询总量卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<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 class="bg-blue-50 rounded-lg p-6 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="total-queries">0</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="queries-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="query-chart"></canvas>
</div>
</div>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="total-queries">0</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="queries-percent">0%</span>
</span>
</div>
</div>
<!-- 屏蔽数量卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<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 class="bg-red-50 rounded-lg p-6 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="blocked-queries">0</p>
<span class="text-danger text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="blocked-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="blocked-chart"></canvas>
</div>
</div>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="blocked-queries">0</p>
<span class="text-danger text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="blocked-percent">0%</span>
</span>
</div>
</div>
<!-- 正常解析卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">正常解析</h3>
<div class="p-2 rounded-full bg-success/10 text-success">
<i class="fa fa-check"></i>
<div class="bg-green-50 rounded-lg p-6 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">正常解析</h3>
<div class="p-2 rounded-full bg-success/10 text-success">
<i class="fa fa-check"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="allowed-queries">0</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="allowed-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="allowed-chart"></canvas>
</div>
</div>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="allowed-queries">0</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="allowed-percent">0%</span>
</span>
</div>
</div>
<!-- 错误数量卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">错误数量</h3>
<div class="p-2 rounded-full bg-warning/10 text-warning">
<i class="fa fa-exclamation-triangle"></i>
<div class="bg-yellow-50 rounded-lg p-6 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning 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-warning/10 text-warning">
<i class="fa fa-exclamation-triangle"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="error-queries">0</p>
<span class="text-warning text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="error-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="error-chart"></canvas>
</div>
</div>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="error-queries">0</p>
<span class="text-warning text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="error-percent">0%</span>
</span>
</div>
<!-- 平均响应时间卡片 -->
<div class="bg-white rounded-lg p-6 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="avg-response-time">0ms</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-down mr-1"></i>
<span id="response-time-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="response-time-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- 最常用查询类型卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-secondary 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-secondary/10 text-secondary">
<i class="fa fa-database"></i>
</div>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="top-query-type">A</p>
<span class="text-primary text-sm flex items-center">
<i class="fa fa-circle text-xs mr-1"></i>
<span id="query-type-percentage">0%</span>
</span>
</div>
</div>
</div>
<!-- 活跃来源IP数卡片 -->
<div class="bg-white rounded-lg p-6 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="active-ips">0</p>
<span class="text-success text-sm flex items-center">
<i class="fa fa-arrow-up mr-1"></i>
<span id="active-ips-percent">0%</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="ips-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- CPU使用率卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">CPU使用率</h3>
<div class="p-2 rounded-full bg-warning/10 text-warning">
<i class="fa fa-microchip"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="cpu-usage">0%</p>
<span class="text-warning text-sm flex items-center">
<i class="fa fa-bolt mr-1"></i>
<span id="cpu-status">正常</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="cpu-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 图表和数据表格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 查询趋势图表 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-2">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">查询趋势</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 text-sm rounded-full bg-primary/10 text-primary">24小时</button>
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">7天</button>
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">30天</button>
</div>
</div>
<div class="h-80">
<canvas id="query-trend-chart"></canvas>
<!-- 三个图表在同一行显示 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
<div class="h-64 flex items-center justify-center">
<canvas id="ratio-chart"></canvas>
</div>
</div>
<!-- 解析与屏蔽比例 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
<div class="h-80 flex items-center justify-center">
<canvas id="ratio-chart"></canvas>
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<h3 class="text-lg font-semibold mb-6">解析类型统计</h3>
<div class="h-64 flex items-center justify-center">
<canvas id="query-type-chart"></canvas>
</div>
</div>
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
<!-- 展开按钮 -->
<button id="expand-chart-btn" class="p-2 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors" title="展开详细图表">
<i class="fa fa-expand"></i>
</button>
</div>
<div class="h-64">
<canvas id="dns-requests-chart"></canvas>
</div>
</div>
</div>
<!-- 详细图表浮窗 -->
<div id="chart-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg w-full max-w-5xl max-h-[90vh] overflow-hidden">
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<h3 class="text-xl font-semibold">DNS请求趋势详细图表</h3>
<div class="flex items-center space-x-4">
<!-- 时间范围切换按钮 -->
<div class="flex space-x-2">
<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="mixed">混合视图</button>
<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>
<!-- 关闭按钮 -->
<button id="close-modal-btn" class="p-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<div class="h-[600px]">
<canvas id="detailed-dns-requests-chart"></canvas>
</div>
</div>
</div>
</div>
@@ -329,9 +664,10 @@
<script src="js/main.js"></script>
<script src="js/api.js"></script>
<script src="js/dashboard.js"></script>
<script src="js/server-status.js"></script>
<script src="js/shield.js"></script>
<script src="js/hosts.js"></script>
<script src="js/query.js"></script>
<script src="js/config.js"></script>
</body>
</body>
</html>

View File

@@ -10,7 +10,10 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
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) {
@@ -20,12 +23,80 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
try {
const response = await fetch(url, options);
// 获取响应文本,用于调试和错误处理
const responseText = await response.text();
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `请求失败: ${response.status}`);
// 尝试解析错误响应
let errorData = {};
try {
// 首先检查响应文本是否为空或不是有效JSON
if (!responseText || responseText.trim() === '') {
console.warn('错误响应为空');
} else {
try {
errorData = JSON.parse(responseText);
} catch (parseError) {
console.error('无法解析错误响应为JSON:', parseError);
console.error('原始错误响应文本:', responseText);
}
}
// 直接返回错误信息,而不是抛出异常,让上层处理
console.warn(`API请求失败: ${response.status}`, errorData);
return { error: errorData.error || `请求失败: ${response.status}` };
} catch (e) {
console.error('处理错误响应时出错:', e);
return { error: `请求处理失败: ${e.message}` };
}
}
return await response.json();
// 尝试解析成功响应
try {
// 首先检查响应文本是否为空
if (!responseText || responseText.trim() === '') {
console.warn('空响应文本');
return {};
}
// 尝试解析JSON
const parsedData = JSON.parse(responseText);
// 限制所有数字为两位小数
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));
}
// 返回空数组作为默认值,避免页面功能完全中断
console.warn('使用默认空数组作为响应');
return [];
}
} catch (error) {
console.error('API请求错误:', error);
throw error;
@@ -35,52 +106,91 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// API方法集合
const api = {
// 获取统计信息
getStats: () => apiRequest('/stats'),
getStats: () => apiRequest('/stats?t=' + Date.now()),
// 获取系统状态
getStatus: () => apiRequest('/status'),
getStatus: () => apiRequest('/status?t=' + Date.now()),
// 获取Top屏蔽域名
getTopBlockedDomains: () => apiRequest('/top-blocked'),
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
// 获取Top解析域名
getTopResolvedDomains: () => apiRequest('/top-resolved'),
getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
// 获取最近屏蔽域名
getRecentBlockedDomains: () => apiRequest('/recent-blocked'),
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
// 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats'),
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
// 获取屏蔽规则
getShieldRules: () => apiRequest('/shield'),
// 获取每日统计数据7天
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
// 添加屏蔽规则
addShieldRule: (rule) => apiRequest('/shield', 'POST', { rule }),
// 获取每月统计数据30天
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
// 删除屏蔽规则
deleteShieldRule: (rule) => apiRequest('/shield', 'DELETE', { rule }),
// 获取查询类型统计
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
// 更新远程规则
updateRemoteRules: () => apiRequest('/shield', 'PUT', { action: 'update' }),
// 获取屏蔽规则 - 已禁用
getShieldRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({}); // 返回空对象而非API调用
},
// 获取黑名单列表
getBlacklists: () => apiRequest('/shield/blacklists'),
// 添加屏蔽规则 - 已禁用
addShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 添加黑名单
addBlacklist: (url) => apiRequest('/shield/blacklists', 'POST', { url }),
// 删除屏蔽规则 - 已禁用
deleteShieldRule: (rule) => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 删除黑名单
deleteBlacklist: (url) => apiRequest('/shield/blacklists', 'DELETE', { url }),
// 更新远程规则 - 已禁用
updateRemoteRules: () => {
console.log('屏蔽规则功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 获取Hosts内容
getHosts: () => apiRequest('/shield/hosts'),
// 获取黑名单列表 - 已禁用
getBlacklists: () => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve([]); // 返回空数组而非API调用
},
// 保存Hosts内容
saveHosts: (content) => apiRequest('/shield/hosts', 'POST', { content }),
// 添加黑名单 - 已禁用
addBlacklist: (url) => {
console.log('屏蔽规则相关功能已禁用');
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
},
// 刷新Hosts
refreshHosts: () => apiRequest('/shield/hosts', 'PUT', { action: 'refresh' }),
// 删除黑名单 - 已禁用
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) {

View File

@@ -0,0 +1,53 @@
// 颜色配置文件 - 集中管理所有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;
}

File diff suppressed because it is too large Load Diff

292
static/js/server-status.js Normal file
View File

@@ -0,0 +1,292 @@
// 服务器状态组件 - 显示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) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// 在DOM加载完成后初始化
window.addEventListener('DOMContentLoaded', function() {
// 延迟初始化,确保页面完全加载
setTimeout(initServerStatusWidget, 500);
});
// 在页面卸载时清理资源
window.addEventListener('beforeunload', function() {
if (serverStatusUpdateTimer) {
clearInterval(serverStatusUpdateTimer);
serverStatusUpdateTimer = null;
}
});
// 导出函数供其他模块使用
window.serverStatusWidget = {
init: initServerStatusWidget,
update: updateServerStatusWidget
};

View File

@@ -1,112 +1,41 @@
// 屏蔽管理页面功能实现
// 初始化屏蔽管理页面
// 初始化屏蔽管理页面 - 已禁用加载屏蔽规则功能
function initShieldPage() {
loadShieldRules();
// 不再加载屏蔽规则避免DOM元素不存在导致的错误
setupShieldEventListeners();
}
// 加载屏蔽规则
// 加载屏蔽规则 - 已禁用此功能
async function loadShieldRules() {
try {
const rules = await api.getShieldRules();
updateShieldRulesTable(rules);
} catch (error) {
showErrorMessage('加载屏蔽规则失败: ' + error.message);
}
console.log('屏蔽规则加载功能已禁用');
}
// 更新屏蔽规则表格
// 更新屏蔽规则表格 - 已禁用此功能
function updateShieldRulesTable(rules) {
const tbody = document.getElementById('shield-rules-tbody');
tbody.innerHTML = '';
if (!rules || rules.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-gray-500">暂无屏蔽规则</td></tr>';
return;
}
rules.forEach((rule, index) => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200 hover:bg-gray-50';
tr.innerHTML = `
<td class="py-3 px-4">${index + 1}</td>
<td class="py-3 px-4">${rule}</td>
<td class="py-3 px-4">
<button data-rule="${rule}" class="delete-rule-btn text-red-500 hover:text-red-700">
<i class="fa fa-trash-o"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
// 添加删除按钮事件监听器
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteRule);
});
// 不再更新表格避免DOM元素不存在导致的错误
console.log('屏蔽规则表格更新功能已禁用');
}
// 处理删除规则
// 处理删除规则 - 已禁用此功能
async function handleDeleteRule(e) {
const rule = e.currentTarget.getAttribute('data-rule');
if (confirm(`确定要删除规则: ${rule} 吗?`)) {
try {
await api.deleteShieldRule(rule);
showSuccessMessage('规则删除成功');
loadShieldRules();
} catch (error) {
showErrorMessage('删除规则失败: ' + error.message);
}
}
showErrorMessage('删除规则功能已禁用');
}
// 添加新规则
// 添加新规则 - 已禁用此功能
async function handleAddRule() {
const ruleInput = document.getElementById('new-rule-input');
const rule = ruleInput.value.trim();
if (!rule) {
showErrorMessage('规则不能为空');
return;
}
try {
await api.addShieldRule(rule);
showSuccessMessage('规则添加成功');
loadShieldRules();
ruleInput.value = '';
} catch (error) {
showErrorMessage('添加规则失败: ' + error.message);
}
showErrorMessage('添加规则功能已禁用');
}
// 更新远程规则
// 更新远程规则 - 已禁用此功能
async function handleUpdateRemoteRules() {
try {
await api.updateRemoteRules();
showSuccessMessage('远程规则更新成功');
loadShieldRules();
} catch (error) {
showErrorMessage('远程规则更新失败: ' + error.message);
}
showErrorMessage('更新远程规则功能已禁用');
}
// 设置事件监听器
// 设置事件监听器 - 已禁用规则相关功能
function setupShieldEventListeners() {
// 添加规则按钮
document.getElementById('add-rule-btn')?.addEventListener('click', handleAddRule);
// 按回车键添加规则
document.getElementById('new-rule-input')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleAddRule();
}
});
// 更新远程规则按钮
document.getElementById('update-remote-rules-btn')?.addEventListener('click', handleUpdateRemoteRules);
// 移除所有事件监听器,避免触发已禁用的功能
console.log('屏蔽规则相关事件监听器已设置,但功能已禁用');
}
// 显示成功消息

24
tailwind.config.js Normal file
View File

@@ -0,0 +1,24 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./static/**/*.{html,js}",
],
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFFB',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
info: '#86909C',
dark: '#1D2129',
light: '#F2F3F5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}

45
test_console.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# DNS Web控制台功能测试脚本
echo "开始测试DNS Web控制台功能..."
echo "=================================="
# 检查服务器是否运行
echo "检查DNS服务器运行状态..."
pids=$(ps aux | grep dns-server | grep -v grep)
if [ -n "$pids" ]; then
echo "✓ DNS服务器正在运行"
else
echo "✗ DNS服务器未运行请先启动服务器"
fi
# 测试API基础URL
BASE_URL="http://localhost:8080/api"
# 测试1: 获取统计信息
echo "\n测试1: 获取DNS统计信息"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
# 测试2: 获取系统状态
echo "\n测试2: 获取系统状态"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
# 测试3: 获取屏蔽规则
echo "\n测试3: 获取屏蔽规则列表"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
# 测试4: 获取Top屏蔽域名
echo "\n测试4: 获取Top屏蔽域名"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
# 测试5: 获取Hosts内容
echo "\n测试5: 获取Hosts内容"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
# 测试6: 访问Web控制台主页
echo "\n测试6: 访问Web控制台主页"
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
echo "\n=================================="
echo "测试完成请检查上述状态码。正常情况下应为200。"
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"