Files
dns-server/static/js/modules/dashboard.js
2025-11-24 11:00:59 +08:00

680 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 初始化仪表盘面板
function initDashboardPanel() {
// 加载统计数据
loadDashboardData();
// 启动实时更新
if (typeof startRealTimeUpdate === 'function') {
startRealTimeUpdate();
}
}
// 加载仪表盘数据
function loadDashboardData() {
// 加载统计卡片数据
updateStatCards();
// 加载最常屏蔽的域名
loadTopBlockedDomains();
// 加载最常解析的域名
loadTopResolvedDomains();
}
// 更新统计卡片数据
function updateStatCards() {
// 获取所有统计数据
apiRequest('/stats')
.then(data => {
// 更新请求统计
if (data && data.dns) {
// 屏蔽请求
const blockedCount = data.dns.Blocked || data.dns.blocked || 0;
updateStatCard('blocked-count', blockedCount);
// 允许请求
const allowedCount = data.dns.Allowed || data.dns.allowed || 0;
updateStatCard('allowed-count', allowedCount);
// 错误请求
const errorCount = data.dns.Errors || data.dns.errors || 0;
updateStatCard('error-count', errorCount);
// 总请求数
const totalCount = blockedCount + allowedCount + errorCount;
updateStatCard('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));
updateStatCard('blocked-count', blockedValue);
updateStatCard('allowed-count', allowedValue);
updateStatCard('error-count', errorValue);
const totalCount = blockedValue + allowedValue + errorValue;
updateStatCard('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;
}
updateStatCard('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);
});
// 获取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;
}
updateStatCard('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);
}
}
// 加载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 loadTopBlockedDomains() {
// 首先获取表格元素并显示加载状态
// 修复语法错误使用传统的DOM访问方式
const topBlockedTable = document.getElementById('top-blocked-table');
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
if (tbody) {
// 显示加载中状态
tbody.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">加载中...</td>`;
}
apiRequest('/top-blocked')
.then(data => {
// 处理多种可能的数据格式,特别优化对用户提供格式的支持
let processedData = [];
console.log('最常屏蔽域名API返回数据:', data);
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));
console.log('处理后的域名数据:', processedData);
} 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]
}));
}
renderTopBlockedDomains(processedData);
})
.catch(error => {
console.error('获取最常屏蔽域名失败:', error);
// 显示默认空数据而不是错误消息,保持界面一致性
if (tbody) {
showEmpty(tbody, '获取数据失败');
}
// 使用全局通知功能
if (typeof showNotification === 'function') {
showNotification('danger', '获取最常屏蔽域名失败');
}
});
}
// 渲染最常屏蔽的域名表格
function renderTopBlockedDomains(domains) {
// 修复语法错误使用传统的DOM访问方式
const topBlockedTable = document.getElementById('top-blocked-table');
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
if (!tbody) return;
console.log('准备渲染的域名数据:', domains);
if (!domains || domains.length === 0) {
showEmpty(tbody, '暂无屏蔽记录');
return;
}
tbody.innerHTML = '';
domains.forEach((domain, index) => {
if (!domain) return;
// 支持不同的字段名和格式,特别针对用户提供的数据格式优化
const domainName = domain.domain || domain.name || domain.Domain || domain.Name || '未知域名';
const count = domain.count !== undefined ? domain.count :
(domain.Count !== undefined ? domain.Count :
(domain.hits !== undefined ? domain.hits :
(domain.Hits !== undefined ? domain.Hits : 0)));
console.log(`渲染域名 ${index + 1}:`, {domainName, count});
const row = document.createElement('tr');
row.innerHTML = `
<td>${domainName}</td>
<td>${formatNumber(count)}</td>
`;
tbody.appendChild(row);
});
// 初始化表格排序
if (typeof initTableSort === 'function') {
initTableSort('top-blocked-table');
}
}
// 加载最常解析的域名
function loadTopResolvedDomains() {
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]
}));
}
renderTopResolvedDomains(processedData);
})
.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 renderTopResolvedDomains(domains) {
const tbody = document.getElementById('top-resolved-table').querySelector('tbody');
if (!tbody) return;
if (!domains || domains.length === 0) {
showEmpty(tbody, '暂无解析记录');
return;
}
tbody.innerHTML = '';
domains.forEach((domain, index) => {
// 支持不同的字段名和格式
const domainName = domain.domain || domain.name || domain.Domain || domain.Name || '未知域名';
const count = domain.count || domain.Count || domain.hits || domain.Hits || 0;
const row = document.createElement('tr');
row.innerHTML = `
<td>${domainName}</td>
<td>${formatNumber(count)}</td>
`;
tbody.appendChild(row);
});
// 初始化表格排序
initTableSort('top-resolved-table');
}