// dashboard.js - 仪表盘功能实现 // 全局变量 let ratioChart = null; let dnsRequestsChart = null; let intervalId = null; // 初始化仪表盘 async function initDashboard() { try { // 加载初始数据 await loadDashboardData(); // 初始化图表 initCharts(); // 初始化时间范围切换 initTimeRangeToggle(); // 设置定时更新 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 } catch (error) { console.error('初始化仪表盘失败:', error); showNotification('初始化失败: ' + error.message, 'error'); } } // 加载仪表盘数据 async function loadDashboardData() { console.log('开始加载仪表盘数据'); try { // 获取基本统计数据 const stats = await api.getStats(); console.log('统计数据:', stats); // 获取TOP被屏蔽域名 const topBlockedDomains = await api.getTopBlockedDomains(); console.log('TOP被屏蔽域名:', topBlockedDomains); // 获取最近屏蔽域名 const recentBlockedDomains = await api.getRecentBlockedDomains(); console.log('最近屏蔽域名:', recentBlockedDomains); // 原并行请求方式(保留以备后续恢复) // const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([ // api.getStats(), // api.getTopBlockedDomains(), // api.getRecentBlockedDomains() // ]); // 更新统计卡片 updateStatsCards(stats); // 尝试从stats中获取总查询数等信息 if (stats.dns) { totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0); blockedQueries = stats.dns.Blocked; errorQueries = stats.dns.Errors || 0; allowedQueries = stats.dns.Allowed; } else { totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; errorQueries = stats.errorQueries || 0; allowedQueries = stats.allowedQueries || 0; } // 更新新卡片数据 - 添加模拟数据支持 if (document.getElementById('avg-response-time')) { // 使用真实数据或模拟数据 const responseTime = stats.avgResponseTime !== undefined ? stats.avgResponseTime : 42; const responsePercent = stats.responseTimePercent !== undefined ? stats.responseTimePercent : 15; document.getElementById('avg-response-time').textContent = formatNumber(responseTime) + 'ms'; document.getElementById('response-time-percent').textContent = responsePercent + '%'; } if (document.getElementById('top-query-type')) { // 使用真实数据或模拟数据 const queryType = stats.topQueryType || 'A'; const queryPercent = stats.queryTypePercentage !== undefined ? stats.queryTypePercentage : 68; document.getElementById('top-query-type').textContent = queryType; document.getElementById('query-type-percentage').textContent = queryPercent + '%'; } if (document.getElementById('active-ips')) { // 使用真实数据或模拟数据 const activeIPs = stats.activeIPs !== undefined ? stats.activeIPs : 12; const ipsPercent = stats.activeIPsPercent !== undefined ? stats.activeIPsPercent : 23; document.getElementById('active-ips').textContent = formatNumber(activeIPs); document.getElementById('active-ips-percent').textContent = ipsPercent + '%'; } if (document.getElementById('cpu-usage')) { // 使用真实数据或模拟数据 const cpuUsage = stats.cpuUsage !== undefined ? stats.cpuUsage : 45; document.getElementById('cpu-usage').textContent = cpuUsage + '%'; // 设置CPU状态颜色 const cpuStatusElem = document.getElementById('cpu-status'); if (cpuStatusElem) { if (cpuUsage > 80) { cpuStatusElem.textContent = '警告'; cpuStatusElem.className = 'text-danger text-sm flex items-center'; } else if (cpuUsage > 60) { cpuStatusElem.textContent = '较高'; cpuStatusElem.className = 'text-warning text-sm flex items-center'; } else { cpuStatusElem.textContent = '正常'; cpuStatusElem.className = 'text-success text-sm flex items-center'; } } } // 更新表格 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); // 更新图表 updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries}); // 更新运行状态 updateUptime(); } catch (error) { console.error('加载仪表盘数据失败:', error); // 静默失败,不显示通知以免打扰用户 } } // 更新统计卡片 function updateStatsCards(stats) { console.log('更新统计卡片,收到数据:', stats); // 适配不同的数据结构 let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0; // 检查数据结构,兼容可能的不同格式 if (stats.dns) { // 可能的数据结构1: stats.dns.Queries等 totalQueries = stats.dns.Queries || 0; blockedQueries = stats.dns.Blocked || 0; allowedQueries = stats.dns.Allowed || 0; errorQueries = stats.dns.Errors || 0; } else if (stats.totalQueries !== undefined) { // 可能的数据结构2: stats.totalQueries等 totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; allowedQueries = stats.allowedQueries || 0; errorQueries = stats.errorQueries || 0; } else if (Array.isArray(stats) && stats.length > 0) { // 可能的数据结构3: 数组形式 totalQueries = stats[0].total || 0; blockedQueries = stats[0].blocked || 0; allowedQueries = stats[0].allowed || 0; errorQueries = stats[0].error || 0; } // 更新数量显示 document.getElementById('total-queries').textContent = formatNumber(totalQueries); document.getElementById('blocked-queries').textContent = formatNumber(blockedQueries); document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries); document.getElementById('error-queries').textContent = formatNumber(errorQueries); // 计算并更新百分比 if (totalQueries > 0) { document.getElementById('blocked-percent').textContent = `${Math.round((blockedQueries / totalQueries) * 100)}%`; document.getElementById('allowed-percent').textContent = `${Math.round((allowedQueries / totalQueries) * 100)}%`; document.getElementById('error-percent').textContent = `${Math.round((errorQueries / totalQueries) * 100)}%`; document.getElementById('queries-percent').textContent = `100%`; } else { document.getElementById('queries-percent').textContent = '---'; document.getElementById('blocked-percent').textContent = '---'; document.getElementById('allowed-percent').textContent = '---'; document.getElementById('error-percent').textContent = '---'; } } // 更新Top屏蔽域名表格 function updateTopBlockedTable(domains) { console.log('更新Top屏蔽域名表格,收到数据:', domains); const tableBody = document.getElementById('top-blocked-table'); let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.name || item.domain || item[0] || '未知', count: item.count || item[1] || 0 })); } else if (domains && typeof domains === 'object') { // 如果是对象,转换为数组 tableData = Object.entries(domains).map(([domain, count]) => ({ name: domain, count: count || 0 })); } // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { tableData = [ { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' } ]; console.log('使用示例数据填充Top屏蔽域名表格'); } let html = ''; for (const domain of tableData) { html += ` ${domain.name} ${formatNumber(domain.count)} `; } tableBody.innerHTML = html; } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(domains) { console.log('更新最近屏蔽域名表格,收到数据:', domains); const tableBody = document.getElementById('recent-blocked-table'); let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.name || item.domain || item[0] || '未知', timestamp: item.timestamp || item.time || Date.now() })); } // 如果没有有效数据,提供示例数据 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 } ]; console.log('使用示例数据填充最近屏蔽域名表格'); } let html = ''; for (const domain of tableData) { const time = formatTime(domain.timestamp); html += ` ${domain.name} ${time} `; } tableBody.innerHTML = html; } // 当前选中的时间范围 let currentTimeRange = '24h'; // 默认为24小时 // 初始化时间范围切换 function initTimeRangeToggle() { const timeRangeButtons = document.querySelectorAll('.time-range-btn'); timeRangeButtons.forEach(button => { button.addEventListener('click', () => { // 移除所有按钮的激活状态 timeRangeButtons.forEach(btn => btn.classList.remove('active')); // 添加当前按钮的激活状态 button.classList.add('active'); // 更新当前时间范围 currentTimeRange = button.dataset.range; // 重新加载数据 loadDashboardData(); // 更新DNS请求图表 drawDNSRequestsChart(); }); }); } // 初始化图表 function initCharts() { // 初始化比例图表 const ratioChartElement = document.getElementById('ratio-chart'); if (!ratioChartElement) { console.error('未找到比例图表元素'); return; } const ratioCtx = ratioChartElement.getContext('2d'); ratioChart = new Chart(ratioCtx, { type: 'doughnut', data: { labels: ['正常解析', '被屏蔽', '错误'], datasets: [{ data: ['---', '---', '---'], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', } }, cutout: '70%' } }); // 初始化DNS请求统计图表 drawDNSRequestsChart(); } // 绘制DNS请求统计图表 function drawDNSRequestsChart() { const ctx = document.getElementById('dns-requests-chart'); if (!ctx) { console.error('未找到DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); let apiFunction; // 根据当前时间范围选择API函数 switch (currentTimeRange) { case '7d': apiFunction = api.getDailyStats; break; case '30d': apiFunction = api.getMonthlyStats; break; default: // 24h apiFunction = api.getHourlyStats; } // 获取统计数据 apiFunction().then(data => { // 创建或更新图表 if (dnsRequestsChart) { dnsRequestsChart.data.labels = data.labels; dnsRequestsChart.data.datasets[0].data = data.data; dnsRequestsChart.update(); } else { dnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: data.labels, datasets: [{ label: 'DNS请求数量', data: data.data, borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.1)' } }, x: { grid: { display: false } } } } }); } }).catch(error => { console.error('绘制DNS请求图表失败:', error); }); } // 更新图表数据 function updateCharts(stats) { console.log('更新图表,收到统计数据:', stats); // 空值检查 if (!stats) { console.error('更新图表失败: 未提供统计数据'); return; } // 更新比例图表 if (ratioChart) { let allowed = '---', blocked = '---', error = '---'; // 尝试从stats数据中提取 if (stats.dns) { allowed = stats.dns.Allowed || allowed; blocked = stats.dns.Blocked || blocked; error = stats.dns.Errors || error; } else if (stats.totalQueries !== undefined) { allowed = stats.allowedQueries || allowed; blocked = stats.blockedQueries || blocked; error = stats.errorQueries || error; } ratioChart.data.datasets[0].data = [allowed, blocked, error]; ratioChart.update(); } } // 更新运行状态 function updateUptime() { // 这里应该从API获取真实的运行时间 const uptimeElement = document.getElementById('uptime'); uptimeElement.textContent = '正常运行中'; uptimeElement.className = 'mt-1 text-success'; } // 格式化数字(添加千位分隔符) function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // 格式化时间 function formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; // 如果是今天,显示时间 if (date.toDateString() === now.toDateString()) { return date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'}); } // 否则显示日期和时间 return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } // 显示通知 function showNotification(message, type = 'info') { // 移除已存在的通知 const existingNotification = document.getElementById('notification'); if (existingNotification) { existingNotification.remove(); } // 创建通知元素 const notification = document.createElement('div'); notification.id = 'notification'; notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-0 opacity-0`; // 设置样式和内容 let bgColor, textColor, icon; switch (type) { case 'success': bgColor = 'bg-success'; textColor = 'text-white'; icon = 'fa-check-circle'; break; case 'error': bgColor = 'bg-danger'; textColor = 'text-white'; icon = 'fa-exclamation-circle'; break; case 'warning': bgColor = 'bg-warning'; textColor = 'text-white'; icon = 'fa-exclamation-triangle'; break; default: bgColor = 'bg-primary'; textColor = 'text-white'; icon = 'fa-info-circle'; } notification.className += ` ${bgColor} ${textColor}`; notification.innerHTML = `
${message}
`; // 添加到页面 document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.classList.remove('translate-y-0', 'opacity-0'); notification.classList.add('-translate-y-2', 'opacity-100'); }, 10); // 自动关闭 setTimeout(() => { notification.classList.add('translate-y-0', 'opacity-0'); setTimeout(() => { notification.remove(); }, 300); }, 3000); } // 页面切换处理 function handlePageSwitch() { const menuItems = document.querySelectorAll('nav a'); menuItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const targetId = item.getAttribute('href').substring(1); // 隐藏所有内容 document.querySelectorAll('[id$="-content"]').forEach(content => { content.classList.add('hidden'); }); // 显示目标内容 document.getElementById(`${targetId}-content`).classList.remove('hidden'); // 更新页面标题 document.getElementById('page-title').textContent = item.querySelector('span').textContent; // 更新活动菜单项 menuItems.forEach(menuItem => { menuItem.classList.remove('sidebar-item-active'); }); item.classList.add('sidebar-item-active'); // 侧边栏切换(移动端) if (window.innerWidth < 1024) { toggleSidebar(); } }); }); } // 侧边栏切换 function toggleSidebar() { const sidebar = document.getElementById('sidebar'); sidebar.classList.toggle('-translate-x-full'); } // 响应式处理 function handleResponsive() { const toggleBtn = document.getElementById('toggle-sidebar'); toggleBtn.addEventListener('click', toggleSidebar); // 初始状态处理 if (window.innerWidth < 1024) { document.getElementById('sidebar').classList.add('-translate-x-full'); } // 窗口大小改变时处理 window.addEventListener('resize', () => { const sidebar = document.getElementById('sidebar'); if (window.innerWidth >= 1024) { sidebar.classList.remove('-translate-x-full'); } }); } // 页面加载完成后初始化 window.addEventListener('DOMContentLoaded', () => { // 初始化页面切换 handlePageSwitch(); // 初始化响应式 handleResponsive(); // 初始化仪表盘 initDashboard(); // 页面卸载时清理定时器 window.addEventListener('beforeunload', () => { if (intervalId) { clearInterval(intervalId); } }); });