Files
dns-server/static/js/modules/dashboard.js
T
Alex Yang c570f303e7 更新
2025-11-25 00:35:37 +08:00

1220 lines
44 KiB
JavaScript

// 全局变量
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 = '<i class="fas fa-bars"></i>';
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('/api/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('/api/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('/api/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('/api/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 + '<br/>';
params.forEach(param => {
result += param.marker + param.seriesName + ': ' + param.value + '<br/>';
});
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 = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
}
return apiRequest('/api/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 = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
}
return apiRequest('/api/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 = `
<td>${domainName}</td>
<td class="count-cell" style="position: relative; padding-right: 80px;">
<div class="count-number">${formatNumber(count)}</div>
<div class="percentage-text">${percentage}%</div>
<div class="percentage-bar-container">
<div class="percentage-bar" style="width: ${percentage}%; background-color: ${barColor};"></div>
</div>
</td>
`;
// 设置动画延迟,创建级联效果
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);
}