添加了Swagger API文档以及诸多优化
This commit is contained in:
@@ -1,5 +1,32 @@
|
||||
// 配置管理页面功能实现
|
||||
|
||||
// 工具函数:安全获取DOM元素
|
||||
function getElement(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
console.warn(`Element with id "${id}" not found`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
// 工具函数:验证端口号
|
||||
function validatePort(port) {
|
||||
// 确保port是字符串类型
|
||||
var portStr = port;
|
||||
if (port === null || port === undefined || typeof port !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 去除前后空白并验证是否为纯数字
|
||||
portStr = port.trim();
|
||||
if (!/^\d+$/.test(portStr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const num = parseInt(portStr, 10);
|
||||
return num >= 1 && num <= 65535 ? num : null;
|
||||
}
|
||||
|
||||
// 初始化配置管理页面
|
||||
function initConfigPage() {
|
||||
loadConfig();
|
||||
@@ -9,85 +36,192 @@ function initConfigPage() {
|
||||
// 加载系统配置
|
||||
async function loadConfig() {
|
||||
try {
|
||||
showLoading(true);
|
||||
const config = await api.getConfig();
|
||||
populateConfigForm(config);
|
||||
} catch (error) {
|
||||
showErrorMessage('加载配置失败: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 填充配置表单
|
||||
function populateConfigForm(config) {
|
||||
// DNS配置
|
||||
document.getElementById('dns-port')?.value = config.DNSServer.Port || 53;
|
||||
document.getElementById('dns-upstream-servers')?.value = (config.DNSServer.UpstreamServers || []).join(', ');
|
||||
document.getElementById('dns-timeout')?.value = config.DNSServer.Timeout || 5;
|
||||
document.getElementById('dns-stats-file')?.value = config.DNSServer.StatsFile || './stats.json';
|
||||
document.getElementById('dns-save-interval')?.value = config.DNSServer.SaveInterval || 300;
|
||||
// 安全获取配置对象,防止未定义属性访问
|
||||
const dnsServerConfig = config.DNSServer || {};
|
||||
const httpServerConfig = config.HTTPServer || {};
|
||||
const shieldConfig = config.Shield || {};
|
||||
|
||||
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||
|
||||
// HTTP配置
|
||||
document.getElementById('http-port')?.value = config.HTTPServer.Port || 8080;
|
||||
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||
|
||||
// 屏蔽配置
|
||||
document.getElementById('shield-local-rules-file')?.value = config.Shield.LocalRulesFile || './rules.txt';
|
||||
document.getElementById('shield-update-interval')?.value = config.Shield.UpdateInterval || 3600;
|
||||
document.getElementById('shield-hosts-file')?.value = config.Shield.HostsFile || '/etc/hosts';
|
||||
document.getElementById('shield-block-method')?.value = config.Shield.BlockMethod || '0.0.0.0';
|
||||
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, '0.0.0.0'));
|
||||
}
|
||||
|
||||
// 工具函数:安全设置元素值
|
||||
function setElementValue(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
element.value = value;
|
||||
} else if (!element) {
|
||||
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:安全获取值,如果未定义或为null则返回默认值
|
||||
function getSafeValue(value, defaultValue) {
|
||||
// 更严格的检查,避免0、空字符串等被默认值替换
|
||||
return value === undefined || value === null ? defaultValue : value;
|
||||
}
|
||||
|
||||
// 工具函数:安全获取数组,如果不是数组则返回空数组
|
||||
function getSafeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function handleSaveConfig() {
|
||||
const formData = collectFormData();
|
||||
|
||||
if (!formData) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
await api.saveConfig(formData);
|
||||
showSuccessMessage('配置保存成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('保存配置失败: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 重启服务
|
||||
async function handleRestartService() {
|
||||
if (confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) {
|
||||
try {
|
||||
await api.restartService();
|
||||
showSuccessMessage('服务重启成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('重启服务失败: ' + error.message);
|
||||
}
|
||||
if (!confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
await api.restartService();
|
||||
showSuccessMessage('服务重启成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('重启服务失败: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集表单数据
|
||||
// 收集表单数据并验证
|
||||
function collectFormData() {
|
||||
// 验证端口号 - 使用安全获取元素值的函数
|
||||
const dnsPortValue = getElementValue('dns-port');
|
||||
const httpPortValue = getElementValue('http-port');
|
||||
|
||||
const dnsPort = validatePort(dnsPortValue);
|
||||
const httpPort = validatePort(httpPortValue);
|
||||
|
||||
if (!dnsPort) {
|
||||
showErrorMessage('DNS端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!httpPort) {
|
||||
showErrorMessage('HTTP端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全获取上游服务器列表
|
||||
const upstreamServersText = getElementValue('dns-upstream-servers');
|
||||
const upstreamServers = upstreamServersText ?
|
||||
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||
[];
|
||||
|
||||
// 安全获取并转换整数值
|
||||
const timeoutValue = getElementValue('dns-timeout');
|
||||
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||
|
||||
const saveIntervalValue = getElementValue('dns-save-interval');
|
||||
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
|
||||
|
||||
const updateIntervalValue = getElementValue('shield-update-interval');
|
||||
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||
|
||||
return {
|
||||
DNSServer: {
|
||||
Port: parseInt(document.getElementById('dns-port')?.value) || 53,
|
||||
UpstreamServers: document.getElementById('dns-upstream-servers')?.value.split(',').map(s => s.trim()).filter(Boolean) || [],
|
||||
Timeout: parseInt(document.getElementById('dns-timeout')?.value) || 5,
|
||||
StatsFile: document.getElementById('dns-stats-file')?.value || './data/stats.json',
|
||||
SaveInterval: parseInt(document.getElementById('dns-save-interval')?.value) || 300
|
||||
Port: dnsPort,
|
||||
UpstreamServers: upstreamServers,
|
||||
Timeout: timeout,
|
||||
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||
SaveInterval: saveInterval
|
||||
},
|
||||
HTTPServer: {
|
||||
Port: parseInt(document.getElementById('http-port')?.value) || 8080
|
||||
Port: httpPort
|
||||
},
|
||||
Shield: {
|
||||
LocalRulesFile: document.getElementById('shield-local-rules-file')?.value || './data/rules.txt',
|
||||
UpdateInterval: parseInt(document.getElementById('shield-update-interval')?.value) || 3600,
|
||||
HostsFile: document.getElementById('shield-hosts-file')?.value || './data/hosts.txt',
|
||||
BlockMethod: document.getElementById('shield-block-method')?.value || '0.0.0.0'
|
||||
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||
UpdateInterval: updateInterval,
|
||||
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||
BlockMethod: getElementValue('shield-block-method') || '0.0.0.0'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 工具函数:安全获取元素值
|
||||
function getElementValue(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
return element.value;
|
||||
}
|
||||
return ''; // 默认返回空字符串
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupConfigEventListeners() {
|
||||
// 保存配置按钮
|
||||
document.getElementById('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||
|
||||
// 重启服务按钮
|
||||
document.getElementById('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading(show) {
|
||||
const loadingElement = document.getElementById('loading-overlay');
|
||||
if (show) {
|
||||
if (!loadingElement) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'loading-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
`;
|
||||
overlay.innerHTML = '<div>处理中...</div>';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
} else {
|
||||
loadingElement?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
@@ -112,13 +246,28 @@ 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`;
|
||||
|
||||
// 设置通知样式
|
||||
// 设置通知样式(兼容Tailwind和原生CSS)
|
||||
notification.style.cssText += `
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
notification.style.backgroundColor = '#10b981';
|
||||
notification.style.color = 'white';
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
notification.style.backgroundColor = '#ef4444';
|
||||
notification.style.color = 'white';
|
||||
} else {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
notification.style.backgroundColor = '#3b82f6';
|
||||
notification.style.color = 'white';
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
@@ -126,14 +275,12 @@ function showNotification(message, type = 'info') {
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
notification.classList.add('opacity-100');
|
||||
notification.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-100');
|
||||
notification.classList.add('opacity-0');
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
|
||||
@@ -948,22 +948,28 @@ function updateTopBlockedTable(domains) {
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' }
|
||||
{ name: 'example1.com', count: 150 },
|
||||
{ name: 'example2.com', count: 130 },
|
||||
{ name: 'example3.com', count: 120 },
|
||||
{ name: 'example4.com', count: 110 },
|
||||
{ name: 'example5.com', count: 100 }
|
||||
];
|
||||
console.log('使用示例数据填充Top屏蔽域名表格');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const domain of tableData) {
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
const domain = tableData[i];
|
||||
html += `
|
||||
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||
<td class="py-3 px-4 text-sm">${domain.name}</td>
|
||||
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
|
||||
</tr>
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
||||
<span class="font-medium truncate">${domain.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -981,7 +987,8 @@ function updateRecentBlockedTable(domains) {
|
||||
if (Array.isArray(domains)) {
|
||||
tableData = domains.map(item => ({
|
||||
name: item.name || item.domain || item[0] || '未知',
|
||||
timestamp: item.timestamp || item.time || Date.now()
|
||||
timestamp: item.timestamp || item.time || Date.now(),
|
||||
type: item.type || '广告'
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -989,23 +996,27 @@ function updateRecentBlockedTable(domains) {
|
||||
if (tableData.length === 0) {
|
||||
const now = Date.now();
|
||||
tableData = [
|
||||
{ name: '---', timestamp: now - 5 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 15 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 30 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 45 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 60 * 60 * 1000 }
|
||||
{ name: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' },
|
||||
{ name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' },
|
||||
{ name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' },
|
||||
{ name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' },
|
||||
{ name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' }
|
||||
];
|
||||
console.log('使用示例数据填充最近屏蔽域名表格');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const domain of tableData) {
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
const domain = tableData[i];
|
||||
const time = formatTime(domain.timestamp);
|
||||
html += `
|
||||
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||
<td class="py-3 px-4 text-sm">${domain.name}</td>
|
||||
<td class="py-3 px-4 text-sm text-right text-gray-500">${time}</td>
|
||||
</tr>
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-warning">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium truncate">${domain.name}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">${time}</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">${domain.type}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1036,22 +1047,28 @@ function updateTopClientsTable(clients) {
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' }
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' }
|
||||
];
|
||||
console.log('使用示例数据填充TOP客户端表格');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const client of tableData) {
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
const client = tableData[i];
|
||||
html += `
|
||||
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||
<td class="py-3 px-4 text-sm">${client.ip}</td>
|
||||
<td class="py-3 px-4 text-sm text-right">${formatNumber(client.count)}</td>
|
||||
</tr>
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||
<span class="font-medium truncate">${client.ip}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,74 @@ function setupNavigation() {
|
||||
|
||||
// 移动端侧边栏切换
|
||||
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
if (toggleSidebar && sidebar) {
|
||||
toggleSidebar.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
});
|
||||
// 打开侧边栏函数
|
||||
function openSidebar() {
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
}
|
||||
// 防止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// 关闭侧边栏函数
|
||||
function closeSidebar() {
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.add('hidden');
|
||||
}
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 切换侧边栏函数
|
||||
function toggleSidebarVisibility() {
|
||||
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||
openSidebar();
|
||||
} else {
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定切换按钮事件
|
||||
if (toggleSidebar) {
|
||||
toggleSidebar.addEventListener('click', openSidebar);
|
||||
}
|
||||
|
||||
// 绑定关闭按钮事件
|
||||
if (closeSidebarBtn) {
|
||||
closeSidebarBtn.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 绑定遮罩层点击事件
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 移动端点击菜单项后自动关闭侧边栏
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// 检查是否是移动设备视图
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加键盘事件监听,按ESC键关闭侧边栏
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
|
||||
Reference in New Issue
Block a user