// 全局变量
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('/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 + '
';
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 = `