4 Commits
beta2 ... main

Author SHA1 Message Date
ce6e4224f2 上传文件至 / 2025-11-23 12:30:33 +00:00
d34ba6cfff 删除 hosts.txt 2025-11-23 12:09:29 +00:00
c470b97001 删除 rules.txt 2025-11-23 12:09:20 +00:00
332eeb1fed 删除 dns-server.log 2025-11-23 12:08:51 +00:00
52 changed files with 3800 additions and 17698 deletions

234
README.md
View File

@@ -1,234 +0,0 @@
# DNS服务器项目
## 项目简介
这是一个基于Go语言开发的DNS服务器具有屏蔽规则管理、查询日志记录和统计、Web控制台等功能。该服务器可以拦截特定域名的DNS查询提供实时的查询统计和日志记录并通过Web控制台进行管理。
### 技术栈
- Go语言
- Gorilla Mux (HTTP路由)
- Gorilla WebSocket (实时通信)
- Chart.js (数据可视化)
- Tailwind CSS (样式框架)
## 功能特性
### 1. DNS查询处理
- 支持UDP和TCP协议
- 支持常见DNS查询类型(A, AAAA, CNAME, MX, NS, TXT等)
- 高性能查询处理
### 2. 屏蔽规则管理
- 支持域名规则和正则表达式规则
- 支持规则例外
- 支持远程规则列表
- 支持本地规则管理
### 3. 查询日志记录和统计
- 实时记录DNS查询日志
- 支持日志持久化到文件
- 提供查询统计和趋势分析
- 支持日志搜索和过滤
- 支持日志排序
### 4. Web控制台
- 直观的仪表盘
- 实时统计数据
- 图表可视化
- 规则管理界面
- 查询日志详情页面
- 支持分页和自定义记录数量
### 5. WebSocket实时更新
- 实时更新统计数据
- 实时更新图表
- 支持连接状态管理
### 6. 查询日志持久化
- 将查询日志保存到`querylog.json`文件
- 定期自动保存
- 服务器重启后自动加载
## 安装步骤
### 环境要求
- Go 1.18或更高版本
- Linux或Windows操作系统
### 安装依赖
```bash
go mod download
```
### 编译和运行
```bash
# 编译
go build -o dns-server main.go
# 运行
./dns-server
```
## 配置说明
### 主要配置项
- `ListenPort`: DNS服务器监听端口默认53
- `HTTPPort`: HTTP控制台监听端口默认8080
- `StatsFile`: 统计数据保存文件,默认`data/stats.json`
- `SaveInterval`: 自动保存间隔默认300
- `MaxQueryLogs`: 最大保存日志数量默认1000
### 配置文件格式
配置文件使用JSON格式位于`config.json`
```json
{
"ListenPort": 53,
"HTTPPort": 8080,
"StatsFile": "data/stats.json",
"SaveInterval": 300,
"MaxQueryLogs": 1000
}
```
## 使用方法
### 启动服务器
```bash
./dns-server
```
### 访问Web控制台
在浏览器中访问:
```
http://localhost:8080
```
### 管理屏蔽规则
1. 登录Web控制台
2. 点击左侧菜单中的"屏蔽管理"
3. 在"本地规则管理"中添加或删除规则
4. 在"远程黑名单管理"中添加或删除远程规则列表
### 查看查询日志
1. 登录Web控制台
2. 点击左侧菜单中的"查询日志"
3. 查看日志统计和趋势
4. 使用搜索和过滤功能查找特定日志
5. 点击列头进行排序
6. 使用刷新按钮手动刷新日志
## API文档
### 主要API端点
#### 1. DNS查询
```
GET /api/query?domain=example.com
```
#### 2. 屏蔽规则管理
```
GET /api/shield/rules
POST /api/shield/rules
DELETE /api/shield/rules/:id
```
#### 3. Hosts管理
```
GET /api/hosts
POST /api/hosts
DELETE /api/hosts/:id
```
#### 4. 查询日志
```
GET /api/logs/stats
GET /api/logs/query
GET /api/logs/count
```
#### 5. WebSocket
```
ws://localhost:8080/ws/stats
```
## 开发说明
### 项目结构
```
/root/dnsbak/
├── config/ # 配置文件
├── data/ # 数据文件
├── dns/ # DNS服务器相关代码
├── http/ # HTTP服务器相关代码
├── shield/ # 屏蔽规则管理
├── static/ # 静态资源
│ ├── css/ # CSS文件
│ ├── js/ # JavaScript文件
│ └── index.html # 主页面
├── main.go # 入口文件
├── go.mod # Go模块文件
└── go.sum # Go依赖校验文件
```
### 开发流程
1. 克隆仓库
2. 安装依赖
3. 开发功能
4. 编译和测试
5. 提交代码
### 测试
```bash
go test ./...
```
## 许可证
MIT License
## 贡献
欢迎提交Issue和Pull Request
## 联系方式
如有问题或建议,请通过以下方式联系:
- Email: wxf26054@live.cn
- Git: https://gitea.amazehome.xyz/AMAZEHOME/dns-server
## 更新日志
### v1.0.0 (2025-11-30)
- 初始版本
- 实现基本DNS服务器功能
- 实现屏蔽规则管理
- 实现查询日志记录和统计
- 实现Web控制台
- 实现WebSocket实时更新
- 实现查询日志持久化
### v1.0.1 (2025-11-30)
- 修复搜索和过滤功能
- 优化查询日志显示
- 修复样式间隔问题
- 添加查询日志刷新按钮
## 致谢
感谢所有为该项目做出贡献的开源项目和开发者!

81
Readme.md Normal file
View File

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

View File

@@ -1,115 +1,24 @@
{
"dns": {
"port": 53,
"upstreamDNS": [
"223.5.5.5:53",
"223.6.6.6:53"
],
"timeout": 5000,
"statsFile": "data/stats.json",
"saveInterval": 300
"upstreamDNS": ["223.5.5.5:53", "223.6.6.6:53"],
"timeout": 5000
},
"http": {
"port": 8080,
"host": "0.0.0.0",
"enableAPI": true,
"username": "admin",
"password": "admin"
"enableAPI": true
},
"shield": {
"localRulesFile": "data/rules.txt",
"blacklists": [
{
"name": "AdGuard DNS filter",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
"enabled": true,
"lastUpdateTime": "2025-11-28T16:13:03.564Z"
},
{
"name": "Adaway Default Blocklist",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
"enabled": true,
"lastUpdateTime": "2025-11-28T15:36:43.086Z"
},
{
"name": "CHN-anti-AD",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
"enabled": true,
"lastUpdateTime": "2025-11-28T15:26:24.833Z"
},
{
"name": "My GitHub Rules",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
"enabled": true,
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
},
{
"name": "CNList",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
"enabled": false
},
{
"name": "大圣净化",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt",
"enabled": true
},
{
"name": "Hate \u0026 Junk",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
"enabled": true
},
{
"name": "My Gitlab Hosts",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
"enabled": true,
"lastUpdateTime": "2025-11-29T17:11:28.130Z"
},
{
"name": "Anti Remote Requests",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt",
"enabled": true
},
{
"name": "URL-Based.txt",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt",
"enabled": true
},
{
"name": "My Gitlab A/T Rules",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
"enabled": true
},
{
"name": "My Gitlab Malware List",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt",
"enabled": true
},
{
"name": "hosts",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt",
"enabled": true
},
{
"name": "AWAvenue-Ads-Rule",
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt",
"enabled": true
},
{
"name": "诈骗域名",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt",
"enabled": true
}
"remoteRules": [
"https://example.com/rules.txt"
],
"updateInterval": 3600,
"hostsFile": "data/hosts.txt",
"blockMethod": "NXDOMAIN",
"customBlockIP": "",
"statsFile": "./data/shield_stats.json",
"statsSaveInterval": 60,
"remoteRulesCacheDir": "data/remote_rules"
"hostsFile": "data/hosts.txt"
},
"log": {
"file": "logs/dns-server.log",
"file": "dns-server.log",
"level": "debug",
"maxSize": 100,
"maxBackups": 10,

View File

@@ -12,7 +12,6 @@ type DNSConfig struct {
Timeout int `json:"timeout"`
StatsFile string `json:"statsFile"` // 统计数据持久化文件
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间分钟
}
// HTTPConfig HTTP控制台配置
@@ -20,30 +19,18 @@ type HTTPConfig struct {
Port int `json:"port"`
Host string `json:"host"`
EnableAPI bool `json:"enableAPI"`
Username string `json:"username"` // 登录用户名
Password string `json:"password"` // 登录密码
}
// BlacklistEntry 黑名单条目
type BlacklistEntry struct {
Name string `json:"name"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
RuleCount int `json:"ruleCount,omitempty"`
LastUpdateTime string `json:"lastUpdateTime,omitempty"`
}
// ShieldConfig 屏蔽规则配置
type ShieldConfig struct {
LocalRulesFile string `json:"localRulesFile"`
Blacklists []BlacklistEntry `json:"blacklists"`
UpdateInterval int `json:"updateInterval"`
HostsFile string `json:"hostsFile"`
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP当BlockMethod为"customIP"时使用
StatsFile string `json:"statsFile"` // 计数数据持久化文件
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
RemoteRulesCacheDir string `json:"remoteRulesCacheDir"` // 远程规则缓存目录
LocalRulesFile string `json:"localRulesFile"`
RemoteRules []string `json:"remoteRules"`
UpdateInterval int `json:"updateInterval"`
HostsFile string `json:"hostsFile"`
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP当BlockMethod为"customIP"时使用
StatsFile string `json:"statsFile"` // 计数数据持久化文件
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
}
// LogConfig 日志配置
@@ -89,23 +76,12 @@ func LoadConfig(path string) (*Config, error) {
if config.DNS.SaveInterval == 0 {
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
}
// 默认DNS缓存TTL为30分钟
if config.DNS.CacheTTL == 0 {
config.DNS.CacheTTL = 30 // 默认30分钟
}
if config.HTTP.Port == 0 {
config.HTTP.Port = 8080
}
if config.HTTP.Host == "" {
config.HTTP.Host = "0.0.0.0"
}
// 默认用户名和密码如果未配置则使用admin/admin
if config.HTTP.Username == "" {
config.HTTP.Username = "admin"
}
if config.HTTP.Password == "" {
config.HTTP.Password = "admin"
}
if config.Shield.UpdateInterval == 0 {
config.Shield.UpdateInterval = 3600
}
@@ -118,19 +94,6 @@ func LoadConfig(path string) (*Config, error) {
if config.Shield.StatsSaveInterval == 0 {
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
}
if config.Shield.RemoteRulesCacheDir == "" {
config.Shield.RemoteRulesCacheDir = "./data/remote_rules" // 默认远程规则缓存目录
}
// 如果黑名单列表为空,添加一些默认的黑名单
if len(config.Shield.Blacklists) == 0 {
config.Shield.Blacklists = []BlacklistEntry{
{Name: "AdGuard DNS filter", URL: "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt", Enabled: true},
{Name: "Adaway Default Blocklist", URL: "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt", Enabled: true},
{Name: "CHN-anti-AD", URL: "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt", Enabled: true},
{Name: "My GitHub Rules", URL: "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt", Enabled: true},
}
}
if config.Log.Level == "" {
config.Log.Level = "info"
}

6
data/hosts.txt Normal file
View File

@@ -0,0 +1,6 @@
# DNS Server Hosts File
# Generated by DNS Server
::1 localhost
ad.qq.com 127.0.0.1
ad.qq.com 0.0.0.0

5
data/rules.txt Normal file
View File

@@ -0,0 +1,5 @@
||hm.baidu.com
||baidu.com
/.*tracking.*/
/adjust.net/
/ad./

0
data/shield_stats.json Normal file
View File

42
data/stats.json Normal file
View File

@@ -0,0 +1,42 @@
{
"stats": {
"Queries": 33,
"Blocked": 24,
"Allowed": 19,
"Errors": 2,
"LastQuery": "2025-11-23T19:06:10.694259822+08:00"
},
"blockedDomains": {
"ad.qq.com": {
"Domain": "ad.qq.com",
"Count": 6,
"LastSeen": "2025-11-23T19:06:10.694532789+08:00"
},
"ad.qq.com.amazehome.xyz": {
"Domain": "ad.qq.com.amazehome.xyz",
"Count": 6,
"LastSeen": "2025-11-23T19:06:10.691565251+08:00"
}
},
"resolvedDomains": {
"ad.qq.com": {
"Domain": "ad.qq.com",
"Count": 10,
"LastSeen": "2025-11-23T19:05:54.429002187+08:00"
},
"ad.qq.com.amazehome.xyz": {
"Domain": "ad.qq.com.amazehome.xyz",
"Count": 8,
"LastSeen": "2025-11-23T19:05:54.356216418+08:00"
},
"apd-pcdnwxstat.teg.tencent-cloud.net": {
"Domain": "apd-pcdnwxstat.teg.tencent-cloud.net",
"Count": 1,
"LastSeen": "2025-11-23T19:05:56.842653777+08:00"
}
},
"hourlyStats": {
"2025-11-23-19": 12
},
"lastSaved": "2025-11-23T19:06:21.194988376+08:00"
}

Binary file not shown.

View File

@@ -1,127 +0,0 @@
package dns
import (
"sync"
"time"
"github.com/miekg/dns"
)
// DNSCacheItem 表示缓存中的DNS响应项
type DNSCacheItem struct {
Response *dns.Msg // DNS响应消息
Expiry time.Time // 过期时间
}
// DNSCache DNS缓存结构
type DNSCache struct {
cache map[string]*DNSCacheItem // 缓存映射表
mutex sync.RWMutex // 读写锁,保护缓存
defaultTTL time.Duration // 默认缓存TTL
}
// NewDNSCache 创建新的DNS缓存实例
func NewDNSCache(defaultTTL time.Duration) *DNSCache {
cache := &DNSCache{
cache: make(map[string]*DNSCacheItem),
defaultTTL: defaultTTL,
}
// 启动缓存清理协程
go cache.startCleanupLoop()
return cache
}
// cacheKey 生成缓存键
func cacheKey(qName string, qType uint16) string {
return qName + "|" + dns.TypeToString[qType]
}
// Set 设置缓存项
func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) {
if ttl <= 0 {
ttl = c.defaultTTL
}
key := cacheKey(qName, qType)
item := &DNSCacheItem{
Response: response.Copy(), // 复制响应以避免外部修改
Expiry: time.Now().Add(ttl),
}
c.mutex.Lock()
c.cache[key] = item
c.mutex.Unlock()
}
// Get 获取缓存项
func (c *DNSCache) Get(qName string, qType uint16) (*dns.Msg, bool) {
key := cacheKey(qName, qType)
c.mutex.RLock()
item, found := c.cache[key]
if !found {
c.mutex.RUnlock()
return nil, false
}
// 检查是否过期
if time.Now().After(item.Expiry) {
c.mutex.RUnlock()
// 过期了,删除缓存项(在写锁中)
c.delete(key)
return nil, false
}
// 返回缓存的响应副本
response := item.Response.Copy()
c.mutex.RUnlock()
return response, true
}
// delete 删除缓存项
func (c *DNSCache) delete(key string) {
c.mutex.Lock()
delete(c.cache, key)
c.mutex.Unlock()
}
// Clear 清空缓存
func (c *DNSCache) Clear() {
c.mutex.Lock()
c.cache = make(map[string]*DNSCacheItem)
c.mutex.Unlock()
}
// Size 获取缓存大小
func (c *DNSCache) Size() int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache)
}
// startCleanupLoop 启动定期清理过期缓存的协程
func (c *DNSCache) startCleanupLoop() {
ticker := time.NewTicker(time.Minute * 5) // 每5分钟清理一次
defer ticker.Stop()
for range ticker.C {
c.cleanupExpired()
}
}
// cleanupExpired 清理过期的缓存项
func (c *DNSCache) cleanupExpired() {
now := time.Now()
c.mutex.Lock()
defer c.mutex.Unlock()
for key, item := range c.cache {
if now.After(item.Expiry) {
delete(c.cache, key)
}
}
}

File diff suppressed because it is too large Load Diff

13
go.mod
View File

@@ -5,7 +5,6 @@ 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
)
@@ -14,11 +13,9 @@ require (
// 之前的go.sum可能包含lumberjack的记录但现在已经不再使用
require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/tools v0.33.0 // indirect
)

32
go.sum
View File

@@ -1,10 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
@@ -12,21 +10,19 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load Diff

1574
index.html Normal file

File diff suppressed because it is too large Load Diff

View File

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

169
main.go
View File

@@ -1,14 +1,3 @@
// DNS Server API
// @title DNS Server API
// @version 1.0
// @description DNS服务器API文档
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email support@example.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api
package main
import (
@@ -17,7 +6,6 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"dns-server/config"
@@ -27,156 +15,19 @@ import (
"dns-server/shield"
)
// createDefaultConfig 创建默认配置文件
func createDefaultConfig(configFile string) error {
// 默认配置内容
defaultConfig := `{
"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,
"username": "admin",
"password": "admin"
},
"shield": {
"localRulesFile": "data/rules.txt",
"blacklists": [
{
"name": "AdGuard DNS filter",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
"enabled": true
},
{
"name": "Adaway Default Blocklist",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
"enabled": true
},
{
"name": "CHN-anti-AD",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
"enabled": true
},
{
"name": "My GitHub Rules",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
"enabled": true
}
],
"updateInterval": 3600,
"hostsFile": "data/hosts.txt",
"blockMethod": "NXDOMAIN",
"customBlockIP": "",
"statsFile": "./data/shield_stats.json",
"statsSaveInterval": 60,
"remoteRulesCacheDir": "./data/remote_rules"
},
"log": {
"file": "logs/dns-server.log",
"level": "debug",
"maxSize": 100,
"maxBackups": 10,
"maxAge": 30
}
}`
// 写入默认配置到文件
return os.WriteFile(configFile, []byte(defaultConfig), 0644)
}
// createRequiredFiles 创建所需的文件和文件夹
func createRequiredFiles(cfg *config.Config) error {
// 创建数据文件夹
dataDir := "./data"
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("创建数据文件夹失败: %w", err)
}
// 创建远程规则缓存文件夹
if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
}
// 创建日志文件夹
logDir := filepath.Dir(cfg.Log.File)
if logDir != "." {
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("创建日志文件夹失败: %w", err)
}
}
// 创建本地规则文件
if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 本地规则文件\n# 格式:域名\n# 例如example.com\n"), 0644); err != nil {
return fmt.Errorf("创建本地规则文件失败: %w", err)
}
}
// 创建Hosts文件
if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式IP 域名\n# 例如127.0.0.1 localhost\n"), 0644); err != nil {
return fmt.Errorf("创建Hosts文件失败: %w", err)
}
}
// 创建统计数据文件
if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
return fmt.Errorf("创建统计数据文件失败: %w", err)
}
}
// 创建Shield统计数据文件
if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
}
}
return nil
}
func main() {
// 命令行参数解析
var configFile string
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
// 解析命令行参数
configPath := flag.String("config", "config.json", "配置文件路径")
flag.Parse()
// 检查配置文件是否存在,如果不存在则创建默认配置文件
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
if err := createDefaultConfig(configFile); err != nil {
log.Fatalf("创建默认配置文件失败: %v", err)
}
log.Printf("默认配置文件 %s 创建成功", configFile)
}
// 初始化配置
var cfg *config.Config
var err error
cfg, err = config.LoadConfig(configFile)
cfg, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 创建所需的文件和文件夹
log.Println("正在创建所需的文件和文件夹...")
if err := createRequiredFiles(cfg); err != nil {
log.Fatalf("创建所需文件和文件夹失败: %v", err)
}
log.Println("所需文件和文件夹创建成功")
// 初始化日志系统
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil {
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
log.Fatalf("初始化日志系统失败: %v", err)
}
defer logger.Close()
@@ -210,16 +61,14 @@ func main() {
logger.Info(fmt.Sprintf("DNS服务器已启动监听端口: %d", cfg.DNS.Port))
logger.Info(fmt.Sprintf("HTTP控制台已启动监听端口: %d", cfg.HTTP.Port))
// 监听信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
// 等待退出信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// 清理资源
logger.Info("正在关闭服务...")
dnsServer.Stop()
httpServer.Stop()
shieldManager.StopAutoUpdate()
logger.Info("服务已关闭")
logger.Info("所有服务已关闭")
}

View File

@@ -1,18 +0,0 @@
{
"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"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
package shield
import (
"testing"
"dns-server/config"
)
func TestRuleParsing(t *testing.T) {
// 创建一个简单的配置
cfg := &config.ShieldConfig{
LocalRulesFile: "",
RemoteRulesCacheDir: ".",
UpdateInterval: 3600,
StatsFile: "",
StatsSaveInterval: 300,
HostsFile: "",
Blacklists: []config.BlacklistEntry{},
}
// 测试规则
testCases := []struct {
rule string
domain string
blocked bool
desc string
}{
// 测试关键字匹配规则
{"/ad.qq.com/", "ad.qq.com", true, "精确匹配"},
{"/ad.qq.com/", "sub.ad.qq.com", true, "子域名包含匹配"},
{"/ad/", "ad.example.com", true, "开头匹配"},
{"/ad/", "example.ad.com", true, "中间匹配"},
{"/ad/", "example.com.ad", true, "结尾匹配"},
{"/AD/", "ad.example.com", true, "不区分大小写匹配"},
{"/example.com/", "example.com", true, "特殊字符转义匹配"},
{"/ad/", "example.com", false, "不包含关键字,不应匹配"},
{"/test/", "example.com", false, "不同关键字,不应匹配"},
// 测试排除规则
{"@@/ad/", "ad.example.com", false, "排除规则,不应匹配"},
}
// 运行测试
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// 为每个测试用例创建一个新的屏蔽管理器实例
manager := NewShieldManager(cfg)
// 添加规则
manager.AddRule(tc.rule)
// 检查域名是否被屏蔽
result := manager.CheckDomainBlockDetails(tc.domain)
blocked := result["blocked"].(bool)
// 验证结果
if blocked != tc.blocked {
t.Errorf("Rule %q: Domain %q expected %t, got %t", tc.rule, tc.domain, tc.blocked, blocked)
}
})
}
}

View File

@@ -1,5 +0,0 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
}

View File

@@ -1,488 +0,0 @@
/* 基础样式 */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #ffffff;
color: #333333;
}
/* 默认浅色主题样式 */
.swagger-ui .topbar {
background-color: #2c3e50;
padding: 15px 0;
}
.swagger-ui .topbar .topbar-wrapper .link {
color: #ecf0f1;
font-size: 1.2rem;
}
.swagger-ui .info {
margin: 20px 0;
}
.swagger-ui .info .title {
font-size: 2rem;
margin-bottom: 10px;
color: #333;
}
.swagger-ui .info .description {
font-size: 1rem;
color: #555;
margin-bottom: 15px;
}
/* 修复服务器URL输入框样式 */
.swagger-ui .servers li input[type="text"] {
padding: 8px 12px;
width: 100%;
box-sizing: border-box;
}
/* 修复服务器选择区域的背景颜色和布局 */
.swagger-ui .servers {
padding: 16px;
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保服务器列表容器有正确的背景色和布局 */
.swagger-ui .servers-wrapper {
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保整个顶部区域颜色一致和布局正确 */
.swagger-ui .info {
margin: 0;
padding: 20px 16px;
width: 100%;
box-sizing: border-box;
}
/* 确保顶部主容器颜色一致和布局正确 */
.swagger-ui {
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 确保API信息区域颜色一致和布局正确 */
.swagger-ui .info-container {
width: 100%;
box-sizing: border-box;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 修复服务器URL输入框深色模式样式 */
body.dark-mode .swagger-ui .servers li input[type="text"] {
background-color: #1a202c !important;
color: #ffffff !important;
border-color: #4a5568 !important;
padding: 8px 12px !important;
width: 100% !important;
}
/* 修复服务器选择区域的深色模式背景颜色和布局 */
body.dark-mode .swagger-ui .servers {
background-color: #1a202c !important;
border: none !important;
padding: 16px !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保服务器列表容器在深色模式下也有正确的背景色和布局 */
body.dark-mode .swagger-ui .servers-wrapper {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保整个顶部区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info {
background-color: #1a202c !important;
margin: 0 !important;
padding: 20px 16px !important;
border-bottom: 1px solid #4a5568 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保顶部主容器在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 确保API信息区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info-container {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下内容区域的布局问题 */
body.dark-mode .swagger-ui .wrapper {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下API操作块的布局 */
body.dark-mode .swagger-ui .opblock {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下过滤器的布局 */
body.dark-mode .swagger-ui .filter {
width: 100% !important;
box-sizing: border-box !important;
padding: 16px !important;
margin: 0 !important;
}
/* 修复深色模式下顶部栏布局 */
body.dark-mode .swagger-ui .topbar {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 15px 0 !important;
}
/* 修复深色模式下顶部栏包装器布局 */
body.dark-mode .swagger-ui .topbar .topbar-wrapper {
width: 100% !important;
box-sizing: border-box !important;
padding: 0 16px !important;
}
/* 修复深色模式下响应容器布局 */
body.dark-mode .swagger-ui .responses-inner {
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下操作块摘要布局 */
body.dark-mode .swagger-ui .opblock-summary {
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保深色模式下所有容器元素都使用box-sizing */
body.dark-mode * {
box-sizing: border-box !important;
}
/* 增强标签标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
}
/* 增强标签标题h3深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag.h3 {
color: #ffffff !important;
background-color: #2d3748 !important;
}
/* 增强标签部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag-section {
background-color: #2d3748 !important;
padding: 16px !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
}
/* 增强API描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-description-wrapper {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-description-wrapper p {
color: #ffffff !important;
line-height: 1.5 !important;
}
/* 增强stats标签描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-summary-description {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 增强操作块标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-title_normal h4 {
color: #ffffff !important;
font-weight: 600 !important;
}
/* 增强参数部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-body {
background-color: #2d3748 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff !important;
font-weight: 600 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description,
body.dark-mode .swagger-ui .parameters-col_name,
body.dark-mode .swagger-ui .parameters-col_type {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description p,
body.dark-mode .swagger-ui .parameters-col_name p,
body.dark-mode .swagger-ui .parameters-col_type p {
color: #ffffff !important;
}
/* 新增适配API文档展开界面的所有文字元素 */
body.dark-mode .swagger-ui .opblock-body {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options .body-param-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner h4 {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-code {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-name {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .required {
color: #ffffff;
}
body.dark-mode .swagger-ui .scroll-to-top {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-tag-section {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
body.dark-mode .swagger-ui .auth-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-list {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item label {
color: #ffffff;
}
/* 确保代码块内的文字也清晰可见 */
body.dark-mode .swagger-ui pre {
color: #ffffff;
}
body.dark-mode .swagger-ui code {
color: #ffffff;
}
/* 确保所有表单元素的文字颜色正确 */
body.dark-mode .swagger-ui form {
color: #ffffff;
}
body.dark-mode .swagger-ui form label {
color: #ffffff;
}
body.dark-mode .swagger-ui select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
/* 适配可能的嵌套内容 */
body.dark-mode .swagger-ui .opblock-body .schema {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .title {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .required {
color: #ffffff;
}
/* 适配可能的按钮组 */
body.dark-mode .swagger-ui .btn-group {
color: #ffffff;
}
/* 适配可能的标签 */
body.dark-mode .swagger-ui .tag {
color: #ffffff;
}
/* 适配可能的警告和提示信息 */
body.dark-mode .swagger-ui .warning {
color: #ffffff;
}
body.dark-mode .swagger-ui .hint {
color: #ffffff;
}
/* 适配可能的表格内容 */
body.dark-mode .swagger-ui table {
color: #ffffff;
}
body.dark-mode .swagger-ui table th {
color: #ffffff;
}
body.dark-mode .swagger-ui table td {
color: #ffffff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.topbar-controls {
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.theme-toggle-btn {
padding: 6px 10px;
font-size: 12px;
}
.theme-toggle-btn span {
display: none;
}
}

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DNS Server API 文档</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-standalone-preset.js"></script>
<script src="js/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.sidebar-item-active {
background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
}
}
/* 服务器状态组件光晕效果 */
.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);
}
/* 加载状态样式 */
.status-loading {
animation: status-pulse 1.5s ease-in-out infinite;
}
/* 状态脉冲动画 */
@keyframes status-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 保存按钮状态样式 */
#save-blacklist-status {
transition: all 0.3s ease-in-out;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,305 +0,0 @@
// API模块 - 统一管理所有API调用
// API路径定义
const API_BASE_URL = '/api';
// API请求封装
async function apiRequest(endpoint, method = 'GET', data = null) {
const url = `${API_BASE_URL}${endpoint}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma': 'no-cache',
},
credentials: 'same-origin',
};
if (data) {
options.body = JSON.stringify(data);
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, 10000); // 10秒超时
});
try {
// 竞争:请求或超时
const response = await Promise.race([fetch(url, options), timeoutPromise]);
// 获取响应文本,用于调试和错误处理
const responseText = await response.text();
if (!response.ok) {
// 优化错误响应处理
console.warn(`API请求失败: ${response.status}`);
// 处理401未授权错误重定向到登录页面
if (response.status === 401) {
console.warn('未授权访问,重定向到登录页面');
window.location.href = '/login';
return { error: '未授权访问' };
}
// 尝试解析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;

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,623 +0,0 @@
// logs.js - 查询日志页面功能
// 全局变量
let currentPage = 1;
let totalPages = 1;
let logsPerPage = 30; // 默认显示30条记录
let currentFilter = '';
let currentSearch = '';
let logsChart = null;
let currentSortField = '';
let currentSortDirection = 'desc'; // 默认降序
// IP地理位置缓存
let ipGeolocationCache = {};
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
// WebSocket连接和重连计时器
let logsWsConnection = null;
let logsWsReconnectTimer = null;
// 初始化查询日志页面
function initLogsPage() {
console.log('初始化查询日志页面');
// 加载日志统计数据
loadLogsStats();
// 加载日志详情
loadLogs();
// 初始化图表
initLogsChart();
// 绑定事件
bindLogsEvents();
// 建立WebSocket连接用于实时更新统计数据和图表
connectLogsWebSocket();
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupLogsResources);
}
// 清理资源
function cleanupLogsResources() {
// 清除WebSocket连接
if (logsWsConnection) {
logsWsConnection.close();
logsWsConnection = null;
}
// 清除重连计时器
if (logsWsReconnectTimer) {
clearTimeout(logsWsReconnectTimer);
logsWsReconnectTimer = null;
}
}
// 绑定事件
function bindLogsEvents() {
// 搜索按钮
const searchBtn = document.getElementById('logs-search-btn');
if (searchBtn) {
searchBtn.addEventListener('click', () => {
currentSearch = document.getElementById('logs-search').value;
currentPage = 1;
loadLogs();
});
}
// 搜索框回车事件
const searchInput = document.getElementById('logs-search');
if (searchInput) {
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
currentSearch = searchInput.value;
currentPage = 1;
loadLogs();
}
});
}
// 结果过滤
const resultFilter = document.getElementById('logs-result-filter');
if (resultFilter) {
resultFilter.addEventListener('change', () => {
currentFilter = resultFilter.value;
currentPage = 1;
loadLogs();
});
}
// 自定义记录数量
const perPageSelect = document.getElementById('logs-per-page');
if (perPageSelect) {
perPageSelect.addEventListener('change', () => {
logsPerPage = parseInt(perPageSelect.value);
currentPage = 1;
loadLogs();
});
}
// 分页按钮
const prevBtn = document.getElementById('logs-prev-page');
const nextBtn = document.getElementById('logs-next-page');
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
loadLogs();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
loadLogs();
}
});
}
// 时间范围切换
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
timeRangeBtns.forEach(btn => {
btn.addEventListener('click', () => {
// 更新按钮样式
timeRangeBtns.forEach(b => {
b.classList.remove('bg-primary', 'text-white');
b.classList.add('bg-gray-200', 'text-gray-700');
});
btn.classList.remove('bg-gray-200', 'text-gray-700');
btn.classList.add('bg-primary', 'text-white');
// 更新图表
const range = btn.getAttribute('data-range');
updateLogsChart(range);
});
});
// 刷新按钮事件
const refreshBtn = document.getElementById('logs-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
// 重新加载日志
currentPage = 1;
loadLogs();
});
}
// 排序按钮事件
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
header.addEventListener('click', () => {
const sortField = header.getAttribute('data-sort');
// 如果点击的是当前排序字段,则切换排序方向
if (sortField === currentSortField) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
// 否则,设置新的排序字段,默认降序
currentSortField = sortField;
currentSortDirection = 'desc';
}
// 更新排序图标
updateSortIcons();
// 重新加载日志
currentPage = 1;
loadLogs();
});
});
}
// 更新排序图标
function updateSortIcons() {
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
const sortField = header.getAttribute('data-sort');
const icon = header.querySelector('i');
// 重置所有图标
icon.className = 'fa fa-sort ml-1 text-xs';
// 设置当前排序字段的图标
if (sortField === currentSortField) {
if (currentSortDirection === 'asc') {
icon.className = 'fa fa-sort-asc ml-1 text-xs';
} else {
icon.className = 'fa fa-sort-desc ml-1 text-xs';
}
}
});
}
// 加载日志统计数据
function loadLogsStats() {
// 使用封装的apiRequest函数进行API调用
apiRequest('/logs/stats')
.then(data => {
if (data && data.error) {
console.error('加载日志统计数据失败:', data.error);
return;
}
// 更新统计卡片
document.getElementById('logs-total-queries').textContent = data.totalQueries;
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
document.getElementById('logs-active-ips').textContent = data.activeIPs;
// 计算屏蔽率
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
document.getElementById('logs-block-rate').textContent = blockRate + '%';
})
.catch(error => {
console.error('加载日志统计数据失败:', error);
});
}
// 加载日志详情
function loadLogs() {
// 显示加载状态
const loadingEl = document.getElementById('logs-loading');
if (loadingEl) {
loadingEl.classList.remove('hidden');
}
// 构建请求URL
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
// 添加过滤条件
if (currentFilter) {
endpoint += `&result=${currentFilter}`;
}
// 添加搜索条件
if (currentSearch) {
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
}
// 添加排序条件
if (currentSortField) {
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('加载日志详情失败:', data.error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
// 加载日志总数
return apiRequest('/logs/count').then(countData => {
return { logs: data, count: countData.count };
});
})
.then(result => {
if (!result || !result.logs) {
console.error('加载日志详情失败: 无效的响应数据');
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
const logs = result.logs;
const totalLogs = result.count;
// 计算总页数
totalPages = Math.ceil(totalLogs / logsPerPage);
// 更新日志表格
updateLogsTable(logs);
// 更新分页信息
updateLogsPagination();
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
})
.catch(error => {
console.error('加载日志详情失败:', error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
});
}
// 更新日志表格
function updateLogsTable(logs) {
const tableBody = document.getElementById('logs-table-body');
if (!tableBody) return;
// 清空表格
tableBody.innerHTML = '';
if (logs.length === 0) {
// 显示空状态
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = `
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
`;
tableBody.appendChild(emptyRow);
return;
}
// 填充表格
logs.forEach(log => {
const row = document.createElement('tr');
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
const time = new Date(log.Timestamp);
const formattedDate = time.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const formattedTime = time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// 根据结果添加不同的背景色
let rowClass = '';
switch (log.Result) {
case 'blocked':
rowClass = 'bg-red-50'; // 淡红色填充
break;
case 'allowed':
// 检查是否是规则允许项目
if (log.BlockRule && log.BlockRule.includes('allow')) {
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
} else {
rowClass = ''; // 允许的不填充
}
break;
default:
rowClass = '';
}
// 添加行背景色
if (rowClass) {
row.classList.add(rowClass);
}
// 添加被屏蔽或允许显示,并增加颜色
let statusText = '';
let statusClass = '';
switch (log.Result) {
case 'blocked':
statusText = '被屏蔽';
statusClass = 'text-danger';
break;
case 'allowed':
statusText = '允许';
statusClass = 'text-success';
break;
case 'error':
statusText = '错误';
statusClass = 'text-warning';
break;
default:
statusText = '';
statusClass = '';
}
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
// 添加缓存状态显示
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
row.innerHTML = `
<td class="py-3 px-4">
<div class="text-sm font-medium">${formattedTime}</div>
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="font-medium">${log.ClientIP}</div>
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="font-medium">${log.Domain}</div>
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span></div>
</td>
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
`;
tableBody.appendChild(row);
});
}
// 更新分页信息
function updateLogsPagination() {
// 更新页码显示
document.getElementById('logs-current-page').textContent = currentPage;
document.getElementById('logs-total-pages').textContent = totalPages;
// 更新按钮状态
const prevBtn = document.getElementById('logs-prev-page');
const nextBtn = document.getElementById('logs-next-page');
if (prevBtn) {
prevBtn.disabled = currentPage === 1;
}
if (nextBtn) {
nextBtn.disabled = currentPage === totalPages;
}
}
// 初始化日志图表
function initLogsChart() {
const ctx = document.getElementById('logs-trend-chart');
if (!ctx) return;
// 获取24小时统计数据
apiRequest('/hourly-stats')
.then(data => {
if (data && data.error) {
console.error('初始化日志图表失败:', data.error);
return;
}
// 创建图表
logsChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '查询数',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
})
.catch(error => {
console.error('初始化日志图表失败:', error);
});
}
// 更新日志图表
function updateLogsChart(range) {
if (!logsChart) return;
let endpoint = '';
switch (range) {
case '24h':
endpoint = '/hourly-stats';
break;
case '7d':
endpoint = '/daily-stats';
break;
case '30d':
endpoint = '/monthly-stats';
break;
default:
endpoint = '/hourly-stats';
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('更新日志图表失败:', data.error);
return;
}
// 更新图表数据
logsChart.data.labels = data.labels;
logsChart.data.datasets[0].data = data.data;
logsChart.update();
})
.catch(error => {
console.error('更新日志图表失败:', error);
});
}
// 建立WebSocket连接
function connectLogsWebSocket() {
try {
// 构建WebSocket URL
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
console.log('正在连接WebSocket:', wsUrl);
// 创建WebSocket连接
logsWsConnection = new WebSocket(wsUrl);
// 连接打开事件
logsWsConnection.onopen = function() {
console.log('WebSocket连接已建立');
};
// 接收消息事件
logsWsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'initial_data' || data.type === 'stats_update') {
console.log('收到实时数据更新');
// 只更新统计数据,不更新日志详情
updateLogsStatsFromWebSocket(data.data);
}
} catch (error) {
console.error('处理WebSocket消息失败:', error);
}
};
// 连接关闭事件
logsWsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭代码:', event.code);
logsWsConnection = null;
// 设置重连
setupLogsReconnect();
};
// 连接错误事件
logsWsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
}
}
// 设置重连逻辑
function setupLogsReconnect() {
if (logsWsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
logsWsReconnectTimer = setTimeout(() => {
connectLogsWebSocket();
}, reconnectDelay);
}
// 从WebSocket更新日志统计数据
function updateLogsStatsFromWebSocket(stats) {
try {
// 更新统计卡片
if (stats.dns) {
// 适配不同的数据结构
const totalQueries = stats.dns.Queries || 0;
const blockedQueries = stats.dns.Blocked || 0;
const allowedQueries = stats.dns.Allowed || 0;
const errorQueries = stats.dns.Errors || 0;
const avgResponseTime = stats.dns.AvgResponseTime || 0;
const activeIPs = stats.activeIPs || Object.keys(stats.dns.SourceIPs || {}).length;
// 更新统计卡片
document.getElementById('logs-total-queries').textContent = totalQueries;
document.getElementById('logs-avg-response-time').textContent = avgResponseTime.toFixed(2) + 'ms';
document.getElementById('logs-active-ips').textContent = activeIPs;
// 计算屏蔽率
const blockRate = totalQueries > 0 ? (blockedQueries / totalQueries * 100).toFixed(1) : '0';
document.getElementById('logs-block-rate').textContent = blockRate + '%';
}
} catch (error) {
console.error('从WebSocket更新日志统计数据失败:', error);
}
}
// 定期更新日志统计数据(备用方案)
setInterval(() => {
// 只有在查询日志页面时才更新
if (window.location.hash === '#logs') {
loadLogsStats();
// 不自动更新日志详情,只更新统计数据
}
}, 30000); // 每30秒更新一次

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +0,0 @@
tailwind.config = {
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'],
},
},
}
}

View File

@@ -1,194 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS服务器控制台 - 登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: #333;
}
.login-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 24px;
color: #2c3e50;
margin-bottom: 8px;
}
.login-header p {
color: #7f8c8d;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #e1e5e9;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
}
.btn {
width: 100%;
padding: 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: #2980b9;
}
.btn:active {
transform: translateY(1px);
}
.error-message {
background-color: #fee;
color: #c00;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
display: none;
}
.loading {
opacity: 0.7;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>DNS服务器控制台</h1>
<p>请输入您的登录凭据</p>
</div>
<div class="error-message" id="errorMessage"></div>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="btn" id="loginBtn">登录</button>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
const errorMessage = document.getElementById('errorMessage');
// 显示加载状态
loginBtn.textContent = '登录中...';
loginBtn.classList.add('loading');
errorMessage.style.display = 'none';
// 发送登录请求
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
})
.then(response => {
if (!response.ok) {
if (response.status === 401) {
throw new Error('未知用户名或密码');
} else {
throw new Error('登录失败');
}
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
// 登录成功,重定向到主页
window.location.href = '/';
} else {
if (data.error === '用户名或密码错误') {
throw new Error('未知用户名或密码');
} else {
throw new Error(data.error || '登录失败');
}
}
})
.catch(error => {
// 显示错误信息
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
loginBtn.textContent = '登录';
loginBtn.classList.remove('loading');
});
});
</script>
</body>
</html>

View File

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

View File

@@ -1,77 +0,0 @@
package main
import (
"flag"
"fmt"
"os/exec"
"strings"
)
// testRuleMatching 测试DNS规则匹配功能
func main() {
// 定义命令行参数
rulePtr := flag.String("rule", "||cntvwb.cn^", "规则字符串")
testDomainPtr := flag.String("domain", "vdapprecv.app.cntvwb.cn", "测试域名")
flag.Parse()
// 打印测试信息
fmt.Printf("测试规则: %s\n", *rulePtr)
fmt.Printf("测试域名: %s\n", *testDomainPtr)
// 发送HTTP请求到API端点来测试规则匹配
fmt.Println("\n测试规则匹配功能...")
cmd := exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s&rule=%s", *testDomainPtr, *rulePtr))
output, err := cmd.CombinedOutput()
if err != nil {
// 如果直接的API测试失败尝试另一种方法
fmt.Printf("直接测试失败: %v, %s\n", err, string(output))
fmt.Println("尝试添加规则并测试...")
testAddRuleAndCheck(*rulePtr, *testDomainPtr)
return
}
fmt.Printf("测试结果: %s\n", string(output))
// 验证规则是否生效(模拟测试)
if strings.Contains(*rulePtr, "||cntvwb.cn^") && strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("\n验证结果:")
if strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("✅ 子域名匹配测试通过:||cntvwb.cn^ 应该阻止所有 cntvwb.cn 的子域名")
} else {
fmt.Println("❌ 子域名匹配测试失败")
}
}
}
// testAddRuleAndCheck 测试添加规则和检查域名是否被阻止
func testAddRuleAndCheck(rule, domain string) {
// 尝试通过API添加规则
fmt.Printf("添加规则: %s\n", rule)
cmd := exec.Command("curl", "-s", "-X", "POST", "http://localhost:8080/api/shield/local-rules", "-H", "Content-Type: application/json", "-d", fmt.Sprintf(`{\"rule\":\"%s\"}`, rule))
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("添加规则失败: %v, %s\n", err, string(output))
// 尝试重新加载规则
fmt.Println("尝试重新加载规则...")
cmd = exec.Command("curl", "-s", "-X", "PUT", "http://localhost:8080/api/shield", "-H", "Content-Type: application/json", "-d", `{\"reload\":true}`)
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("重新加载规则失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("重新加载规则结果: %s\n", string(output))
}
return
}
fmt.Printf("添加规则结果: %s\n", string(output))
// 测试域名是否被阻止
fmt.Printf("测试域名 %s 是否被阻止...\n", domain)
cmd = exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s", domain))
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("测试阻止失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("阻止测试结果: %s\n", string(output))
}
}

View File

@@ -1,45 +0,0 @@
#!/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"