// dashboard.js - 仪表盘功能实现 // 全局变量 let queryTrendChart = null; let ratioChart = null; let intervalId = null; // 初始化仪表盘 async function initDashboard() { try { // 加载初始数据 await loadDashboardData(); // 初始化图表 initCharts(); // 设置定时更新 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 } catch (error) { console.error('初始化仪表盘失败:', error); showNotification('初始化失败: ' + error.message, 'error'); } } // 加载仪表盘数据 async function loadDashboardData() { try { // 并行请求所有数据 const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([ api.getStats(), api.getTopBlockedDomains(), api.getRecentBlockedDomains(), api.getHourlyStats() ]); // 更新统计卡片 updateStatsCards(stats); // 更新数据表格 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); // 更新图表 updateCharts(stats, hourlyStats); // 更新运行状态 updateUptime(); } catch (error) { console.error('加载仪表盘数据失败:', error); // 静默失败,不显示通知以免打扰用户 } } // 更新统计卡片 function updateStatsCards(stats) { const totalQueries = stats.totalQueries || 0; const blockedQueries = stats.blockedQueries || 0; const allowedQueries = stats.allowedQueries || 0; const errorQueries = stats.errorQueries || 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); // 更新百分比(模拟数据,实际应该从API获取) document.getElementById('queries-percent').textContent = '12%'; document.getElementById('blocked-percent').textContent = '8%'; document.getElementById('allowed-percent').textContent = '15%'; document.getElementById('error-percent').textContent = '2%'; } // 更新Top屏蔽域名表格 function updateTopBlockedTable(domains) { const tableBody = document.getElementById('top-blocked-table'); if (!domains || domains.length === 0) { tableBody.innerHTML = ` 暂无数据 `; return; } let html = ''; for (const domain of domains) { html += ` ${domain.name || '未知'} ${formatNumber(domain.count || 0)} `; } tableBody.innerHTML = html; } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(domains) { const tableBody = document.getElementById('recent-blocked-table'); if (!domains || domains.length === 0) { tableBody.innerHTML = ` 暂无数据 `; return; } let html = ''; for (const domain of domains) { const time = formatTime(domain.timestamp || Date.now()); html += ` ${domain.name || '未知'} ${time} `; } tableBody.innerHTML = html; } // 初始化图表 function initCharts() { // 初始化查询趋势图表 const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d'); queryTrendChart = new Chart(queryTrendCtx, { type: 'line', data: { labels: Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`), datasets: [ { label: '总查询', data: Array(24).fill(0), borderColor: '#165DFF', backgroundColor: 'rgba(22, 93, 255, 0.1)', tension: 0.4, fill: true }, { label: '屏蔽数量', data: Array(24).fill(0), borderColor: '#F53F3F', backgroundColor: 'rgba(245, 63, 63, 0.1)', tension: 0.4, fill: true } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, tooltip: { mode: 'index', intersect: false } }, scales: { y: { beginAtZero: true, grid: { drawBorder: false } }, x: { grid: { display: false } } } } }); // 初始化比例图表 const ratioCtx = document.getElementById('ratio-chart').getContext('2d'); ratioChart = new Chart(ratioCtx, { type: 'doughnut', data: { labels: ['正常解析', '被屏蔽', '错误'], datasets: [{ data: [70, 25, 5], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', } }, cutout: '70%' } }); } // 更新图表数据 function updateCharts(stats, hourlyStats) { // 更新比例图表 if (ratioChart) { const total = (stats.totalQueries || 0); const blocked = (stats.blockedQueries || 0); const allowed = (stats.allowedQueries || 0); const error = (stats.errorQueries || 0); if (total > 0) { ratioChart.data.datasets[0].data = [allowed, blocked, error]; ratioChart.update(); } } // 更新趋势图表 if (queryTrendChart && hourlyStats && hourlyStats.length > 0) { const labels = hourlyStats.map(h => `${h.hour}:00`); const totalData = hourlyStats.map(h => h.total || 0); const blockedData = hourlyStats.map(h => h.blocked || 0); queryTrendChart.data.labels = labels; queryTrendChart.data.datasets[0].data = totalData; queryTrendChart.data.datasets[1].data = blockedData; queryTrendChart.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); } }); });