// dashboard.js - 仪表盘页面功能 // 加载仪表盘数据 async function loadDashboardData() { try { // 并行加载所有需要的数据 const [statsData, topBlockedData, recentBlockedData, hourlyStatsData] = await Promise.all([ api.getStats(), api.getTopBlockedDomains(), api.getRecentBlockedDomains(), api.getHourlyStats() ]); // 更新统计卡片 updateStatsCards(statsData); // 更新表格数据 updateTopBlockedTable(topBlockedData); updateRecentBlockedTable(recentBlockedData); // 更新图表 updateQueryTrendChart(hourlyStatsData); updateRatioChart(statsData); } catch (error) { console.error('加载仪表盘数据失败:', error); showErrorMessage('数据加载失败,请刷新页面重试'); } } // 更新统计卡片 function updateStatsCards(data) { const dnsStats = data.dns; // 更新总查询数 document.getElementById('total-queries').textContent = formatNumber(dnsStats.Queries); // 更新屏蔽数量 document.getElementById('blocked-queries').textContent = formatNumber(dnsStats.Blocked); // 更新正常解析数量 document.getElementById('allowed-queries').textContent = formatNumber(dnsStats.Allowed); // 更新错误数量 document.getElementById('error-queries').textContent = formatNumber(dnsStats.Errors); // 计算百分比(简化计算,实际可能需要与历史数据比较) if (dnsStats.Queries > 0) { const blockedPercent = Math.round((dnsStats.Blocked / dnsStats.Queries) * 100); const allowedPercent = Math.round((dnsStats.Allowed / dnsStats.Queries) * 100); const errorPercent = Math.round((dnsStats.Errors / dnsStats.Queries) * 100); document.getElementById('blocked-percent').textContent = `${blockedPercent}%`; document.getElementById('allowed-percent').textContent = `${allowedPercent}%`; document.getElementById('error-percent').textContent = `${errorPercent}%`; document.getElementById('queries-percent').textContent = '100%'; } } // 更新最常屏蔽域名表格 function updateTopBlockedTable(data) { const tableBody = document.getElementById('top-blocked-table'); tableBody.innerHTML = ''; if (data.length === 0) { const row = document.createElement('tr'); row.innerHTML = `暂无数据`; tableBody.appendChild(row); return; } data.forEach(item => { const row = document.createElement('tr'); row.className = 'border-b border-gray-100 hover:bg-gray-50'; row.innerHTML = ` ${item.domain} ${formatNumber(item.count)} `; tableBody.appendChild(row); }); } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(data) { const tableBody = document.getElementById('recent-blocked-table'); tableBody.innerHTML = ''; if (data.length === 0) { const row = document.createElement('tr'); row.innerHTML = `暂无数据`; tableBody.appendChild(row); return; } data.forEach(item => { const row = document.createElement('tr'); row.className = 'border-b border-gray-100 hover:bg-gray-50'; row.innerHTML = ` ${item.domain} ${item.time} `; tableBody.appendChild(row); }); } // 更新查询趋势图表 function updateQueryTrendChart(data) { const ctx = document.getElementById('query-trend-chart').getContext('2d'); // 创建图表 new Chart(ctx, { type: 'line', data: { labels: data.labels, datasets: [{ label: '查询数量', data: data.data, borderColor: '#165DFF', backgroundColor: 'rgba(22, 93, 255, 0.1)', borderWidth: 2, tension: 0.3, fill: true, pointBackgroundColor: '#FFFFFF', pointBorderColor: '#165DFF', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: 12, cornerRadius: 8, titleFont: { size: 14, weight: 'bold' }, bodyFont: { size: 13 } } }, scales: { x: { grid: { display: false }, ticks: { font: { size: 12 } } }, y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { font: { size: 12 }, callback: function(value) { return formatNumber(value); } } } }, interaction: { intersect: false, mode: 'index' } } }); } // 更新比例图表 function updateRatioChart(data) { const dnsStats = data.dns; const ctx = document.getElementById('ratio-chart').getContext('2d'); // 准备数据 const chartData = [dnsStats.Allowed, dnsStats.Blocked, dnsStats.Errors]; const chartColors = ['#00B42A', '#F53F3F', '#FF7D00']; const chartLabels = ['正常解析', '屏蔽', '错误']; // 创建图表 new Chart(ctx, { type: 'doughnut', data: { labels: chartLabels, datasets: [{ data: chartData, backgroundColor: chartColors, borderWidth: 0, hoverOffset: 10 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom',',\, labels: { padding: 20, font: { size: 13 } } }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: 12, cornerRadius: 8, callbacks: { label: function(context) { const value = context.parsed; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${context.label}: ${formatNumber(value)} (${percentage}%)`; } } } }, cutout: '70%' } }); } // 格式化数字 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 showErrorMessage(message) { // 创建错误消息元素 const errorElement = document.createElement('div'); errorElement.className = 'fixed bottom-4 right-4 bg-danger text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center'; errorElement.innerHTML = ` ${message} `; document.body.appendChild(errorElement); // 3秒后自动移除 setTimeout(() => { errorElement.classList.add('opacity-0', 'transition-opacity', 'duration-300'); setTimeout(() => { document.body.removeChild(errorElement); }, 300); }, 3000); } // 定期刷新数据 setInterval(loadDashboardData, 30000); // 每30秒刷新一次