Files
dns-server/static/js/shield.js
2025-11-28 18:41:55 +08:00

689 lines
20 KiB
JavaScript
Raw 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();
}
// 加载屏蔽规则统计信息
async function loadShieldStats() {
try {
const response = await fetch('/api/shield');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
}
const stats = await response.json();
// 更新统计信息
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) {
element.textContent = item.value || 0;
}
});
} catch (error) {
console.error('加载屏蔽规则统计信息失败:', error);
showErrorMessage('加载屏蔽规则统计信息失败');
}
}
// 加载本地规则
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);
showErrorMessage('加载本地规则失败');
}
}
// 加载远程规则
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);
showErrorMessage('加载远程规则失败');
}
}
// 更新规则表格
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="2" 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 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(tdAction);
fragment.appendChild(tr);
});
// 一次性添加所有行到DOM
tbody.appendChild(fragment);
// 如果有更多规则,添加提示
if (rules.length > maxRulesToShow) {
const infoRow = document.createElement('tr');
infoRow.innerHTML = `<td colspan="2" 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 {
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);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to delete rule: ${response.status} ${errorText}`);
}
const responseData = await response.json();
console.log('Response data:', responseData);
showSuccessMessage('规则删除成功');
console.log('Current rules type:', currentRulesType);
// 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') {
console.log('Reloading local rules');
loadLocalRules();
} else {
console.log('Reloading remote rules');
loadRemoteRules();
}
// 重新加载统计信息
loadShieldStats();
} catch (error) {
console.error('Error deleting rule:', error);
showErrorMessage('删除规则失败: ' + error.message);
}
}
// 添加新规则
async function handleAddRule() {
const rule = document.getElementById('new-rule').value.trim();
if (!rule) {
showErrorMessage('规则不能为空');
return;
}
try {
const response = await fetch('/api/shield', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ rule })
});
if (!response.ok) {
throw new Error('Failed to add rule');
}
showSuccessMessage('规则添加成功');
// 清空输入框
document.getElementById('new-rule').value = '';
// 重新加载规则
loadLocalRules();
// 重新加载统计信息
loadShieldStats();
} catch (error) {
console.error('Error adding rule:', error);
showErrorMessage('添加规则失败');
}
}
// 加载远程黑名单
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);
showErrorMessage('加载远程黑名单失败');
}
}
// 判断黑名单是否过期超过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) {
const expired = isBlacklistExpired(blacklist.lastUpdateTime || blacklist.LastUpdateTime);
if (expired) {
statusColor = 'bg-warning'; // 黄色表示过期
statusText = '过期';
} else {
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 tdActions = document.createElement('td');
tdActions.className = 'py-3 px-4 text-right space-x-2';
// 刷新按钮
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(refreshBtn);
tdActions.appendChild(deleteBtn);
tr.appendChild(tdName);
tr.appendChild(tdUrl);
tr.appendChild(tdStatus);
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 url = e.target.closest('.update-blacklist-btn').dataset.url;
if (!url) {
showToast('无效的黑名单URL', 'error');
return;
}
try {
const response = await fetch(`/api/shield/blacklists/${encodeURIComponent(url)}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to update blacklist');
}
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showToast('黑名单更新成功');
} catch (error) {
console.error('更新黑名单失败:', error);
showToast('更新黑名单失败: ' + error.message, 'error');
}
}
// 处理删除黑名单
async function handleDeleteBlacklist(e) {
const url = e.target.closest('.delete-blacklist-btn').dataset.url;
if (!url) {
showToast('无效的黑名单URL', 'error');
return;
}
// 确认删除
if (!confirm('确定要删除这个黑名单吗?删除后将无法恢复。')) {
return;
}
try {
const response = await fetch(`/api/shield/blacklists/${encodeURIComponent(url)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to delete blacklist');
}
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showToast('黑名单删除成功');
} catch (error) {
console.error('删除黑名单失败:', error);
showToast('删除黑名单失败: ' + 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 name = nameInput ? nameInput.value.trim() : '';
const url = urlInput ? urlInput.value.trim() : '';
// 简单验证
if (!name || !url) {
showErrorMessage('名称和URL不能为空');
return;
}
// 验证URL格式
try {
new URL(url);
} catch (e) {
showErrorMessage('URL格式不正确');
return;
}
try {
const response = await fetch('/api/shield/blacklists', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, url })
});
if (!response.ok) {
// 尝试从响应中获取更详细的错误信息
let errorMessage = '添加黑名单失败';
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error;
}
} catch (jsonError) {
// 忽略JSON解析错误
}
throw new Error(errorMessage);
}
showSuccessMessage('黑名单添加成功');
// 清空输入框
if (nameInput) nameInput.value = '';
if (urlInput) urlInput.value = '';
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
} catch (error) {
console.error('Error adding blacklist:', error);
showErrorMessage(error.message || '添加黑名单失败');
}
}
// 当前显示的规则类型:'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();
}
});
});