// dashboard.js - 仪表盘功能实现 // 全局变量 let ratioChart = null; let dnsRequestsChart = null; let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗) let queryTypeChart = null; // 解析类型统计饼图 let intervalId = null; let dashboardWsConnection = null; let dashboardWsReconnectTimer = null; // 存储统计卡片图表实例 let statCardCharts = {}; // 存储统计卡片历史数据 let statCardHistoryData = {}; // 存储仪表盘历史数据,用于计算趋势 window.dashboardHistoryData = window.dashboardHistoryData || { prevResponseTime: null, prevActiveIPs: null, prevTopQueryTypeCount: null }; // 节流相关变量 let lastProcessedTime = 0; const PROCESS_THROTTLE_INTERVAL = 1000; // 1秒节流间隔 // 引入颜色配置文件 const COLOR_CONFIG = window.COLOR_CONFIG || {}; // 全局统计变量 let totalQueries = 0; let blockedQueries = 0; let allowedQueries = 0; let errorQueries = 0; // 初始化仪表盘 async function initDashboard() { try { // 优先加载初始数据,确保页面显示最新信息 await loadDashboardData(); // 初始化图表 initCharts(); // 初始化时间范围切换 initTimeRangeToggle(); // 建立WebSocket连接 connectWebSocket(); // 在页面卸载时清理资源 window.addEventListener('beforeunload', cleanupResources); } catch (error) { console.error('初始化仪表盘失败:', error); showNotification('初始化失败: ' + error.message, 'error'); } } // 建立WebSocket连接 function connectWebSocket() { try { // 构建WebSocket URL const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`; // 创建WebSocket连接 dashboardWsConnection = new WebSocket(wsUrl); // 连接打开事件 dashboardWsConnection.onopen = function() { showNotification('数据更新成功', 'success'); // 清除重连计时器 if (dashboardWsReconnectTimer) { clearTimeout(dashboardWsReconnectTimer); dashboardWsReconnectTimer = null; } }; // 接收消息事件 dashboardWsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === 'initial_data' || data.type === 'stats_update') { processRealTimeData(data.data); } } catch (error) { console.error('处理WebSocket消息失败:', error); } }; // 连接关闭事件 dashboardWsConnection.onclose = function(event) { console.warn('WebSocket连接已关闭,代码:', event.code); dashboardWsConnection = null; // 设置重连 setupReconnect(); }; // 连接错误事件 dashboardWsConnection.onerror = function(error) { console.error('WebSocket连接错误:', error); }; } catch (error) { console.error('创建WebSocket连接失败:', error); // 如果WebSocket连接失败,回退到定时刷新 fallbackToIntervalRefresh(); } } // 设置重连逻辑 function setupReconnect() { if (dashboardWsReconnectTimer) { return; // 已经有重连计时器在运行 } const reconnectDelay = 5000; // 5秒后重连 dashboardWsReconnectTimer = setTimeout(() => { connectWebSocket(); }, reconnectDelay); } // 处理实时数据更新 - 添加节流机制 function processRealTimeData(stats) { // 节流处理,限制执行频率 const now = Date.now(); if (now - lastProcessedTime < PROCESS_THROTTLE_INTERVAL) { return; // 跳过执行 } lastProcessedTime = now; try { // 确保stats是有效的对象 if (!stats || typeof stats !== 'object') { console.error('无效的实时数据:', stats); return; } // 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片 updateStatsCards(stats); // 获取查询类型统计数据 let queryTypeStats = null; if (stats.dns && stats.dns.QueryTypes) { queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({ type, count: Number(count) || 0 })); } // 更新图表数据 updateCharts(stats, queryTypeStats); // 尝试从stats中获取总查询数等信息,并更新全局变量 // 确保使用数字类型 if (stats.dns) { const allowed = Number(stats.dns.Allowed) || 0; const blocked = Number(stats.dns.Blocked) || 0; const errors = Number(stats.dns.Errors || 0); totalQueries = allowed + blocked + errors; blockedQueries = blocked; errorQueries = errors; allowedQueries = allowed; } else { totalQueries = Number(stats.totalQueries) || 0; blockedQueries = Number(stats.blockedQueries) || 0; errorQueries = Number(stats.errorQueries) || 0; allowedQueries = Number(stats.allowedQueries) || 0; } if (document.getElementById('top-query-type')) { const queryType = stats.topQueryType || '---'; document.getElementById('top-query-type').textContent = queryType; const queryPercentElem = document.getElementById('query-type-percentage'); if (queryPercentElem) { // 计算最常用查询类型的百分比 let queryTypePercentage = 0; if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) { const topTypeCount = stats.dns.QueryTypes[queryType] || 0; queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100; } queryPercentElem.textContent = `${Math.round(queryTypePercentage)}%`; queryPercentElem.className = 'text-sm flex items-center text-primary'; } } if (document.getElementById('active-ips')) { const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---'; // 计算活跃IP趋势 let ipsPercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '•'; // 查找箭头元素 const activeIpsPercentElem = document.getElementById('active-ips-percent'); let parent = null; let arrowIcon = null; if (activeIpsPercentElem) { parent = activeIpsPercentElem.parentElement; if (parent) { arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle'); } } if (stats.activeIPs !== undefined) { const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs; // 首次加载时初始化历史数据,不计算趋势 if (prevActiveIPs === null) { window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; ipsPercent = '0.0%'; trendIcon = '•'; trendClass = 'text-gray-500'; if (arrowIcon) { arrowIcon.className = 'fa fa-circle mr-1 text-xs'; parent.className = 'text-gray-500 text-sm flex items-center'; } } else { if (prevActiveIPs > 0) { const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100; ipsPercent = Math.abs(changePercent).toFixed(1) + '%'; // 处理-0.0%的情况 if (ipsPercent === '-0.0%') { ipsPercent = '0.0%'; } // 根据用户要求:数量下降显示红色箭头,上升显示绿色箭头 if (changePercent > 0) { trendIcon = '↑'; trendClass = 'text-success'; if (arrowIcon) { arrowIcon.className = 'fa fa-arrow-up mr-1'; parent.className = 'text-success text-sm flex items-center'; } } else if (changePercent < 0) { trendIcon = '↓'; trendClass = 'text-danger'; if (arrowIcon) { arrowIcon.className = 'fa fa-arrow-down mr-1'; parent.className = 'text-danger text-sm flex items-center'; } } else { // 趋势为0时,显示圆点图标 trendIcon = '•'; trendClass = 'text-gray-500'; if (arrowIcon) { arrowIcon.className = 'fa fa-circle mr-1 text-xs'; parent.className = 'text-gray-500 text-sm flex items-center'; } } } // 更新历史数据 window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; } } document.getElementById('active-ips').textContent = activeIPs; if (activeIpsPercentElem) { activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent; activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`; } } // 实时更新TOP客户端和TOP域名数据 updateTopData(); } catch (error) { console.error('处理实时数据失败:', error); } } // 实时更新TOP客户端和TOP域名数据 async function updateTopData() { try { // 隐藏所有加载中状态 const clientsLoadingElement = document.getElementById('top-clients-loading'); if (clientsLoadingElement) { clientsLoadingElement.classList.add('hidden'); } const domainsLoadingElement = document.getElementById('top-domains-loading'); if (domainsLoadingElement) { domainsLoadingElement.classList.add('hidden'); } // 获取最新的TOP客户端数据 let clientsData = []; try { clientsData = await api.getTopClients(); } catch (error) { console.error('获取TOP客户端数据失败:', error); } if (clientsData && !clientsData.error && Array.isArray(clientsData)) { if (clientsData.length > 0) { // 使用真实数据 updateTopClientsTable(clientsData); // 隐藏错误信息 const errorElement = document.getElementById('top-clients-error'); if (errorElement) errorElement.classList.add('hidden'); } else { // 数据为空,使用模拟数据 const mockClients = [ { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' } ]; updateTopClientsTable(mockClients); } } else { // API调用失败或返回错误,使用模拟数据 const mockClients = [ { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' }, { ip: '---.---.---.---', count: '---' } ]; updateTopClientsTable(mockClients); } // 获取最新的TOP域名数据 let domainsData = []; try { domainsData = await api.getTopDomains(); } catch (error) { console.error('获取TOP域名数据失败:', error); } if (domainsData && !domainsData.error && Array.isArray(domainsData)) { if (domainsData.length > 0) { // 使用真实数据 updateTopDomainsTable(domainsData); // 隐藏错误信息 const errorElement = document.getElementById('top-domains-error'); if (errorElement) errorElement.classList.add('hidden'); } else { // 数据为空,使用模拟数据 const mockDomains = [ { domain: 'example.com', count: 50 }, { domain: 'google.com', count: 45 }, { domain: 'facebook.com', count: 40 }, { domain: 'twitter.com', count: 35 }, { domain: 'youtube.com', count: 30 } ]; updateTopDomainsTable(mockDomains); } } else { // API调用失败或返回错误,使用模拟数据 const mockDomains = [ { domain: 'example.com', count: 50 }, { domain: 'google.com', count: 45 }, { domain: 'facebook.com', count: 40 }, { domain: 'twitter.com', count: 35 }, { domain: 'youtube.com', count: 30 } ]; updateTopDomainsTable(mockDomains); } } catch (error) { console.error('更新TOP数据失败:', error); // 出错时使用模拟数据 const mockDomains = [ { domain: 'example.com', count: 50 }, { domain: 'google.com', count: 45 }, { domain: 'facebook.com', count: 40 }, { domain: 'twitter.com', count: 35 }, { domain: 'youtube.com', count: 30 } ]; updateTopDomainsTable(mockDomains); } } // 回退到定时刷新 function fallbackToIntervalRefresh() { console.warn('回退到定时刷新模式'); showNotification('实时更新连接失败,已切换到定时刷新模式', 'warning'); // 如果已经有定时器,先清除 if (intervalId) { clearInterval(intervalId); } // 设置新的定时器 intervalId = setInterval(async () => { try { await loadDashboardData(); } catch (error) { console.error('定时刷新失败:', error); } }, 5000); // 每5秒更新一次 } // 清理资源 function cleanupResources() { // 清除WebSocket连接 if (dashboardWsConnection) { dashboardWsConnection.close(); dashboardWsConnection = null; } // 清除重连计时器 if (dashboardWsReconnectTimer) { clearTimeout(dashboardWsReconnectTimer); dashboardWsReconnectTimer = null; } // 清除定时刷新 if (intervalId) { clearInterval(intervalId); intervalId = null; } // 清除图表实例,释放内存 if (ratioChart) { ratioChart.destroy(); ratioChart = null; } if (dnsRequestsChart) { dnsRequestsChart.destroy(); dnsRequestsChart = null; } if (detailedDnsRequestsChart) { detailedDnsRequestsChart.destroy(); detailedDnsRequestsChart = null; } if (queryTypeChart) { queryTypeChart.destroy(); queryTypeChart = null; } // 清除统计卡片图表实例 for (const key in statCardCharts) { if (statCardCharts[key]) { statCardCharts[key].destroy(); delete statCardCharts[key]; } } // 清除事件监听器 window.removeEventListener('beforeunload', cleanupResources); } // 加载仪表盘数据 // 更新统计卡片 function updateStatsCards(stats) { // 适配不同的数据结构 // 保存当前显示的值,用于在数据缺失时保留 let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0; let topQueryType = 'A', queryTypePercentage = 0; let activeIPs = 0, activeIPsPercentage = 0; let avgResponseTime = 0; // 优先从API数据中获取值,仅在API数据不可用时使用DOM中的当前值 // 解析当前显示的值,作为备用默认值 const getCurrentValue = (elem) => { if (!elem) return 0; const text = elem.textContent.replace(/,/g, '').replace(/[^0-9]/g, ''); return parseInt(text) || 0; }; // 检查数据结构,兼容可能的不同格式 if (stats) { // 优先使用顶层字段,只有当值存在时才更新 if (stats.totalQueries !== undefined) { totalQueries = Number(stats.totalQueries) || 0; } if (stats.blockedQueries !== undefined) { blockedQueries = Number(stats.blockedQueries) || 0; } if (stats.allowedQueries !== undefined) { allowedQueries = Number(stats.allowedQueries) || 0; } if (stats.errorQueries !== undefined) { errorQueries = Number(stats.errorQueries) || 0; } if (stats.topQueryType !== undefined) { topQueryType = stats.topQueryType; } if (stats.queryTypePercentage !== undefined) { queryTypePercentage = stats.queryTypePercentage; } if (stats.activeIPs !== undefined) { activeIPs = Number(stats.activeIPs) || 0; } if (stats.activeIPsPercentage !== undefined) { activeIPsPercentage = stats.activeIPsPercentage; } if (stats.avgResponseTime !== undefined) { avgResponseTime = Number(stats.avgResponseTime) || 0; } if (stats.responseTime !== undefined) { avgResponseTime = Number(stats.responseTime) || 0; } // 如果dns对象存在,优先使用其中的数据 if (stats.dns) { // 计算总查询数,确保准确性 if (stats.dns.Allowed !== undefined && stats.dns.Blocked !== undefined) { const allowed = Number(stats.dns.Allowed) || 0; const blocked = Number(stats.dns.Blocked) || 0; const errors = Number(stats.dns.Errors || 0); totalQueries = allowed + blocked + errors; allowedQueries = allowed; blockedQueries = blocked; errorQueries = errors; } else if (stats.dns.Queries !== undefined) { totalQueries = Number(stats.dns.Queries) || 0; } // 确保使用dns对象中的具体数值 if (stats.dns.Blocked !== undefined) { blockedQueries = Number(stats.dns.Blocked) || 0; } if (stats.dns.Allowed !== undefined) { allowedQueries = Number(stats.dns.Allowed) || 0; } if (stats.dns.Errors !== undefined) { errorQueries = Number(stats.dns.Errors) || 0; } // 计算最常用查询类型的百分比 if (stats.dns.QueryTypes && stats.dns.Queries > 0) { const topTypeCount = stats.dns.QueryTypes[topQueryType] || 0; queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100; } // 计算活跃IP百分比(基于已有的活跃IP数) if (activeIPs > 0 && stats.dns.SourceIPs) { activeIPsPercentage = activeIPs / Object.keys(stats.dns.SourceIPs).length * 100; } // 检查并更新平均响应时间 if (stats.dns.AvgResponseTime !== undefined) { avgResponseTime = Number(stats.dns.AvgResponseTime) || 0; } } } else if (Array.isArray(stats) && stats.length > 0) { // 可能的数据结构3: 数组形式 if (stats[0].total !== undefined) { totalQueries = Number(stats[0].total) || 0; } if (stats[0].blocked !== undefined) { blockedQueries = Number(stats[0].blocked) || 0; } if (stats[0].allowed !== undefined) { allowedQueries = Number(stats[0].allowed) || 0; } if (stats[0].error !== undefined) { errorQueries = Number(stats[0].error) || 0; } if (stats[0].topQueryType !== undefined) { topQueryType = stats[0].topQueryType; } if (stats[0].queryTypePercentage !== undefined) { queryTypePercentage = stats[0].queryTypePercentage; } if (stats[0].activeIPs !== undefined) { activeIPs = Number(stats[0].activeIPs) || 0; } if (stats[0].activeIPsPercentage !== undefined) { activeIPsPercentage = stats[0].activeIPsPercentage; } if (stats[0].avgResponseTime !== undefined) { avgResponseTime = Number(stats[0].avgResponseTime) || 0; } if (stats[0].responseTime !== undefined) { avgResponseTime = Number(stats[0].responseTime) || 0; } } else { // 仅在API数据完全不可用时,才从DOM中获取当前值作为默认值 const totalQueriesElem = document.getElementById('total-queries'); const blockedQueriesElem = document.getElementById('blocked-queries'); const allowedQueriesElem = document.getElementById('allowed-queries'); const errorQueriesElem = document.getElementById('error-queries'); const activeIPsElem = document.getElementById('active-ips'); totalQueries = getCurrentValue(totalQueriesElem); blockedQueries = getCurrentValue(blockedQueriesElem); allowedQueries = getCurrentValue(allowedQueriesElem); errorQueries = getCurrentValue(errorQueriesElem); activeIPs = getCurrentValue(activeIPsElem); } // 存储正在进行的动画状态,避免动画重叠 const animationInProgress = {}; // 为数字元素添加翻页滚动特效 function animateValue(elementId, newValue) { const element = document.getElementById(elementId); if (!element) return; const formattedNewValue = formatNumber(newValue); const currentValue = element.textContent; // 如果值没有变化,不执行任何操作 if (currentValue === formattedNewValue) { return; } // 简化动画:使用CSS opacity过渡实现平滑更新 element.style.transition = 'opacity 0.3s ease-in-out'; element.style.opacity = '0'; // 使用requestAnimationFrame确保平滑过渡 requestAnimationFrame(() => { element.textContent = formattedNewValue; element.style.opacity = '1'; // 移除transition样式,避免影响后续更新 setTimeout(() => { element.style.transition = ''; }, 300); }); } // 更新百分比元素的函数 function updatePercentage(elementId, value) { const element = document.getElementById(elementId); if (!element) return; // 检查是否有正在进行的动画 if (animationInProgress[elementId + '_percent']) { clearTimeout(animationInProgress[elementId + '_percent']); } try { element.style.opacity = '0'; element.style.transition = 'opacity 200ms ease-out'; // 保存定时器ID,便于后续可能的取消 animationInProgress[elementId + '_percent'] = setTimeout(() => { try { element.textContent = value; element.style.opacity = '1'; } catch (e) { console.error('更新百分比元素失败:', e); } finally { // 清除动画状态标记 delete animationInProgress[elementId + '_percent']; } }, 200); } catch (e) { console.error('设置百分比动画失败:', e); // 出错时直接设置值 try { element.textContent = value; element.style.opacity = '1'; } catch (e2) { console.error('直接更新百分比元素也失败:', e2); } } } // 平滑更新数量显示 animateValue('total-queries', totalQueries); animateValue('blocked-queries', blockedQueries); animateValue('allowed-queries', allowedQueries); animateValue('error-queries', errorQueries); animateValue('active-ips', activeIPs); // DNSSEC相关数据 // 优先从DOM中获取当前显示的值,作为默认值 const dnssecSuccessElem = document.getElementById('dnssec-success'); const dnssecFailedElem = document.getElementById('dnssec-failed'); const dnssecQueriesElem = document.getElementById('dnssec-queries'); // 从当前显示值初始化,确保数据刷新前保留前一次结果 let dnssecEnabled = false; let dnssecQueries = getCurrentValue(dnssecQueriesElem); let dnssecSuccess = getCurrentValue(dnssecSuccessElem); let dnssecFailed = getCurrentValue(dnssecFailedElem); let dnssecUsage = 0; // 检查DNSSEC数据 if (stats) { // 优先使用顶层字段,只有当值存在时才更新 if (stats.dnssecEnabled !== undefined) dnssecEnabled = stats.dnssecEnabled; if (stats.dnssecQueries !== undefined) dnssecQueries = stats.dnssecQueries; if (stats.dnssecSuccess !== undefined) dnssecSuccess = stats.dnssecSuccess; if (stats.dnssecFailed !== undefined) dnssecFailed = stats.dnssecFailed; if (stats.dnssecUsage !== undefined) dnssecUsage = stats.dnssecUsage; // 如果dns对象存在,优先使用其中的数据 if (stats.dns) { if (stats.dns.DNSSECEnabled !== undefined) dnssecEnabled = stats.dns.DNSSECEnabled; if (stats.dns.DNSSECQueries !== undefined) dnssecQueries = stats.dns.DNSSECQueries; if (stats.dns.DNSSECSuccess !== undefined) dnssecSuccess = stats.dns.DNSSECSuccess; if (stats.dns.DNSSECFailed !== undefined) dnssecFailed = stats.dns.DNSSECFailed; } // 如果没有直接提供使用率,计算使用率 if (dnssecUsage === 0 && totalQueries > 0) { dnssecUsage = (dnssecQueries / totalQueries) * 100; } } // 更新DNSSEC统计卡片 const dnssecUsageElement = document.getElementById('dnssec-usage'); const dnssecStatusElement = document.getElementById('dnssec-status'); const dnssecSuccessElement = document.getElementById('dnssec-success'); const dnssecFailedElement = document.getElementById('dnssec-failed'); const dnssecQueriesElement = document.getElementById('dnssec-queries'); if (dnssecUsageElement) { dnssecUsageElement.textContent = `${Math.round(dnssecUsage)}%`; } if (dnssecStatusElement) { dnssecStatusElement.textContent = dnssecEnabled ? '已启用' : '已禁用'; dnssecStatusElement.className = `text-sm flex items-center ${dnssecEnabled ? 'text-success' : 'text-danger'}`; } if (dnssecSuccessElement) { dnssecSuccessElement.textContent = formatNumber(dnssecSuccess); } if (dnssecFailedElement) { dnssecFailedElement.textContent = formatNumber(dnssecFailed); } if (dnssecQueriesElement) { dnssecQueriesElement.textContent = formatNumber(dnssecQueries); } // 直接更新文本和百分比,移除动画效果 const topQueryTypeElement = document.getElementById('top-query-type'); const queryTypePercentageElement = document.getElementById('query-type-percentage'); const activeIpsPercentElement = document.getElementById('active-ips-percent'); if (topQueryTypeElement) topQueryTypeElement.textContent = topQueryType; if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`; if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`; // 计算并平滑更新百分比,同时更新箭头颜色和方向 function updatePercentWithArrow(elementId, percentage, prevValue, currentValue) { const element = document.getElementById(elementId); if (!element) return; // 更新百分比数值 updatePercentage(elementId, percentage); // 查找父元素,获取箭头图标 const parent = element.parentElement; if (!parent) return; let arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle'); if (!arrowIcon) return; // 计算变化趋势 let isIncrease = currentValue > prevValue; let isDecrease = currentValue < prevValue; let isNoChange = currentValue === prevValue; // 处理百分比显示,避免-0.0%的情况 let formattedPercentage = percentage; if (percentage === '-0.0%') { formattedPercentage = '0.0%'; updatePercentage(elementId, formattedPercentage); } // 响应时间特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头 if (elementId === 'response-time-percent') { // 反转箭头逻辑 [isIncrease, isDecrease] = [isDecrease, isIncrease]; } // 更新箭头图标和颜色 if (isIncrease) { arrowIcon.className = 'fa fa-arrow-up mr-1'; parent.className = 'text-success text-sm flex items-center'; } else if (isDecrease) { arrowIcon.className = 'fa fa-arrow-down mr-1'; parent.className = 'text-danger text-sm flex items-center'; } else if (isNoChange) { // 趋势为0时,显示圆点图标 arrowIcon.className = 'fa fa-circle mr-1 text-xs'; parent.className = 'text-gray-500 text-sm flex items-center'; } } // 保存历史数据,用于计算趋势 window.dashboardHistoryData = window.dashboardHistoryData || { totalQueries: 0, blockedQueries: 0, allowedQueries: 0, errorQueries: 0, avgResponseTime: 0 }; // 计算百分比并更新箭头 if (totalQueries > 0) { const queriesPercent = '100%'; const blockedPercent = `${Math.round((blockedQueries / totalQueries) * 100)}%`; const allowedPercent = `${Math.round((allowedQueries / totalQueries) * 100)}%`; const errorPercent = `${Math.round((errorQueries / totalQueries) * 100)}%`; updatePercentWithArrow('queries-percent', queriesPercent, window.dashboardHistoryData.totalQueries, totalQueries); updatePercentWithArrow('blocked-percent', blockedPercent, window.dashboardHistoryData.blockedQueries, blockedQueries); updatePercentWithArrow('allowed-percent', allowedPercent, window.dashboardHistoryData.allowedQueries, allowedQueries); updatePercentWithArrow('error-percent', errorPercent, window.dashboardHistoryData.errorQueries, errorQueries); } else { updatePercentage('queries-percent', '---'); updatePercentage('blocked-percent', '---'); updatePercentage('allowed-percent', '---'); updatePercentage('error-percent', '---'); } // 更新平均响应时间卡片 if (document.getElementById('avg-response-time')) { const responseTime = avgResponseTime ? avgResponseTime.toFixed(2) + 'ms' : '---'; document.getElementById('avg-response-time').textContent = responseTime; // 更新平均响应时间的百分比和箭头,使用与其他统计卡片相同的逻辑 if (avgResponseTime !== undefined && avgResponseTime !== null) { // 计算变化百分比 let responsePercent = '0.0%'; const prevResponseTime = window.dashboardHistoryData.avgResponseTime || 0; const currentResponseTime = avgResponseTime; if (prevResponseTime > 0) { const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100; responsePercent = Math.abs(changePercent).toFixed(1) + '%'; } // 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头 // updatePercentWithArrow函数内部已添加响应时间的特殊处理 updatePercentWithArrow('response-time-percent', responsePercent, prevResponseTime, currentResponseTime); } else { updatePercentage('response-time-percent', '---'); } } // 更新历史数据 window.dashboardHistoryData.totalQueries = totalQueries; window.dashboardHistoryData.blockedQueries = blockedQueries; window.dashboardHistoryData.allowedQueries = allowedQueries; window.dashboardHistoryData.errorQueries = errorQueries; // 只在avgResponseTime不为0时更新历史数据,保留上一次不为0的状态 if (avgResponseTime > 0) { window.dashboardHistoryData.avgResponseTime = avgResponseTime; } } // 更新Top屏蔽域名表格 async function updateTopBlockedTable(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: '---' } ]; } // 计算总拦截次数 const totalCount = tableData.reduce((sum, domain) => { return sum + (typeof domain.count === 'number' ? domain.count : 0); }, 0); let html = ''; for (let i = 0; i < tableData.length; i++) { const domain = tableData[i]; // 检查域名是否是跟踪器 const trackerInfo = await isDomainInTrackerDatabase(domain.name); const isTracker = trackerInfo !== null; // 构建跟踪器浮窗内容 const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
URL: ${trackerInfo.url}
` : ''} ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : ''; // 计算百分比 const percentage = totalCount > 0 && typeof domain.count === 'number' ? ((domain.count / totalCount) * 100).toFixed(2) : '0.00'; html += `
${i + 1}
${domain.name} ${isTracker ? `
${trackerTooltip}
` : ''}
${formatNumber(domain.count)} ${percentage}%
`; } tableBody.innerHTML = html; // 添加跟踪器图标悬停事件 const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container'); trackerIconContainers.forEach(container => { const tooltip = container.querySelector('.tracker-tooltip'); if (tooltip) { // 移除内联样式,使用CSS类控制显示 tooltip.removeAttribute('style'); container.addEventListener('mouseenter', () => { tooltip.classList.add('visible'); }); container.addEventListener('mouseleave', () => { tooltip.classList.remove('visible'); }); } }); } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(domains) { const tableBody = document.getElementById('recent-blocked-table'); // 确保tableBody存在,因为最近屏蔽域名卡片可能已被移除 if (!tableBody) { return; } let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.name || item.domain || item[0] || '未知', timestamp: item.timestamp || item.time || Date.now(), type: item.type || '广告' })); } // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { const now = Date.now(); tableData = [ { name: '---.---.---', timestamp: now - 5 * 60 * 1000, type: '广告' }, { name: '---.---.---', timestamp: now - 15 * 60 * 1000, type: '恶意' }, { name: '---.---.---', timestamp: now - 30 * 60 * 1000, type: '广告' }, { name: '---.---.---', timestamp: now - 45 * 60 * 1000, type: '追踪' }, { name: '---.---.---', timestamp: now - 60 * 60 * 1000, type: '恶意' } ]; } let html = ''; for (let i = 0; i < tableData.length; i++) { const domain = tableData[i]; const time = formatTime(domain.timestamp); html += `
${domain.name}
${time}
${domain.type}
`; } tableBody.innerHTML = html; } // IP地理位置缓存(检查是否已经存在,避免重复声明) if (typeof ipGeolocationCache === 'undefined') { var ipGeolocationCache = {}; var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 } // 跟踪器数据库缓存(检查是否已经存在,避免重复声明) if (typeof trackersDatabase === 'undefined') { var trackersDatabase = null; var trackersLoaded = false; var trackersLoading = false; } // 加载跟踪器数据库 async function loadTrackersDatabase() { if (trackersLoaded) return trackersDatabase; if (trackersLoading) { // 等待正在进行的加载完成 while (trackersLoading) { await new Promise(resolve => setTimeout(resolve, 100)); } return trackersDatabase; } trackersLoading = true; try { const response = await fetch('domain-info/tracker/trackers.json'); if (!response.ok) { console.error('加载跟踪器数据库失败:', response.statusText); trackersDatabase = { trackers: {} }; return trackersDatabase; } trackersDatabase = await response.json(); trackersLoaded = true; return trackersDatabase; } catch (error) { console.error('加载跟踪器数据库失败:', error); trackersDatabase = { trackers: {} }; return trackersDatabase; } finally { trackersLoading = false; } } // 检查域名是否在跟踪器数据库中 async function isDomainInTrackerDatabase(domain) { if (!trackersDatabase || !trackersLoaded) { await loadTrackersDatabase(); } if (!trackersDatabase || !trackersDatabase.trackers) { return null; } // 检查域名是否直接作为跟踪器键存在 if (trackersDatabase.trackers.hasOwnProperty(domain)) { return trackersDatabase.trackers[domain]; } // 检查域名是否在跟踪器URL中 for (const trackerKey in trackersDatabase.trackers) { if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) { const tracker = trackersDatabase.trackers[trackerKey]; if (tracker && tracker.url) { try { const trackerUrl = new URL(tracker.url); if (trackerUrl.hostname === domain) { return tracker; } } catch (e) { // 忽略无效URL } } } } return null; } // 获取IP地理位置信息 async function getIpGeolocation(ip) { // 检查是否为内网IP if (isPrivateIP(ip)) { return "内网 内网"; } // 检查缓存 const now = Date.now(); if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) { return ipGeolocationCache[ip].location; } try { // 使用whois.pconline.com.cn API获取IP地理位置 const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`; const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 解析响应数据 const data = await response.json(); let location = "未知 未知"; if (data && data.addr) { // 直接使用addr字段作为完整的地理位置信息 location = data.addr; } // 保存到缓存 ipGeolocationCache[ip] = { location: location, timestamp: now }; return location; } catch (error) { console.error('获取IP地理位置失败:', error); return "未知 未知"; } } // 检查是否为内网IP function isPrivateIP(ip) { const parts = ip.split('.'); // 检查IPv4内网地址 if (parts.length === 4) { const first = parseInt(parts[0]); const second = parseInt(parts[1]); // 10.0.0.0/8 if (first === 10) { return true; } // 172.16.0.0/12 if (first === 172 && second >= 16 && second <= 31) { return true; } // 192.168.0.0/16 if (first === 192 && second === 168) { return true; } // 127.0.0.0/8 (localhost) if (first === 127) { return true; } // 169.254.0.0/16 (link-local) if (first === 169 && second === 254) { return true; } } // 检查IPv6内网地址 if (ip.includes(':')) { // ::1/128 (localhost) if (ip === '::1' || ip.startsWith('0:0:0:0:0:0:0:1')) { return true; } // fc00::/7 (unique local address) if (ip.startsWith('fc') || ip.startsWith('fd')) { return true; } // fe80::/10 (link-local) if (ip.startsWith('fe80:')) { return true; } } return false; } // 更新TOP客户端表格 async function updateTopClientsTable(clients) { const tableBody = document.getElementById('top-clients-table'); // 确保tableBody存在 if (!tableBody) { console.error('未找到top-clients-table元素'); return; } // 隐藏加载中状态 const loadingElement = document.getElementById('top-clients-loading'); if (loadingElement) { loadingElement.classList.add('hidden'); } // 显示数据区域 tableBody.classList.remove('hidden'); let tableData = []; // 适配不同的数据结构 if (Array.isArray(clients)) { tableData = clients.map(item => ({ ip: item.ip || item[0] || '未知', count: item.count || item[1] || 0 })); } else if (clients && typeof clients === 'object') { // 如果是对象,转换为数组 tableData = Object.entries(clients).map(([ip, count]) => ({ ip, count: count || 0 })); } // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { tableData = [ { ip: '---.---.---', count: '---' }, { ip: '---.---.---', count: '---' }, { ip: '---.---.---', count: '---' }, { ip: '---.---.---', count: '---' }, { ip: '---.---.---', count: '---' } ]; } let html = ''; for (let i = 0; i < tableData.length; i++) { const client = tableData[i]; // 获取IP地理信息 const location = await getIpGeolocation(client.ip); html += `
${i + 1}
${client.ip} ${location}
${formatNumber(client.count)}
`; } tableBody.innerHTML = html; } // 更新请求域名排行表格 async function updateTopDomainsTable(domains) { const tableBody = document.getElementById('top-domains-table'); // 确保tableBody存在 if (!tableBody) { console.error('未找到top-domains-table元素'); return; } let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.domain || item.name || 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: 'example.com', count: 50 }, { name: 'google.com', count: 45 }, { name: 'facebook.com', count: 40 }, { name: 'twitter.com', count: 35 }, { name: 'youtube.com', count: 30 } ]; } // 计算总请求次数 const totalCount = tableData.reduce((sum, domain) => { return sum + (typeof domain.count === 'number' ? domain.count : 0); }, 0); let html = ''; for (let i = 0; i < tableData.length; i++) { const domain = tableData[i]; // 检查域名是否是跟踪器 const trackerInfo = await isDomainInTrackerDatabase(domain.name); const isTracker = trackerInfo !== null; // 构建跟踪器浮窗内容 const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
URL: ${trackerInfo.url}
` : ''} ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : ''; // 计算百分比 const percentage = totalCount > 0 && typeof domain.count === 'number' ? ((domain.count / totalCount) * 100).toFixed(2) : '0.00'; html += `
${i + 1}
${domain.name}${domain.dnssec ? ' ' : ''} ${isTracker ? `
${trackerTooltip}
` : ''}
${formatNumber(domain.count)} ${percentage}%
`; } tableBody.innerHTML = html; // 添加跟踪器图标悬停事件 const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container'); trackerIconContainers.forEach(container => { const tooltip = container.querySelector('.tracker-tooltip'); if (tooltip) { // 移除内联样式,使用CSS类控制显示 tooltip.removeAttribute('style'); container.addEventListener('mouseenter', () => { tooltip.classList.add('visible'); }); container.addEventListener('mouseleave', () => { tooltip.classList.remove('visible'); }); } }); } // 当前选中的时间范围 let currentTimeRange = '24h'; // 默认为24小时 let lastSelectedIndex = 0; // 最后选中的按钮索引,24小时是第一个按钮 // 详细图表专用变量 let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围 // 初始化时间范围切换 function initTimeRangeToggle() { // 查找所有可能的时间范围按钮类名 const allTimeRangeButtons = document.querySelectorAll('.time-range-btn, .time-range-button, .timerange-btn, button[data-range]'); // 排除图表模态框内的按钮 const chartModal = document.getElementById('chart-modal'); const timeRangeButtons = Array.from(allTimeRangeButtons).filter(button => { // 检查按钮是否是图表模态框的后代 return !chartModal || !chartModal.contains(button); }); if (timeRangeButtons.length === 0) { console.warn('未找到时间范围按钮,请检查HTML中的类名'); return; } // 定义三个按钮的不同样式配置,增加activeHover属性 const buttonStyles = [ { // 24小时按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-blue-100'], active: ['bg-blue-500', 'text-white'], activeHover: ['hover:bg-blue-400'] // 选中时的浅色悬停 }, { // 7天按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-green-100'], active: ['bg-green-500', 'text-white'], activeHover: ['hover:bg-green-400'] // 选中时的浅色悬停 }, { // 30天按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-purple-100'], active: ['bg-purple-500', 'text-white'], activeHover: ['hover:bg-purple-400'] // 选中时的浅色悬停 } ]; // 为所有按钮设置初始样式和事件 timeRangeButtons.forEach((button, index) => { // 使用相应的样式配置 const styleConfig = buttonStyles[index % buttonStyles.length]; // 移除所有按钮的初始样式 button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700', 'bg-green-500', 'bg-purple-500', 'bg-gray-100'); // 设置非选中状态样式 button.classList.add('transition-colors', 'duration-200'); button.classList.add(...styleConfig.normal); button.classList.add(...styleConfig.hover); // 移除鼠标悬停提示 button.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); // 重置所有按钮为非选中状态 timeRangeButtons.forEach((btn, btnIndex) => { const btnStyle = buttonStyles[btnIndex % buttonStyles.length]; // 移除所有可能的激活状态类 btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', 'bg-gray-500'); btn.classList.remove(...btnStyle.active); btn.classList.remove(...btnStyle.activeHover); // 添加非选中状态类 btn.classList.add(...btnStyle.normal); btn.classList.add(...btnStyle.hover); }); // 普通选中模式 lastSelectedIndex = index; // 设置当前按钮为激活状态 button.classList.remove(...styleConfig.normal); button.classList.remove(...styleConfig.hover); button.classList.add('active'); button.classList.add(...styleConfig.active); button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停 // 获取并更新当前时间范围 let rangeValue; if (button.dataset.range) { rangeValue = button.dataset.range; } else { const btnText = button.textContent.trim(); if (btnText.includes('24')) { rangeValue = '24h'; } else if (btnText.includes('7')) { rangeValue = '7d'; } else if (btnText.includes('30')) { rangeValue = '30d'; } else { rangeValue = btnText.replace(/[^0-9a-zA-Z]/g, ''); } } currentTimeRange = rangeValue; // 重新加载数据 loadDashboardData(); // 更新DNS请求图表 drawDNSRequestsChart(); }); // 移除自定义鼠标悬停提示效果 }); // 确保默认选中24小时按钮 if (timeRangeButtons.length > 0) { // 选择24小时按钮(索引为0),如果不存在则使用第一个按钮 const defaultButtonIndex = 0; const defaultButton = timeRangeButtons[defaultButtonIndex] || timeRangeButtons[0]; const defaultStyle = buttonStyles[defaultButtonIndex % buttonStyles.length] || buttonStyles[0]; // 先重置所有按钮 timeRangeButtons.forEach((btn, index) => { const btnStyle = buttonStyles[index % buttonStyles.length]; btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', 'bg-gray-500', 'mixed-view-active'); btn.classList.remove(...btnStyle.active); btn.classList.remove(...btnStyle.activeHover); btn.classList.add(...btnStyle.normal); btn.classList.add(...btnStyle.hover); }); // 然后设置24小时按钮为激活状态 defaultButton.classList.remove(...defaultStyle.normal); defaultButton.classList.remove(...defaultStyle.hover); defaultButton.classList.add('active'); defaultButton.classList.add(...defaultStyle.active); defaultButton.classList.add(...defaultStyle.activeHover); // 设置默认时间范围为24小时 currentTimeRange = '24h'; } } // 注意:这个函数已被后面的实现覆盖,请使用后面的drawDetailedDNSRequestsChart函数 // 初始化图表 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: [0, 0, 0], backgroundColor: ['#34D399', '#EF4444', '#F59E0B'], // 优化的现代化配色 borderWidth: 3, // 增加边框宽度,增强区块分隔 borderColor: '#FFFFFF', // 白色边框,使各个扇区更清晰 hoverOffset: 15, // 增加悬停偏移效果,增强交互体验 hoverBorderWidth: 4, // 悬停时增加边框宽度 hoverBackgroundColor: ['#10B981', '#DC2626', '#D97706'], // 悬停时的深色效果 borderRadius: 10, // 添加圆角效果,增强现代感 borderSkipped: false // 显示所有边框 }] }, options: { responsive: true, maintainAspectRatio: false, // 简化动画,提高性能 animation: { duration: 300, // 缩短动画时间 easing: 'easeOutQuart', // 简化缓动函数 animateRotate: true, // 仅保留旋转动画 animateScale: false // 禁用缩放动画 }, plugins: { legend: { position: 'bottom', align: 'center', labels: { boxWidth: 12, // 调整图例框的宽度 font: { size: 11, // 调整字体大小 family: 'Inter, system-ui, sans-serif', // 使用现代字体 weight: 500 // 字体粗细 }, padding: 12, // 调整内边距 lineHeight: 1.5, // 调整行高 usePointStyle: true, // 使用点样式代替方形图例,节省空间 pointStyle: 'circle', // 使用圆形点样式 color: '#4B5563', // 图例文本颜色 generateLabels: function(chart) { // 自定义图例生成,添加更多样式控制 const data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map((label, i) => { const dataset = data.datasets[0]; return { text: label, fillStyle: dataset.backgroundColor[i], strokeStyle: dataset.borderColor, lineWidth: dataset.borderWidth, pointStyle: 'circle', hidden: !chart.isDatasetVisible(0), index: i }; }); } return []; } } }, tooltip: { enabled: true, backgroundColor: 'rgba(17, 24, 39, 0.9)', // 深背景,增强可读性 padding: 12, // 增加内边距 titleFont: { size: 13, // 标题字体大小 family: 'Inter, system-ui, sans-serif', weight: 600 }, bodyFont: { size: 12, // 正文字体大小 family: 'Inter, system-ui, sans-serif' }, bodySpacing: 6, // 正文行间距 displayColors: true, // 显示颜色指示器 callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((acc, val) => acc + val, 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; }, afterLabel: function(context) { // 可以添加额外的信息 } }, cornerRadius: 8, // 圆角 boxPadding: 6, // 盒子内边距 borderColor: 'rgba(255, 255, 255, 0.2)', // 边框颜色 borderWidth: 1 // 边框宽度 }, title: { display: false // 不显示标题,由HTML标题代替 } }, cutout: '70%', // 调整中心空白区域比例,增强现代感 // 增强元素配置 elements: { arc: { borderAlign: 'center', tension: 0.1, // 添加轻微的张力,使圆弧更平滑 borderWidth: 3 // 统一边框宽度 } }, layout: { padding: { top: 20, // 增加顶部内边距 right: 20, bottom: 30, // 增加底部内边距,为图例预留更多空间 left: 20 } }, // 添加交互配置 interaction: { mode: 'nearest', // 交互模式 axis: 'x', // 交互轴 intersect: false // 不要求精确相交 }, // 增强悬停效果 hover: { mode: 'nearest', intersect: true, animationDuration: 300 // 悬停动画持续时间 } } }); // 初始化解析类型统计饼图 const queryTypeChartElement = document.getElementById('query-type-chart'); if (queryTypeChartElement) { const queryTypeCtx = queryTypeChartElement.getContext('2d'); // 预定义的颜色数组,用于解析类型 const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e']; queryTypeChart = new Chart(queryTypeCtx, { type: 'doughnut', data: { labels: ['暂无数据'], datasets: [{ data: [1], backgroundColor: [queryTypeColors[0]], borderWidth: 3, // 增加边框宽度,增强区块分隔 borderColor: '#fff', // 白色边框,使各个扇区更清晰 hoverOffset: 15, // 增加悬停偏移效果,增强交互体验 hoverBorderWidth: 4, // 悬停时增加边框宽度 hoverBackgroundColor: queryTypeColors.map(color => { // 生成悬停时的深色效果 const hex = color.replace('#', ''); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`; }), borderRadius: 10, // 添加圆角效果,增强现代感 borderSkipped: false // 显示所有边框 }] }, options: { responsive: true, maintainAspectRatio: false, // 简化动画,提高性能 animation: { duration: 300, // 缩短动画时间 easing: 'easeOutQuart', // 简化缓动函数 animateRotate: true, // 仅保留旋转动画 animateScale: false // 禁用缩放动画 }, plugins: { legend: { position: 'bottom', align: 'center', labels: { boxWidth: 12, // 调整图例框的宽度 font: { size: 11, // 调整字体大小 family: 'Inter, system-ui, sans-serif', // 使用现代字体 weight: 500 // 字体粗细 }, padding: 12, // 调整内边距 lineHeight: 1.5, // 调整行高 usePointStyle: true, // 使用点样式代替方形图例,节省空间 pointStyle: 'circle', // 使用圆形点样式 color: '#4B5563', // 图例文本颜色 generateLabels: function(chart) { // 自定义图例生成,添加更多样式控制 const data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map((label, i) => { const dataset = data.datasets[0]; return { text: label, fillStyle: dataset.backgroundColor[i], strokeStyle: dataset.borderColor, lineWidth: dataset.borderWidth, pointStyle: 'circle', hidden: !chart.isDatasetVisible(0), index: i }; }); } return []; }, // 启用图例点击交互 onClick: function(event, legendItem, legend) { // 切换对应数据的显示 const index = legendItem.index; const ci = legend.chart; ci.toggleDataVisibility(index); ci.update(); }, // 图例悬停样式 fontColor: '#4B5563', usePointStyle: true, pointStyle: 'circle' } }, tooltip: { enabled: true, backgroundColor: 'rgba(17, 24, 39, 0.9)', // 深背景,增强可读性 padding: 12, // 增加内边距 titleFont: { size: 13, // 标题字体大小 family: 'Inter, system-ui, sans-serif', weight: 600 }, bodyFont: { size: 12, // 正文字体大小 family: 'Inter, system-ui, sans-serif' }, bodySpacing: 6, // 正文行间距 displayColors: true, // 显示颜色指示器 callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((acc, val) => acc + val, 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; }, afterLabel: function(context) { // 可以添加额外的信息 } }, cornerRadius: 8, // 圆角 boxPadding: 6, // 盒子内边距 borderColor: 'rgba(255, 255, 255, 0.2)', // 边框颜色 borderWidth: 1 // 边框宽度 }, title: { display: false // 不显示标题,由HTML标题代替 } }, cutout: '70%', // 调整中心空白区域比例,增强现代感 // 增强元素配置 elements: { arc: { borderAlign: 'center', tension: 0.1, // 添加轻微的张力,使圆弧更平滑 borderWidth: 3 // 统一边框宽度 } }, layout: { padding: { top: 20, // 增加顶部内边距 right: 20, bottom: 30, // 增加底部内边距,为图例预留更多空间 left: 20 } }, // 添加交互配置 interaction: { mode: 'nearest', // 交互模式 axis: 'x', // 交互轴 intersect: false // 不要求精确相交 }, // 增强悬停效果 hover: { mode: 'nearest', intersect: true, animationDuration: 300 // 悬停动画持续时间 } } }); } else { console.warn('未找到解析类型统计图表元素'); } // 初始化DNS请求统计图表 drawDNSRequestsChart(); // 初始化展开按钮功能 initExpandButton(); } // 初始化展开按钮事件 function initExpandButton() { const expandBtn = document.getElementById('expand-chart-btn'); const chartModal = document.getElementById('chart-modal'); const closeModalBtn = document.getElementById('close-modal-btn'); // 修复ID匹配 // 添加调试日志 if (expandBtn && chartModal && closeModalBtn) { // 展开按钮点击事件 expandBtn.addEventListener('click', () => { // 显示浮窗 chartModal.classList.remove('hidden'); // 初始化或更新详细图表 drawDetailedDNSRequestsChart(); // 初始化浮窗中的时间范围切换 initDetailedTimeRangeToggle(); // 延迟更新图表大小,确保容器大小已计算 setTimeout(() => { if (detailedDnsRequestsChart) { detailedDnsRequestsChart.resize(); } }, 100); }); // 关闭按钮点击事件 closeModalBtn.addEventListener('click', () => { chartModal.classList.add('hidden'); }); // 点击遮罩层关闭浮窗(使用chartModal作为遮罩层) chartModal.addEventListener('click', (e) => { // 检查点击目标是否是遮罩层本身(即最外层div) if (e.target === chartModal) { chartModal.classList.add('hidden'); } }); // ESC键关闭浮窗 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !chartModal.classList.contains('hidden')) { chartModal.classList.add('hidden'); } }); } else { console.error('无法找到必要的DOM元素'); } } // 初始化详细图表的时间范围切换 function initDetailedTimeRangeToggle() { // 只选择图表模态框内的时间范围按钮,避免与主视图冲突 const chartModal = document.getElementById('chart-modal'); const detailedTimeRangeButtons = chartModal ? chartModal.querySelectorAll('.time-range-btn') : []; // 初始化详细图表的默认状态,与主图表保持一致 detailedCurrentTimeRange = currentTimeRange; // 定义按钮样式配置,与主视图保持一致 const buttonStyles = [ { // 24小时按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-blue-100'], active: ['bg-blue-500', 'text-white'], activeHover: ['hover:bg-blue-400'] }, { // 7天按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-green-100'], active: ['bg-green-500', 'text-white'], activeHover: ['hover:bg-green-400'] }, { // 30天按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-purple-100'], active: ['bg-purple-500', 'text-white'], activeHover: ['hover:bg-purple-400'] } ]; // 设置初始按钮状态 detailedTimeRangeButtons.forEach((button, index) => { const styleConfig = buttonStyles[index % buttonStyles.length]; // 移除所有初始样式 button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700', 'bg-green-500', 'bg-purple-500', 'bg-gray-100', 'mixed-view-active'); // 设置非选中状态样式 button.classList.add('transition-colors', 'duration-200'); button.classList.add(...styleConfig.normal); button.classList.add(...styleConfig.hover); }); detailedTimeRangeButtons.forEach((button, index) => { button.addEventListener('click', () => { const styleConfig = buttonStyles[index % buttonStyles.length]; // 重置所有按钮为非选中状态 detailedTimeRangeButtons.forEach((btn, btnIndex) => { const btnStyle = buttonStyles[btnIndex % buttonStyles.length]; // 移除所有可能的激活状态类 btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', 'bg-gray-500', 'mixed-view-active'); btn.classList.remove(...btnStyle.active); btn.classList.remove(...btnStyle.activeHover); // 添加非选中状态类 btn.classList.add(...btnStyle.normal); btn.classList.add(...btnStyle.hover); }); // 设置当前按钮为激活状态 button.classList.remove(...styleConfig.normal); button.classList.remove(...styleConfig.hover); button.classList.add('active'); button.classList.add(...styleConfig.active); button.classList.add(...styleConfig.activeHover); // 获取并更新当前时间范围 let rangeValue; if (button.dataset.range) { rangeValue = button.dataset.range; } else { const btnText = button.textContent.trim(); if (btnText.includes('24')) { rangeValue = '24h'; } else if (btnText.includes('7')) { rangeValue = '7d'; } else if (btnText.includes('30')) { rangeValue = '30d'; } else { rangeValue = btnText.replace(/[^0-9a-zA-Z]/g, ''); } } detailedCurrentTimeRange = rangeValue; // 重新绘制详细图表 drawDetailedDNSRequestsChart(); }); }); } // 绘制详细的DNS请求趋势图表 function drawDetailedDNSRequestsChart() { const ctx = document.getElementById('detailed-dns-requests-chart'); if (!ctx) { console.error('未找到详细DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); // 根据详细视图时间范围选择API函数和对应的颜色 let apiFunction; let chartColor; let chartFillColor; switch (detailedCurrentTimeRange) { case '7d': apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#22c55e'; // 绿色,与7天按钮颜色一致 chartFillColor = 'rgba(34, 197, 94, 0.1)'; break; case '30d': apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#a855f7'; // 紫色,与30天按钮颜色一致 chartFillColor = 'rgba(168, 85, 247, 0.1)'; break; default: // 24h apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#3b82f6'; // 蓝色,与24小时按钮颜色一致 chartFillColor = 'rgba(59, 130, 246, 0.1)'; } // 获取统计数据 apiFunction().then(data => { // 创建或更新图表 if (detailedDnsRequestsChart) { detailedDnsRequestsChart.data.labels = data.labels; detailedDnsRequestsChart.data.datasets = [{ label: 'DNS请求数量', data: data.data, borderColor: chartColor, backgroundColor: chartFillColor, tension: 0.4, fill: true }]; detailedDnsRequestsChart.options.plugins.legend.display = false; // 使用平滑过渡动画更新图表 detailedDnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } else { detailedDnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: data.labels, datasets: [{ label: 'DNS请求数量', data: data.data, borderColor: chartColor, backgroundColor: chartFillColor, tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 800, easing: 'easeInOutQuart' }, plugins: { legend: { display: false }, title: { display: true, text: 'DNS请求趋势', font: { size: 14 } }, 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); // 错误处理:使用空数据 const count = detailedCurrentTimeRange === '24h' ? 24 : (detailedCurrentTimeRange === '7d' ? 7 : 30); const emptyData = { labels: Array(count).fill(''), data: Array(count).fill(0) }; if (detailedDnsRequestsChart) { detailedDnsRequestsChart.data.labels = emptyData.labels; detailedDnsRequestsChart.data.datasets[0].data = emptyData.data; detailedDnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } }); } // 绘制DNS请求统计图表 function drawDNSRequestsChart() { const ctx = document.getElementById('dns-requests-chart'); if (!ctx) { console.error('未找到DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); // 普通视图 // 根据当前时间范围选择API函数和对应的颜色 let apiFunction; let chartColor; let chartFillColor; switch (currentTimeRange) { case '7d': apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#22c55e'; // 绿色,与7天按钮颜色一致 chartFillColor = 'rgba(34, 197, 94, 0.1)'; break; case '30d': apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#a855f7'; // 紫色,与30天按钮颜色一致 chartFillColor = 'rgba(168, 85, 247, 0.1)'; break; default: // 24h apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })); chartColor = '#3b82f6'; // 蓝色,与24小时按钮颜色一致 chartFillColor = 'rgba(59, 130, 246, 0.1)'; } // 获取统计数据 apiFunction().then(data => { // 创建或更新图表 if (dnsRequestsChart) { dnsRequestsChart.data.labels = data.labels; dnsRequestsChart.data.datasets = [{ label: 'DNS请求数量', data: data.data, borderColor: chartColor, backgroundColor: chartFillColor, tension: 0.4, fill: true }]; dnsRequestsChart.options.plugins.legend.display = false; // 使用平滑过渡动画更新图表 dnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } else { dnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: data.labels, datasets: [{ label: 'DNS请求数量', data: data.data, borderColor: chartColor, backgroundColor: chartFillColor, tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 800, easing: 'easeInOutQuart' }, 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); // 错误处理:使用空数据而不是模拟数据 const count = currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30); const emptyData = { labels: Array(count).fill(''), data: Array(count).fill(0) }; if (dnsRequestsChart) { dnsRequestsChart.data.labels = emptyData.labels; dnsRequestsChart.data.datasets[0].data = emptyData.data; dnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } }); } // 更新图表数据 function updateCharts(stats, queryTypeStats) { // 空值检查 if (!stats) { console.error('更新图表失败: 未提供统计数据'); return; } // 更新比例图表 if (ratioChart) { let allowed = 0, blocked = 0, error = 0; // 尝试从stats数据中提取 if (stats.dns) { allowed = parseInt(stats.dns.Allowed) || allowed; blocked = parseInt(stats.dns.Blocked) || blocked; error = parseInt(stats.dns.Errors) || error; } else if (stats.totalQueries !== undefined) { allowed = parseInt(stats.allowedQueries) || allowed; blocked = parseInt(stats.blockedQueries) || blocked; error = parseInt(stats.errorQueries) || error; } ratioChart.data.datasets[0].data = [allowed, blocked, error]; // 使用自定义动画配置更新图表,确保平滑过渡 ratioChart.update({ duration: 500, easing: 'easeInOutQuart' }); } // 更新解析类型统计饼图 if (queryTypeChart && queryTypeStats && Array.isArray(queryTypeStats)) { const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e']; // 检查是否有有效的数据项 const validData = queryTypeStats.filter(item => item && item.count > 0); if (validData.length > 0) { // 准备标签和数据 const labels = validData.map(item => item.type); const data = validData.map(item => item.count); // 为每个解析类型分配颜色 const colors = labels.map((_, index) => queryTypeColors[index % queryTypeColors.length]); // 更新图表数据 queryTypeChart.data.labels = labels; queryTypeChart.data.datasets[0].data = data; queryTypeChart.data.datasets[0].backgroundColor = colors; } else { // 如果没有数据,显示默认值 queryTypeChart.data.labels = ['暂无数据']; queryTypeChart.data.datasets[0].data = [1]; queryTypeChart.data.datasets[0].backgroundColor = [queryTypeColors[0]]; } // 使用自定义动画配置更新图表,确保平滑过渡 queryTypeChart.update({ duration: 500, easing: 'easeInOutQuart' }); } } // 更新统计卡片折线图 function updateStatCardCharts(stats) { if (!stats || Object.keys(statCardCharts).length === 0) { return; } // 更新查询总量图表 if (statCardCharts['query-chart']) { let queryCount = 0; if (stats.dns) { queryCount = stats.dns.Queries || 0; } else if (stats.totalQueries !== undefined) { queryCount = stats.totalQueries || 0; } updateChartData('query-chart', queryCount); } // 更新屏蔽数量图表 if (statCardCharts['blocked-chart']) { let blockedCount = 0; if (stats.dns) { blockedCount = stats.dns.Blocked || 0; } else if (stats.blockedQueries !== undefined) { blockedCount = stats.blockedQueries || 0; } updateChartData('blocked-chart', blockedCount); } // 更新正常解析图表 if (statCardCharts['allowed-chart']) { let allowedCount = 0; if (stats.dns) { allowedCount = stats.dns.Allowed || 0; } else if (stats.allowedQueries !== undefined) { allowedCount = stats.allowedQueries || 0; } else if (stats.dns && stats.dns.Queries && stats.dns.Blocked) { allowedCount = stats.dns.Queries - stats.dns.Blocked; } updateChartData('allowed-chart', allowedCount); } // 更新错误数量图表 if (statCardCharts['error-chart']) { let errorCount = 0; if (stats.dns) { errorCount = stats.dns.Errors || 0; } else if (stats.errorQueries !== undefined) { errorCount = stats.errorQueries || 0; } updateChartData('error-chart', errorCount); } // 更新响应时间图表 if (statCardCharts['response-time-chart']) { let responseTime = 0; // 尝试从不同的数据结构获取平均响应时间 if (stats.dns && stats.dns.AvgResponseTime) { responseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { responseTime = stats.avgResponseTime; } else if (stats.responseTime) { responseTime = stats.responseTime; } // 限制小数位数为两位 responseTime = parseFloat(responseTime).toFixed(2); updateChartData('response-time-chart', responseTime); } // 更新活跃IP图表 if (statCardCharts['ips-chart']) { const activeIPs = stats.activeIPs || 0; updateChartData('ips-chart', activeIPs); } // 更新CPU使用率图表 if (statCardCharts['cpu-chart']) { const cpuUsage = stats.cpuUsage || 0; updateChartData('cpu-chart', cpuUsage); } // 更新规则数图表 if (statCardCharts['rules-chart']) { // 尝试获取规则数,如果没有则使用模拟数据 const rulesCount = getRulesCountFromStats(stats) || Math.floor(Math.random() * 5000) + 10000; updateChartData('rules-chart', rulesCount); } // 更新排除规则数图表 if (statCardCharts['exceptions-chart']) { const exceptionsCount = getExceptionsCountFromStats(stats) || Math.floor(Math.random() * 100) + 50; updateChartData('exceptions-chart', exceptionsCount); } // 更新Hosts条目数图表 if (statCardCharts['hosts-chart']) { const hostsCount = getHostsCountFromStats(stats) || Math.floor(Math.random() * 1000) + 2000; updateChartData('hosts-chart', hostsCount); } } // 更新单个图表的数据 function updateChartData(chartId, newValue) { const chart = statCardCharts[chartId]; const historyData = statCardHistoryData[chartId]; if (!chart || !historyData) { return; } // 添加新数据,移除最旧的数据 historyData.push(newValue); if (historyData.length > 12) { historyData.shift(); } // 更新图表数据 chart.data.datasets[0].data = historyData; chart.data.labels = generateTimeLabels(historyData.length); // 使用自定义动画配置更新图表,确保平滑过渡,避免空白区域 chart.update({ duration: 300, // 增加动画持续时间 easing: 'easeInOutQuart', // 使用平滑的缓动函数 transition: { duration: 300, easing: 'easeInOutQuart' } }); } // 从统计数据中获取规则数 function getRulesCountFromStats(stats) { // 尝试从stats中获取规则数 if (stats.shield && stats.shield.rules) { return stats.shield.rules; } return null; } // 从统计数据中获取排除规则数 function getExceptionsCountFromStats(stats) { // 尝试从stats中获取排除规则数 if (stats.shield && stats.shield.exceptions) { return stats.shield.exceptions; } return null; } // 从统计数据中获取Hosts条目数 function getHostsCountFromStats(stats) { // 尝试从stats中获取Hosts条目数 if (stats.shield && stats.shield.hosts) { return stats.shield.hosts; } return null; } // 初始化统计卡片折线图 function initStatCardCharts() { // 清理已存在的图表实例 for (const key in statCardCharts) { if (statCardCharts.hasOwnProperty(key)) { statCardCharts[key].destroy(); } } statCardCharts = {}; statCardHistoryData = {}; // 检查Chart.js是否加载 // 统计卡片配置信息 const cardConfigs = [ { id: 'query-chart', color: '#9b59b6', label: '查询总量' }, { id: 'blocked-chart', color: '#e74c3c', label: '屏蔽数量' }, { id: 'allowed-chart', color: '#2ecc71', label: '正常解析' }, { id: 'error-chart', color: '#f39c12', label: '错误数量' }, { id: 'response-time-chart', color: '#3498db', label: '响应时间' }, { id: 'ips-chart', color: '#1abc9c', label: '活跃IP' }, { id: 'cpu-chart', color: '#e67e22', label: 'CPU使用率' }, { id: 'rules-chart', color: '#95a5a6', label: '屏蔽规则数' }, { id: 'exceptions-chart', color: '#34495e', label: '排除规则数' }, { id: 'hosts-chart', color: '#16a085', label: 'Hosts条目数' } ]; cardConfigs.forEach(config => { const canvas = document.getElementById(config.id); if (!canvas) { console.warn(`未找到统计卡片图表元素: ${config.id}`); return; } const ctx = canvas.getContext('2d'); // 为不同类型的卡片生成更合适的初始数据 let initialData; if (config.id === 'response-time-chart') { // 响应时间图表使用空数组,将通过API实时数据更新 initialData = Array(12).fill(null); } else if (config.id === 'cpu-chart') { initialData = generateMockData(12, 0, 10); } else { initialData = generateMockData(12, 0, 100); } // 初始化历史数据数组 statCardHistoryData[config.id] = [...initialData]; // 创建图表 statCardCharts[config.id] = new Chart(ctx, { type: 'line', data: { labels: generateTimeLabels(12), datasets: [{ label: config.label, data: initialData, borderColor: config.color, backgroundColor: `${config.color}20`, // 透明度20% borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0, // 隐藏数据点 pointHoverRadius: 4, // 鼠标悬停时显示数据点 pointBackgroundColor: config.color }] }, options: { responsive: true, maintainAspectRatio: false, // 添加动画配置,确保平滑过渡 animation: { duration: 800, easing: 'easeInOutQuart' }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, backgroundColor: 'rgba(0, 0, 0, 0.9)', titleColor: '#fff', bodyColor: '#fff', borderColor: config.color, borderWidth: 1, padding: 8, displayColors: false, cornerRadius: 4, titleFont: { size: 12, weight: 'normal' }, bodyFont: { size: 11 }, // 确保HTML渲染正确 useHTML: true, filter: function(tooltipItem) { return tooltipItem.datasetIndex === 0; }, callbacks: { title: function(tooltipItems) { // 简化时间显示格式 return tooltipItems[0].label; }, label: function(context) { const value = context.parsed.y; // 格式化大数字 const formattedValue = formatNumber(value); // 使用CSS类显示变化趋势 let trendInfo = ''; const data = context.dataset.data; const currentIndex = context.dataIndex; if (currentIndex > 0) { const prevValue = data[currentIndex - 1]; const change = value - prevValue; if (change !== 0) { const changeSymbol = change > 0 ? '↑' : '↓'; // 取消颜色显示,简化显示 trendInfo = (changeSymbol + Math.abs(change)); } } // 简化标签格式 return `${config.label}: ${formattedValue}${trendInfo}`; }, // 移除平均值显示 afterLabel: function(context) { return ''; } } } }, scales: { x: { display: false // 隐藏X轴 }, y: { display: false, // 隐藏Y轴 beginAtZero: true } }, interaction: { intersect: false, mode: 'index' } } }); }); } // 生成模拟数据 function generateMockData(count, min, max) { const data = []; for (let i = 0; i < count; i++) { data.push(Math.floor(Math.random() * (max - min + 1)) + min); } return data; } // 生成时间标签 function generateTimeLabels(count) { const labels = []; const now = new Date(); for (let i = count - 1; i >= 0; i--) { const time = new Date(now.getTime() - i * 5 * 60 * 1000); // 每5分钟一个点 labels.push(`${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`); } return labels; } // 格式化数字显示(使用K/M后缀) function formatNumber(num) { // 如果不是数字,直接返回 if (isNaN(num) || num === '---') { return num; } // 显示完整数字的最大长度阈值 const MAX_FULL_LENGTH = 5; // 先获取完整数字字符串 const fullNumStr = num.toString(); // 如果数字长度小于等于阈值,直接返回完整数字 if (fullNumStr.length <= MAX_FULL_LENGTH) { return fullNumStr; } // 否则使用缩写格式 if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return fullNumStr; } // 更新运行状态 function updateUptime() { // 实现更新运行时间的逻辑 // 这里应该调用API获取当前运行时间并更新到UI // 由于API暂时没有提供运行时间,我们先使用模拟数据 const uptimeElement = document.getElementById('uptime'); if (uptimeElement) { uptimeElement.textContent = '---'; } } // 格式化数字(添加千位分隔符) function formatWithCommas(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' }); } // 根据颜色代码获取对应的CSS类名(兼容方式) function getColorClassName(colorCode) { // 优先使用配置文件中的颜色处理 if (COLOR_CONFIG.getColorClassName) { return COLOR_CONFIG.getColorClassName(colorCode); } // 备用颜色映射 const colorMap = { '#1890ff': 'blue', '#52c41a': 'green', '#fa8c16': 'orange', '#f5222d': 'red', '#722ed1': 'purple', '#13c2c2': 'cyan', '#36cfc9': 'teal' }; // 返回映射的类名,如果没有找到则返回默认的blue return colorMap[colorCode] || 'blue'; } // 显示通知 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'); // 页面切换逻辑 function switchPage(targetId, menuItem) { // 隐藏所有内容 document.querySelectorAll('[id$="-content"]').forEach(content => { content.classList.add('hidden'); }); // 显示目标内容 document.getElementById(`${targetId}-content`).classList.remove('hidden'); // 更新页面标题 document.getElementById('page-title').textContent = menuItem.querySelector('span').textContent; // 更新活动菜单项 menuItems.forEach(item => { item.classList.remove('sidebar-item-active'); }); menuItem.classList.add('sidebar-item-active'); // 侧边栏切换(移动端) if (window.innerWidth < 1024) { toggleSidebar(); } } menuItems.forEach(item => { item.addEventListener('click', (e) => { // 允许默认的hash变化 // 页面切换会由hashchange事件处理 }); }); } // 处理hash变化 - 全局函数,确保在页面加载时就能被调用 function handleHashChange() { let hash = window.location.hash; // 如果没有hash,默认设置为#dashboard if (!hash) { hash = '#dashboard'; window.location.hash = hash; return; } const targetId = hash.substring(1); const menuItems = document.querySelectorAll('nav a'); // 首先检查是否存在对应的内容元素 const contentElement = document.getElementById(`${targetId}-content`); // 查找对应的菜单项 let targetMenuItem = null; menuItems.forEach(item => { if (item.getAttribute('href') === hash) { targetMenuItem = item; } }); // 如果找到了对应的内容元素,直接显示 if (contentElement) { // 隐藏所有内容 document.querySelectorAll('[id$="-content"]').forEach(content => { content.classList.add('hidden'); }); // 显示目标内容 contentElement.classList.remove('hidden'); // 更新活动菜单项和页面标题 menuItems.forEach(item => { item.classList.remove('sidebar-item-active'); }); if (targetMenuItem) { targetMenuItem.classList.add('sidebar-item-active'); // 更新页面标题 const pageTitle = targetMenuItem.querySelector('span').textContent; document.getElementById('page-title').textContent = pageTitle; } else { // 如果没有找到对应的菜单项,尝试根据hash更新页面标题 const titleElement = document.getElementById(`${targetId}-title`); if (titleElement) { document.getElementById('page-title').textContent = titleElement.textContent; } } } else if (targetMenuItem) { // 隐藏所有内容 document.querySelectorAll('[id$="-content"]').forEach(content => { content.classList.add('hidden'); }); // 显示目标内容 document.getElementById(`${targetId}-content`).classList.remove('hidden'); // 更新页面标题 document.getElementById('page-title').textContent = targetMenuItem.querySelector('span').textContent; // 更新活动菜单项 menuItems.forEach(item => { item.classList.remove('sidebar-item-active'); }); targetMenuItem.classList.add('sidebar-item-active'); // 侧边栏切换(移动端) if (window.innerWidth < 1024) { toggleSidebar(); } } else { // 如果既没有找到内容元素也没有找到菜单项,默认显示dashboard window.location.hash = '#dashboard'; } } // 初始化hash路由 function initHashRoute() { handleHashChange(); } // 监听hash变化事件 - 全局事件监听器 window.addEventListener('hashchange', handleHashChange); // 初始化hash路由 - 确保在页面加载时就能被调用 initHashRoute(); // 响应式处理 function handleResponsive() { // 窗口大小改变时处理 window.addEventListener('resize', () => { // 更新所有图表大小 if (dnsRequestsChart) { dnsRequestsChart.update(); } if (ratioChart) { ratioChart.update(); } if (queryTypeChart) { queryTypeChart.update(); } if (detailedDnsRequestsChart) { detailedDnsRequestsChart.update(); } // 更新统计卡片图表 Object.values(statCardCharts).forEach(chart => { if (chart) { chart.update(); } }); }); // 添加触摸事件支持,用于移动端 let touchStartX = 0; let touchEndX = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; }, false); document.addEventListener('touchend', (e) => { touchEndX = e.changedTouches[0].screenX; handleSwipe(); }, false); function handleSwipe() { // 从左向右滑动,打开侧边栏 if (touchEndX - touchStartX > 50 && window.innerWidth < 1024) { sidebar.classList.remove('-translate-x-full'); } // 从右向左滑动,关闭侧边栏 if (touchStartX - touchEndX > 50 && window.innerWidth < 1024) { sidebar.classList.add('-translate-x-full'); } } } // 添加重试功能 function addRetryEventListeners() { // TOP客户端重试按钮 const retryTopClientsBtn = document.getElementById('retry-top-clients'); if (retryTopClientsBtn) { retryTopClientsBtn.addEventListener('click', async () => { const clientsData = await api.getTopClients(); if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) { // 使用真实数据 updateTopClientsTable(clientsData); hideLoading('top-clients'); const errorElement = document.getElementById('top-clients-error'); if (errorElement) errorElement.classList.add('hidden'); } else { // 重试失败,保持原有状态 console.warn('重试获取TOP客户端数据失败'); } }); } // TOP域名重试按钮 const retryTopDomainsBtn = document.getElementById('retry-top-domains'); if (retryTopDomainsBtn) { retryTopDomainsBtn.addEventListener('click', async () => { const domainsData = await api.getTopDomains(); if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) { // 使用真实数据 updateTopDomainsTable(domainsData); hideLoading('top-domains'); const errorElement = document.getElementById('top-domains-error'); if (errorElement) errorElement.classList.add('hidden'); } else { // 重试失败,保持原有状态 console.warn('重试获取TOP域名数据失败'); } }); } } // 页面加载完成后初始化 window.addEventListener('DOMContentLoaded', () => { // 初始化页面切换 handlePageSwitch(); // 初始化响应式 handleResponsive(); // 初始化仪表盘 initDashboard(); // 添加重试事件监听器 addRetryEventListeners(); // 页面卸载时清理定时器 window.addEventListener('beforeunload', () => { if (intervalId) { clearInterval(intervalId); } }); });// 重写loadDashboardData函数,修复语法错误 async function loadDashboardData() { try { // 并行获取所有数据,提高加载效率 const [stats, queryTypeStatsResult, topBlockedDomainsResult, recentBlockedDomainsResult, topClientsResult] = await Promise.all([ // 获取基本统计数据 api.getStats().catch(error => { console.error('获取基本统计数据失败:', error); return null; }), // 获取查询类型统计数据 api.getQueryTypeStats().catch(() => null), // 获取TOP被屏蔽域名 api.getTopBlockedDomains().catch(() => null), // 获取最近屏蔽域名 api.getRecentBlockedDomains().catch(() => null), // 获取TOP客户端 api.getTopClients().catch(() => null) ]); // 确保stats是有效的对象 if (!stats || typeof stats !== 'object') { console.error('无效的统计数据:', stats); return; } // 处理查询类型统计数据 let queryTypeStats = null; if (queryTypeStatsResult) { queryTypeStats = queryTypeStatsResult; } else if (stats.dns && stats.dns.QueryTypes) { queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({ type, count })); } // 处理TOP被屏蔽域名 let topBlockedDomains = []; if (topBlockedDomainsResult && Array.isArray(topBlockedDomainsResult)) { topBlockedDomains = topBlockedDomainsResult; } else { topBlockedDomains = [ { domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() }, { domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() }, { domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() } ]; } // 处理最近屏蔽域名 let recentBlockedDomains = []; if (recentBlockedDomainsResult && Array.isArray(recentBlockedDomainsResult)) { recentBlockedDomains = recentBlockedDomainsResult; } else { recentBlockedDomains = [ { domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }, { domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() } ]; } // 显示错误的辅助函数 function showError(elementId) { const loadingElement = document.getElementById(elementId + '-loading'); const errorElement = document.getElementById(elementId + '-error'); if (loadingElement) loadingElement.classList.add('hidden'); if (errorElement) errorElement.classList.remove('hidden'); } // 处理TOP客户端 let topClients = []; if (topClientsResult && !topClientsResult.error && Array.isArray(topClientsResult) && topClientsResult.length > 0) { topClients = topClientsResult; } else { console.warn('获取TOP客户端失败或数据无效,使用模拟数据'); topClients = [ { ip: '192.168.1.100', count: 120 }, { ip: '192.168.1.101', count: 95 }, { ip: '192.168.1.102', count: 80 }, { ip: '192.168.1.103', count: 65 }, { ip: '192.168.1.104', count: 50 } ]; showError('top-clients'); } // 处理TOP域名 - 注意:这个API调用不在Promise.all中,所以需要try-catch let topDomains = []; try { const domainsData = await api.getTopDomains(); if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) { topDomains = domainsData; } else { console.warn('获取TOP域名失败或数据无效,使用模拟数据'); topDomains = [ { domain: 'example.com', count: 50 }, { domain: 'google.com', count: 45 }, { domain: 'facebook.com', count: 40 }, { domain: 'twitter.com', count: 35 }, { domain: 'youtube.com', count: 30 } ]; showError('top-domains'); } } catch (error) { console.warn('获取TOP域名失败:', error); topDomains = [ { domain: 'example.com', count: 50 }, { domain: 'google.com', count: 45 }, { domain: 'facebook.com', count: 40 }, { domain: 'twitter.com', count: 35 }, { domain: 'youtube.com', count: 30 } ]; showError('top-domains'); } // 存储统计卡片历史数据,用于计算趋势 const getStatValue = (statPath, defaultValue = 0) => { const path = statPath.split('.'); let value = stats; for (const key of path) { if (!value || typeof value !== 'object') { return defaultValue; } value = value[key]; } return value !== undefined ? value : defaultValue; }; // 更新主页面的统计卡片数据 updateStatsCards(stats); // 更新TOP客户端表格 updateTopClientsTable(topClients); // 更新TOP域名表格 updateTopDomainsTable(topDomains); // 更新TOP被屏蔽域名表格 updateTopBlockedTable(topBlockedDomains); // 更新最近屏蔽域名表格 updateRecentBlockedTable(recentBlockedDomains); // 更新图表 updateCharts(stats, queryTypeStats); // 初始化或更新查询类型统计饼图 if (queryTypeStats) { drawQueryTypeChart(queryTypeStats); } // 更新查询类型统计信息 if (document.getElementById('top-query-type')) { const topQueryTypeElement = document.getElementById('top-query-type'); const topQueryTypeCountElement = document.getElementById('top-query-type-count'); // 从stats中获取查询类型统计数据 if (stats.dns && stats.dns.QueryTypes) { const queryTypes = stats.dns.QueryTypes; // 找出数量最多的查询类型 let maxCount = 0; let topType = 'A'; for (const [type, count] of Object.entries(queryTypes)) { const numCount = Number(count) || 0; if (numCount > maxCount) { maxCount = numCount; topType = type; } } // 更新DOM if (topQueryTypeElement) { topQueryTypeElement.textContent = topType; } if (topQueryTypeCountElement) { topQueryTypeCountElement.textContent = formatNumber(maxCount); } // 保存到历史数据,用于计算趋势 window.dashboardHistoryData.prevTopQueryTypeCount = maxCount; } } // 更新活跃IP信息 if (document.getElementById('active-ips')) { const activeIPsElement = document.getElementById('active-ips'); // 从stats中获取活跃IP数 let activeIPs = getStatValue('dns.ActiveIPs', 0); // 更新DOM if (activeIPsElement) { activeIPsElement.textContent = formatNumber(activeIPs); } // 保存到历史数据,用于计算趋势 window.dashboardHistoryData.prevActiveIPs = activeIPs; } // 更新平均响应时间 if (document.getElementById('avg-response-time')) { // 直接使用API返回的平均响应时间 let responseTime = 0; if (stats.dns && stats.dns.AvgResponseTime) { responseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { responseTime = stats.avgResponseTime; } else if (stats.responseTime) { responseTime = stats.responseTime; } if (responseTime > 0 && statCardCharts['response-time-chart']) { // 限制小数位数为两位并更新图表 updateChartData('response-time-chart', parseFloat(responseTime).toFixed(2)); } } // 更新运行状态 updateUptime(); // 确保TOP域名数据被正确加载 updateTopData(); } catch (error) { console.error('加载仪表盘数据失败:', error); // 静默失败,不显示通知以免打扰用户 } }