屏蔽规则页面丰富显示

This commit is contained in:
Alex Yang
2025-11-28 22:55:43 +08:00
parent 24b8cf19aa
commit 8e2ea02a62
19 changed files with 509 additions and 820646 deletions

View File

@@ -0,0 +1,96 @@
## 问题分析
当前系统已经实现了黑名单管理的后端API但前端界面缺少完整的黑名单管理功能。具体来说
1. 后端API已经实现了黑名单管理的所有功能
- GET /api/shield/blacklists - 获取所有黑名单
- POST /api/shield/blacklists - 添加黑名单
- PUT /api/shield/blacklists - 更新黑名单
- DELETE /api/shield/blacklists/{name} - 删除黑名单
2. 前端已经有了一些基础功能:
- loadRemoteBlacklists函数用于加载远程黑名单
- setupShieldEventListeners函数用于设置事件监听器
- 页面上有blacklist-count元素用于显示黑名单数量
3. 但是,前端缺少完整的黑名单管理界面,包括:
- 黑名单列表展示
- 添加/编辑/删除黑名单的功能
- 启用/禁用黑名单的功能
- 更新黑名单的功能
## 实现方案
1. **添加黑名单管理HTML界面**
- 在shield-content中添加黑名单管理区域
- 包括黑名单列表、添加/编辑表单、操作按钮等
2. **实现黑名单列表的加载和展示**
- 完善loadRemoteBlacklists函数将黑名单数据渲染到页面上
- 显示黑名单的名称、URL、状态、更新时间等信息
- 添加操作按钮(编辑、删除、启用/禁用、更新)
3. **实现添加/编辑黑名单功能**
- 添加表单用于输入黑名单名称、URL等信息
- 实现表单提交功能调用后端API添加/编辑黑名单
4. **实现删除黑名单功能**
- 为删除按钮添加事件监听器
- 调用后端API删除黑名单
- 更新黑名单列表
5. **实现启用/禁用黑名单功能**
- 为启用/禁用按钮添加事件监听器
- 调用后端API更新黑名单状态
- 更新黑名单列表
6. **实现更新黑名单功能**
- 为更新按钮添加事件监听器
- 调用后端API更新黑名单
- 显示更新状态
## 实现步骤
1. **修改HTML文件**
- 在shield-content中添加黑名单管理区域
- 添加黑名单列表表格
- 添加添加/编辑黑名单表单
- 添加操作按钮
2. **修改shield.js文件**
- 完善loadRemoteBlacklists函数渲染黑名单列表
- 添加添加/编辑/删除黑名单的函数
- 添加启用/禁用黑名单的函数
- 添加更新黑名单的函数
- 完善setupShieldEventListeners函数添加黑名单管理相关的事件监听器
3. **测试功能**
- 测试黑名单列表的加载和展示
- 测试添加/编辑/删除黑名单功能
- 测试启用/禁用黑名单功能
- 测试更新黑名单功能
## 预期效果
- 用户可以在web界面上查看所有黑名单
- 用户可以添加新的黑名单
- 用户可以编辑现有黑名单
- 用户可以删除黑名单
- 用户可以启用/禁用黑名单
- 用户可以手动更新黑名单
- 所有操作都能实时反映到页面上
## 注意事项
- 确保界面设计与现有系统风格一致
- 确保所有操作都有适当的错误处理和提示
- 确保所有操作都能实时更新页面数据
- 确保功能实现符合后端API的要求
## 技术细节
- 使用HTML、CSSTailwind CSS和JavaScript实现前端界面
- 使用fetch API调用后端API
- 使用事件监听器处理用户交互
- 使用DOM操作更新页面内容
- 使用异步/等待处理异步操作

View File

@@ -0,0 +1,33 @@
# 实现远程黑名单管理功能
## 1. 分析现有代码
- 远程黑名单管理功能已经在`shield.js`文件中实现
- 页面结构已经存在于HTML文件中
- 存在`blacklists.js``shield.js`两个可能冲突的实现
## 2. 解决方案
### 2.1 检查并解决代码冲突
- 移除或整合`blacklists.js`文件,避免与`shield.js`冲突
- 确保只使用一个实现来管理远程黑名单
### 2.2 确保页面加载时正确初始化
- 检查`shield.js`中的初始化逻辑
- 确保`initShieldPage`函数在页面加载和切换到屏蔽管理页面时被正确调用
### 2.3 确保web更新数据时同时更新服务器
- 检查现有的添加、更新、删除、启用/禁用黑名单的功能
- 确保每个操作都通过API请求更新服务器数据
- 验证操作完成后是否重新加载数据
## 3. 实现步骤
1. 移除`blacklists.js`文件,避免与`shield.js`冲突
2. 检查并确保`shield.js`中的初始化逻辑正确
3. 测试远程黑名单管理功能的各个操作
4. 验证页面加载时是否正确拉取服务器数据
5. 验证web更新数据时是否同时更新服务器数据
## 4. 预期结果
- 页面加载时自动拉取服务器数据
- 添加、更新、删除、启用/禁用黑名单时,同时更新服务器数据
- 操作完成后,页面数据自动刷新
- 没有代码冲突,功能正常运行

View File

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

View File

@@ -20,26 +20,30 @@
{ {
"name": "AdGuard DNS filter", "name": "AdGuard DNS filter",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
"enabled": true "enabled": true,
"lastUpdateTime": "2025-11-28T14:43:26.972Z"
}, },
{ {
"name": "Adaway Default Blocklist", "name": "Adaway Default Blocklist",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
"enabled": true "enabled": true,
"lastUpdateTime": "2025-11-28T14:41:39.123Z"
}, },
{ {
"name": "CHN-anti-AD", "name": "CHN-anti-AD",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
"enabled": true "enabled": true,
"lastUpdateTime": "2025-11-28T14:41:41.318Z"
}, },
{ {
"name": "My GitHub Rules", "name": "My GitHub Rules",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
"enabled": true "enabled": true,
"lastUpdateTime": "2025-11-28T14:42:09.271Z"
}, },
{ {
"name": "China List", "name": "China List",
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/china.txt", "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
"enabled": true "enabled": true
} }
], ],

View File

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

View File

@@ -1,2 +0,0 @@
/example.com/
/ad./

View File

@@ -1,5 +0,0 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-11-28T19:19:31.852280335+08:00"
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -875,23 +875,41 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(map[string]string{"status": "success"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
case http.MethodPut: case http.MethodPut:
// 更新所有远程黑名单 // 更新黑名单列表(包括启用/禁用状态)
blacklists := s.shieldManager.GetBlacklists() var updatedBlacklists []struct {
for i := range blacklists { Name string `json:"Name" json:"name"`
// 更新每个黑名单的时间戳 URL string `json:"URL" json:"url"`
blacklists[i].LastUpdateTime = time.Now().Format(time.RFC3339) Enabled bool `json:"Enabled" json:"enabled"`
LastUpdateTime string `json:"LastUpdateTime" json:"lastUpdateTime"`
} }
s.shieldManager.UpdateBlacklist(blacklists) if err := json.NewDecoder(r.Body).Decode(&updatedBlacklists); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 转换为config.BlacklistEntry类型
var newBlacklists []config.BlacklistEntry
for _, entry := range updatedBlacklists {
newBlacklists = append(newBlacklists, config.BlacklistEntry{
Name: entry.Name,
URL: entry.URL,
Enabled: entry.Enabled,
LastUpdateTime: entry.LastUpdateTime,
})
}
// 更新黑名单
s.shieldManager.UpdateBlacklist(newBlacklists)
// 更新全局配置中的黑名单 // 更新全局配置中的黑名单
s.globalConfig.Shield.Blacklists = blacklists s.globalConfig.Shield.Blacklists = newBlacklists
// 保存配置到文件 // 保存配置到文件
if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil { if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
logger.Error("保存配置文件失败", "error", err) logger.Error("保存配置文件失败", "error", err)
http.Error(w, "保存配置失败", http.StatusInternalServerError) http.Error(w, "保存配置失败", http.StatusInternalServerError)
return return
} }
// 重新加载所有规则 // 重新加载规则
s.shieldManager.LoadRules() s.shieldManager.LoadRules()
json.NewEncoder(w).Encode(map[string]string{"status": "success"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -691,35 +691,39 @@
<h4 class="text-sm font-medium text-gray-500">域名例外</h4> <h4 class="text-sm font-medium text-gray-500">域名例外</h4>
<i class="fa fa-check-circle text-green-500"></i> <i class="fa fa-check-circle text-green-500"></i>
</div> </div>
<p class="text-2xl font-bold" id="domain-exceptions-count">0</p> <p class="text-2xl font-bold counter" id="domain-exceptions-count">0</p>
</div> </div>
<div class="bg-purple-50 p-4 rounded-lg"> <div class="bg-purple-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则规则</h4> <h4 class="text-sm font-medium text-gray-500">正则规则</h4>
<i class="fa fa-code text-purple-500"></i> <i class="fa fa-code text-purple-500"></i>
</div> </div>
<p class="text-2xl font-bold" id="regex-rules-count">0</p> <p class="text-2xl font-bold counter" id="regex-rules-count">0</p>
</div> </div>
<div class="bg-yellow-50 p-4 rounded-lg"> <div class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则例外</h4> <h4 class="text-sm font-medium text-gray-500">正则例外</h4>
<i class="fa fa-exclamation-circle text-yellow-500"></i> <i class="fa fa-exclamation-circle text-yellow-500"></i>
</div> </div>
<p class="text-2xl font-bold" id="regex-exceptions-count">0</p> <p class="text-2xl font-bold counter" id="regex-exceptions-count">0</p>
</div> </div>
<div class="bg-red-50 p-4 rounded-lg"> <div class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">Hosts规则</h4> <h4 class="text-sm font-medium text-gray-500">Hosts规则</h4>
<i class="fa fa-file-text text-red-500"></i> <i class="fa fa-file-text text-red-500"></i>
</div> </div>
<p class="text-2xl font-bold" id="hosts-rules-count">0</p> <p class="text-2xl font-bold counter" id="hosts-rules-count">0</p>
</div> </div>
<div class="bg-indigo-50 p-4 rounded-lg"> <div class="bg-indigo-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">黑名单数量</h4> <h4 class="text-sm font-medium text-gray-500">黑名单数量</h4>
<i class="fa fa-ban text-indigo-500"></i> <i class="fa fa-ban text-indigo-500"></i>
</div> </div>
<p class="text-2xl font-bold" id="blacklist-count">0</p> <p class="text-2xl font-bold counter" id="blacklist-count">0</p>
<div class="flex items-center justify-between mt-2">
<h5 class="text-xs font-medium text-gray-500">禁用数量</h5>
<p class="text-sm font-bold text-red-600 counter" id="blacklist-disabled-count">0</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -771,10 +775,11 @@
<label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label> <label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label>
<input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"> <input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div> </div>
<div class="flex items-end"> <div class="flex items-end space-x-2">
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors"> <button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存 保存
</button> </button>
<div id="save-blacklist-status" class="flex items-center text-sm"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -787,12 +792,13 @@
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th> <th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th> <th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th> <th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">更新状态</th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th> <th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="blacklists-table-body"> <tbody id="blacklists-table-body">
<tr> <tr>
<td colspan="4" class="py-4 text-center text-gray-500">暂无黑名单</td> <td colspan="5" class="py-4 text-center text-gray-500">暂无黑名单</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -12,16 +12,115 @@ async function initShieldPage() {
setupShieldEventListeners(); setupShieldEventListeners();
} }
// 更新状态显示函数
function updateStatus(url, status, message) {
const statusElement = document.getElementById(`update-status-${encodeURIComponent(url)}`);
if (!statusElement) return;
let statusHTML = '';
switch (status) {
case 'loading':
statusHTML = '<span class="text-blue-500"><i class="fa fa-spinner fa-spin"></i> 处理中...</span>';
break;
case 'success':
statusHTML = `<span class="text-green-500"><i class="fa fa-check"></i> ${message || '成功'}</span>`;
break;
case 'error':
statusHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${message || '失败'}</span>`;
break;
default:
statusHTML = '<span class="text-gray-400">-</span>';
}
statusElement.innerHTML = statusHTML;
// 如果是成功或失败状态3秒后恢复默认状态
if (status === 'success' || status === 'error') {
setTimeout(() => {
updateStatus(url, 'default');
}, 3000);
}
}
// 数字更新动画函数
function animateCounter(element, target, duration = 1000) {
// 确保element存在
if (!element) return;
// 清除元素上可能存在的现有定时器
if (element.animationTimer) {
clearInterval(element.animationTimer);
}
// 确保target是数字
const targetNum = typeof target === 'number' ? target : parseInt(target) || 0;
// 获取起始值,使用更安全的方法
const startText = element.textContent.replace(/[^0-9]/g, '');
const start = parseInt(startText) || 0;
// 如果起始值和目标值相同,直接返回
if (start === targetNum) {
element.textContent = targetNum;
return;
}
let current = start;
const increment = (targetNum - start) / (duration / 16); // 16ms per frame
// 使用requestAnimationFrame实现更平滑的动画
let startTime = null;
function updateCounter(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用缓动函数使动画更自然
const easeOutQuad = progress * (2 - progress);
current = start + (targetNum - start) * easeOutQuad;
// 根据方向使用floor或ceil确保平滑过渡
const displayValue = targetNum > start ? Math.floor(current) : Math.ceil(current);
element.textContent = displayValue;
if (progress < 1) {
// 继续动画
element.animationTimer = requestAnimationFrame(updateCounter);
} else {
// 动画结束,确保显示准确值
element.textContent = targetNum;
// 清除定时器引用
element.animationTimer = null;
}
}
// 开始动画
element.animationTimer = requestAnimationFrame(updateCounter);
}
// 加载屏蔽规则统计信息 // 加载屏蔽规则统计信息
async function loadShieldStats() { async function loadShieldStats() {
try { try {
const response = await fetch('/api/shield'); // 获取屏蔽规则统计信息
const shieldResponse = await fetch('/api/shield');
if (!response.ok) { if (!shieldResponse.ok) {
throw new Error(`加载失败: ${response.status}`); throw new Error(`加载屏蔽统计失败: ${shieldResponse.status}`);
} }
const stats = await response.json(); const stats = await shieldResponse.json();
// 获取黑名单列表,计算禁用数量
const blacklistsResponse = await fetch('/api/shield/blacklists');
if (!blacklistsResponse.ok) {
throw new Error(`加载黑名单列表失败: ${blacklistsResponse.status}`);
}
const blacklists = await blacklistsResponse.json();
const disabledBlacklistCount = blacklists.filter(blacklist => !blacklist.enabled).length;
// 更新统计信息 // 更新统计信息
const elements = [ const elements = [
@@ -36,12 +135,18 @@ async function loadShieldStats() {
elements.forEach(item => { elements.forEach(item => {
const element = document.getElementById(item.id); const element = document.getElementById(item.id);
if (element) { if (element) {
element.textContent = item.value || 0; animateCounter(element, item.value || 0);
} }
}); });
// 更新禁用黑名单数量
const disabledBlacklistElement = document.getElementById('blacklist-disabled-count');
if (disabledBlacklistElement) {
animateCounter(disabledBlacklistElement, disabledBlacklistCount);
}
} catch (error) { } catch (error) {
console.error('加载屏蔽规则统计信息失败:', error); console.error('加载屏蔽规则统计信息失败:', error);
showErrorMessage('加载屏蔽规则统计信息失败'); showNotification('加载屏蔽规则统计信息失败', 'error');
} }
} }
@@ -86,7 +191,7 @@ async function loadLocalRules() {
updateRulesTable(rules); updateRulesTable(rules);
} catch (error) { } catch (error) {
console.error('加载本地规则失败:', error); console.error('加载本地规则失败:', error);
showErrorMessage('加载本地规则失败'); showNotification('加载本地规则失败', 'error');
} }
} }
@@ -130,7 +235,7 @@ async function loadRemoteRules() {
updateRulesTable(rules); updateRulesTable(rules);
} catch (error) { } catch (error) {
console.error('加载远程规则失败:', error); console.error('加载远程规则失败:', error);
showErrorMessage('加载远程规则失败'); showNotification('加载远程规则失败', 'error');
} }
} }
@@ -251,7 +356,7 @@ async function handleDeleteRule(e) {
const responseData = await response.json(); const responseData = await response.json();
console.log('Response data:', responseData); console.log('Response data:', responseData);
showSuccessMessage('规则删除成功'); showNotification('规则删除成功', 'success');
console.log('Current rules type:', currentRulesType); console.log('Current rules type:', currentRulesType);
// 根据当前显示的规则类型重新加载对应的规则列表 // 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') { if (currentRulesType === 'local') {
@@ -265,7 +370,7 @@ async function handleDeleteRule(e) {
loadShieldStats(); loadShieldStats();
} catch (error) { } catch (error) {
console.error('Error deleting rule:', error); console.error('Error deleting rule:', error);
showErrorMessage('删除规则失败: ' + error.message); showNotification('删除规则失败: ' + error.message, 'error');
} }
} }
@@ -273,7 +378,7 @@ async function handleDeleteRule(e) {
async function handleAddRule() { async function handleAddRule() {
const rule = document.getElementById('new-rule').value.trim(); const rule = document.getElementById('new-rule').value.trim();
if (!rule) { if (!rule) {
showErrorMessage('规则不能为空'); showNotification('规则不能为空', 'error');
return; return;
} }
@@ -290,7 +395,7 @@ async function handleAddRule() {
throw new Error('Failed to add rule'); throw new Error('Failed to add rule');
} }
showSuccessMessage('规则添加成功'); showNotification('规则添加成功', 'success');
// 清空输入框 // 清空输入框
document.getElementById('new-rule').value = ''; document.getElementById('new-rule').value = '';
// 重新加载规则 // 重新加载规则
@@ -299,7 +404,7 @@ async function handleAddRule() {
loadShieldStats(); loadShieldStats();
} catch (error) { } catch (error) {
console.error('Error adding rule:', error); console.error('Error adding rule:', error);
showErrorMessage('添加规则失败'); showNotification('添加规则失败', 'error');
} }
} }
@@ -319,7 +424,7 @@ async function loadRemoteBlacklists() {
updateBlacklistsTable(blacklistArray); updateBlacklistsTable(blacklistArray);
} catch (error) { } catch (error) {
console.error('加载远程黑名单失败:', error); console.error('加载远程黑名单失败:', error);
showErrorMessage('加载远程黑名单失败'); showNotification('加载远程黑名单失败', 'error');
} }
} }
@@ -365,30 +470,24 @@ function updateBlacklistsTable(blacklists) {
// 名称单元格 // 名称单元格
const tdName = document.createElement('td'); const tdName = document.createElement('td');
tdName.className = 'py-3 px-4'; tdName.className = 'py-3 px-4';
tdName.textContent = blacklist.Name || '未命名'; tdName.textContent = blacklist.name || '未命名';
// URL单元格 // URL单元格
const tdUrl = document.createElement('td'); const tdUrl = document.createElement('td');
tdUrl.className = 'py-3 px-4 truncate max-w-xs'; tdUrl.className = 'py-3 px-4 truncate max-w-xs';
tdUrl.textContent = blacklist.URL; tdUrl.textContent = blacklist.url;
// 状态单元格 // 状态单元格
const tdStatus = document.createElement('td'); const tdStatus = document.createElement('td');
tdStatus.className = 'py-3 px-4 text-center'; tdStatus.className = 'py-3 px-4 text-center';
// 判断状态颜色:绿色(正常)、黄色(过期)、灰色(禁用) // 判断状态颜色:绿色(启用)、灰色(禁用)
let statusColor = 'bg-gray-300'; // 默认禁用 let statusColor = 'bg-gray-300'; // 默认禁用
let statusText = '禁用'; let statusText = '禁用';
if (blacklist.Enabled) { if (blacklist.enabled) {
const expired = isBlacklistExpired(blacklist.lastUpdateTime || blacklist.LastUpdateTime); statusColor = 'bg-success'; // 绿色表示启用
if (expired) { statusText = '启用';
statusColor = 'bg-warning'; // 黄色表示过期
statusText = '过期';
} else {
statusColor = 'bg-success'; // 绿色表示正常
statusText = '正常';
}
} }
const statusContainer = document.createElement('div'); const statusContainer = document.createElement('div');
@@ -406,14 +505,29 @@ function updateBlacklistsTable(blacklists) {
statusContainer.appendChild(statusTextSpan); statusContainer.appendChild(statusTextSpan);
tdStatus.appendChild(statusContainer); tdStatus.appendChild(statusContainer);
// 更新状态单元格
const tdUpdateStatus = document.createElement('td');
tdUpdateStatus.className = 'py-3 px-4 text-center';
tdUpdateStatus.id = `update-status-${encodeURIComponent(blacklist.url)}`;
tdUpdateStatus.innerHTML = '<span class="text-gray-400">-</span>';
// 操作单元格 // 操作单元格
const tdActions = document.createElement('td'); const tdActions = document.createElement('td');
tdActions.className = 'py-3 px-4 text-right space-x-2'; tdActions.className = 'py-3 px-4 text-right space-x-2';
// 启用/禁用按钮
const toggleBtn = document.createElement('button');
toggleBtn.className = `toggle-blacklist-btn px-3 py-1 rounded-md transition-colors text-sm ${blacklist.enabled ? 'bg-warning text-white hover:bg-warning/90' : 'bg-success text-white hover:bg-success/90'}`;
toggleBtn.dataset.url = blacklist.url;
toggleBtn.dataset.enabled = blacklist.enabled;
toggleBtn.innerHTML = `<i class="fa fa-${blacklist.enabled ? 'toggle-on' : 'toggle-off'}"></i>`;
toggleBtn.title = blacklist.enabled ? '禁用黑名单' : '启用黑名单';
toggleBtn.addEventListener('click', handleToggleBlacklist);
// 刷新按钮 // 刷新按钮
const refreshBtn = document.createElement('button'); const refreshBtn = document.createElement('button');
refreshBtn.className = 'update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm'; refreshBtn.className = 'update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm';
refreshBtn.dataset.url = blacklist.URL; refreshBtn.dataset.url = blacklist.url;
refreshBtn.innerHTML = '<i class="fa fa-refresh"></i>'; refreshBtn.innerHTML = '<i class="fa fa-refresh"></i>';
refreshBtn.title = '刷新黑名单'; refreshBtn.title = '刷新黑名单';
refreshBtn.addEventListener('click', handleUpdateBlacklist); refreshBtn.addEventListener('click', handleUpdateBlacklist);
@@ -421,17 +535,19 @@ function updateBlacklistsTable(blacklists) {
// 删除按钮 // 删除按钮
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-blacklist-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm'; deleteBtn.className = 'delete-blacklist-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm';
deleteBtn.dataset.url = blacklist.URL; deleteBtn.dataset.url = blacklist.url;
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>'; deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
deleteBtn.title = '删除黑名单'; deleteBtn.title = '删除黑名单';
deleteBtn.addEventListener('click', handleDeleteBlacklist); deleteBtn.addEventListener('click', handleDeleteBlacklist);
tdActions.appendChild(toggleBtn);
tdActions.appendChild(refreshBtn); tdActions.appendChild(refreshBtn);
tdActions.appendChild(deleteBtn); tdActions.appendChild(deleteBtn);
tr.appendChild(tdName); tr.appendChild(tdName);
tr.appendChild(tdUrl); tr.appendChild(tdUrl);
tr.appendChild(tdStatus); tr.appendChild(tdStatus);
tr.appendChild(tdUpdateStatus);
tr.appendChild(tdActions); tr.appendChild(tdActions);
fragment.appendChild(tr); fragment.appendChild(tr);
}); });
@@ -449,43 +565,93 @@ function updateBlacklistsTable(blacklists) {
// 处理更新单个黑名单 // 处理更新单个黑名单
async function handleUpdateBlacklist(e) { async function handleUpdateBlacklist(e) {
const url = e.target.closest('.update-blacklist-btn').dataset.url; // 确保获取到正确的按钮元素
const btn = e.target.closest('.update-blacklist-btn');
if (!btn) {
console.error('未找到更新按钮元素');
return;
}
const url = btn.dataset.url;
if (!url) { if (!url) {
showToast('无效的黑名单URL', 'error'); showNotification('无效的黑名单URL', 'error');
return; return;
} }
try { try {
const response = await fetch(`/api/shield/blacklists/${encodeURIComponent(url)}/update`, { // 显示加载状态
method: 'POST', updateStatus(url, 'loading');
headers: {
'Content-Type': 'application/json' // 获取当前所有黑名单
const response = await fetch('/api/shield/blacklists');
if (!response.ok) {
throw new Error(`获取黑名单失败: ${response.status}`);
}
const blacklists = await response.json();
// 找到目标黑名单并更新其状态
const updatedBlacklists = blacklists.map(blacklist => {
if (blacklist.url === url) {
return {
Name: blacklist.name,
URL: blacklist.url,
Enabled: blacklist.enabled,
LastUpdateTime: new Date().toISOString()
};
} }
return {
Name: blacklist.name,
URL: blacklist.url,
Enabled: blacklist.enabled,
LastUpdateTime: blacklist.lastUpdateTime || blacklist.LastUpdateTime
};
}); });
if (!response.ok) { // 发送更新请求
const errorData = await response.json().catch(() => ({})); const updateResponse = await fetch('/api/shield/blacklists', {
throw new Error(errorData.error || 'Failed to update blacklist'); method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedBlacklists)
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`更新失败: ${updateResponse.status} ${errorText}`);
} }
// 显示成功状态
updateStatus(url, 'success');
// 重新加载黑名单 // 重新加载黑名单
loadRemoteBlacklists(); loadRemoteBlacklists();
// 重新加载统计信息 // 重新加载统计信息
loadShieldStats(); loadShieldStats();
showToast('黑名单更新成功'); showNotification('黑名单更新成功', 'success');
} catch (error) { } catch (error) {
console.error('更新黑名单失败:', error); console.error('更新黑名单失败:', error);
showToast('更新黑名单失败: ' + error.message, 'error'); // 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('更新黑名单失败: ' + error.message, 'error');
} }
} }
// 处理删除黑名单 // 处理删除黑名单
async function handleDeleteBlacklist(e) { async function handleDeleteBlacklist(e) {
const url = e.target.closest('.delete-blacklist-btn').dataset.url; // 确保获取到正确的按钮元素
const btn = e.target.closest('.delete-blacklist-btn');
if (!btn) {
console.error('未找到删除按钮元素');
return;
}
const url = btn.dataset.url;
if (!url) { if (!url) {
showToast('无效的黑名单URL', 'error'); showNotification('无效的黑名单URL', 'error');
return; return;
} }
@@ -495,26 +661,124 @@ async function handleDeleteBlacklist(e) {
} }
try { try {
const response = await fetch(`/api/shield/blacklists/${encodeURIComponent(url)}`, { // 显示加载状态
method: 'DELETE', updateStatus(url, 'loading');
// 获取当前所有黑名单
const response = await fetch('/api/shield/blacklists');
if (!response.ok) {
throw new Error(`获取黑名单失败: ${response.status}`);
}
const blacklists = await response.json();
// 过滤掉要删除的黑名单
const updatedBlacklists = blacklists.filter(blacklist => blacklist.url !== url);
// 发送更新请求
const updateResponse = await fetch('/api/shield/blacklists', {
method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
body: JSON.stringify(updatedBlacklists)
}); });
if (!response.ok) { if (!updateResponse.ok) {
const errorData = await response.json().catch(() => ({})); const errorText = await updateResponse.text();
throw new Error(errorData.error || 'Failed to delete blacklist'); throw new Error(`删除失败: ${updateResponse.status} ${errorText}`);
} }
// 显示成功状态
updateStatus(url, 'success', '已删除');
// 重新加载黑名单 // 重新加载黑名单
loadRemoteBlacklists(); loadRemoteBlacklists();
// 重新加载统计信息 // 重新加载统计信息
loadShieldStats(); loadShieldStats();
showToast('黑名单删除成功'); showNotification('黑名单删除成功', 'success');
} catch (error) { } catch (error) {
console.error('删除黑名单失败:', error); console.error('删除黑名单失败:', error);
showToast('删除黑名单失败: ' + error.message, 'error'); // 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('删除黑名单失败: ' + error.message, 'error');
}
}
// 处理启用/禁用黑名单
async function handleToggleBlacklist(e) {
// 确保获取到正确的按钮元素
const btn = e.target.closest('.toggle-blacklist-btn');
if (!btn) {
console.error('未找到启用/禁用按钮元素');
return;
}
const url = btn.dataset.url;
const currentEnabled = btn.dataset.enabled === 'true';
if (!url) {
showNotification('无效的黑名单URL', 'error');
return;
}
try {
// 显示加载状态
updateStatus(url, 'loading');
// 获取当前所有黑名单
const response = await fetch('/api/shield/blacklists');
if (!response.ok) {
throw new Error(`获取黑名单失败: ${response.status}`);
}
const blacklists = await response.json();
// 找到目标黑名单并更新其状态
const updatedBlacklists = blacklists.map(blacklist => {
if (blacklist.url === url) {
return {
Name: blacklist.name,
URL: blacklist.url,
Enabled: !currentEnabled,
LastUpdateTime: blacklist.lastUpdateTime || blacklist.LastUpdateTime
};
}
return {
Name: blacklist.name,
URL: blacklist.url,
Enabled: blacklist.enabled,
LastUpdateTime: blacklist.lastUpdateTime || blacklist.LastUpdateTime
};
});
// 发送更新请求
const updateResponse = await fetch('/api/shield/blacklists', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedBlacklists)
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`更新状态失败: ${updateResponse.status} ${errorText}`);
}
// 显示成功状态
updateStatus(url, 'success', currentEnabled ? '已禁用' : '已启用');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showNotification(`黑名单已${currentEnabled ? '禁用' : '启用'}`, 'success');
} catch (error) {
console.error('启用/禁用黑名单失败:', error);
// 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('启用/禁用黑名单失败: ' + error.message, 'error');
} }
} }
@@ -527,13 +791,14 @@ async function handleAddBlacklist(event) {
const nameInput = document.getElementById('blacklist-name'); const nameInput = document.getElementById('blacklist-name');
const urlInput = document.getElementById('blacklist-url'); const urlInput = document.getElementById('blacklist-url');
const statusElement = document.getElementById('save-blacklist-status');
const name = nameInput ? nameInput.value.trim() : ''; const name = nameInput ? nameInput.value.trim() : '';
const url = urlInput ? urlInput.value.trim() : ''; const url = urlInput ? urlInput.value.trim() : '';
// 简单验证 // 简单验证
if (!name || !url) { if (!name || !url) {
showErrorMessage('名称和URL不能为空'); showNotification('名称和URL不能为空', 'error');
return; return;
} }
@@ -541,11 +806,15 @@ async function handleAddBlacklist(event) {
try { try {
new URL(url); new URL(url);
} catch (e) { } catch (e) {
showErrorMessage('URL格式不正确'); showNotification('URL格式不正确', 'error');
return; return;
} }
try { try {
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 发送添加请求
const response = await fetch('/api/shield/blacklists', { const response = await fetch('/api/shield/blacklists', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -568,7 +837,10 @@ async function handleAddBlacklist(event) {
throw new Error(errorMessage); throw new Error(errorMessage);
} }
showSuccessMessage('黑名单添加成功'); // 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
showNotification('黑名单添加成功', 'success');
// 清空输入框 // 清空输入框
if (nameInput) nameInput.value = ''; if (nameInput) nameInput.value = '';
if (urlInput) urlInput.value = ''; if (urlInput) urlInput.value = '';
@@ -578,7 +850,14 @@ async function handleAddBlacklist(event) {
loadShieldStats(); loadShieldStats();
} catch (error) { } catch (error) {
console.error('Error adding blacklist:', error); console.error('Error adding blacklist:', error);
showErrorMessage(error.message || '添加黑名单失败'); // 显示错误状态
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> 失败</span>`;
showNotification(error.message || '添加黑名单失败', 'error');
} finally {
// 3秒后清除状态显示
setTimeout(() => {
statusElement.innerHTML = '';
}, 3000);
} }
} }