// 初始化仪表盘面板 function initDashboardPanel() { // 加载统计数据 loadDashboardData(); // 启动实时更新 if (typeof startRealTimeUpdate === 'function') { startRealTimeUpdate(); } } // 加载仪表盘数据 function loadDashboardData() { // 加载统计卡片数据 updateStatCards(); // 加载24小时统计数据 loadHourlyStats(); // 加载请求类型分布 loadRequestsDistribution(); // 加载最常屏蔽的域名 loadTopBlockedDomains(); // 加载最常解析的域名 loadTopResolvedDomains(); } // 更新统计卡片数据 function updateStatCards() { // 获取所有统计数据 apiRequest('/stats') .then(data => { // 更新请求统计 if (data && data.dns) { // 屏蔽请求 const blockedCount = data.dns.Blocked || data.dns.blocked || 0; updateStatCard('blocked-count', blockedCount); // 允许请求 const allowedCount = data.dns.Allowed || data.dns.allowed || 0; updateStatCard('allowed-count', allowedCount); // 错误请求 const errorCount = data.dns.Errors || data.dns.errors || 0; updateStatCard('error-count', errorCount); // 总请求数 const totalCount = blockedCount + allowedCount + errorCount; updateStatCard('total-queries', totalCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('blocked', blockedCount); updateDataHistory('query', totalCount); } // 更新小型图表 if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('blocked-chart', dataHistory.blocked); updateMiniChart('query-chart', dataHistory.query); } } else { // 处理其他可能的数据格式 // 修复语法错误,使用传统的对象属性访问方式 const blockedValue = data && (data.Blocked !== undefined ? data.Blocked : (data.blocked !== undefined ? data.blocked : 0)); const allowedValue = data && (data.Allowed !== undefined ? data.Allowed : (data.allowed !== undefined ? data.allowed : 0)); const errorValue = data && (data.Errors !== undefined ? data.Errors : (data.errors !== undefined ? data.errors : 0)); updateStatCard('blocked-count', blockedValue); updateStatCard('allowed-count', allowedValue); updateStatCard('error-count', errorValue); const totalCount = blockedValue + allowedValue + errorValue; updateStatCard('total-queries', totalCount); } }) .catch(error => { console.error('获取统计数据失败:', error); }); // 获取规则数 apiRequest('/shield') .then(data => { let rulesCount = 0; if (Array.isArray(data)) { rulesCount = data.length; } else if (data && data.rules && Array.isArray(data.rules)) { rulesCount = data.rules.length; } updateStatCard('rules-count', rulesCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('rules', rulesCount); } if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('rules-chart', dataHistory.rules); } }) .catch(error => { console.error('获取规则数失败:', error); }); // 获取Hosts条目数 apiRequest('/shield/hosts') .then(data => { let hostsCount = 0; if (Array.isArray(data)) { hostsCount = data.length; } else if (data && data.hosts && Array.isArray(data.hosts)) { hostsCount = data.hosts.length; } updateStatCard('hosts-count', hostsCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('hosts', hostsCount); } if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('hosts-chart', dataHistory.hosts); } }) .catch(error => { console.error('获取Hosts条目数失败:', error); }); } // 更新单个统计卡片 function updateStatCard(elementId, value) { const element = document.getElementById(elementId); if (!element) return; // 格式化为可读数字 const formattedValue = formatNumber(value); // 更新显示 element.textContent = formattedValue; // 使用全局checkAndAnimate函数检测变化并添加光晕效果 if (typeof checkAndAnimate === 'function') { checkAndAnimate(elementId, value); } } // 加载24小时统计数据 function loadHourlyStats() { apiRequest('/hourly-stats') .then(data => { // 检查数据是否变化,避免不必要的重绘 if (typeof previousChartData !== 'undefined' && JSON.stringify(previousChartData) === JSON.stringify(data)) { return; // 数据未变化,无需更新图表 } previousChartData = JSON.parse(JSON.stringify(data)); // 处理不同可能的数据格式 if (data) { // 优先处理用户提供的实际数据格式 {data: [], labels: []} if (data.labels && data.data && Array.isArray(data.labels) && Array.isArray(data.data)) { // 确保labels和data数组长度一致 if (data.labels.length === data.data.length) { // 假设data数组包含的是屏蔽请求数据,允许请求设为0 renderHourlyChart(data.labels, data.data, Array(data.data.length).fill(0)); return; } } // 处理其他可能的数据格式 if (data.labels && data.blocked && data.allowed) { // 完整数据格式:分别有屏蔽和允许的数据 renderHourlyChart(data.labels, data.blocked, data.allowed); } else if (data.labels && data.data) { // 简化数据格式:只有一组数据 renderHourlyChart(data.labels, data.data, Array(data.data.length).fill(0)); } else { // 尝试直接使用数据对象的属性 const hours = []; const blocked = []; const allowed = []; // 假设数据是按小时组织的对象 for (const key in data) { if (data.hasOwnProperty(key)) { hours.push(key); // 尝试不同的数据结构访问方式 if (typeof data[key] === 'object' && data[key] !== null) { blocked.push(data[key].Blocked || data[key].blocked || 0); allowed.push(data[key].Allowed || data[key].allowed || 0); } else { blocked.push(data[key]); allowed.push(0); } } } // 只在有数据时渲染 if (hours.length > 0) { renderHourlyChart(hours, blocked, allowed); } } } }) .catch(error => { console.error('获取24小时统计失败:', error); // 显示默认空数据,避免图表区域空白 const emptyHours = Array.from({length: 24}, (_, i) => `${i}:00`); const emptyData = Array(24).fill(0); renderHourlyChart(emptyHours, emptyData, emptyData); }); } // 渲染24小时统计图表 function renderHourlyChart(hours, blocked, allowed) { const ctx = document.getElementById('hourly-chart'); if (!ctx) return; // 销毁现有图表 if (window.hourlyChart) { window.hourlyChart.destroy(); } // 创建新图表 window.hourlyChart = new Chart(ctx, { type: 'line', data: { labels: hours, datasets: [ { label: '屏蔽请求', data: blocked, borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)', borderWidth: 2, tension: 0.3, fill: true }, { label: '允许请求', data: allowed, borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)', borderWidth: 2, tension: 0.3, fill: true } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, title: { display: true, text: '请求数' } }, x: { title: { display: true, text: '时间(小时)' } } }, plugins: { legend: { position: 'top', }, tooltip: { mode: 'index', intersect: false } }, animation: { duration: 500 // 快速动画,提升实时更新体验 } } }); } // 加载请求类型分布 - 注意:后端可能没有这个API,暂时注释掉 function loadRequestsDistribution() { // 后端没有对应的API路由,暂时跳过 console.log('请求类型分布API暂不可用'); return Promise.resolve() .then(data => { // 检查数据是否变化,避免不必要的重绘 if (typeof previousFullData !== 'undefined' && JSON.stringify(previousFullData) === JSON.stringify(data)) { return; // 数据未变化,无需更新图表 } previousFullData = JSON.parse(JSON.stringify(data)); // 构造饼图所需的数据,支持多种数据格式 const labels = ['允许请求', '屏蔽请求', '错误请求']; let requestData = [0, 0, 0]; // 默认值 if (data) { // 尝试多种可能的数据结构 if (data.dns) { // 主要数据结构 requestData = [ data.dns.Allowed || data.dns.allowed || 0, data.dns.Blocked || data.dns.blocked || 0, data.dns.Errors || data.dns.errors || 0 ]; } else if (data.Allowed !== undefined || data.Blocked !== undefined) { // 直接在顶级对象中 requestData = [ data.Allowed || data.allowed || 0, data.Blocked || data.blocked || 0, data.Errors || data.errors || 0 ]; } else if (data.requests) { // 可能在requests属性中 requestData = [ data.requests.Allowed || data.requests.allowed || 0, data.requests.Blocked || data.requests.blocked || 0, data.requests.Errors || data.requests.errors || 0 ]; } } // 渲染图表,即使数据全为0也渲染,避免空白 renderRequestsPieChart(labels, requestData); }) .catch(error => { console.error('获取请求类型分布失败:', error); // 显示默认空数据的图表 const labels = ['允许请求', '屏蔽请求', '错误请求']; const defaultData = [0, 0, 0]; renderRequestsPieChart(labels, defaultData); }); } // 渲染请求类型饼图 function renderRequestsPieChart(labels, data) { const ctx = document.getElementById('requests-pie-chart'); if (!ctx) return; // 销毁现有图表 if (window.requestsPieChart) { window.requestsPieChart.destroy(); } // 创建新图表 window.requestsPieChart = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: [ '#2ecc71', // 允许 '#e74c3c', // 屏蔽 '#f39c12', // 错误 '#9b59b6' // 其他 ], borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return `${label}: ${value} (${percentage}%)`; } } } }, cutout: '60%', animation: { duration: 500 // 快速动画,提升实时更新体验 } } }); } // 加载最常屏蔽的域名 function loadTopBlockedDomains() { // 首先获取表格元素并显示加载状态 // 修复语法错误,使用传统的DOM访问方式 const topBlockedTable = document.getElementById('top-blocked-table'); const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null; if (tbody) { // 显示加载中状态 tbody.innerHTML = `