Files
dns-server/static/js/shield.js
Alex Yang 3207510c91 Web优化
2025-11-28 23:59:58 +08:00

1302 lines
42 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 屏蔽管理页面功能实现
// 初始化屏蔽管理页面
async function initShieldPage() {
// 并行加载所有数据
await Promise.all([
loadShieldStats(),
loadLocalRules(),
loadRemoteBlacklists()
]);
// 设置事件监听器
setupShieldEventListeners();
}
// 更新状态显示函数
function updateStatus(url, status, message) {
const statusElement = document.getElementById(`update-status-${encodeURIComponent(url)}`);
if (!statusElement) return;
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
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>';
}
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 设置新的HTML内容
statusElement.innerHTML = statusHTML;
// 添加过渡类和对应状态类
statusElement.classList.add('status-transition');
// 如果不是默认状态,添加淡入动画和对应状态类
if (status !== 'default') {
statusElement.classList.add('status-fade-in');
statusElement.classList.add(`status-${status}`);
}
// 如果是成功或失败状态3秒后渐变消失
if (status === 'success' || status === 'error') {
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后切换到默认状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 设置默认状态
statusElement.innerHTML = '<span class="text-gray-400">-</span>';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
// 更新规则状态显示函数
function updateRuleStatus(rule, status, message) {
const statusElement = document.getElementById(`rule-status-${encodeURIComponent(rule)}`);
if (!statusElement) return;
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
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>';
}
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 设置新的HTML内容
statusElement.innerHTML = statusHTML;
// 添加过渡类和对应状态类
statusElement.classList.add('status-transition');
// 如果不是默认状态,添加淡入动画和对应状态类
if (status !== 'default') {
statusElement.classList.add('status-fade-in');
statusElement.classList.add(`status-${status}`);
}
// 如果是成功或失败状态3秒后渐变消失
if (status === 'success' || status === 'error') {
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后切换到默认状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 设置默认状态
statusElement.innerHTML = '<span class="text-gray-400">-</span>';
}, 300); // 与CSS动画持续时间一致
}, 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() {
try {
// 获取屏蔽规则统计信息
const shieldResponse = await fetch('/api/shield');
if (!shieldResponse.ok) {
throw new Error(`加载屏蔽统计失败: ${shieldResponse.status}`);
}
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 = [
{ id: 'domain-rules-count', value: stats.domainRulesCount },
{ id: 'domain-exceptions-count', value: stats.domainExceptionsCount },
{ id: 'regex-rules-count', value: stats.regexRulesCount },
{ id: 'regex-exceptions-count', value: stats.regexExceptionsCount },
{ id: 'hosts-rules-count', value: stats.hostsRulesCount },
{ id: 'blacklist-count', value: stats.blacklistCount }
];
elements.forEach(item => {
const element = document.getElementById(item.id);
if (element) {
animateCounter(element, item.value || 0);
}
});
// 更新禁用黑名单数量
const disabledBlacklistElement = document.getElementById('blacklist-disabled-count');
if (disabledBlacklistElement) {
animateCounter(disabledBlacklistElement, disabledBlacklistCount);
}
} catch (error) {
console.error('加载屏蔽规则统计信息失败:', error);
showNotification('加载屏蔽规则统计信息失败', 'error');
}
}
// 加载本地规则
async function loadLocalRules() {
try {
const response = await fetch('/api/shield/localrules');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
}
const data = await response.json();
// 更新本地规则数量显示
if (document.getElementById('local-rules-count')) {
document.getElementById('local-rules-count').textContent = data.localRulesCount || 0;
}
// 设置当前规则类型
currentRulesType = 'local';
// 合并所有本地规则
let rules = [];
// 添加域名规则
if (Array.isArray(data.domainRules)) {
rules = rules.concat(data.domainRules);
}
// 添加域名排除规则
if (Array.isArray(data.domainExceptions)) {
rules = rules.concat(data.domainExceptions);
}
// 添加正则规则
if (Array.isArray(data.regexRules)) {
rules = rules.concat(data.regexRules);
}
// 添加正则排除规则
if (Array.isArray(data.regexExceptions)) {
rules = rules.concat(data.regexExceptions);
}
updateRulesTable(rules);
} catch (error) {
console.error('加载本地规则失败:', error);
showNotification('加载本地规则失败', 'error');
}
}
// 加载远程规则
async function loadRemoteRules() {
try {
// 设置当前规则类型
currentRulesType = 'remote';
const response = await fetch('/api/shield/remoterules');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
}
const data = await response.json();
// 更新远程规则数量显示
if (document.getElementById('remote-rules-count')) {
document.getElementById('remote-rules-count').textContent = data.remoteRulesCount || 0;
}
// 合并所有远程规则
let rules = [];
// 添加域名规则
if (Array.isArray(data.domainRules)) {
rules = rules.concat(data.domainRules);
}
// 添加域名排除规则
if (Array.isArray(data.domainExceptions)) {
rules = rules.concat(data.domainExceptions);
}
// 添加正则规则
if (Array.isArray(data.regexRules)) {
rules = rules.concat(data.regexRules);
}
// 添加正则排除规则
if (Array.isArray(data.regexExceptions)) {
rules = rules.concat(data.regexExceptions);
}
updateRulesTable(rules);
} catch (error) {
console.error('加载远程规则失败:', error);
showNotification('加载远程规则失败', 'error');
}
}
// 更新规则表格
function updateRulesTable(rules) {
const tbody = document.getElementById('rules-table-body');
// 清空表格
tbody.innerHTML = '';
if (rules.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="3" class="py-4 text-center text-gray-500">暂无规则</td>';
tbody.appendChild(emptyRow);
return;
}
// 对于大量规则,限制显示数量
const maxRulesToShow = 1000; // 限制最大显示数量
const rulesToShow = rules.length > maxRulesToShow ? rules.slice(0, maxRulesToShow) : rules;
// 使用DocumentFragment提高性能
const fragment = document.createDocumentFragment();
rulesToShow.forEach(rule => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200';
const tdRule = document.createElement('td');
tdRule.className = 'py-3 px-4';
tdRule.textContent = rule;
const tdStatus = document.createElement('td');
tdStatus.className = 'py-3 px-4 text-center';
tdStatus.id = `rule-status-${encodeURIComponent(rule)}`;
tdStatus.innerHTML = '<span class="text-gray-400">-</span>';
const tdAction = document.createElement('td');
tdAction.className = 'py-3 px-4 text-right';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-rule-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm';
deleteBtn.dataset.rule = rule;
// 创建删除图标
const deleteIcon = document.createElement('i');
deleteIcon.className = 'fa fa-trash';
deleteIcon.style.pointerEvents = 'none'; // 防止图标拦截点击事件
deleteBtn.appendChild(deleteIcon);
// 使用普通函数确保this指向按钮元素
deleteBtn.onclick = function(e) {
e.stopPropagation(); // 阻止事件冒泡
handleDeleteRule(e);
};
tdAction.appendChild(deleteBtn);
tr.appendChild(tdRule);
tr.appendChild(tdStatus);
tr.appendChild(tdAction);
fragment.appendChild(tr);
});
// 一次性添加所有行到DOM
tbody.appendChild(fragment);
// 如果有更多规则,添加提示
if (rules.length > maxRulesToShow) {
const infoRow = document.createElement('tr');
infoRow.innerHTML = `<td colspan="3" class="py-4 text-center text-gray-500">显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条</td>`;
tbody.appendChild(infoRow);
}
}
// 处理删除规则
async function handleDeleteRule(e) {
console.log('Delete button clicked');
let deleteBtn;
// 尝试从事件目标获取按钮元素
deleteBtn = e.target.closest('.delete-rule-btn');
console.log('Delete button from event target:', deleteBtn);
// 尝试从this获取按钮元素备用方案
if (!deleteBtn && this && typeof this.classList === 'object' && this.classList) {
if (this.classList.contains('delete-rule-btn')) {
deleteBtn = this;
console.log('Delete button from this:', deleteBtn);
}
}
if (!deleteBtn) {
console.error('Delete button not found');
return;
}
const rule = deleteBtn.dataset.rule;
console.log('Rule to delete:', rule);
if (!rule) {
console.error('Rule not found in data-rule attribute');
return;
}
try {
// 显示加载状态
updateRuleStatus(rule, 'loading');
console.log('Sending DELETE request to /api/shield');
const response = await fetch('/api/shield', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ rule })
});
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
console.log('Response data:', responseData);
// 根据服务器响应判断是否成功
if (response.ok && responseData.status === 'success') {
// 显示成功状态
updateRuleStatus(rule, 'success', '已删除');
showNotification('规则删除成功', 'success');
console.log('Current rules type:', currentRulesType);
// 延迟重新加载规则列表和统计信息,让用户能看到成功状态
setTimeout(() => {
// 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') {
console.log('Reloading local rules');
loadLocalRules();
} else {
console.log('Reloading remote rules');
loadRemoteRules();
}
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
const errorMessage = responseData.error || responseData.message || `删除规则失败: ${response.status}`;
// 显示错误状态
updateRuleStatus(rule, 'error', errorMessage);
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error deleting rule:', error);
// 显示错误状态
updateRuleStatus(rule, 'error', error.message);
showNotification('删除规则失败: ' + error.message, 'error');
}
}
// 添加新规则
async function handleAddRule() {
const rule = document.getElementById('new-rule').value.trim();
const statusElement = document.getElementById('save-rule-status');
if (!rule) {
showNotification('规则不能为空', 'error');
return;
}
try {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和加载状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-loading');
const response = await fetch('/api/shield', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ rule })
});
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
// 根据服务器响应判断是否成功
if (response.ok && responseData.status === 'success') {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和成功状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-success');
showNotification('规则添加成功', 'success');
// 清空输入框
document.getElementById('new-rule').value = '';
// 延迟重新加载规则和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载规则
loadLocalRules();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示失败状态
const errorMessage = responseData.error || responseData.message || '添加规则失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
}
} catch (error) {
console.error('Error adding rule:', error);
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示错误状态
const errorMessage = error.message || '添加规则失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
} finally {
// 3秒后渐变消失
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后清除状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 清空状态显示
statusElement.innerHTML = '';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
// 加载远程黑名单
async function loadRemoteBlacklists() {
try {
const response = await fetch('/api/shield/blacklists');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
}
const blacklists = await response.json();
// 确保blacklists是数组
const blacklistArray = Array.isArray(blacklists) ? blacklists : [];
updateBlacklistsTable(blacklistArray);
} catch (error) {
console.error('加载远程黑名单失败:', error);
showNotification('加载远程黑名单失败', 'error');
}
}
// 判断黑名单是否过期超过24小时未更新视为过期
function isBlacklistExpired(lastUpdateTime) {
if (!lastUpdateTime) {
return true; // 从未更新过,视为过期
}
const lastUpdate = new Date(lastUpdateTime);
const now = new Date();
const hoursDiff = (now - lastUpdate) / (1000 * 60 * 60);
return hoursDiff > 24; // 超过24小时视为过期
}
// 更新黑名单表格
function updateBlacklistsTable(blacklists) {
const tbody = document.getElementById('blacklists-table-body');
// 清空表格
tbody.innerHTML = '';
// 检查黑名单数据是否为空
if (!blacklists || blacklists.length === 0) {
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-gray-500">暂无黑名单</td></tr>';
tbody.appendChild(emptyRow);
return;
}
// 对于大量黑名单,限制显示数量
const maxBlacklistsToShow = 100; // 限制最大显示数量
const blacklistsToShow = blacklists.length > maxBlacklistsToShow ? blacklists.slice(0, maxBlacklistsToShow) : blacklists;
// 使用DocumentFragment提高性能
const fragment = document.createDocumentFragment();
blacklistsToShow.forEach(blacklist => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200 hover:bg-gray-50';
// 名称单元格
const tdName = document.createElement('td');
tdName.className = 'py-3 px-4';
tdName.textContent = blacklist.name || '未命名';
// URL单元格
const tdUrl = document.createElement('td');
tdUrl.className = 'py-3 px-4 truncate max-w-xs';
tdUrl.textContent = blacklist.url;
// 状态单元格
const tdStatus = document.createElement('td');
tdStatus.className = 'py-3 px-4 text-center';
// 判断状态颜色:绿色(启用)、灰色(禁用)
let statusColor = 'bg-gray-300'; // 默认禁用
let statusText = '禁用';
if (blacklist.enabled) {
statusColor = 'bg-success'; // 绿色表示启用
statusText = '启用';
}
const statusContainer = document.createElement('div');
statusContainer.className = 'flex items-center justify-center';
const statusDot = document.createElement('span');
statusDot.className = `inline-block w-3 h-3 rounded-full ${statusColor}`;
statusDot.title = statusText;
const statusTextSpan = document.createElement('span');
statusTextSpan.className = 'text-sm ml-2';
statusTextSpan.textContent = statusText;
statusContainer.appendChild(statusDot);
statusContainer.appendChild(statusTextSpan);
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');
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');
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.innerHTML = '<i class="fa fa-refresh"></i>';
refreshBtn.title = '刷新黑名单';
refreshBtn.addEventListener('click', handleUpdateBlacklist);
// 删除按钮
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.dataset.url = blacklist.url;
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
deleteBtn.title = '删除黑名单';
deleteBtn.addEventListener('click', handleDeleteBlacklist);
tdActions.appendChild(toggleBtn);
tdActions.appendChild(refreshBtn);
tdActions.appendChild(deleteBtn);
tr.appendChild(tdName);
tr.appendChild(tdUrl);
tr.appendChild(tdStatus);
tr.appendChild(tdUpdateStatus);
tr.appendChild(tdActions);
fragment.appendChild(tr);
});
// 一次性添加所有行到DOM
tbody.appendChild(fragment);
// 如果有更多黑名单,添加提示
if (blacklists.length > maxBlacklistsToShow) {
const infoRow = document.createElement('tr');
infoRow.innerHTML = `<td colspan="4" class="py-4 text-center text-gray-500">显示前 ${maxBlacklistsToShow} 个黑名单,共 ${blacklists.length} 个</td>`;
tbody.appendChild(infoRow);
}
}
// 处理更新单个黑名单
async function handleUpdateBlacklist(e) {
// 确保获取到正确的按钮元素
const btn = e.target.closest('.update-blacklist-btn');
if (!btn) {
console.error('未找到更新按钮元素');
return;
}
const url = btn.dataset.url;
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: blacklist.enabled,
LastUpdateTime: new Date().toISOString()
};
}
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)
});
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 根据服务器响应判断是否成功
if (updateResponse.ok && (responseData.status === 'success' || !responseData.status)) {
// 显示成功状态
updateStatus(url, 'success');
// 显示通知
showNotification('黑名单更新成功', 'success');
// 延迟重新加载黑名单和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 显示失败状态
updateStatus(url, 'error', responseData.error || responseData.message || `更新失败: ${updateResponse.status}`);
showNotification(`黑名单更新失败: ${responseData.error || responseData.message || updateResponse.status}`, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('更新黑名单失败:', error);
// 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('更新黑名单失败: ' + error.message, 'error');
}
}
// 处理删除黑名单
async function handleDeleteBlacklist(e) {
// 确保获取到正确的按钮元素
const btn = e.target.closest('.delete-blacklist-btn');
if (!btn) {
console.error('未找到删除按钮元素');
return;
}
const url = btn.dataset.url;
if (!url) {
showNotification('无效的黑名单URL', 'error');
return;
}
// 确认删除
if (!confirm('确定要删除这个黑名单吗?删除后将无法恢复。')) {
return;
}
try {
// 获取当前行元素
const tr = btn.closest('tr');
if (!tr) {
console.error('未找到行元素');
return;
}
// 显示加载状态
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: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedBlacklists)
});
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 根据服务器响应判断是否成功
if (updateResponse.ok && responseData.status === 'success') {
// 显示成功状态
updateStatus(url, 'success', '已删除');
// 显示通知
showNotification('黑名单删除成功', 'success');
// 延迟后渐变移除该行
setTimeout(() => {
// 添加渐变移除类
tr.style.transition = 'all 0.3s ease-in-out';
tr.style.opacity = '0';
tr.style.transform = 'translateX(-10px)';
tr.style.height = tr.offsetHeight + 'px';
tr.style.overflow = 'hidden';
// 等待过渡效果完成后,隐藏该行
setTimeout(() => {
tr.style.display = 'none';
// 延迟重新加载黑名单和统计信息,确保视觉效果完成
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 100);
}, 300);
}, 3000);
} else {
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `删除失败: ${updateResponse.status}`;
updateStatus(url, 'error', errorMessage);
showNotification(errorMessage, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('删除黑名单失败:', 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)
});
// 解析服务器响应
let responseData;
try {
responseData = await updateResponse.json();
} catch (jsonError) {
responseData = {};
}
// 根据服务器响应判断是否成功
if (updateResponse.ok && responseData.status === 'success') {
// 显示成功状态
updateStatus(url, 'success', currentEnabled ? '已禁用' : '已启用');
// 显示通知
showNotification(`黑名单已${currentEnabled ? '禁用' : '启用'}`, 'success');
// 延迟重新加载黑名单和统计信息,让用户能看到成功状态
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
} else {
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `更新状态失败: ${updateResponse.status}`;
updateStatus(url, 'error', errorMessage);
showNotification(errorMessage, 'error');
// 延迟重新加载黑名单和统计信息
setTimeout(() => {
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
}, 3000);
}
} catch (error) {
console.error('启用/禁用黑名单失败:', error);
// 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('启用/禁用黑名单失败: ' + error.message, 'error');
}
}
// 处理添加黑名单
async function handleAddBlacklist(event) {
// 如果存在event参数则调用preventDefault()防止表单默认提交
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
const nameInput = document.getElementById('blacklist-name');
const urlInput = document.getElementById('blacklist-url');
const statusElement = document.getElementById('save-blacklist-status');
const name = nameInput ? nameInput.value.trim() : '';
const url = urlInput ? urlInput.value.trim() : '';
// 简单验证
if (!name || !url) {
showNotification('名称和URL不能为空', 'error');
return;
}
// 验证URL格式
try {
new URL(url);
} catch (e) {
showNotification('URL格式不正确', 'error');
return;
}
try {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和加载状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-loading');
// 发送添加请求
const response = await fetch('/api/shield/blacklists', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, url })
});
// 解析服务器响应
let responseData;
try {
responseData = await response.json();
} catch (jsonError) {
responseData = {};
}
// 根据服务器响应判断是否成功
if (response.ok && (responseData.status === 'success' || !responseData.status)) {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示成功状态
statusElement.innerHTML = '<span class="text-green-500"><i class="fa fa-check"></i> 成功</span>';
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和成功状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-success');
showNotification('黑名单添加成功', 'success');
// 清空输入框
if (nameInput) nameInput.value = '';
if (urlInput) urlInput.value = '';
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
} else {
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示失败状态
const errorMessage = responseData.error || responseData.message || `添加失败: ${response.status}`;
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
}
} catch (error) {
console.error('Error adding blacklist:', error);
// 清除之前的所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 显示错误状态
const errorMessage = error.message || '添加黑名单失败';
statusElement.innerHTML = `<span class="text-red-500"><i class="fa fa-times"></i> ${errorMessage}</span>`;
// 强制重排,确保过渡效果生效
void statusElement.offsetWidth;
// 添加过渡类和错误状态类
statusElement.classList.add('status-transition', 'status-fade-in', 'status-error');
showNotification(errorMessage, 'error');
} finally {
// 3秒后渐变消失
setTimeout(() => {
// 添加淡出类
statusElement.classList.add('status-fade-out');
// 等待淡出动画完成后清除状态
setTimeout(() => {
// 清除所有类
statusElement.classList.remove('status-transition', 'status-loading', 'status-success', 'status-error', 'status-fade-in', 'status-fade-out');
// 清空状态显示
statusElement.innerHTML = '';
}, 300); // 与CSS动画持续时间一致
}, 3000);
}
}
// 当前显示的规则类型:'local' 或 'remote'
let currentRulesType = 'local';
// 设置事件监听器
function setupShieldEventListeners() {
// 本地规则管理事件
const saveRuleBtn = document.getElementById('save-rule-btn');
if (saveRuleBtn) {
saveRuleBtn.addEventListener('click', handleAddRule);
}
// 远程黑名单管理事件
const saveBlacklistBtn = document.getElementById('save-blacklist-btn');
if (saveBlacklistBtn) {
saveBlacklistBtn.addEventListener('click', handleAddBlacklist);
}
// 添加切换查看本地规则和远程规则的事件监听
const viewLocalRulesBtn = document.getElementById('view-local-rules-btn');
if (viewLocalRulesBtn) {
viewLocalRulesBtn.addEventListener('click', loadLocalRules);
}
const viewRemoteRulesBtn = document.getElementById('view-remote-rules-btn');
if (viewRemoteRulesBtn) {
viewRemoteRulesBtn.addEventListener('click', loadRemoteRules);
}
}
// 显示成功消息
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', initShieldPage);
} else {
initShieldPage();
}
// 当切换到屏蔽管理页面时重新加载数据
document.addEventListener('DOMContentLoaded', () => {
// 监听hash变化当切换到屏蔽管理页面时重新加载数据
window.addEventListener('hashchange', () => {
if (window.location.hash === '#shield') {
initShieldPage();
}
});
});