// 全局变量 let domainDataCache = { blocked: null, resolved: null }; let domainUpdateTimer = null; const DOMAIN_UPDATE_INTERVAL = 5000; // 域名排行更新间隔,设为5秒,比统计数据更新慢一些 // 初始化小型图表 - 修复Canvas重用问题 function initMiniCharts() { // 获取所有图表容器 const chartContainers = document.querySelectorAll('.chart-card canvas'); // 全局图表实例存储 window.chartInstances = window.chartInstances || {}; chartContainers.forEach(canvas => { // 获取图表数据属性 const chartId = canvas.id; const chartType = canvas.dataset.chartType || 'line'; const chartData = JSON.parse(canvas.dataset.chartData || '{}'); // 设置图表上下文 const ctx = canvas.getContext('2d'); // 销毁已存在的图表实例,避免Canvas重用错误 if (window.chartInstances[chartId]) { window.chartInstances[chartId].destroy(); } // 创建新图表 window.chartInstances[chartId] = new Chart(ctx, { type: chartType, data: chartData, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: 10, cornerRadius: 4 } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.05)' } } }, animation: { duration: 1000, easing: 'easeOutQuart' } } }); }); } // 初始化仪表盘面板 function initDashboardPanel() { // 初始化小型图表 if (typeof initMiniCharts === 'function') { initMiniCharts(); } // 加载统计数据 loadDashboardData(); // 启动实时更新 if (typeof startRealTimeUpdate === 'function') { startRealTimeUpdate(); } // 启动域名排行的独立更新 startDomainUpdate(); // 初始化响应式侧边栏 initResponsiveSidebar(); } // 初始化响应式侧边栏 function initResponsiveSidebar() { // 创建侧边栏切换按钮 const toggleBtn = document.createElement('button'); toggleBtn.className = 'sidebar-toggle'; toggleBtn.innerHTML = ''; document.body.appendChild(toggleBtn); // 侧边栏切换逻辑 toggleBtn.addEventListener('click', function() { const sidebar = document.querySelector('.sidebar'); sidebar.classList.toggle('open'); // 更新按钮图标 const icon = toggleBtn.querySelector('i'); if (sidebar.classList.contains('open')) { icon.className = 'fas fa-times'; } else { icon.className = 'fas fa-bars'; } }); // 在侧边栏打开时点击内容区域关闭侧边栏 const content = document.querySelector('.content'); content.addEventListener('click', function() { const sidebar = document.querySelector('.sidebar'); const toggleBtn = document.querySelector('.sidebar-toggle'); if (sidebar.classList.contains('open') && window.innerWidth <= 768) { sidebar.classList.remove('open'); if (toggleBtn) { const icon = toggleBtn.querySelector('i'); icon.className = 'fas fa-bars'; } } }); // 窗口大小变化时调整侧边栏状态 window.addEventListener('resize', function() { const sidebar = document.querySelector('.sidebar'); const toggleBtn = document.querySelector('.sidebar-toggle'); if (window.innerWidth > 768) { sidebar.classList.remove('open'); if (toggleBtn) { const icon = toggleBtn.querySelector('i'); icon.className = 'fas fa-bars'; } } }); } // 加载仪表盘数据 function loadDashboardData() { // 加载统计卡片数据 updateStatCards(); // 首次加载时获取域名排行数据 if (!domainDataCache.blocked) { loadTopBlockedDomains(); } if (!domainDataCache.resolved) { loadTopResolvedDomains(); } } // 启动域名排行的独立更新 function startDomainUpdate() { if (domainUpdateTimer) { clearInterval(domainUpdateTimer); } // 立即执行一次更新 updateDomainRankings(); // 设置定时器 domainUpdateTimer = setInterval(() => { // 仅当当前面板是仪表盘时更新数据 if (document.getElementById('dashboard') && document.getElementById('dashboard').classList.contains('active')) { updateDomainRankings(); } }, DOMAIN_UPDATE_INTERVAL); } // 停止域名排行更新 function stopDomainUpdate() { if (domainUpdateTimer) { clearInterval(domainUpdateTimer); domainUpdateTimer = null; } } // 更新域名排行数据 function updateDomainRankings() { // 使用Promise.all并行加载,提高效率 Promise.all([ loadTopBlockedDomains(true), loadTopResolvedDomains(true) ]).catch(error => { console.error('更新域名排行数据失败:', error); }); } // 更新统计卡片数据 function updateStatCards() { // 获取所有统计数据 apiRequest('/stats') .then(data => { // 更新请求统计 if (data && data.dns) { // 屏蔽请求 const blockedCount = data.dns.Blocked || data.dns.blocked || 0; smoothUpdateStatCard('blocked-count', blockedCount); // 允许请求 const allowedCount = data.dns.Allowed || data.dns.allowed || 0; smoothUpdateStatCard('allowed-count', allowedCount); // 错误请求 const errorCount = data.dns.Errors || data.dns.errors || 0; smoothUpdateStatCard('error-count', errorCount); // 总请求数 const totalCount = blockedCount + allowedCount + errorCount; smoothUpdateStatCard('total-queries', totalCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('blocked', blockedCount); updateDataHistory('query', totalCount); } // 更新小型图表 if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('blocked-chart', dataHistory.blocked); updateMiniChart('query-chart', dataHistory.query); } } else { // 处理其他可能的数据格式 const blockedValue = data && (data.Blocked !== undefined ? data.Blocked : (data.blocked !== undefined ? data.blocked : 0)); const allowedValue = data && (data.Allowed !== undefined ? data.Allowed : (data.allowed !== undefined ? data.allowed : 0)); const errorValue = data && (data.Errors !== undefined ? data.Errors : (data.errors !== undefined ? data.errors : 0)); smoothUpdateStatCard('blocked-count', blockedValue); smoothUpdateStatCard('allowed-count', allowedValue); smoothUpdateStatCard('error-count', errorValue); const totalCount = blockedValue + allowedValue + errorValue; smoothUpdateStatCard('total-queries', totalCount); } }) .catch(error => { console.error('获取统计数据失败:', error); }); // 获取规则数 apiRequest('/shield') .then(data => { let rulesCount = 0; // 增强的数据格式处理,确保能正确处理各种返回格式 if (Array.isArray(data)) { rulesCount = data.length; } else if (data && data.rules && Array.isArray(data.rules)) { rulesCount = data.rules.length; } else if (data && data.domainRules) { // 处理可能的规则分类格式 let domainRulesCount = 0; let regexRulesCount = 0; if (Array.isArray(data.domainRules)) { domainRulesCount = data.domainRules.length; } else if (typeof data.domainRules === 'object') { domainRulesCount = Object.keys(data.domainRules).length; } if (data.regexRules && Array.isArray(data.regexRules)) { regexRulesCount = data.regexRules.length; } rulesCount = domainRulesCount + regexRulesCount; } // 确保至少显示0而不是-- smoothUpdateStatCard('rules-count', rulesCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('rules', rulesCount); } if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('rules-chart', dataHistory.rules); } }) .catch(error => { console.error('获取规则数失败:', error); // 即使出错也要设置为0,避免显示-- smoothUpdateStatCard('rules-count', 0); }); // 获取Hosts条目数量 apiRequest('/shield/hosts') .then(data => { let hostsCount = 0; // 处理各种可能的数据格式 if (Array.isArray(data)) { hostsCount = data.length; } else if (data && data.hosts && Array.isArray(data.hosts)) { hostsCount = data.hosts.length; } else if (data && typeof data === 'object' && data !== null) { // 如果是对象格式,计算键的数量 hostsCount = Object.keys(data).length; } // 确保至少显示0而不是-- smoothUpdateStatCard('hosts-count', hostsCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('hosts', hostsCount); } if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('hosts-chart', dataHistory.hosts); } }) .catch(error => { console.error('获取Hosts数量失败:', error); // 即使出错也要设置为0,避免显示-- smoothUpdateStatCard('hosts-count', 0); }); // 获取Hosts条目数 apiRequest('/shield/hosts') .then(data => { let hostsCount = 0; if (Array.isArray(data)) { hostsCount = data.length; } else if (data && data.hosts && Array.isArray(data.hosts)) { hostsCount = data.hosts.length; } smoothUpdateStatCard('hosts-count', hostsCount); // 更新数据历史记录和小型图表 if (typeof updateDataHistory === 'function') { updateDataHistory('hosts', hostsCount); } if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') { updateMiniChart('hosts-chart', dataHistory.hosts); } }) .catch(error => { console.error('获取Hosts条目数失败:', error); }); } // 更新单个统计卡片 function updateStatCard(elementId, value) { const element = document.getElementById(elementId); if (!element) return; // 格式化为可读数字 const formattedValue = formatNumber(value); // 更新显示 element.textContent = formattedValue; // 使用全局checkAndAnimate函数检测变化并添加光晕效果 if (typeof checkAndAnimate === 'function') { checkAndAnimate(elementId, value); } } // 平滑更新统计卡片(数字递增动画) function smoothUpdateStatCard(elementId, newValue) { const element = document.getElementById(elementId); if (!element) return; // 获取旧值 const oldValue = previousStats[elementId] || 0; // 如果值相同,不更新 if (newValue === oldValue) return; // 如果是初始值,直接更新 if (oldValue === 0 || oldValue === '--') { updateStatCard(elementId, newValue); return; } // 设置动画持续时间 const duration = 500; // 500ms const startTime = performance.now(); // 动画函数 function animate(currentTime) { const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / duration, 1); // 使用缓动函数 const easeOutQuad = 1 - (1 - progress) * (1 - progress); // 计算当前值 const currentValue = Math.floor(oldValue + (newValue - oldValue) * easeOutQuad); // 更新显示 element.textContent = formatNumber(currentValue); // 继续动画 if (progress < 1) { requestAnimationFrame(animate); } else { // 动画完成,设置最终值 element.textContent = formatNumber(newValue); // 添加光晕效果 element.classList.add('update'); setTimeout(() => { element.classList.remove('update'); }, 1000); // 更新记录 previousStats[elementId] = newValue; } } // 开始动画 requestAnimationFrame(animate); } // 加载24小时统计数据 function loadHourlyStats() { apiRequest('/hourly-stats') .then(data => { // 检查数据是否变化,避免不必要的重绘 if (typeof previousChartData !== 'undefined' && JSON.stringify(previousChartData) === JSON.stringify(data)) { return; // 数据未变化,无需更新图表 } previousChartData = JSON.parse(JSON.stringify(data)); // 处理不同可能的数据格式 if (data) { // 优先处理用户提供的实际数据格式 {data: [], labels: []} if (data.labels && data.data && Array.isArray(data.labels) && Array.isArray(data.data)) { // 确保labels和data数组长度一致 if (data.labels.length === data.data.length) { // 假设data数组包含的是屏蔽请求数据,允许请求设为0 renderHourlyChart(data.labels, data.data, Array(data.data.length).fill(0)); return; } } // 处理其他可能的数据格式 if (data.labels && data.blocked && data.allowed) { // 完整数据格式:分别有屏蔽和允许的数据 renderHourlyChart(data.labels, data.blocked, data.allowed); } else if (data.labels && data.data) { // 简化数据格式:只有一组数据 renderHourlyChart(data.labels, data.data, Array(data.data.length).fill(0)); } else { // 尝试直接使用数据对象的属性 const hours = []; const blocked = []; const allowed = []; // 假设数据是按小时组织的对象 for (const key in data) { if (data.hasOwnProperty(key)) { hours.push(key); // 尝试不同的数据结构访问方式 if (typeof data[key] === 'object' && data[key] !== null) { blocked.push(data[key].Blocked || data[key].blocked || 0); allowed.push(data[key].Allowed || data[key].allowed || 0); } else { blocked.push(data[key]); allowed.push(0); } } } // 只在有数据时渲染 if (hours.length > 0) { renderHourlyChart(hours, blocked, allowed); } } } }) .catch(error => { console.error('获取24小时统计失败:', error); // 显示默认空数据,避免图表区域空白 const emptyHours = Array.from({length: 24}, (_, i) => `${i}:00`); const emptyData = Array(24).fill(0); renderHourlyChart(emptyHours, emptyData, emptyData); }); } // 渲染24小时统计图表 - 使用ECharts重新设计 function renderHourlyChart(hours, blocked, allowed) { const chartContainer = document.getElementById('hourly-chart'); if (!chartContainer) return; // 销毁现有ECharts实例 if (window.hourlyChart) { window.hourlyChart.dispose(); } // 创建ECharts实例 window.hourlyChart = echarts.init(chartContainer); // 计算24小时内的最大请求数,为Y轴设置合适的上限 const maxRequests = Math.max(...blocked, ...allowed); const yAxisMax = maxRequests > 0 ? Math.ceil(maxRequests * 1.2) : 10; // 设置ECharts配置 const option = { title: { text: '24小时请求统计', left: 'center', textStyle: { fontSize: 16, fontWeight: 'normal' } }, tooltip: { trigger: 'axis', backgroundColor: 'rgba(255, 255, 255, 0.95)', borderColor: '#ddd', borderWidth: 1, textStyle: { color: '#333' }, formatter: function(params) { let result = params[0].name + '
'; params.forEach(param => { result += param.marker + param.seriesName + ': ' + param.value + '
'; }); return result; } }, legend: { data: ['屏蔽请求', '允许请求'], top: '10%', textStyle: { color: '#666' } }, grid: { left: '3%', right: '4%', bottom: '10%', top: '25%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: hours, axisLabel: { color: '#666', interval: 1, // 每隔一个小时显示一个标签,避免拥挤 rotate: 30 // 标签旋转30度,提高可读性 }, axisLine: { lineStyle: { color: '#ddd' } }, axisTick: { show: false } }, yAxis: { type: 'value', min: 0, max: yAxisMax, axisLabel: { color: '#666', formatter: '{value}' }, axisLine: { lineStyle: { color: '#ddd' } }, splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } } }, series: [ { name: '屏蔽请求', type: 'line', smooth: true, // 平滑曲线 symbol: 'circle', // 拐点形状 symbolSize: 6, // 拐点大小 data: blocked, itemStyle: { color: '#e74c3c' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(231, 76, 60, 0.3)' }, { offset: 1, color: 'rgba(231, 76, 60, 0.05)' } ]) }, emphasis: { focus: 'series', itemStyle: { borderWidth: 2, borderColor: '#fff', shadowBlur: 10, shadowColor: 'rgba(231, 76, 60, 0.5)' } }, animationDuration: 800, animationEasing: 'cubicOut' }, { name: '允许请求', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: allowed, itemStyle: { color: '#2ecc71' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(46, 204, 113, 0.3)' }, { offset: 1, color: 'rgba(46, 204, 113, 0.05)' } ]) }, emphasis: { focus: 'series', itemStyle: { borderWidth: 2, borderColor: '#fff', shadowBlur: 10, shadowColor: 'rgba(46, 204, 113, 0.5)' } }, animationDuration: 800, animationEasing: 'cubicOut' } ], // 添加数据提示功能 toolbox: { feature: { dataZoom: { yAxisIndex: 'none' }, dataView: { readOnly: false }, magicType: { type: ['line', 'bar'] }, restore: {}, saveAsImage: {} }, top: '15%' }, // 添加数据缩放功能 dataZoom: [ { type: 'inside', start: 0, end: 100 }, { start: 0, end: 100, handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23.1h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z', handleSize: '80%', handleStyle: { color: '#fff', shadowBlur: 3, shadowColor: 'rgba(0, 0, 0, 0.6)', shadowOffsetX: 2, shadowOffsetY: 2 } } ] }; // 应用配置项 window.hourlyChart.setOption(option); // 添加窗口大小变化时的自适应 window.addEventListener('resize', function() { if (window.hourlyChart) { window.hourlyChart.resize(); } }); } // 加载请求类型分布 - 注意:后端可能没有这个API,暂时注释掉 function loadRequestsDistribution() { // 后端没有对应的API路由,暂时跳过 console.log('请求类型分布API暂不可用'); return Promise.resolve() .then(data => { // 检查数据是否变化,避免不必要的重绘 if (typeof previousFullData !== 'undefined' && JSON.stringify(previousFullData) === JSON.stringify(data)) { return; // 数据未变化,无需更新图表 } previousFullData = JSON.parse(JSON.stringify(data)); // 构造饼图所需的数据,支持多种数据格式 const labels = ['允许请求', '屏蔽请求', '错误请求']; let requestData = [0, 0, 0]; // 默认值 if (data) { // 尝试多种可能的数据结构 if (data.dns) { // 主要数据结构 requestData = [ data.dns.Allowed || data.dns.allowed || 0, data.dns.Blocked || data.dns.blocked || 0, data.dns.Errors || data.dns.errors || 0 ]; } else if (data.Allowed !== undefined || data.Blocked !== undefined) { // 直接在顶级对象中 requestData = [ data.Allowed || data.allowed || 0, data.Blocked || data.blocked || 0, data.Errors || data.errors || 0 ]; } else if (data.requests) { // 可能在requests属性中 requestData = [ data.requests.Allowed || data.requests.allowed || 0, data.requests.Blocked || data.requests.blocked || 0, data.requests.Errors || data.requests.errors || 0 ]; } } // 渲染图表,即使数据全为0也渲染,避免空白 renderRequestsPieChart(labels, requestData); }) .catch(error => { console.error('获取请求类型分布失败:', error); // 显示默认空数据的图表 const labels = ['允许请求', '屏蔽请求', '错误请求']; const defaultData = [0, 0, 0]; renderRequestsPieChart(labels, defaultData); }); } // 渲染请求类型饼图 function renderRequestsPieChart(labels, data) { const ctx = document.getElementById('requests-pie-chart'); if (!ctx) return; // 销毁现有图表 if (window.requestsPieChart) { window.requestsPieChart.destroy(); } // 创建新图表 window.requestsPieChart = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: [ '#2ecc71', // 允许 '#e74c3c', // 屏蔽 '#f39c12', // 错误 '#9b59b6' // 其他 ], borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return `${label}: ${value} (${percentage}%)`; } } } }, cutout: '60%', animation: { duration: 500 // 快速动画,提升实时更新体验 } } }); } // 辅助函数:深度比较两个对象是否相等 function isEqual(obj1, obj2) { // 处理null或undefined情况 if (obj1 === obj2) return true; if (obj1 == null || obj2 == null) return false; // 确保都是数组 if (!Array.isArray(obj1) || !Array.isArray(obj2)) return false; if (obj1.length !== obj2.length) return false; // 比较数组中每个元素 for (let i = 0; i < obj1.length; i++) { const a = obj1[i]; const b = obj2[i]; // 比较域名和计数 if (a.domain !== b.domain || a.count !== b.count) { return false; } } return true; } // 加载最常屏蔽的域名 function loadTopBlockedDomains(isUpdate = false) { // 首先获取表格元素并显示加载状态 const topBlockedTable = document.getElementById('top-blocked-table'); const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null; // 非更新操作时显示加载状态 if (tbody && !isUpdate) { // 显示加载中状态 tbody.innerHTML = `加载中...`; } return apiRequest('/top-blocked') .then(data => { // 处理多种可能的数据格式,特别优化对用户提供格式的支持 let processedData = []; if (Array.isArray(data)) { // 数组格式:直接使用,并过滤出有效的域名数据 processedData = data.filter(item => item && (item.domain || item.name || item.Domain || item.Name) && (item.count !== undefined || item.Count !== undefined || item.hits !== undefined || item.Hits !== undefined)); } else if (data && data.domains && Array.isArray(data.domains)) { // 嵌套在domains属性中 processedData = data.domains; } else if (data && typeof data === 'object') { // 对象格式:转换为数组 processedData = Object.keys(data).map(key => ({ domain: key, count: data[key] })); } // 计算最大值用于百分比计算 if (processedData.length > 0) { const maxCount = Math.max(...processedData.map(item => { return item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); })); // 为每个项目添加百分比 processedData.forEach(item => { const count = item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); item.percentage = maxCount > 0 ? Math.round((count / maxCount) * 100) : 0; }); } // 数据变化检测 const hasDataChanged = !isEqual(domainDataCache.blocked, processedData); // 只在数据发生变化或不是更新操作时重新渲染 if (hasDataChanged || !isUpdate) { // 更新缓存 domainDataCache.blocked = JSON.parse(JSON.stringify(processedData)); // 渲染最常屏蔽的域名表格 smoothRenderTable('top-blocked-table', processedData, renderDomainRow); } }) .catch(error => { console.error('获取最常屏蔽域名失败:', error); // 显示默认空数据而不是错误消息,保持界面一致性 const tbody = document.getElementById('top-blocked-table').querySelector('tbody'); if (tbody) { showEmpty(tbody, '获取数据失败'); } // 使用全局通知功能 if (typeof showNotification === 'function') { showNotification('danger', '获取最常屏蔽域名失败'); } }); } // 加载最常解析的域名 function loadTopResolvedDomains(isUpdate = false) { // 首先获取表格元素 const topResolvedTable = document.getElementById('top-resolved-table'); const tbody = topResolvedTable ? topResolvedTable.querySelector('tbody') : null; // 非更新操作时显示加载状态 if (tbody && !isUpdate) { // 显示加载中状态 tbody.innerHTML = `加载中...`; } return apiRequest('/top-resolved') .then(data => { // 处理多种可能的数据格式 let processedData = []; if (Array.isArray(data)) { // 数组格式:直接使用 processedData = data; } else if (data && data.domains && Array.isArray(data.domains)) { // 嵌套在domains属性中 processedData = data.domains; } else if (data && typeof data === 'object') { // 对象格式:转换为数组 processedData = Object.keys(data).map(key => ({ domain: key, count: data[key] })); } // 计算最大值用于百分比计算 if (processedData.length > 0) { const maxCount = Math.max(...processedData.map(item => { return item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); })); // 为每个项目添加百分比 processedData.forEach(item => { const count = item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); item.percentage = maxCount > 0 ? Math.round((count / maxCount) * 100) : 0; }); } // 数据变化检测 const hasDataChanged = !isEqual(domainDataCache.resolved, processedData); // 只在数据发生变化或不是更新操作时重新渲染 if (hasDataChanged || !isUpdate) { // 更新缓存 domainDataCache.resolved = JSON.parse(JSON.stringify(processedData)); // 渲染最常解析的域名表格 smoothRenderTable('top-resolved-table', processedData, renderDomainRow); } }) .catch(error => { console.error('获取最常解析域名失败:', error); // 显示默认空数据而不是错误消息,保持界面一致性 const tbody = document.getElementById('top-resolved-table').querySelector('tbody'); if (tbody) { showEmpty(tbody, '暂无解析记录'); } // 使用全局通知功能 if (typeof showNotification === 'function') { showNotification('danger', '获取最常解析域名失败'); } }); } // 渲染域名行 function renderDomainRow(item, index) { if (!item) return null; // 支持不同的字段名和格式 const domainName = item.domain || item.name || item.Domain || item.Name || '未知域名'; const count = item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); const percentage = item.percentage || 0; const row = document.createElement('tr'); row.className = 'fade-in'; // 添加淡入动画类 row.dataset.domain = domainName; row.dataset.count = count; row.dataset.percentage = percentage; // 为不同类型的排行使用不同的进度条颜色 let barColor = '#3498db'; // 默认蓝色 if (item.domain && item.domain.includes('microsoft.com')) { barColor = '#2ecc71'; // 绿色 } else if (item.domain && item.domain.includes('tencent.com')) { barColor = '#e74c3c'; // 红色 } row.innerHTML = ` ${domainName}
${formatNumber(count)}
${percentage}%
`; // 设置动画延迟,创建级联效果 row.style.animationDelay = `${index * 50}ms`; return row; } // 平滑渲染表格数据 function smoothRenderTable(tableId, newData, rowRenderer) { const table = document.getElementById(tableId); const tbody = table ? table.querySelector('tbody') : null; if (!tbody) return; // 添加过渡类,用于CSS动画支持 tbody.classList.add('table-transition'); if (!newData || newData.length === 0) { showEmpty(tbody, '暂无数据记录'); // 移除过渡类 setTimeout(() => tbody.classList.remove('table-transition'), 300); return; } // 创建映射以提高查找效率 const oldRows = Array.from(tbody.querySelectorAll('tr')); const rowMap = new Map(); oldRows.forEach(row => { if (!row.querySelector('td:first-child')) return; const key = row.dataset.domain || row.querySelector('td:first-child').textContent; rowMap.set(key, row); }); // 准备新的数据行 const newRows = []; const updatedRows = new Set(); // 处理每一条新数据 newData.forEach((item, index) => { const key = item.domain || item.name || item.Domain || item.Name || '未知域名'; if (rowMap.has(key)) { // 数据项已存在,更新它 const existingRow = rowMap.get(key); const oldCount = parseInt(existingRow.dataset.count) || 0; const count = item.count !== undefined ? item.count : (item.Count !== undefined ? item.Count : (item.hits !== undefined ? item.hits : (item.Hits !== undefined ? item.Hits : 0))); // 更新数据属性 existingRow.dataset.count = count; // 添加高亮效果,用于CSS过渡 existingRow.classList.add('table-row-highlight'); setTimeout(() => { existingRow.classList.remove('table-row-highlight'); }, 1000); // 如果计数变化,应用平滑更新 if (oldCount !== count) { const countCell = existingRow.querySelector('.count-cell'); if (countCell) { smoothUpdateNumber(countCell, oldCount, count); } } // 更新位置 existingRow.style.animationDelay = `${index * 50}ms`; newRows.push(existingRow); updatedRows.add(key); } else { // 新数据项,创建新行 const newRow = rowRenderer(item, index); if (newRow) { // 添加淡入动画类 newRow.classList.add('table-row-fade-in'); // 先设置透明度为0,避免在错误位置闪烁 newRow.style.opacity = '0'; newRows.push(newRow); } } }); // 移除不再存在的数据行 oldRows.forEach(row => { if (!row.querySelector('td:first-child')) return; const key = row.dataset.domain || row.querySelector('td:first-child').textContent; if (!updatedRows.has(key)) { // 添加淡出动画 row.classList.add('table-row-fade-out'); setTimeout(() => { if (row.parentNode === tbody) { tbody.removeChild(row); } }, 300); } }); // 批量更新表格内容,减少重排 requestAnimationFrame(() => { // 保留未移除的行并按新顺序插入 const fragment = document.createDocumentFragment(); newRows.forEach(row => { // 如果是新行,添加到文档片段 if (!row.parentNode || row.parentNode !== tbody) { fragment.appendChild(row); } // 如果是已有行,移除它以便按新顺序重新插入 else if (tbody.contains(row)) { tbody.removeChild(row); fragment.appendChild(row); } }); // 将文档片段添加到表格 tbody.appendChild(fragment); // 触发动画 setTimeout(() => { newRows.forEach(row => { row.style.opacity = '1'; }); // 移除过渡类和动画类 setTimeout(() => { tbody.querySelectorAll('.table-row-fade-in').forEach(row => { row.classList.remove('table-row-fade-in'); }); tbody.classList.remove('table-transition'); }, 300); }, 10); // 初始化表格排序 if (typeof initTableSort === 'function') { initTableSort(tableId); } }); } // 平滑更新数字 function smoothUpdateNumber(element, oldValue, newValue) { // 如果值相同,不更新 if (oldValue === newValue || !element) return; // 根据数值差动态调整持续时间 const valueDiff = Math.abs(newValue - oldValue); const baseDuration = 400; const maxDuration = 1000; // 数值变化越大,动画时间越长,但不超过最大值 const duration = Math.min(baseDuration + Math.log10(valueDiff + 1) * 200, maxDuration); const startTime = performance.now(); function animate(currentTime) { const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / duration, 1); // 使用easeOutQuart缓动函数,使动画更自然 let easeOutProgress; if (progress < 1) { // 四阶缓动函数:easeOutQuart easeOutProgress = 1 - Math.pow(1 - progress, 4); } else { easeOutProgress = 1; } // 根据不同的数值范围使用不同的插值策略 let currentValue; if (valueDiff < 10) { // 小数值变化,使用线性插值 currentValue = Math.floor(oldValue + (newValue - oldValue) * easeOutProgress); } else if (valueDiff < 100) { // 中等数值变化,使用四舍五入 currentValue = Math.round(oldValue + (newValue - oldValue) * easeOutProgress); } else { // 大数值变化,使用更平滑的插值 currentValue = Math.floor(oldValue + (newValue - oldValue) * easeOutProgress); } // 更新显示 element.textContent = formatNumber(currentValue); // 添加微小的缩放动画效果 const scaleFactor = 1 + 0.05 * Math.sin(progress * Math.PI); element.style.transform = `scale(${scaleFactor})`; // 继续动画 if (progress < 1) { requestAnimationFrame(animate); } else { // 动画完成 element.textContent = formatNumber(newValue); // 重置缩放 element.style.transform = 'scale(1)'; // 触发最终的高亮效果 element.classList.add('number-update-complete'); setTimeout(() => { element.classList.remove('number-update-complete'); }, 300); } } // 重置元素样式 element.style.transform = 'scale(1)'; // 开始动画 requestAnimationFrame(animate); }