// dashboard.js - 仪表盘功能实现 // 全局变量 let ratioChart = null; let dnsRequestsChart = null; let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗) let queryTypeChart = null; // 解析类型统计饼图 let intervalId = null; let wsConnection = null; let wsReconnectTimer = null; // 存储统计卡片图表实例 let statCardCharts = {}; // 存储统计卡片历史数据 let statCardHistoryData = {}; // 引入颜色配置文件 const COLOR_CONFIG = window.COLOR_CONFIG || {}; // 初始化仪表盘 async function initDashboard() { try { console.log('页面打开时强制刷新数据...'); // 优先加载初始数据,确保页面显示最新信息 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`; console.log('正在连接WebSocket:', wsUrl); // 创建WebSocket连接 wsConnection = new WebSocket(wsUrl); // 连接打开事件 wsConnection.onopen = function() { console.log('WebSocket连接已建立'); showNotification('数据更新成功', 'success'); // 清除重连计时器 if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; } }; // 接收消息事件 wsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === 'initial_data' || data.type === 'stats_update') { console.log('收到实时数据更新'); processRealTimeData(data.data); } } catch (error) { console.error('处理WebSocket消息失败:', error); } }; // 连接关闭事件 wsConnection.onclose = function(event) { console.warn('WebSocket连接已关闭,代码:', event.code); wsConnection = null; // 设置重连 setupReconnect(); }; // 连接错误事件 wsConnection.onerror = function(error) { console.error('WebSocket连接错误:', error); }; } catch (error) { console.error('创建WebSocket连接失败:', error); // 如果WebSocket连接失败,回退到定时刷新 fallbackToIntervalRefresh(); } } // 设置重连逻辑 function setupReconnect() { if (wsReconnectTimer) { return; // 已经有重连计时器在运行 } const reconnectDelay = 5000; // 5秒后重连 console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`); wsReconnectTimer = setTimeout(() => { connectWebSocket(); }, reconnectDelay); } // 处理实时数据更新 function processRealTimeData(stats) { try { // 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片 updateStatsCards(stats); // 获取查询类型统计数据 let queryTypeStats = null; if (stats.dns && stats.dns.QueryTypes) { queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({ type, count })); } // 更新图表数据 updateCharts(stats, queryTypeStats); // 尝试从stats中获取总查询数等信息 if (stats.dns) { totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0); blockedQueries = stats.dns.Blocked; errorQueries = stats.dns.Errors || 0; allowedQueries = stats.dns.Allowed; } else { totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; errorQueries = stats.errorQueries || 0; allowedQueries = stats.allowedQueries || 0; } // 更新新卡片数据 if (document.getElementById('avg-response-time')) { const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---'; // 计算响应时间趋势 let responsePercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) { // 存储当前值用于下次计算趋势 const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime; window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime; // 计算变化百分比 if (prevResponseTime > 0) { const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100; responsePercent = Math.abs(changePercent).toFixed(1) + '%'; // 设置趋势图标和颜色 if (changePercent > 0) { trendIcon = '↓'; trendClass = 'text-danger'; } else if (changePercent < 0) { trendIcon = '↑'; trendClass = 'text-success'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('avg-response-time').textContent = responseTime; const responseTimePercentElem = document.getElementById('response-time-percent'); if (responseTimePercentElem) { responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent; responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`; } } if (document.getElementById('top-query-type')) { const queryType = stats.topQueryType || '---'; const queryPercentElem = document.getElementById('query-type-percentage'); if (queryPercentElem) { queryPercentElem.textContent = '• ---'; queryPercentElem.className = 'text-sm flex items-center text-gray-500'; } document.getElementById('top-query-type').textContent = queryType; } if (document.getElementById('active-ips')) { const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---'; // 计算活跃IP趋势 let ipsPercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.activeIPs !== undefined) { const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs; window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; if (prevActiveIPs > 0) { const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100; ipsPercent = Math.abs(changePercent).toFixed(1) + '%'; if (changePercent > 0) { trendIcon = '↑'; trendClass = 'text-primary'; } else if (changePercent < 0) { trendIcon = '↓'; trendClass = 'text-secondary'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('active-ips').textContent = activeIPs; const activeIpsPercentElem = document.getElementById('active-ips-percentage'); 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 { // 获取最新的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 (wsConnection) { wsConnection.close(); wsConnection = null; } // 清除重连计时器 if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; } // 清除定时刷新 if (intervalId) { clearInterval(intervalId); intervalId = null; } } // 加载仪表盘数据 async function loadDashboardData() { console.log('开始加载仪表盘数据'); try { // 获取基本统计数据 const stats = await api.getStats(); console.log('统计数据:', stats); // 获取查询类型统计数据 let queryTypeStats = null; try { queryTypeStats = await api.getQueryTypeStats(); console.log('查询类型统计数据:', queryTypeStats); } catch (error) { console.warn('获取查询类型统计失败:', error); // 如果API调用失败,尝试从stats中提取查询类型数据 if (stats && stats.dns && stats.dns.QueryTypes) { queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({ type, count })); console.log('从stats中提取的查询类型统计:', queryTypeStats); } } // 尝试获取TOP被屏蔽域名,如果失败则提供模拟数据 let topBlockedDomains = []; try { topBlockedDomains = await api.getTopBlockedDomains(); console.log('TOP被屏蔽域名:', topBlockedDomains); // 确保返回的数据是数组 if (!Array.isArray(topBlockedDomains)) { console.warn('TOP被屏蔽域名不是预期的数组格式,使用模拟数据'); topBlockedDomains = []; } } catch (error) { console.warn('获取TOP被屏蔽域名失败:', error); // 提供模拟数据 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 = []; try { recentBlockedDomains = await api.getRecentBlockedDomains(); console.log('最近屏蔽域名:', recentBlockedDomains); // 确保返回的数据是数组 if (!Array.isArray(recentBlockedDomains)) { console.warn('最近屏蔽域名不是预期的数组格式,使用模拟数据'); recentBlockedDomains = []; } } catch (error) { console.warn('获取最近屏蔽域名失败:', error); // 提供模拟数据 recentBlockedDomains = [ { domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }, { domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() } ]; } // 实现数据加载状态管理 function showLoading(elementId) { const loadingElement = document.getElementById(elementId + '-loading'); const errorElement = document.getElementById(elementId + '-error'); if (loadingElement) loadingElement.classList.remove('hidden'); if (errorElement) errorElement.classList.add('hidden'); } function hideLoading(elementId) { const loadingElement = document.getElementById(elementId + '-loading'); if (loadingElement) loadingElement.classList.add('hidden'); } 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 = []; showLoading('top-clients'); try { const clientsData = await api.getTopClients(); console.log('TOP客户端:', clientsData); // 检查数据是否有效 if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) { // 使用真实数据 topClients = clientsData; } else if (clientsData && clientsData.error) { // API返回错误 console.warn('获取TOP客户端失败:', clientsData.error); // 使用模拟数据 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'); } 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'); } } catch (error) { console.warn('获取TOP客户端失败:', error); // 使用模拟数据 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'); } finally { hideLoading('top-clients'); } // 尝试获取TOP域名,优先使用真实数据,失败时使用模拟数据 let topDomains = []; showLoading('top-domains'); try { const domainsData = await api.getTopDomains(); console.log('TOP域名:', domainsData); // 检查数据是否有效 if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) { // 使用真实数据 topDomains = domainsData; } else if (domainsData && domainsData.error) { // API返回错误 console.warn('获取TOP域名失败:', domainsData.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'); } 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'); } finally { hideLoading('top-domains'); } // 更新统计卡片 updateStatsCards(stats); // 更新图表数据,传入查询类型统计 updateCharts(stats, queryTypeStats); // 更新表格数据 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); updateTopClientsTable(topClients); updateTopDomainsTable(topDomains); // 尝试从stats中获取总查询数等信息 if (stats.dns) { totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0); blockedQueries = stats.dns.Blocked; errorQueries = stats.dns.Errors || 0; allowedQueries = stats.dns.Allowed; } else { totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; errorQueries = stats.errorQueries || 0; allowedQueries = stats.allowedQueries || 0; } // 全局历史数据对象,用于存储趋势计算所需的上一次值 window.dashboardHistoryData = window.dashboardHistoryData || {}; // 更新新卡片数据 - 使用API返回的真实数据 if (document.getElementById('avg-response-time')) { // 保留两位小数并添加单位 const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---'; // 计算响应时间趋势 let responsePercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) { // 存储当前值用于下次计算趋势 const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime; window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime; // 计算变化百分比 if (prevResponseTime > 0) { const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100; responsePercent = Math.abs(changePercent).toFixed(1) + '%'; // 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的) if (changePercent > 0) { trendIcon = '↓'; trendClass = 'text-danger'; } else if (changePercent < 0) { trendIcon = '↑'; trendClass = 'text-success'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('avg-response-time').textContent = responseTime; const responseTimePercentElem = document.getElementById('response-time-percent'); if (responseTimePercentElem) { responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent; responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`; } } if (document.getElementById('top-query-type')) { // 直接使用API返回的查询类型 const queryType = stats.topQueryType || '---'; // 设置默认趋势显示 const queryPercentElem = document.getElementById('query-type-percentage'); if (queryPercentElem) { queryPercentElem.textContent = '• ---'; queryPercentElem.className = 'text-sm flex items-center text-gray-500'; } document.getElementById('top-query-type').textContent = queryType; } if (document.getElementById('active-ips')) { // 直接使用API返回的活跃IP数 const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---'; // 计算活跃IP趋势 let ipsPercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.activeIPs !== undefined && stats.activeIPs !== null) { // 存储当前值用于下次计算趋势 const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs; window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; // 计算变化百分比 if (prevActiveIPs > 0) { const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100; ipsPercent = Math.abs(changePercent).toFixed(1) + '%'; // 设置趋势图标和颜色 if (changePercent > 0) { trendIcon = '↑'; trendClass = 'text-success'; } else if (changePercent < 0) { trendIcon = '↓'; trendClass = 'text-danger'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('active-ips').textContent = activeIPs; const activeIpsPercentElem = document.getElementById('active-ips-percent'); if (activeIpsPercentElem) { activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent; activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`; } } // 更新图表 updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries}); // 确保响应时间图表使用API实时数据 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); // 静默失败,不显示通知以免打扰用户 } } // 更新统计卡片 function updateStatsCards(stats) { console.log('更新统计卡片,收到数据:', stats); // 适配不同的数据结构 let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0; let topQueryType = 'A', queryTypePercentage = 0; let activeIPs = 0, activeIPsPercentage = 0; // 检查数据结构,兼容可能的不同格式 if (stats) { // 优先使用顶层字段 totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; allowedQueries = stats.allowedQueries || 0; errorQueries = stats.errorQueries || 0; topQueryType = stats.topQueryType || 'A'; queryTypePercentage = stats.queryTypePercentage || 0; activeIPs = stats.activeIPs || 0; activeIPsPercentage = stats.activeIPsPercentage || 0; // 如果dns对象存在,优先使用其中的数据 if (stats.dns) { totalQueries = stats.dns.Queries || totalQueries; blockedQueries = stats.dns.Blocked || blockedQueries; allowedQueries = stats.dns.Allowed || allowedQueries; errorQueries = stats.dns.Errors || errorQueries; // 计算最常用查询类型的百分比 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; } } } else if (Array.isArray(stats) && stats.length > 0) { // 可能的数据结构3: 数组形式 totalQueries = stats[0].total || 0; blockedQueries = stats[0].blocked || 0; allowedQueries = stats[0].allowed || 0; errorQueries = stats[0].error || 0; topQueryType = stats[0].topQueryType || 'A'; queryTypePercentage = stats[0].queryTypePercentage || 0; activeIPs = stats[0].activeIPs || 0; activeIPsPercentage = stats[0].activeIPsPercentage || 0; } // 为数字元素添加翻页滚动特效 function animateValue(elementId, newValue) { const element = document.getElementById(elementId); if (!element) return; const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0; const formattedNewValue = formatNumber(newValue); // 如果值没有变化,不执行动画 if (oldValue === newValue && element.textContent === formattedNewValue) { return; } // 先移除可能存在的光晕效果类 element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow'); // 保存原始样式和内容 const originalStyle = element.getAttribute('style') || ''; const originalContent = element.innerHTML; // 配置翻页容器样式 const containerStyle = ` position: relative; display: inline-block; overflow: hidden; height: ${element.offsetHeight}px; width: ${element.offsetWidth}px; `; // 创建翻页容器 const flipContainer = document.createElement('div'); flipContainer.style.cssText = containerStyle; flipContainer.className = 'number-flip-container'; // 创建旧值元素 const oldValueElement = document.createElement('div'); oldValueElement.textContent = originalContent; oldValueElement.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transition: transform 400ms ease-in-out; transform-origin: center; `; // 创建新值元素 const newValueElement = document.createElement('div'); newValueElement.textContent = formattedNewValue; newValueElement.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transition: transform 400ms ease-in-out; transform-origin: center; transform: translateY(100%); `; // 复制原始元素的样式到新元素 const computedStyle = getComputedStyle(element); [oldValueElement, newValueElement].forEach(el => { el.style.fontSize = computedStyle.fontSize; el.style.fontWeight = computedStyle.fontWeight; el.style.color = computedStyle.color; el.style.fontFamily = computedStyle.fontFamily; el.style.textAlign = computedStyle.textAlign; el.style.lineHeight = computedStyle.lineHeight; }); // 替换原始元素的内容 element.textContent = ''; flipContainer.appendChild(oldValueElement); flipContainer.appendChild(newValueElement); element.appendChild(flipContainer); // 启动翻页动画 setTimeout(() => { oldValueElement.style.transform = 'translateY(-100%)'; newValueElement.style.transform = 'translateY(0)'; }, 50); // 动画结束后,恢复原始元素 setTimeout(() => { // 清理并设置最终值 element.innerHTML = formattedNewValue; if (originalStyle) { element.setAttribute('style', originalStyle); } else { element.removeAttribute('style'); } // 添加当前卡片颜色的深色光晕效果 // 根据父级卡片类型确定光晕颜色 const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50'); let glowColorClass = ''; // 使用更精准的卡片颜色检测 if (card) { // 根据卡片类名确定深色光晕颜色 if (card.classList.contains('bg-blue-50') || card.id.includes('total') || card.id.includes('response')) { // 蓝色卡片 - 深蓝色光晕 glowColorClass = 'number-glow-dark-blue'; } else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) { // 红色卡片 - 深红色光晕 glowColorClass = 'number-glow-dark-red'; } else if (card.classList.contains('bg-green-50') || card.id.includes('allowed') || card.id.includes('active')) { // 绿色卡片 - 深绿色光晕 glowColorClass = 'number-glow-dark-green'; } else if (card.classList.contains('bg-yellow-50') || card.id.includes('error') || card.id.includes('cpu')) { // 黄色卡片 - 深黄色光晕 glowColorClass = 'number-glow-dark-yellow'; } } // 如果确定了光晕颜色类,则添加它 if (glowColorClass) { element.classList.add(glowColorClass); // 2秒后移除光晕效果 setTimeout(() => { element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow'); }, 2000); } }, 450); } // 更新百分比元素的函数 function updatePercentage(elementId, value) { const element = document.getElementById(elementId); if (!element) return; element.style.opacity = '0'; element.style.transition = 'opacity 200ms ease-out'; setTimeout(() => { element.textContent = value; element.style.opacity = '1'; }, 200); } // 平滑更新数量显示 animateValue('total-queries', totalQueries); animateValue('blocked-queries', blockedQueries); animateValue('allowed-queries', allowedQueries); animateValue('error-queries', errorQueries); animateValue('active-ips', activeIPs); // 平滑更新文本和百分比 updatePercentage('top-query-type', topQueryType); updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`); updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`); // 计算并平滑更新百分比 if (totalQueries > 0) { updatePercentage('blocked-percent', `${Math.round((blockedQueries / totalQueries) * 100)}%`); updatePercentage('allowed-percent', `${Math.round((allowedQueries / totalQueries) * 100)}%`); updatePercentage('error-percent', `${Math.round((errorQueries / totalQueries) * 100)}%`); updatePercentage('queries-percent', '100%'); } else { updatePercentage('queries-percent', '---'); updatePercentage('blocked-percent', '---'); updatePercentage('allowed-percent', '---'); updatePercentage('error-percent', '---'); } } // 更新Top屏蔽域名表格 function updateTopBlockedTable(domains) { console.log('更新Top屏蔽域名表格,收到数据:', 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: 'example1.com', count: 150 }, { name: 'example2.com', count: 130 }, { name: 'example3.com', count: 120 }, { name: 'example4.com', count: 110 }, { name: 'example5.com', count: 100 } ]; console.log('使用示例数据填充Top屏蔽域名表格'); } let html = ''; for (let i = 0; i < tableData.length && i < 5; i++) { const domain = tableData[i]; html += `
${i + 1} ${domain.name}
${formatNumber(domain.count)}
`; } tableBody.innerHTML = html; } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(domains) { console.log('更新最近屏蔽域名表格,收到数据:', domains); const tableBody = document.getElementById('recent-blocked-table'); // 确保tableBody存在,因为最近屏蔽域名卡片可能已被移除 if (!tableBody) { console.log('未找到recent-blocked-table元素,跳过更新'); 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: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' }, { name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' }, { name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' }, { name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' }, { name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' } ]; console.log('使用示例数据填充最近屏蔽域名表格'); } let html = ''; for (let i = 0; i < tableData.length && i < 5; i++) { const domain = tableData[i]; const time = formatTime(domain.timestamp); html += `
${domain.name}
${time}
${domain.type}
`; } tableBody.innerHTML = html; } // 更新TOP客户端表格 function updateTopClientsTable(clients) { console.log('更新TOP客户端表格,收到数据:', clients); const tableBody = document.getElementById('top-clients-table'); // 确保tableBody存在 if (!tableBody) { console.error('未找到top-clients-table元素'); return; } 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: '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 } ]; console.log('使用示例数据填充TOP客户端表格'); } // 只显示前5个客户端 tableData = tableData.slice(0, 5); let html = ''; for (let i = 0; i < tableData.length; i++) { const client = tableData[i]; html += `
${i + 1} ${client.ip}
${formatNumber(client.count)}
`; } tableBody.innerHTML = html; } // 更新请求域名排行表格 function updateTopDomainsTable(domains) { console.log('更新请求域名排行表格,收到数据:', 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 } ]; console.log('使用示例数据填充请求域名排行表格'); } // 只显示前5个域名 tableData = tableData.slice(0, 5); let html = ''; for (let i = 0; i < tableData.length; i++) { const domain = tableData[i]; html += `
${i + 1} ${domain.name}
${formatNumber(domain.count)}
`; } tableBody.innerHTML = html; } // 当前选中的时间范围 let currentTimeRange = '24h'; // 默认为24小时 let isMixedView = true; // 是否为混合视图 - 默认显示混合视图 let lastSelectedIndex = 0; // 最后选中的按钮索引 // 详细图表专用变量 let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围 let detailedIsMixedView = false; // 详细图表是否为混合视图 // 初始化时间范围切换 function initTimeRangeToggle() { console.log('初始化时间范围切换'); // 查找所有可能的时间范围按钮类名 const timeRangeButtons = document.querySelectorAll('.time-range-btn, .time-range-button, .timerange-btn, button[data-range]'); console.log('找到时间范围按钮数量:', timeRangeButtons.length); 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'] // 选中时的浅色悬停 }, { // 混合视图按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-gray-200'], active: ['bg-gray-500', 'text-white'], activeHover: ['hover:bg-gray-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); // 移除鼠标悬停提示 console.log('为按钮设置初始样式:', button.textContent.trim(), '索引:', index, '类名:', Array.from(button.classList).join(', ')); button.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); console.log('点击按钮:', button.textContent.trim(), '索引:', index); // 检查是否是再次点击已选中的按钮 const isActive = button.classList.contains('active'); // 重置所有按钮为非选中状态 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); }); if (isActive && index < 3) { // 再次点击已选中的时间范围按钮 // 切换到混合视图 isMixedView = true; currentTimeRange = 'mixed'; console.log('切换到混合视图'); // 设置当前按钮为特殊混合视图状态(保持原按钮选中但添加混合视图标记) button.classList.remove(...styleConfig.normal); button.classList.remove(...styleConfig.hover); button.classList.add('active', 'mixed-view-active'); button.classList.add(...styleConfig.active); button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停 } else { // 普通选中模式 isMixedView = false; 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; console.log('更新时间范围为:', currentTimeRange); } // 重新加载数据 loadDashboardData(); // 更新DNS请求图表 drawDNSRequestsChart(); }); // 移除自定义鼠标悬停提示效果 }); // 确保默认选中第一个按钮并显示混合内容 if (timeRangeButtons.length > 0) { const firstButton = timeRangeButtons[0]; const firstStyle = 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); }); // 然后设置第一个按钮为激活状态,并标记为混合视图 firstButton.classList.remove(...firstStyle.normal); firstButton.classList.remove(...firstStyle.hover); firstButton.classList.add('active', 'mixed-view-active'); firstButton.classList.add(...firstStyle.active); firstButton.classList.add(...firstStyle.activeHover); console.log('默认选中第一个按钮并显示混合内容:', firstButton.textContent.trim()); // 设置默认显示混合内容 isMixedView = true; currentTimeRange = 'mixed'; } } // 注意:这个函数已被后面的实现覆盖,请使用后面的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: ['---', '---', '---'], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], borderWidth: 2, // 添加边框宽度,增强区块分隔 borderColor: '#fff', // 白色边框,使各个扇区更清晰 hoverOffset: 10, // 添加悬停偏移效果,增强交互体验 hoverBorderWidth: 3 // 悬停时增加边框宽度 }] }, options: { responsive: true, maintainAspectRatio: false, // 添加全局动画配置,确保图表创建和更新时都平滑过渡 animation: { duration: 500, // 延长动画时间,使过渡更平滑 easing: 'easeInOutQuart' }, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, // 减小图例框的宽度 font: { size: 11 // 减小字体大小 }, padding: 10 // 减小内边距 } }, tooltip: { enabled: true, backgroundColor: 'rgba(0, 0, 0, 0.8)', padding: 10, titleFont: { size: 12 }, bodyFont: { size: 11 }, callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; } } } }, cutout: '65%', // 减小中心空白区域比例,增大扇形区域以更好显示线段指示 // 添加线段指示相关配置 elements: { arc: { // 确保圆弧绘制时有足够的精度 borderAlign: 'center', tension: 0.1 // 添加轻微的张力,使圆弧更平滑 } } } }); // 初始化解析类型统计饼图 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: 2, // 添加边框宽度,增强区块分隔 borderColor: '#fff', // 白色边框,使各个扇区更清晰 hoverOffset: 10, // 添加悬停偏移效果,增强交互体验 hoverBorderWidth: 3 // 悬停时增加边框宽度 }] }, options: { responsive: true, maintainAspectRatio: false, // 添加全局动画配置,确保图表创建和更新时都平滑过渡 animation: { duration: 300, easing: 'easeInOutQuart' }, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, // 减小图例框的宽度 font: { size: 11 // 减小字体大小 }, padding: 10 // 减小内边距 } }, tooltip: { enabled: true, backgroundColor: 'rgba(0, 0, 0, 0.8)', padding: 10, titleFont: { size: 12 }, bodyFont: { size: 11 }, callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; } } } }, cutout: '65%', // 减小中心空白区域比例,增大扇形区域以更好显示线段指示 // 添加线段指示相关配置 elements: { arc: { // 确保圆弧绘制时有足够的精度 borderAlign: 'center', tension: 0.1 // 添加轻微的张力,使圆弧更平滑 } } } }); } 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匹配 // 添加调试日志 console.log('初始化展开按钮功能:', { expandBtn, chartModal, closeModalBtn }); if (expandBtn && chartModal && closeModalBtn) { // 展开按钮点击事件 expandBtn.addEventListener('click', () => { console.log('展开按钮被点击'); // 显示浮窗 chartModal.classList.remove('hidden'); // 初始化或更新详细图表 drawDetailedDNSRequestsChart(); // 初始化浮窗中的时间范围切换 initDetailedTimeRangeToggle(); // 延迟更新图表大小,确保容器大小已计算 setTimeout(() => { if (detailedDnsRequestsChart) { detailedDnsRequestsChart.resize(); } }, 100); }); // 关闭按钮点击事件 closeModalBtn.addEventListener('click', () => { console.log('关闭按钮被点击'); chartModal.classList.add('hidden'); }); // 点击遮罩层关闭浮窗(使用chartModal作为遮罩层) chartModal.addEventListener('click', (e) => { // 检查点击目标是否是遮罩层本身(即最外层div) if (e.target === chartModal) { console.log('点击遮罩层关闭'); chartModal.classList.add('hidden'); } }); // ESC键关闭浮窗 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !chartModal.classList.contains('hidden')) { console.log('ESC键关闭浮窗'); chartModal.classList.add('hidden'); } }); } else { console.error('无法找到必要的DOM元素'); } } // 初始化详细图表的时间范围切换 function initDetailedTimeRangeToggle() { // 只选择图表模态框内的时间范围按钮,避免与主视图冲突 const chartModal = document.getElementById('chart-modal'); const detailedTimeRangeButtons = chartModal ? chartModal.querySelectorAll('.time-range-btn') : []; console.log('初始化详细图表时间范围切换,找到按钮数量:', detailedTimeRangeButtons.length); // 初始化详细图表的默认状态,与主图表保持一致 detailedCurrentTimeRange = currentTimeRange; detailedIsMixedView = isMixedView; // 定义按钮样式配置,与主视图保持一致 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'] }, { // 混合视图按钮 normal: ['bg-gray-100', 'text-gray-700'], hover: ['hover:bg-gray-200'], active: ['bg-gray-500', 'text-white'], activeHover: ['hover:bg-gray-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); // 如果是第一个按钮且当前是混合视图,设置为混合视图激活状态 if (index === 0 && detailedIsMixedView) { button.classList.remove(...styleConfig.normal); button.classList.remove(...styleConfig.hover); button.classList.add('active', 'mixed-view-active'); button.classList.add(...styleConfig.active); button.classList.add(...styleConfig.activeHover); } }); detailedTimeRangeButtons.forEach((button, index) => { button.addEventListener('click', () => { const styleConfig = buttonStyles[index % buttonStyles.length]; // 检查是否是再次点击已选中的按钮 const isActive = button.classList.contains('active'); // 重置所有按钮为非选中状态 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); }); if (isActive && index < 3) { // 再次点击已选中的时间范围按钮 // 切换到混合视图 detailedIsMixedView = true; detailedCurrentTimeRange = 'mixed'; console.log('详细图表切换到混合视图'); // 设置当前按钮为特殊混合视图状态 button.classList.remove(...styleConfig.normal); button.classList.remove(...styleConfig.hover); button.classList.add('active', 'mixed-view-active'); button.classList.add(...styleConfig.active); button.classList.add(...styleConfig.activeHover); } else { // 普通选中模式 detailedIsMixedView = false; // 设置当前按钮为激活状态 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; console.log('详细图表更新时间范围为:', detailedCurrentTimeRange); } // 重新绘制详细图表 drawDetailedDNSRequestsChart(); }); }); } // 绘制详细的DNS请求趋势图表 function drawDetailedDNSRequestsChart() { console.log('绘制详细DNS请求趋势图表,时间范围:', detailedCurrentTimeRange, '混合视图:', detailedIsMixedView); const ctx = document.getElementById('detailed-dns-requests-chart'); if (!ctx) { console.error('未找到详细DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); // 混合视图配置 const datasetsConfig = [ { label: '24小时', api: (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#3b82f6', fillColor: 'rgba(59, 130, 246, 0.1)' }, { label: '7天', api: (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.1)' }, { label: '30天', api: (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#a855f7', fillColor: 'rgba(168, 85, 247, 0.1)' } ]; // 检查是否为混合视图 if (detailedIsMixedView || detailedCurrentTimeRange === 'mixed') { console.log('渲染混合视图详细图表'); // 显示图例 const showLegend = true; // 获取所有时间范围的数据 Promise.all(datasetsConfig.map(config => config.api().catch(error => { console.error(`获取${config.label}数据失败:`, error); // 返回空数据 const count = config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30); return { labels: Array(count).fill(''), data: Array(count).fill(0) }; }) )).then(results => { // 创建数据集 const datasets = results.map((data, index) => ({ label: datasetsConfig[index].label, data: data.data, borderColor: datasetsConfig[index].color, backgroundColor: datasetsConfig[index].fillColor, tension: 0.4, fill: false, borderWidth: 2 })); // 创建或更新图表 if (detailedDnsRequestsChart) { detailedDnsRequestsChart.data.labels = results[0].labels; detailedDnsRequestsChart.data.datasets = datasets; detailedDnsRequestsChart.options.plugins.legend.display = showLegend; // 使用平滑过渡动画更新图表 detailedDnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } else { detailedDnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: results[0].labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 800, easing: 'easeInOutQuart' }, plugins: { legend: { display: showLegend, position: 'top' }, 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('绘制混合视图详细图表失败:', error); }); } else { // 普通视图 // 根据详细视图时间范围选择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(); } }); } } // 绘制DNS请求统计图表 function drawDNSRequestsChart() { const ctx = document.getElementById('dns-requests-chart'); if (!ctx) { console.error('未找到DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); // 混合视图配置 const datasetsConfig = [ { label: '24小时', api: (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#3b82f6', fillColor: 'rgba(59, 130, 246, 0.1)' }, { label: '7天', api: (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.1)' }, { label: '30天', api: (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#a855f7', fillColor: 'rgba(168, 85, 247, 0.1)' } ]; // 检查是否为混合视图 if (isMixedView || currentTimeRange === 'mixed') { console.log('渲染混合视图图表'); // 显示图例 const showLegend = true; // 获取所有时间范围的数据 Promise.all(datasetsConfig.map(config => config.api().catch(error => { console.error(`获取${config.label}数据失败:`, error); // 返回空数据而不是模拟数据 const count = config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30); return { labels: Array(count).fill(''), data: Array(count).fill(0) }; }) )).then(results => { // 创建数据集 const datasets = results.map((data, index) => ({ label: datasetsConfig[index].label, data: data.data, borderColor: datasetsConfig[index].color, backgroundColor: datasetsConfig[index].fillColor, tension: 0.4, fill: false, // 混合视图不填充 borderWidth: 2 })); // 创建或更新图表 if (dnsRequestsChart) { // 使用第一个数据集的标签,但确保每个数据集使用自己的数据 dnsRequestsChart.data.labels = results[0].labels; dnsRequestsChart.data.datasets = datasets; dnsRequestsChart.options.plugins.legend.display = showLegend; // 使用平滑过渡动画更新图表 dnsRequestsChart.update({ duration: 800, easing: 'easeInOutQuart' }); } else { dnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: results[0].labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 800, easing: 'easeInOutQuart' }, plugins: { legend: { display: showLegend, position: 'top' }, 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('绘制混合视图图表失败:', error); }); } else { // 普通视图 // 根据当前时间范围选择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(); } }); } } // 更新图表数据 function updateCharts(stats, queryTypeStats) { console.log('更新图表,收到统计数据:', stats); console.log('查询类型统计数据:', queryTypeStats); // 空值检查 if (!stats) { console.error('更新图表失败: 未提供统计数据'); return; } // 更新比例图表 if (ratioChart) { let allowed = '---', blocked = '---', error = '---'; // 尝试从stats数据中提取 if (stats.dns) { allowed = stats.dns.Allowed || allowed; blocked = stats.dns.Blocked || blocked; error = stats.dns.Errors || error; } else if (stats.totalQueries !== undefined) { allowed = stats.allowedQueries || allowed; blocked = stats.blockedQueries || blocked; error = 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 (document.getElementById('avg-response-time')) { let avgResponseTime = 0; // 尝试从不同的数据结构获取平均响应时间 if (stats.dns && stats.dns.AvgResponseTime) { avgResponseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { avgResponseTime = stats.avgResponseTime; } else if (stats.responseTime) { avgResponseTime = stats.responseTime; } document.getElementById('avg-response-time').textContent = formatNumber(avgResponseTime); } // 更新规则数图表 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() { console.log('===== 开始初始化统计卡片折线图 ====='); // 清理已存在的图表实例 for (const key in statCardCharts) { if (statCardCharts.hasOwnProperty(key)) { statCardCharts[key].destroy(); } } statCardCharts = {}; statCardHistoryData = {}; // 检查Chart.js是否加载 console.log('Chart.js是否可用:', typeof Chart !== 'undefined'); // 统计卡片配置信息 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条目数' } ]; console.log('图表配置:', cardConfigs); 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(); } } // 处理hash变化 function handleHashChange() { let hash = window.location.hash; // 如果没有hash,默认设置为#dashboard if (!hash) { hash = '#dashboard'; window.location.hash = hash; return; } const targetId = hash.substring(1); // 查找对应的菜单项 let targetMenuItem = null; menuItems.forEach(item => { if (item.getAttribute('href') === hash) { targetMenuItem = item; } }); // 如果找到了对应的菜单项,切换页面 if (targetMenuItem) { switchPage(targetId, targetMenuItem); } else { // 如果没有找到对应的菜单项,尝试显示对应的内容 const contentElement = document.getElementById(`${targetId}-content`); 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 (item.getAttribute('href') === hash) { item.classList.add('sidebar-item-active'); // 更新页面标题 document.getElementById('page-title').textContent = item.querySelector('span').textContent; } }); } else { // 如果没有找到对应的内容,默认显示dashboard window.location.hash = '#dashboard'; } } } // 初始化hash路由 function initHashRoute() { handleHashChange(); } // 监听hash变化事件 window.addEventListener('hashchange', handleHashChange); menuItems.forEach(item => { item.addEventListener('click', (e) => { // 允许默认的hash变化 // 页面切换会由hashchange事件处理 }); }); // 初始化hash路由 initHashRoute(); } // 侧边栏切换 function toggleSidebar() { const sidebar = document.getElementById('sidebar'); sidebar.classList.toggle('-translate-x-full'); } // 响应式处理 function handleResponsive() { const toggleBtn = document.getElementById('toggle-sidebar'); const sidebar = document.getElementById('sidebar'); toggleBtn.addEventListener('click', toggleSidebar); // 初始状态处理 function updateSidebarState() { if (window.innerWidth < 1024) { sidebar.classList.add('-translate-x-full'); } else { sidebar.classList.remove('-translate-x-full'); } } updateSidebarState(); // 窗口大小改变时处理 window.addEventListener('resize', () => { updateSidebarState(); // 更新所有图表大小 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 () => { console.log('重试获取TOP客户端数据'); 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 () => { console.log('重试获取TOP域名数据'); 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); } }); });