web重做

This commit is contained in:
Alex Yang
2025-11-24 01:53:26 +08:00
parent f499a4a84a
commit 85320611cb
17 changed files with 4936 additions and 2346 deletions

760
static/css/style.css Normal file
View File

@@ -0,0 +1,760 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
width: 100%;
height: 100%;
overflow-x: hidden;
}
body {
position: relative;
}
/* 主容器样式 */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%;
max-width: 100%;
background-color: #fff;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
}
/* 头部样式 */
header.header-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
width: 100%;
text-align: center;
box-sizing: border-box;
position: relative;
z-index: 10;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.logo i {
margin-right: 1rem;
color: white;
}
.logo h1 {
font-size: 1.8rem;
margin: 0;
font-weight: 600;
}
header p {
font-size: 1rem;
opacity: 0.9;
}
/* 主体布局容器 */
.main-layout {
display: flex;
flex: 1;
min-height: 0;
}
/* 侧边栏样式 */
.sidebar {
width: 250px;
background-color: #2c3e50;
color: white;
padding: 1rem 0;
flex-shrink: 0;
overflow-y: auto;
height: calc(100vh - 130px); /* 减去header的高度 */
}
.nav-menu {
list-style: none;
}
.nav-item {
padding: 1rem 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
}
.nav-item:hover {
background-color: #34495e;
padding-left: 1.75rem;
}
.nav-item.active {
background-color: #3498db;
border-left: 4px solid #fff;
}
.nav-item i {
margin-right: 1rem;
width: 20px;
text-align: center;
}
/* 主内容区域样式 */
.content {
flex: 1;
padding: 1rem;
overflow-y: auto;
background-color: #f8f9fa;
min-width: 0; /* 防止flex子元素溢出 */
height: calc(100vh - 130px); /* 减去header的高度 */
}
/* 面板样式 */
.panel {
display: none;
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
overflow: hidden;
}
.panel.active {
display: block;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e9ecef;
}
.panel-header h2 {
font-size: 1.5rem;
color: #2c3e50;
}
/* 状态指示器 */
.status-indicator {
display: flex;
align-items: center;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #e74c3c;
margin-right: 8px;
animation: pulse 2s infinite;
}
.status-dot.connected {
background-color: #2ecc71;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 按钮样式 */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
}
.btn i {
margin-right: 0.5rem;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #7f8c8d;
color: white;
}
.btn-secondary:hover {
background-color: #6c757d;
}
.btn-success {
background-color: #2ecc71;
color: white;
}
.btn-success:hover {
background-color: #27ae60;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-warning {
background-color: #f39c12;
color: white;
}
.btn-warning:hover {
background-color: #e67e22;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.8rem;
}
.btn-block {
width: 100%;
}
/* 统计卡片网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.stat-card i {
font-size: 2rem;
margin-bottom: 1rem;
color: #3498db;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.stat-label {
font-size: 0.9rem;
color: #7f8c8d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 图表容器 */
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.chart-card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.chart-card h3 {
margin-bottom: 1rem;
font-size: 1.2rem;
color: #2c3e50;
}
/* 表格容器 */
.tables-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.table-card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.table-card h3 {
margin-bottom: 1rem;
font-size: 1.2rem;
color: #2c3e50;
}
/* 表格样式 */
.table-wrapper {
overflow-x: auto;
border-radius: 8px;
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
display: block;
width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
background-color: #ffffff;
margin: 0;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
td.loading {
text-align: center;
color: #7f8c8d;
font-style: italic;
}
tr:hover {
background-color: #f8f9fa;
}
/* 分页控件样式 */
.pagination-controls {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.pagination-info {
font-size: 14px;
color: #666;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.items-per-page {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.items-per-page select {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
cursor: pointer;
}
.nav-buttons {
display: flex;
gap: 8px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 规则内容样式优化 */
.rule-content {
max-width: 600px;
}
.rule-content pre {
margin: 0;
font-family: inherit;
white-space: pre-wrap;
word-break: break-all;
font-size: 14px;
}
/* 表单样式 */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2c3e50;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #3498db;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* 管理区域样式 */
.rules-management,
.hosts-management,
.blacklists-management {
margin-top: 1rem;
}
.rules-input,
.rules-filter,
.hosts-filter {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
margin-bottom: 1.5rem;
}
.rules-input {
grid-template-columns: 1fr auto auto;
}
/* 查询表单 */
.query-form .form-group {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
}
/* 查询结果样式 */
.query-result {
margin-top: 2rem;
}
#query-result-container {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
}
#query-result-container.hidden {
display: none;
}
.result-header {
margin-bottom: 1rem;
border-bottom: 1px solid #e9ecef;
padding-bottom: 0.5rem;
}
.result-header h3 {
font-size: 1.2rem;
color: #2c3e50;
}
.result-item {
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.result-item:last-child {
border-bottom: none;
}
/* 配置表单样式 */
.config-form {
margin-top: 1rem;
}
.config-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.config-section h3 {
margin-bottom: 1.5rem;
font-size: 1.2rem;
color: #2c3e50;
}
.config-actions {
text-align: center;
margin-top: 2rem;
}
/* 通知组件 */
.notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #3498db;
color: white;
padding: 1rem 1.5rem;
border-radius: 4px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.success {
background-color: #2ecc71;
}
.notification.error {
background-color: #e74c3c;
}
.notification.warning {
background-color: #f39c12;
}
.notification-content {
display: flex;
align-items: center;
}
.notification-content i {
margin-right: 1rem;
}
/* 大屏幕优化 */
@media (min-width: 1200px) {
.container {
max-width: 1400px;
margin: 0 auto;
}
}
/* 平板设备 */
@media (max-width: 1024px) {
.content {
padding: 1rem;
}
.stats-grid,
.charts-container {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.tables-container {
grid-template-columns: 1fr;
}
}
/* 移动设备 */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
max-height: 120px;
}
.nav-menu {
display: flex;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.nav-item {
white-space: nowrap;
padding: 0.75rem 1rem;
}
.stats-grid,
.charts-container,
.tables-container {
grid-template-columns: 1fr;
}
.rules-input,
.rules-filter,
.hosts-filter {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.query-form .form-group {
grid-template-columns: 1fr;
}
.panel {
padding: 1rem;
}
.pagination-controls {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.pagination-buttons {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.nav-buttons {
justify-content: center;
}
}
/* 小屏幕移动设备 */
@media (max-width: 480px) {
header {
padding: 1.5rem 1rem;
}
.logo h1 {
font-size: 1.5rem;
}
.content {
padding: 0.75rem;
}
.panel-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.stat-card {
padding: 1rem;
}
.stat-value {
font-size: 1.5rem;
}
.chart-card {
padding: 1rem;
}
th, td {
padding: 0.5rem;
font-size: 0.9rem;
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 确保按钮在不同容器中保持一致宽度 */
.w-full {
width: 100%;
}
/* 确保输入和按钮在表单组中有合适的高度对齐 */
.form-group button {
height: auto;
align-self: flex-end;
}
/* 优化表格中的操作按钮间距 */
.actions-cell {
display: flex;
gap: 0.5rem;
}

File diff suppressed because it is too large Load Diff

1190
static/index.html.bak Normal file

File diff suppressed because it is too large Load Diff

252
static/js/app.js Normal file
View File

@@ -0,0 +1,252 @@
// 全局配置
const API_BASE_URL = '/api';
// DOM 加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化面板切换
initPanelNavigation();
// 初始化通知组件
initNotification();
// 加载初始数据
loadInitialData();
// 定时更新数据
setInterval(loadInitialData, 60000); // 每分钟更新一次
});
// 初始化面板导航
function initPanelNavigation() {
const navItems = document.querySelectorAll('.nav-item');
const panels = document.querySelectorAll('.panel');
navItems.forEach(item => {
item.addEventListener('click', function() {
// 移除所有活动类
navItems.forEach(nav => nav.classList.remove('active'));
panels.forEach(panel => panel.classList.remove('active'));
// 添加当前活动类
this.classList.add('active');
const target = this.getAttribute('data-target');
document.getElementById(target).classList.add('active');
// 面板激活时执行相应的初始化函数
if (window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]) {
window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]();
}
});
});
}
// 初始化通知组件
function initNotification() {
window.showNotification = function(message, type = 'info') {
const notification = document.getElementById('notification');
const notificationMessage = document.getElementById('notification-message');
// 设置消息和类型
notificationMessage.textContent = message;
notification.className = 'notification show ' + type;
// 自动关闭
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
};
}
// 加载初始数据
function loadInitialData() {
// 加载服务器状态
fetch(`${API_BASE_URL}/status`)
.then(response => response.json())
.then(data => {
// 更新服务器状态指示器
const statusDot = document.querySelector('.status-dot');
const serverStatus = document.getElementById('server-status');
if (data && data.status === 'running') {
statusDot.classList.add('connected');
serverStatus.textContent = '运行中';
} else {
statusDot.classList.remove('connected');
serverStatus.textContent = '离线';
}
})
.catch(error => {
console.error('获取服务器状态失败:', error);
// 更新状态为离线
const statusDot = document.querySelector('.status-dot');
const serverStatus = document.getElementById('server-status');
statusDot.classList.remove('connected');
serverStatus.textContent = '离线';
});
// 加载统计数据
fetch(`${API_BASE_URL}/stats`)
.then(response => response.json())
.then(data => {
// 更新统计数据
if (data && data.dns) {
updateStatCards(data.dns);
}
})
.catch(error => {
console.error('获取统计数据失败:', error);
window.showNotification('获取统计数据失败', 'error');
});
}
// 更新统计卡片数据
function updateStatCards(stats) {
const statElements = {
'blocked-count': stats.blocked || 0,
'allowed-count': stats.allowed || 0,
'error-count': stats.error || 0,
'total-queries': stats.totalQueries || 0,
'rules-count': stats.rulesCount || 0,
'hosts-count': stats.hostsCount || 0
};
for (const [id, value] of Object.entries(statElements)) {
const element = document.getElementById(id);
if (element) {
element.textContent = formatNumber(value);
}
}
}
// 通用API请求函数
function apiRequest(endpoint, method = 'GET', data = null) {
const headers = {
'Content-Type': 'application/json'
};
const config = {
method,
headers
};
if (data && (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
config.body = JSON.stringify(data);
}
return fetch(`${API_BASE_URL}${endpoint}`, config)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
}
// 数字格式化函数
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// 确认对话框函数
function confirmAction(message, onConfirm) {
if (confirm(message)) {
onConfirm();
}
}
// 加载状态函数
function showLoading(element) {
if (element) {
element.innerHTML = '<td colspan="100%" class="loading">加载中...</td>';
}
}
// 错误状态函数
function showError(element, message) {
if (element) {
element.innerHTML = `<td colspan="100%" style="color: #e74c3c;">${message}</td>`;
}
}
// 空状态函数
function showEmpty(element, message) {
if (element) {
element.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">${message}</td>`;
}
}
// 表格排序功能
function initTableSort(tableId) {
const table = document.getElementById(tableId);
if (!table) return;
const headers = table.querySelectorAll('thead th');
headers.forEach(header => {
header.addEventListener('click', function() {
const columnIndex = Array.from(headers).indexOf(this);
const isAscending = this.getAttribute('data-sort') !== 'asc';
// 重置所有标题
headers.forEach(h => h.setAttribute('data-sort', ''));
this.setAttribute('data-sort', isAscending ? 'asc' : 'desc');
// 排序行
sortTable(table, columnIndex, isAscending);
});
});
}
// 表格排序实现
function sortTable(table, columnIndex, isAscending) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// 排序行
rows.sort((a, b) => {
const aValue = a.cells[columnIndex].textContent.trim();
const bValue = b.cells[columnIndex].textContent.trim();
// 尝试数字排序
const aNum = parseFloat(aValue);
const bNum = parseFloat(bValue);
if (!isNaN(aNum) && !isNaN(bNum)) {
return isAscending ? aNum - bNum : bNum - aNum;
}
// 字符串排序
return isAscending
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
// 重新添加行
rows.forEach(row => tbody.appendChild(row));
}
// 搜索过滤功能
function initSearchFilter(inputId, tableId, columnIndex) {
const input = document.getElementById(inputId);
const table = document.getElementById(tableId);
if (!input || !table) return;
input.addEventListener('input', function() {
const filter = this.value.toLowerCase();
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cell = row.cells[columnIndex];
if (cell) {
const text = cell.textContent.toLowerCase();
row.style.display = text.includes(filter) ? '' : 'none';
}
});
});
}

View File

@@ -0,0 +1,230 @@
// 初始化远程黑名单面板
function initBlacklistsPanel() {
// 加载远程黑名单列表
loadBlacklists();
// 初始化事件监听器
initBlacklistsEventListeners();
}
// 初始化事件监听器
function initBlacklistsEventListeners() {
// 添加黑名单按钮
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
// 更新所有黑名单按钮
document.getElementById('update-all-blacklists').addEventListener('click', updateAllBlacklists);
// 按Enter键添加黑名单
document.getElementById('blacklist-url').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addBlacklist();
}
});
}
// 加载远程黑名单列表
function loadBlacklists() {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
showLoading(tbody);
apiRequest('/shield')
.then(data => {
renderBlacklists(data);
})
.catch(error => {
console.error('获取远程黑名单列表失败:', error);
showError(tbody, '获取远程黑名单列表失败');
window.showNotification('获取远程黑名单列表失败', 'error');
});
}
// 渲染远程黑名单表格
function renderBlacklists(blacklists) {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
if (!tbody) return;
if (!blacklists || blacklists.length === 0) {
showEmpty(tbody, '暂无远程黑名单');
return;
}
tbody.innerHTML = '';
blacklists.forEach(list => {
addBlacklistToTable(list);
});
// 初始化表格排序
initTableSort('blacklists-table');
// 初始化操作按钮监听器
initBlacklistsActionListeners();
}
// 添加黑名单到表格
function addBlacklistToTable(list) {
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
const row = document.createElement('tr');
const statusClass = list.status === 'success' ? 'status-success' :
list.status === 'error' ? 'status-error' : 'status-pending';
const statusText = list.status === 'success' ? '正常' :
list.status === 'error' ? '错误' : '等待中';
const lastUpdate = list.lastUpdate ? new Date(list.lastUpdate).toLocaleString() : '从未';
row.innerHTML = `
<td>${list.name}</td>
<td>${list.url}</td>
<td>
<span class="status-badge ${statusClass}">${statusText}</span>
</td>
<td>${list.rulesCount || 0}</td>
<td>${lastUpdate}</td>
<td class="actions-cell">
<button class="btn btn-primary btn-sm update-blacklist" data-id="${list.id}">
<i class="fas fa-sync-alt"></i> 更新
</button>
<button class="btn btn-danger btn-sm delete-blacklist" data-id="${list.id}">
<i class="fas fa-trash-alt"></i> 删除
</button>
</td>
`;
tbody.appendChild(row);
}
// 添加远程黑名单
function addBlacklist() {
const nameInput = document.getElementById('blacklist-name');
const urlInput = document.getElementById('blacklist-url');
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name) {
window.showNotification('请输入黑名单名称', 'warning');
nameInput.focus();
return;
}
if (!url) {
window.showNotification('请输入黑名单URL', 'warning');
urlInput.focus();
return;
}
// 简单的URL格式验证
if (!isValidUrl(url)) {
window.showNotification('请输入有效的URL', 'warning');
urlInput.focus();
return;
}
apiRequest('/shield', 'POST', { name: name, url: url })
.then(data => {
if (data.success) {
window.showNotification('远程黑名单添加成功', 'success');
nameInput.value = '';
urlInput.value = '';
loadBlacklists();
} else {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('添加远程黑名单失败:', error);
window.showNotification('添加远程黑名单失败', 'error');
});
}
// 更新远程黑名单
function updateBlacklist(id) {
apiRequest(`/shield/${id}/update`, 'POST')
.then(data => {
if (data.success) {
window.showNotification('远程黑名单更新成功', 'success');
loadBlacklists();
} else {
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('更新远程黑名单失败:', error);
window.showNotification('更新远程黑名单失败', 'error');
});
}
// 更新所有远程黑名单
function updateAllBlacklists() {
confirmAction(
'确定要更新所有远程黑名单吗?这可能需要一些时间。',
() => {
apiRequest('/shield/update-all', 'POST')
.then(data => {
if (data.success) {
window.showNotification('所有远程黑名单更新成功', 'success');
loadBlacklists();
} else {
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('更新所有远程黑名单失败:', error);
window.showNotification('更新所有远程黑名单失败', 'error');
});
}
);
}
// 删除远程黑名单
function deleteBlacklist(id) {
apiRequest(`/shield/${id}`, 'DELETE')
.then(data => {
if (data.success) {
window.showNotification('远程黑名单删除成功', 'success');
loadBlacklists();
} else {
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('删除远程黑名单失败:', error);
window.showNotification('删除远程黑名单失败', 'error');
});
}
// 为操作按钮添加事件监听器
function initBlacklistsActionListeners() {
// 更新按钮
document.querySelectorAll('.update-blacklist').forEach(button => {
button.addEventListener('click', function() {
const id = this.getAttribute('data-id');
updateBlacklist(id);
});
});
// 删除按钮
document.querySelectorAll('.delete-blacklist').forEach(button => {
button.addEventListener('click', function() {
const id = this.getAttribute('data-id');
confirmAction(
'确定要删除这条远程黑名单吗?',
() => deleteBlacklist(id)
);
});
});
}
// 验证URL格式
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}

148
static/js/modules/config.js Normal file
View File

@@ -0,0 +1,148 @@
// 初始化配置管理面板
function initConfigPanel() {
// 加载当前配置
loadConfig();
// 初始化事件监听器
initConfigEventListeners();
}
// 初始化事件监听器
function initConfigEventListeners() {
// 保存配置按钮
document.getElementById('save-config').addEventListener('click', saveConfig);
// 屏蔽方法变更
document.getElementById('block-method').addEventListener('change', updateCustomBlockIpVisibility);
}
// 加载当前配置
function loadConfig() {
apiRequest('/config')
.then(config => {
renderConfig(config);
})
.catch(error => {
console.error('获取配置失败:', error);
window.showNotification('获取配置失败', 'error');
});
}
// 渲染配置表单
function renderConfig(config) {
if (!config) return;
// 设置屏蔽方法
const blockMethodSelect = document.getElementById('block-method');
if (config.shield && config.shield.blockMethod) {
blockMethodSelect.value = config.shield.blockMethod;
}
// 设置自定义屏蔽IP
const customBlockIpInput = document.getElementById('custom-block-ip');
if (config.shield && config.shield.customBlockIP) {
customBlockIpInput.value = config.shield.customBlockIP;
}
// 设置远程规则更新间隔
const updateIntervalInput = document.getElementById('update-interval');
if (config.shield && config.shield.updateInterval) {
updateIntervalInput.value = config.shield.updateInterval;
}
// 更新自定义屏蔽IP的可见性
updateCustomBlockIpVisibility();
}
// 更新自定义屏蔽IP输入框的可见性
function updateCustomBlockIpVisibility() {
const blockMethod = document.getElementById('block-method').value;
const customBlockIpContainer = document.getElementById('custom-block-ip').closest('.form-group');
if (blockMethod === 'customIP') {
customBlockIpContainer.style.display = 'block';
} else {
customBlockIpContainer.style.display = 'none';
}
}
// 保存配置
function saveConfig() {
// 收集表单数据
const configData = {
shield: {
blockMethod: document.getElementById('block-method').value,
updateInterval: parseInt(document.getElementById('update-interval').value)
}
};
// 如果选择了自定义IP添加到配置中
if (configData.shield.blockMethod === 'customIP') {
const customBlockIp = document.getElementById('custom-block-ip').value.trim();
// 验证自定义IP格式
if (!isValidIp(customBlockIp)) {
window.showNotification('请输入有效的自定义屏蔽IP', 'warning');
return;
}
configData.shield.customBlockIP = customBlockIp;
}
// 验证更新间隔
if (isNaN(configData.shield.updateInterval) || configData.shield.updateInterval < 60) {
window.showNotification('更新间隔必须大于等于60秒', 'warning');
return;
}
// 保存配置
apiRequest('/config', 'PUT', configData)
.then(response => {
if (response.success) {
window.showNotification('配置保存成功', 'success');
// 询问是否需要重启服务以应用配置
confirmAction(
'配置已保存。某些更改可能需要重启服务才能生效。是否现在重启服务?',
() => restartService()
);
} else {
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('保存配置失败:', error);
window.showNotification('保存配置失败', 'error');
});
}
// 重启服务
function restartService() {
apiRequest('/service/restart', 'POST')
.then(response => {
if (response.success) {
window.showNotification('服务正在重启,请稍后刷新页面', 'success');
// 等待几秒后重新加载页面
setTimeout(() => {
location.reload();
}, 3000);
} else {
window.showNotification(`重启失败: ${response.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('重启服务失败:', error);
// 重启服务可能会导致连接中断,这是正常的
window.showNotification('服务重启中,请手动刷新页面确认状态', 'info');
});
}
// 验证IP地址格式
function isValidIp(ip) {
// 支持IPv4和IPv6简单验证
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}

View File

@@ -0,0 +1,246 @@
// 初始化仪表盘面板
function initDashboardPanel() {
// 加载统计数据
loadDashboardData();
}
// 加载仪表盘数据
function loadDashboardData() {
// 加载24小时统计数据
loadHourlyStats();
// 加载请求类型分布
loadRequestsDistribution();
// 加载最常屏蔽的域名
loadTopBlockedDomains();
// 加载最常解析的域名
loadTopResolvedDomains();
}
// 加载24小时统计数据
function loadHourlyStats() {
apiRequest('/api/hourly-stats')
.then(data => {
if (data && data.labels && data.data) {
// 只使用一组数据(假设是屏蔽请求数)
renderHourlyChart(data.labels, data.data, []);
}
})
.catch(error => {
console.error('获取24小时统计失败:', error);
});
}
// 渲染24小时统计图表
function renderHourlyChart(hours, blocked, allowed) {
const ctx = document.getElementById('hourly-chart');
if (!ctx) return;
// 销毁现有图表
if (window.hourlyChart) {
window.hourlyChart.destroy();
}
// 创建新图表
window.hourlyChart = new Chart(ctx, {
type: 'line',
data: {
labels: hours,
datasets: [
{
label: '屏蔽请求',
data: blocked,
borderColor: '#e74c3c',
backgroundColor: 'rgba(231, 76, 60, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true
},
{
label: '允许请求',
data: allowed,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '请求数'
}
},
x: {
title: {
display: true,
text: '时间(小时)'
}
}
},
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
}
}
});
}
// 加载请求类型分布
function loadRequestsDistribution() {
apiRequest('/api/stats')
.then(data => {
if (data && data.dns) {
// 构造饼图所需的数据
const labels = ['允许请求', '屏蔽请求', '错误请求'];
const requestData = [
data.dns.Allowed || 0,
data.dns.Blocked || 0,
data.dns.Error || 0
];
renderRequestsPieChart(labels, requestData);
}
})
.catch(error => {
console.error('获取请求类型分布失败:', error);
});
}
// 渲染请求类型饼图
function renderRequestsPieChart(labels, data) {
const ctx = document.getElementById('requests-pie-chart');
if (!ctx) return;
// 销毁现有图表
if (window.requestsPieChart) {
window.requestsPieChart.destroy();
}
// 创建新图表
window.requestsPieChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#2ecc71', // 允许
'#e74c3c', // 屏蔽
'#f39c12', // 错误
'#9b59b6' // 其他
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '60%'
}
});
}
// 加载最常屏蔽的域名
function loadTopBlockedDomains() {
apiRequest('/api/top-blocked')
.then(data => {
renderTopBlockedDomains(data);
})
.catch(error => {
console.error('获取最常屏蔽域名失败:', error);
showError(document.getElementById('top-blocked-table').querySelector('tbody'), '获取数据失败');
});
}
// 渲染最常屏蔽的域名表格
function renderTopBlockedDomains(domains) {
const tbody = document.getElementById('top-blocked-table').querySelector('tbody');
if (!tbody) return;
if (!domains || domains.length === 0) {
showEmpty(tbody, '暂无屏蔽记录');
return;
}
tbody.innerHTML = '';
domains.forEach((domain, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${domain.domain}</td>
<td>${formatNumber(domain.count)}</td>
`;
tbody.appendChild(row);
});
// 初始化表格排序
initTableSort('top-blocked-table');
}
// 加载最常解析的域名
function loadTopResolvedDomains() {
apiRequest('/api/top-resolved')
.then(data => {
renderTopResolvedDomains(data);
})
.catch(error => {
console.error('获取最常解析域名失败:', error);
showError(document.getElementById('top-resolved-table').querySelector('tbody'), '获取数据失败');
});
}
// 渲染最常解析的域名表格
function renderTopResolvedDomains(domains) {
const tbody = document.getElementById('top-resolved-table').querySelector('tbody');
if (!tbody) return;
if (!domains || domains.length === 0) {
showEmpty(tbody, '暂无解析记录');
return;
}
tbody.innerHTML = '';
domains.forEach((domain, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${domain.domain}</td>
<td>${formatNumber(domain.count)}</td>
`;
tbody.appendChild(row);
});
// 初始化表格排序
initTableSort('top-resolved-table');
}

180
static/js/modules/hosts.js Normal file
View File

@@ -0,0 +1,180 @@
// 初始化Hosts面板
function initHostsPanel() {
// 加载Hosts列表
loadHosts();
// 初始化事件监听器
initHostsEventListeners();
}
// 初始化事件监听器
function initHostsEventListeners() {
// 添加Hosts按钮
document.getElementById('add-hosts').addEventListener('click', addHostsEntry);
// Hosts过滤
document.getElementById('hosts-filter').addEventListener('input', filterHosts);
// 按Enter键添加Hosts
document.getElementById('hosts-domain').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addHostsEntry();
}
});
}
// 加载Hosts列表
function loadHosts() {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
showLoading(tbody);
apiRequest('/shield/hosts', 'GET')
.then(data => {
renderHosts(data);
})
.catch(error => {
console.error('获取Hosts列表失败:', error);
showError(tbody, '获取Hosts列表失败');
window.showNotification('获取Hosts列表失败', 'error');
});
}
// 渲染Hosts表格
function renderHosts(hosts) {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
if (!tbody) return;
if (!hosts || hosts.length === 0) {
showEmpty(tbody, '暂无Hosts条目');
return;
}
tbody.innerHTML = '';
hosts.forEach(entry => {
addHostsToTable(entry.ip, entry.domain);
});
// 初始化表格排序
initTableSort('hosts-table');
// 初始化删除按钮监听器
initDeleteHostsListeners();
}
// 添加Hosts到表格
function addHostsToTable(ip, domain) {
const tbody = document.getElementById('hosts-table').querySelector('tbody');
const row = document.createElement('tr');
row.innerHTML = `
<td>${ip}</td>
<td>${domain}</td>
<td class="actions-cell">
<button class="btn btn-danger btn-sm delete-hosts" data-ip="${ip}" data-domain="${domain}">
<i class="fas fa-trash-alt"></i> 删除
</button>
</td>
`;
tbody.appendChild(row);
}
// 添加Hosts条目
function addHostsEntry() {
const ipInput = document.getElementById('hosts-ip');
const domainInput = document.getElementById('hosts-domain');
const ip = ipInput.value.trim();
const domain = domainInput.value.trim();
if (!ip) {
window.showNotification('请输入IP地址', 'warning');
ipInput.focus();
return;
}
if (!domain) {
window.showNotification('请输入域名', 'warning');
domainInput.focus();
return;
}
// 简单的IP地址格式验证
if (!isValidIp(ip)) {
window.showNotification('请输入有效的IP地址', 'warning');
ipInput.focus();
return;
}
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain });
apiRequest('/shield/hosts', 'POST', { ip: ip, domain: domain })
.then(data => {
if (data.success) {
window.showNotification('Hosts条目添加成功', 'success');
ipInput.value = '';
domainInput.value = '';
loadHosts();
} else {
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('添加Hosts条目失败:', error);
window.showNotification('添加Hosts条目失败', 'error');
});
}
// 删除Hosts条目
function deleteHostsEntry(ip, domain) {
apiRequest('/shield/hosts', 'DELETE', { ip: ip, domain: domain })
.then(data => {
if (data.success) {
window.showNotification('Hosts条目删除成功', 'success');
loadHosts();
} else {
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
}
})
.catch(error => {
console.error('删除Hosts条目失败:', error);
window.showNotification('删除Hosts条目失败', 'error');
});
}
// 过滤Hosts
function filterHosts() {
const filterText = document.getElementById('hosts-filter').value.toLowerCase();
const rows = document.querySelectorAll('#hosts-table tbody tr');
rows.forEach(row => {
const ip = row.cells[0].textContent.toLowerCase();
const domain = row.cells[1].textContent.toLowerCase();
row.style.display = (ip.includes(filterText) || domain.includes(filterText)) ? '' : 'none';
});
}
// 为删除按钮添加事件监听器
function initDeleteHostsListeners() {
document.querySelectorAll('.delete-hosts').forEach(button => {
button.addEventListener('click', function() {
const ip = this.getAttribute('data-ip');
const domain = this.getAttribute('data-domain');
confirmAction(
`确定要删除这条Hosts条目吗\n${ip} ${domain}`,
() => deleteHostsEntry(ip, domain)
);
});
});
}
// 验证IP地址格式
function isValidIp(ip) {
// 支持IPv4和IPv6简单验证
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}

145
static/js/modules/query.js Normal file
View File

@@ -0,0 +1,145 @@
// 初始化DNS查询面板
function initQueryPanel() {
// 初始化事件监听器
initQueryEventListeners();
}
// 初始化事件监听器
function initQueryEventListeners() {
// 查询按钮
document.getElementById('run-query').addEventListener('click', runDnsQuery);
// 按Enter键执行查询
document.getElementById('query-domain').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
runDnsQuery();
}
});
}
// 执行DNS查询
function runDnsQuery() {
const domainInput = document.getElementById('query-domain');
const domain = domainInput.value.trim();
if (!domain) {
window.showNotification('请输入要查询的域名', 'warning');
domainInput.focus();
return;
}
// 显示查询中状态
showQueryLoading();
apiRequest('/query?domain=' + domain, 'GET', { domain: domain })
.then(data => {
renderQueryResult(data);
})
.catch(error => {
console.error('DNS查询失败:', error);
showQueryError('查询失败,请稍后重试');
window.showNotification('DNS查询失败', 'error');
});
}
// 显示查询加载状态
function showQueryLoading() {
const resultContainer = document.getElementById('query-result-container');
resultContainer.classList.remove('hidden');
// 清空之前的结果
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
resultHeader.textContent = '查询中...';
resultContent.innerHTML = '<div class="loading"></div>';
}
// 显示查询错误
function showQueryError(message) {
const resultContainer = document.getElementById('query-result-container');
resultContainer.classList.remove('hidden');
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
resultHeader.textContent = '查询错误';
resultContent.innerHTML = `<div class="result-item" style="color: #e74c3c;">${message}</div>`;
}
// 渲染查询结果
function renderQueryResult(result) {
const resultContainer = document.getElementById('query-result-container');
resultContainer.classList.remove('hidden');
const resultHeader = resultContainer.querySelector('.result-header h3');
const resultContent = resultContainer.querySelector('.result-content');
resultHeader.textContent = '查询结果';
// 根据查询结果构建内容
let content = '';
// 域名
content += `<div class="result-item"><strong>域名:</strong> <span id="result-domain">${result.domain || ''}</span></div>`;
// 状态
const statusText = result.isBlocked ? '被屏蔽' : result.isAllowed ? '允许访问' : '未知';
const statusClass = result.isBlocked ? 'status-error' : result.isAllowed ? 'status-success' : '';
content += `<div class="result-item"><strong>状态:</strong> <span id="result-status" class="${statusClass}">${statusText}</span></div>`;
// 规则类型
let ruleType = '';
if (result.isBlocked) {
if (result.isRegexMatch) {
ruleType = '正则表达式规则';
} else if (result.isDomainMatch) {
ruleType = '域名规则';
} else {
ruleType = '未知规则类型';
}
} else {
ruleType = result.isWhitelist ? '白名单规则' : result.isHosts ? 'Hosts记录' : '未匹配任何规则';
}
content += `<div class="result-item"><strong>规则类型:</strong> <span id="result-rule-type">${ruleType}</span></div>`;
// 匹配规则
const matchedRule = result.matchedRule || '无';
content += `<div class="result-item"><strong>匹配规则:</strong> <span id="result-rule">${matchedRule}</span></div>`;
// Hosts记录
const hostsRecord = result.hostsRecord ? `${result.hostsRecord.ip} ${result.hostsRecord.domain}` : '无';
content += `<div class="result-item"><strong>Hosts记录:</strong> <span id="result-hosts">${hostsRecord}</span></div>`;
// 查询时间
const queryTime = `${(result.queryTime || 0).toFixed(2)} ms`;
content += `<div class="result-item"><strong>查询时间:</strong> <span id="result-time">${queryTime}</span></div>`;
// DNS响应如果有
if (result.dnsResponse) {
content += '<div class="result-item"><strong>DNS响应:</strong></div>';
content += '<div class="dns-response">';
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
content += '<ul>';
result.dnsResponse.answers.forEach(answer => {
content += `<li>${answer.name} ${answer.type} ${answer.value}</li>`;
});
content += '</ul>';
} else {
content += '<p>无DNS响应记录</p>';
}
content += '</div>';
}
resultContent.innerHTML = content;
// 更新结果元素的内容(确保数据一致性)
document.getElementById('result-domain').textContent = result.domain || '';
document.getElementById('result-status').textContent = statusText;
document.getElementById('result-status').className = statusClass;
document.getElementById('result-rule-type').textContent = ruleType;
document.getElementById('result-rule').textContent = matchedRule;
document.getElementById('result-hosts').textContent = hostsRecord;
document.getElementById('result-time').textContent = queryTime;
}

270
static/js/modules/rules.js Normal file
View File

@@ -0,0 +1,270 @@
// 屏蔽规则管理模块
// 全局变量
let rules = [];
let currentPage = 1;
let itemsPerPage = 50; // 默认每页显示50条规则
let filteredRules = [];
// 初始化屏蔽规则面板
function initRulesPanel() {
// 加载规则列表
loadRules();
// 绑定添加规则按钮事件
document.getElementById('add-rule-btn').addEventListener('click', addNewRule);
// 绑定刷新规则按钮事件
document.getElementById('reload-rules-btn').addEventListener('click', reloadRules);
// 绑定搜索框事件
document.getElementById('rule-search').addEventListener('input', filterRules);
// 绑定每页显示数量变更事件
document.getElementById('items-per-page').addEventListener('change', () => {
itemsPerPage = parseInt(document.getElementById('items-per-page').value);
currentPage = 1; // 重置为第一页
renderRulesList();
});
// 绑定分页按钮事件
document.getElementById('prev-page-btn').addEventListener('click', goToPreviousPage);
document.getElementById('next-page-btn').addEventListener('click', goToNextPage);
document.getElementById('first-page-btn').addEventListener('click', goToFirstPage);
document.getElementById('last-page-btn').addEventListener('click', goToLastPage);
}
// 加载规则列表
async function loadRules() {
try {
const rulesPanel = document.getElementById('rules-panel');
showLoading(rulesPanel);
const data = await apiRequest('/shield', 'GET');
rules = data.rules || [];
filteredRules = [...rules];
currentPage = 1; // 重置为第一页
renderRulesList();
} catch (error) {
showError('加载规则失败:' + error.message);
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
}
}
// 渲染规则列表
function renderRulesList() {
const rulesList = document.getElementById('rules-list');
const paginationInfo = document.getElementById('pagination-info');
// 清空列表
rulesList.innerHTML = '';
if (filteredRules.length === 0) {
rulesList.innerHTML = '<tr><td colspan="4" class="text-center">暂无规则</td></tr>';
paginationInfo.textContent = '共0条规则';
updatePaginationButtons();
return;
}
// 计算分页数据
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredRules.length);
const currentRules = filteredRules.slice(startIndex, endIndex);
// 渲染当前页的规则
currentRules.forEach((rule, index) => {
const row = document.createElement('tr');
const globalIndex = startIndex + index;
row.innerHTML = `
<td class="rule-id">${globalIndex + 1}</td>
<td class="rule-content"><pre>${escapeHtml(rule)}</pre></td>
<td class="rule-actions">
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
<i class="fas fa-trash"></i> 删除
</button>
</td>
`;
rulesList.appendChild(row);
});
// 绑定删除按钮事件
document.querySelectorAll('.delete-rule').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
deleteRule(index);
});
});
// 更新分页信息
paginationInfo.textContent = `显示 ${startIndex + 1}-${endIndex} 条,共 ${filteredRules.length} 条规则,第 ${currentPage}/${totalPages}`;
// 更新分页按钮状态
updatePaginationButtons();
}
// 更新分页按钮状态
function updatePaginationButtons() {
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
const firstBtn = document.getElementById('first-page-btn');
const lastBtn = document.getElementById('last-page-btn');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
firstBtn.disabled = currentPage === 1;
lastBtn.disabled = currentPage === totalPages || totalPages === 0;
}
// 上一页
function goToPreviousPage() {
if (currentPage > 1) {
currentPage--;
renderRulesList();
}
}
// 下一页
function goToNextPage() {
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderRulesList();
}
}
// 第一页
function goToFirstPage() {
currentPage = 1;
renderRulesList();
}
// 最后一页
function goToLastPage() {
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
renderRulesList();
}
// 添加新规则
async function addNewRule() {
const ruleInput = document.getElementById('rule-input');
const rule = ruleInput.value.trim();
if (!rule) {
showNotification('请输入规则内容', 'warning');
return;
}
try {
const response = await apiRequest('/shield/rule', 'POST', { rule });
if (response.success) {
rules.push(rule);
filteredRules = [...rules];
ruleInput.value = '';
// 添加后跳转到最后一页,显示新添加的规则
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
renderRulesList();
showNotification('规则添加成功', 'success');
} else {
showNotification('规则添加失败:' + (response.message || '未知错误'), 'error');
}
} catch (error) {
showError('添加规则失败:' + error.message);
}
}
// 删除规则
async function deleteRule(index) {
if (!confirm('确定要删除这条规则吗?')) {
return;
}
try {
const rule = filteredRules[index];
const response = await apiRequest('/shield/rule', 'DELETE', { rule });
if (response.success) {
// 在原规则列表中找到并删除
const originalIndex = rules.indexOf(rule);
if (originalIndex !== -1) {
rules.splice(originalIndex, 1);
}
// 在过滤后的列表中删除
filteredRules.splice(index, 1);
// 如果当前页没有数据了,回到上一页
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
}
renderRulesList();
showNotification('规则删除成功', 'success');
} else {
showNotification('规则删除失败:' + (response.message || '未知错误'), 'error');
}
} catch (error) {
showError('删除规则失败:' + error.message);
}
}
// 重新加载规则
async function reloadRules() {
if (!confirm('确定要重新加载所有规则吗?这将覆盖当前内存中的规则。')) {
return;
}
try {
const rulesPanel = document.getElementById('rules-panel');
showLoading(rulesPanel);
await apiRequest('/shield/reload', 'POST');
// 重新加载规则列表
await loadRules();
showNotification('规则重新加载成功', 'success');
} catch (error) {
showError('重新加载规则失败:' + error.message);
} finally {
const rulesPanel = document.getElementById('rules-panel');
hideLoading(rulesPanel);
}
}
// 过滤规则
function filterRules() {
const searchTerm = document.getElementById('rule-search').value.toLowerCase();
if (searchTerm) {
filteredRules = rules.filter(rule => rule.toLowerCase().includes(searchTerm));
} else {
filteredRules = [...rules];
}
currentPage = 1; // 重置为第一页
renderRulesList();
}
// HTML转义防止XSS攻击
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// 导出初始化函数
window.initRulesPanel = initRulesPanel;