屏蔽规则页面丰富显示

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

@@ -691,35 +691,39 @@
<h4 class="text-sm font-medium text-gray-500">域名例外</h4>
<i class="fa fa-check-circle text-green-500"></i>
</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 class="bg-purple-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则规则</h4>
<i class="fa fa-code text-purple-500"></i>
</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 class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则例外</h4>
<i class="fa fa-exclamation-circle text-yellow-500"></i>
</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 class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">Hosts规则</h4>
<i class="fa fa-file-text text-red-500"></i>
</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 class="bg-indigo-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">黑名单数量</h4>
<i class="fa fa-ban text-indigo-500"></i>
</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>
@@ -771,10 +775,11 @@
<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">
<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>
<div id="save-blacklist-status" class="flex items-center text-sm"></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">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-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="blacklists-table-body">
<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>
</tbody>
</table>

View File

@@ -12,16 +12,115 @@ async function initShieldPage() {
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() {
try {
const response = await fetch('/api/shield');
// 获取屏蔽规则统计信息
const shieldResponse = await fetch('/api/shield');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
if (!shieldResponse.ok) {
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 = [
@@ -36,12 +135,18 @@ async function loadShieldStats() {
elements.forEach(item => {
const element = document.getElementById(item.id);
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) {
console.error('加载屏蔽规则统计信息失败:', error);
showErrorMessage('加载屏蔽规则统计信息失败');
showNotification('加载屏蔽规则统计信息失败', 'error');
}
}
@@ -86,7 +191,7 @@ async function loadLocalRules() {
updateRulesTable(rules);
} catch (error) {
console.error('加载本地规则失败:', error);
showErrorMessage('加载本地规则失败');
showNotification('加载本地规则失败', 'error');
}
}
@@ -130,7 +235,7 @@ async function loadRemoteRules() {
updateRulesTable(rules);
} catch (error) {
console.error('加载远程规则失败:', error);
showErrorMessage('加载远程规则失败');
showNotification('加载远程规则失败', 'error');
}
}
@@ -251,7 +356,7 @@ async function handleDeleteRule(e) {
const responseData = await response.json();
console.log('Response data:', responseData);
showSuccessMessage('规则删除成功');
showNotification('规则删除成功', 'success');
console.log('Current rules type:', currentRulesType);
// 根据当前显示的规则类型重新加载对应的规则列表
if (currentRulesType === 'local') {
@@ -265,7 +370,7 @@ async function handleDeleteRule(e) {
loadShieldStats();
} catch (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() {
const rule = document.getElementById('new-rule').value.trim();
if (!rule) {
showErrorMessage('规则不能为空');
showNotification('规则不能为空', 'error');
return;
}
@@ -290,7 +395,7 @@ async function handleAddRule() {
throw new Error('Failed to add rule');
}
showSuccessMessage('规则添加成功');
showNotification('规则添加成功', 'success');
// 清空输入框
document.getElementById('new-rule').value = '';
// 重新加载规则
@@ -299,7 +404,7 @@ async function handleAddRule() {
loadShieldStats();
} catch (error) {
console.error('Error adding rule:', error);
showErrorMessage('添加规则失败');
showNotification('添加规则失败', 'error');
}
}
@@ -319,7 +424,7 @@ async function loadRemoteBlacklists() {
updateBlacklistsTable(blacklistArray);
} catch (error) {
console.error('加载远程黑名单失败:', error);
showErrorMessage('加载远程黑名单失败');
showNotification('加载远程黑名单失败', 'error');
}
}
@@ -365,30 +470,24 @@ function updateBlacklistsTable(blacklists) {
// 名称单元格
const tdName = document.createElement('td');
tdName.className = 'py-3 px-4';
tdName.textContent = blacklist.Name || '未命名';
tdName.textContent = blacklist.name || '未命名';
// URL单元格
const tdUrl = document.createElement('td');
tdUrl.className = 'py-3 px-4 truncate max-w-xs';
tdUrl.textContent = blacklist.URL;
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 = '正常';
}
if (blacklist.enabled) {
statusColor = 'bg-success'; // 绿色表示启用
statusText = '启用';
}
const statusContainer = document.createElement('div');
@@ -406,14 +505,29 @@ function updateBlacklistsTable(blacklists) {
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.dataset.url = blacklist.url;
refreshBtn.innerHTML = '<i class="fa fa-refresh"></i>';
refreshBtn.title = '刷新黑名单';
refreshBtn.addEventListener('click', handleUpdateBlacklist);
@@ -421,17 +535,19 @@ function updateBlacklistsTable(blacklists) {
// 删除按钮
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.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);
});
@@ -449,43 +565,93 @@ function updateBlacklistsTable(blacklists) {
// 处理更新单个黑名单
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) {
showToast('无效的黑名单URL', 'error');
showNotification('无效的黑名单URL', 'error');
return;
}
try {
const response = await fetch(`/api/shield/blacklists/${encodeURIComponent(url)}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
// 显示加载状态
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
};
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to update blacklist');
// 发送更新请求
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');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showToast('黑名单更新成功');
showNotification('黑名单更新成功', 'success');
} catch (error) {
console.error('更新黑名单失败:', error);
showToast('更新黑名单失败: ' + error.message, 'error');
// 显示错误状态
updateStatus(url, 'error', error.message);
showNotification('更新黑名单失败: ' + error.message, 'error');
}
}
// 处理删除黑名单
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) {
showToast('无效的黑名单URL', 'error');
showNotification('无效的黑名单URL', 'error');
return;
}
@@ -495,26 +661,124 @@ async function handleDeleteBlacklist(e) {
}
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: {
'Content-Type': 'application/json'
}
},
body: JSON.stringify(updatedBlacklists)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to delete blacklist');
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`删除失败: ${updateResponse.status} ${errorText}`);
}
// 显示成功状态
updateStatus(url, 'success', '已删除');
// 重新加载黑名单
loadRemoteBlacklists();
// 重新加载统计信息
loadShieldStats();
showToast('黑名单删除成功');
showNotification('黑名单删除成功', 'success');
} catch (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 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) {
showErrorMessage('名称和URL不能为空');
showNotification('名称和URL不能为空', 'error');
return;
}
@@ -541,11 +806,15 @@ async function handleAddBlacklist(event) {
try {
new URL(url);
} catch (e) {
showErrorMessage('URL格式不正确');
showNotification('URL格式不正确', 'error');
return;
}
try {
// 显示加载状态
statusElement.innerHTML = '<i class="fa fa-spinner fa-spin text-blue-500"></i> 正在添加...';
// 发送添加请求
const response = await fetch('/api/shield/blacklists', {
method: 'POST',
headers: {
@@ -568,7 +837,10 @@ async function handleAddBlacklist(event) {
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 (urlInput) urlInput.value = '';
@@ -578,7 +850,14 @@ async function handleAddBlacklist(event) {
loadShieldStats();
} catch (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);
}
}