From 747f53b997c36bde5f6168bc98840b29aebac813 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Tue, 25 Nov 2025 01:34:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0beta2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/dashboard.js | 492 ++++++++++++++++++++++++++--------------- 1 file changed, 315 insertions(+), 177 deletions(-) diff --git a/static/js/dashboard.js b/static/js/dashboard.js index b056494..b9395d0 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -1,10 +1,32 @@ -// dashboard.js - 仪表盘页面功能 +// 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 [statsData, topBlockedData, recentBlockedData, hourlyStatsData] = await Promise.all([ + // 并行请求所有数据 + const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([ api.getStats(), api.getTopBlockedDomains(), api.getRecentBlockedDomains(), @@ -12,195 +34,161 @@ async function loadDashboardData() { ]); // 更新统计卡片 - updateStatsCards(statsData); + updateStatsCards(stats); - // 更新表格数据 - updateTopBlockedTable(topBlockedData); - updateRecentBlockedTable(recentBlockedData); + // 更新数据表格 + updateTopBlockedTable(topBlockedDomains); + updateRecentBlockedTable(recentBlockedDomains); // 更新图表 - updateQueryTrendChart(hourlyStatsData); - updateRatioChart(statsData); + updateCharts(stats, hourlyStats); + // 更新运行状态 + updateUptime(); } catch (error) { console.error('加载仪表盘数据失败:', error); - showErrorMessage('数据加载失败,请刷新页面重试'); + // 静默失败,不显示通知以免打扰用户 } } // 更新统计卡片 -function updateStatsCards(data) { - const dnsStats = data.dns; +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(dnsStats.Queries); + // 更新数量显示 + 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); - // 更新屏蔽数量 - 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%'; - } + // 更新百分比(模拟数据,实际应该从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%'; } -// 更新最常屏蔽域名表格 -function updateTopBlockedTable(data) { +// 更新Top屏蔽域名表格 +function updateTopBlockedTable(domains) { const tableBody = document.getElementById('top-blocked-table'); - tableBody.innerHTML = ''; - if (data.length === 0) { - const row = document.createElement('tr'); - row.innerHTML = `暂无数据`; - tableBody.appendChild(row); + if (!domains || domains.length === 0) { + tableBody.innerHTML = ` + + 暂无数据 + + `; 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)} + let html = ''; + for (const domain of domains) { + html += ` + + ${domain.name || '未知'} + ${formatNumber(domain.count || 0)} + `; - tableBody.appendChild(row); - }); + } + + tableBody.innerHTML = html; } // 更新最近屏蔽域名表格 -function updateRecentBlockedTable(data) { +function updateRecentBlockedTable(domains) { const tableBody = document.getElementById('recent-blocked-table'); - tableBody.innerHTML = ''; - if (data.length === 0) { - const row = document.createElement('tr'); - row.innerHTML = `暂无数据`; - tableBody.appendChild(row); + if (!domains || domains.length === 0) { + tableBody.innerHTML = ` + + 暂无数据 + + `; 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} + let html = ''; + for (const domain of domains) { + const time = formatTime(domain.timestamp || Date.now()); + html += ` + + ${domain.name || '未知'} + ${time} + `; - tableBody.appendChild(row); - }); + } + + tableBody.innerHTML = html; } -// 更新查询趋势图表 -function updateQueryTrendChart(data) { - const ctx = document.getElementById('query-trend-chart').getContext('2d'); - - // 创建图表 - new Chart(ctx, { +// 初始化图表 +function initCharts() { + // 初始化查询趋势图表 + const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d'); + queryTrendChart = new Chart(queryTrendCtx, { 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 - }] + 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: { - display: false + position: 'top', }, tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - padding: 12, - cornerRadius: 8, - titleFont: { - size: 14, - weight: 'bold' - }, - bodyFont: { - size: 13 - } + mode: 'index', + intersect: false } }, 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); - } + drawBorder: false + } + }, + x: { + grid: { + display: false } } - }, - 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, { + // 初始化比例图表 + const ratioCtx = document.getElementById('ratio-chart').getContext('2d'); + ratioChart = new Chart(ratioCtx, { type: 'doughnut', data: { - labels: chartLabels, + labels: ['正常解析', '被屏蔽', '错误'], datasets: [{ - data: chartData, - backgroundColor: chartColors, - borderWidth: 0, - hoverOffset: 10 + data: [70, 25, 5], + backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], + borderWidth: 0 }] }, options: { @@ -208,26 +196,7 @@ function updateRatioChart(data) { 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}%)`; - } - } + position: 'bottom', } }, cutout: '70%' @@ -235,36 +204,205 @@ function updateRatioChart(data) { }); } -// 格式化数字 -function formatNumber(num) { - if (num >= 1000000) { - return (num / 1000000).toFixed(1) + 'M'; - } else if (num >= 1000) { - return (num / 1000).toFixed(1) + 'K'; +// 更新图表数据 +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(); } - 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} +// 更新运行状态 +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(errorElement); + // 添加到页面 + document.body.appendChild(notification); - // 3秒后自动移除 + // 显示通知 setTimeout(() => { - errorElement.classList.add('opacity-0', 'transition-opacity', 'duration-300'); + 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(() => { - document.body.removeChild(errorElement); + notification.remove(); }, 300); }, 3000); } -// 定期刷新数据 -setInterval(loadDashboardData, 30000); // 每30秒刷新一次 \ No newline at end of file +// 页面切换处理 +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); + } + }); +}); \ No newline at end of file