增加web功能

This commit is contained in:
Alex Yang
2025-11-28 00:17:56 +08:00
parent 7dd31c8f5a
commit b1c63f6713
8 changed files with 605 additions and 409 deletions

View File

@@ -687,23 +687,15 @@
<!-- 本地规则管理 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">本地规则管理</h3>
<button id="add-rule-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-2"></i>添加规则
</button>
</div>
<h3 class="text-lg font-semibold mb-6">本地规则管理</h3>
<!-- 添加规则表单 -->
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="flex items-center space-x-4">
<input type="text" id="new-rule" placeholder="输入规则例如example.com 或 regex:/example\.com/" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<button id="save-rule-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<button id="cancel-rule-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
</div>
</div>
@@ -727,15 +719,10 @@
<!-- 远程黑名单管理 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">远程黑名单管理</h3>
<button id="add-blacklist-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-2"></i>添加黑名单
</button>
</div>
<h3 class="text-lg font-semibold mb-6">远程黑名单管理</h3>
<!-- 添加黑名单表单 -->
<div id="add-blacklist-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
<div id="add-blacklist-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="blacklist-name" class="block text-sm font-medium text-gray-700 mb-1">名称</label>
@@ -745,13 +732,10 @@
<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">
</div>
<div class="flex items-end space-x-4">
<div class="flex items-end">
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<button id="cancel-blacklist-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
</div>
</div>
</div>
@@ -776,26 +760,22 @@
</div>
</div>
<!-- Hosts条目管理 -->
</div>
<div id="hosts-content" class="hidden space-y-6">
<!-- Hosts管理页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">Hosts条目管理</h3>
<button id="add-hosts-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-2"></i>添加条目
</button>
</div>
<h3 class="text-lg font-semibold mb-6">Hosts条目管理</h3>
<!-- 添加hosts条目表单 -->
<div id="add-hosts-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
<div id="add-hosts-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="flex items-center space-x-4">
<input type="text" id="hosts-ip" placeholder="IP地址" class="w-32 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="hosts-domain" placeholder="域名" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<button id="save-hosts-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<button id="cancel-hosts-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
</div>
</div>
@@ -819,15 +799,6 @@
</div>
</div>
<div id="hosts-content" class="hidden">
<!-- Hosts管理页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">Hosts管理</h3>
<!-- 这里将添加Hosts管理相关内容 -->
<p>Hosts管理页面内容待实现</p>
</div>
</div>
<div id="blacklists-content" class="hidden">
<!-- 黑名单管理页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow">

View File

@@ -2,94 +2,140 @@
// 初始化Hosts管理页面
function initHostsPage() {
loadHostsContent();
// 加载Hosts规则
loadHostsRules();
// 设置事件监听器
setupHostsEventListeners();
}
// 加载Hosts内容
async function loadHostsContent() {
// 加载Hosts规则
async function loadHostsRules() {
try {
const hostsContent = await api.getHosts();
document.getElementById('hosts-content').value = hostsContent;
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) {
showErrorMessage('加载Hosts文件失败: ' + error.message);
console.error('Error loading hosts rules:', error);
showErrorMessage('加载Hosts规则失败');
}
}
// 保存Hosts内容
async function handleSaveHosts() {
const hostsContent = document.getElementById('hosts-content').value;
// 更新Hosts表格
function updateHostsTable(hostsRules) {
const tbody = document.getElementById('hosts-table-body');
try {
await api.saveHosts(hostsContent);
showSuccessMessage('Hosts文件保存成功');
} catch (error) {
showErrorMessage('保存Hosts文件失败: ' + error.message);
}
}
// 刷新Hosts
async function handleRefreshHosts() {
try {
await api.refreshHosts();
showSuccessMessage('Hosts刷新成功');
loadHostsContent();
} catch (error) {
showErrorMessage('刷新Hosts失败: ' + error.message);
}
}
// 添加新的Hosts条目
function handleAddHostsEntry() {
const ipInput = document.getElementById('hosts-ip');
const domainInput = document.getElementById('hosts-domain');
const ip = ipInput.value.trim();
const domain = domainInput.value.trim();
if (!ip || !domain) {
showErrorMessage('IP和域名不能为空');
if (hostsRules.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
return;
}
// 简单的IP验证
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(ip)) {
showErrorMessage('请输入有效的IP地址');
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('');
const hostsTextarea = document.getElementById('hosts-content');
const newEntry = `\n${ip} ${domain}`;
hostsTextarea.value += newEntry;
// 清空输入框
ipInput.value = '';
domainInput.value = '';
// 滚动到文本区域底部
hostsTextarea.scrollTop = hostsTextarea.scrollHeight;
showSuccessMessage('Hosts条目已添加到编辑器');
// 重新绑定删除事件
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteHostsRule);
});
}
// 设置事件监听器
function setupHostsEventListeners() {
// 保存按钮
document.getElementById('save-hosts-btn')?.addEventListener('click', handleSaveHosts);
// 保存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();
// 刷新按钮
document.getElementById('refresh-hosts-btn')?.addEventListener('click', handleRefreshHosts);
if (!ip || !domain) {
showErrorMessage('IP地址和域名不能为空');
return;
}
// 添加Hosts条目按钮
document.getElementById('add-hosts-entry-btn')?.addEventListener('click', handleAddHostsEntry);
// 按回车键添加条目
document.getElementById('hosts-domain')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleAddHostsEntry();
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规则失败');
}
}
// 显示成功消息
@@ -102,6 +148,8 @@ function showErrorMessage(message) {
showNotification(message, 'error');
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
@@ -112,7 +160,7 @@ function showNotification(message, type = 'info') {
// 创建新通知
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`;
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') {
@@ -123,18 +171,22 @@ function showNotification(message, type = 'info') {
notification.classList.add('bg-blue-500', 'text-white');
}
notification.textContent = message;
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');
notification.classList.add('opacity-100');
}, 10);
}, 100);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0');
setTimeout(() => {
notification.remove();
@@ -147,4 +199,4 @@ if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHostsPage);
} else {
initHostsPage();
}
}

View File

@@ -38,6 +38,13 @@ function setupNavigation() {
// 更新页面标题
pageTitle.textContent = item.querySelector('span').textContent;
// 页面特定初始化
if (target === 'shield' && typeof initShieldPage === 'function') {
initShieldPage();
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
initHostsPage();
}
});
});

View File

@@ -54,13 +54,60 @@ function displayQueryResult(result, domain) {
// 解析结果
const status = result.blocked ? '被屏蔽' : '正常';
const statusClass = result.blocked ? 'text-danger' : 'text-success';
const blockType = result.blocked ? result.blockType || '未知' : '正常';
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);
}
@@ -76,7 +123,9 @@ function saveQueryHistory(domain, result) {
timestamp: new Date().toISOString(),
result: {
blocked: result.blocked,
blockType: result.blockType
blockRuleType: result.blockRuleType,
blockRule: result.blockRule,
blocksource: result.blocksource
}
};
@@ -109,7 +158,9 @@ function loadQueryHistory() {
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.blockType : '正常';
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 `
@@ -120,6 +171,8 @@ function loadQueryHistory() {
<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}')">

View File

@@ -8,8 +8,6 @@ function initShieldPage() {
loadLocalRules();
// 加载远程黑名单
loadRemoteBlacklists();
// 加载hosts条目
loadHostsEntries();
// 设置事件监听器
setupShieldEventListeners();
}
@@ -19,21 +17,33 @@ async function loadShieldStats() {
showLoading('加载屏蔽规则统计信息...');
try {
const response = await fetch('/api/shield');
if (!response.ok) {
throw new Error('Failed to load shield stats');
throw new Error(`加载失败: ${response.status}`);
}
const stats = await response.json();
// 更新统计信息
document.getElementById('domain-rules-count').textContent = stats.domainRulesCount || 0;
document.getElementById('domain-exceptions-count').textContent = stats.domainExceptionsCount || 0;
document.getElementById('regex-rules-count').textContent = stats.regexRulesCount || 0;
document.getElementById('regex-exceptions-count').textContent = stats.regexExceptionsCount || 0;
document.getElementById('hosts-rules-count').textContent = stats.hostsRulesCount || 0;
document.getElementById('blacklist-count').textContent = stats.blacklistCount || 0;
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;
}
});
hideLoading();
} catch (error) {
console.error('Error loading shield stats:', error);
console.error('加载屏蔽规则统计信息失败:', error);
showErrorMessage('加载屏蔽规则统计信息失败');
hideLoading();
}
@@ -43,17 +53,37 @@ async function loadShieldStats() {
async function loadLocalRules() {
showLoading('加载本地规则...');
try {
const response = await fetch('/api/shield');
const response = await fetch('/api/shield?all=true');
if (!response.ok) {
throw new Error('Failed to load local rules');
throw new Error(`加载失败: ${response.status}`);
}
// 注意当前API不返回完整规则列表这里只是示例
// 实际实现需要后端提供获取本地规则的API
const rules = [];
const data = await response.json();
// 合并所有本地规则
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);
hideLoading();
} catch (error) {
console.error('Error loading local rules:', error);
console.error('加载本地规则失败:', error);
showErrorMessage('加载本地规则失败');
hideLoading();
}
@@ -63,26 +93,55 @@ async function loadLocalRules() {
function updateRulesTable(rules) {
const tbody = document.getElementById('rules-table-body');
// 清空表格
tbody.innerHTML = '';
if (rules.length === 0) {
tbody.innerHTML = '<tr><td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td></tr>';
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = '<td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td>';
tbody.appendChild(emptyRow);
return;
}
tbody.innerHTML = rules.map(rule => `
<tr class="border-b border-gray-200">
<td class="py-3 px-4">${rule}</td>
<td class="py-3 px-4 text-right">
<button class="delete-rule-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-rule="${rule}">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`).join('');
// 对于大量规则,限制显示数量
const maxRulesToShow = 1000; // 限制最大显示数量
const rulesToShow = rules.length > maxRulesToShow ? rules.slice(0, maxRulesToShow) : rules;
// 重新绑定删除事件
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteRule);
// 使用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;
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
deleteBtn.addEventListener('click', handleDeleteRule);
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);
}
}
// 处理删除规则
@@ -140,8 +199,6 @@ async function handleAddRule() {
showSuccessMessage('规则添加成功');
// 清空输入框
document.getElementById('new-rule').value = '';
// 隐藏表单
document.getElementById('add-rule-form').classList.add('hidden');
// 重新加载规则
loadLocalRules();
// 重新加载统计信息
@@ -159,14 +216,19 @@ async function loadRemoteBlacklists() {
showLoading('加载远程黑名单...');
try {
const response = await fetch('/api/shield/blacklists');
if (!response.ok) {
throw new Error('Failed to load remote blacklists');
throw new Error(`加载失败: ${response.status}`);
}
const blacklists = await response.json();
updateBlacklistsTable(blacklists);
// 确保blacklists是数组
const blacklistArray = Array.isArray(blacklists) ? blacklists : [];
updateBlacklistsTable(blacklistArray);
hideLoading();
} catch (error) {
console.error('Error loading remote blacklists:', error);
console.error('加载远程黑名单失败:', error);
showErrorMessage('加载远程黑名单失败');
hideLoading();
}
@@ -176,37 +238,81 @@ async function loadRemoteBlacklists() {
function updateBlacklistsTable(blacklists) {
const tbody = document.getElementById('blacklists-table-body');
// 清空表格
tbody.innerHTML = '';
if (blacklists.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-gray-500">暂无黑名单</td></tr>';
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;
}
tbody.innerHTML = blacklists.map(blacklist => `
<tr class="border-b border-gray-200">
<td class="py-3 px-4">${blacklist.Name}</td>
<td class="py-3 px-4 truncate max-w-xs">${blacklist.URL}</td>
<td class="py-3 px-4 text-center">
<span class="inline-block w-3 h-3 rounded-full ${blacklist.Enabled ? 'bg-success' : 'bg-gray-300'}"></span>
</td>
<td class="py-3 px-4 text-right space-x-2">
<button class="update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm" data-url="${blacklist.URL}">
<i class="fa fa-refresh"></i>
</button>
<button class="delete-blacklist-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-url="${blacklist.URL}">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`).join('');
// 对于大量黑名单,限制显示数量
const maxBlacklistsToShow = 100; // 限制最大显示数量
const blacklistsToShow = blacklists.length > maxBlacklistsToShow ? blacklists.slice(0, maxBlacklistsToShow) : blacklists;
// 重新绑定事件
document.querySelectorAll('.update-blacklist-btn').forEach(btn => {
btn.addEventListener('click', handleUpdateBlacklist);
// 使用DocumentFragment提高性能
const fragment = document.createDocumentFragment();
blacklistsToShow.forEach(blacklist => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200';
// 名称单元格
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';
const statusDot = document.createElement('span');
statusDot.className = `inline-block w-3 h-3 rounded-full ${blacklist.Enabled ? 'bg-success' : 'bg-gray-300'}`;
tdStatus.appendChild(statusDot);
// 操作单元格
const tdActions = document.createElement('td');
tdActions.className = 'py-3 px-4 text-right space-x-2';
// 更新按钮
const updateBtn = document.createElement('button');
updateBtn.className = 'update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm';
updateBtn.dataset.url = blacklist.URL;
updateBtn.innerHTML = '<i class="fa fa-refresh"></i>';
updateBtn.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.addEventListener('click', handleDeleteBlacklist);
tdActions.appendChild(updateBtn);
tdActions.appendChild(deleteBtn);
tr.appendChild(tdName);
tr.appendChild(tdUrl);
tr.appendChild(tdStatus);
tr.appendChild(tdActions);
fragment.appendChild(tr);
});
document.querySelectorAll('.delete-blacklist-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteBlacklist);
});
// 一次性添加所有行到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);
}
}
// 处理更新单个黑名单
@@ -289,8 +395,6 @@ async function handleAddBlacklist() {
// 清空输入框
document.getElementById('blacklist-name').value = '';
document.getElementById('blacklist-url').value = '';
// 隐藏表单
document.getElementById('add-blacklist-form').classList.add('hidden');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
@@ -303,162 +407,15 @@ async function handleAddBlacklist() {
}
}
// 加载hosts条目
async function loadHostsEntries() {
showLoading('加载Hosts条目...');
try {
const response = await fetch('/api/shield/hosts');
if (!response.ok) {
throw new Error('Failed to load hosts entries');
}
const data = await response.json();
updateHostsTable(data.hosts || []);
hideLoading();
} catch (error) {
console.error('Error loading hosts entries:', error);
showErrorMessage('加载Hosts条目失败');
hideLoading();
}
}
// 更新hosts表格
function updateHostsTable(hosts) {
const tbody = document.getElementById('hosts-table-body');
if (hosts.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
return;
}
tbody.innerHTML = hosts.map(entry => `
<tr class="border-b border-gray-200">
<td class="py-3 px-4">${entry.ip}</td>
<td class="py-3 px-4">${entry.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-domain="${entry.domain}">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
`).join('');
// 重新绑定删除事件
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
btn.addEventListener('click', handleDeleteHostsEntry);
});
}
// 处理删除hosts条目
async function handleDeleteHostsEntry(e) {
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
showLoading('删除Hosts条目...');
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 entry');
}
showSuccessMessage('Hosts条目删除成功');
// 重新加载hosts条目
loadHostsEntries();
// 重新加载统计信息
loadShieldStats();
hideLoading();
} catch (error) {
console.error('Error deleting hosts entry:', error);
showErrorMessage('删除Hosts条目失败');
hideLoading();
}
}
// 处理添加hosts条目
async function handleAddHostsEntry() {
const ip = document.getElementById('hosts-ip').value.trim();
const domain = document.getElementById('hosts-domain').value.trim();
if (!ip || !domain) {
showErrorMessage('IP地址和域名不能为空');
return;
}
showLoading('添加Hosts条目...');
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 entry');
}
showSuccessMessage('Hosts条目添加成功');
// 清空输入框
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
// 隐藏表单
document.getElementById('add-hosts-form').classList.add('hidden');
// 重新加载hosts条目
loadHostsEntries();
// 重新加载统计信息
loadShieldStats();
hideLoading();
} catch (error) {
console.error('Error adding hosts entry:', error);
showErrorMessage('添加Hosts条目失败');
hideLoading();
}
}
// 设置事件监听器
function setupShieldEventListeners() {
// 本地规则管理事件
document.getElementById('add-rule-btn').addEventListener('click', () => {
document.getElementById('add-rule-form').classList.toggle('hidden');
});
document.getElementById('save-rule-btn').addEventListener('click', handleAddRule);
document.getElementById('cancel-rule-btn').addEventListener('click', () => {
document.getElementById('add-rule-form').classList.add('hidden');
document.getElementById('new-rule').value = '';
});
// 远程黑名单管理事件
document.getElementById('add-blacklist-btn').addEventListener('click', () => {
document.getElementById('add-blacklist-form').classList.toggle('hidden');
});
document.getElementById('save-blacklist-btn').addEventListener('click', handleAddBlacklist);
document.getElementById('cancel-blacklist-btn').addEventListener('click', () => {
document.getElementById('add-blacklist-form').classList.add('hidden');
document.getElementById('blacklist-name').value = '';
document.getElementById('blacklist-url').value = '';
});
// Hosts条目管理事件
document.getElementById('add-hosts-btn').addEventListener('click', () => {
document.getElementById('add-hosts-form').classList.toggle('hidden');
});
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsEntry);
document.getElementById('cancel-hosts-btn').addEventListener('click', () => {
document.getElementById('add-hosts-form').classList.add('hidden');
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
});
}
// 显示成功消息