Files
dns-server/static/js/dashboard.js
2025-11-26 00:48:04 +08:00

1538 lines
59 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.
// dashboard.js - 仪表盘功能实现
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
// 存储统计卡片图表实例
let statCardCharts = {};
// 存储统计卡片历史数据
let statCardHistoryData = {};
// 引入颜色配置文件
const COLOR_CONFIG = window.COLOR_CONFIG || {};
// 初始化仪表盘
async function initDashboard() {
try {
console.log('页面打开时强制刷新数据...');
// 优先加载初始数据,确保页面显示最新信息
await loadDashboardData();
// 初始化图表
initCharts();
// 初始化统计卡片折线图
initStatCardCharts();
// 初始化时间范围切换
initTimeRangeToggle();
// 设置定时更新
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
} catch (error) {
console.error('初始化仪表盘失败:', error);
showNotification('初始化失败: ' + error.message, 'error');
}
}
// 加载仪表盘数据
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: 'latest-blocked.com', ip: '192.168.1.1', timestamp: new Date().toISOString() },
{ domain: 'recent-ads.org', ip: '192.168.1.2', timestamp: new Date().toISOString() }
];
}
// 更新统计卡片
updateStatsCards(stats);
// 更新图表数据,传入查询类型统计
updateCharts(stats, queryTypeStats);
// 更新表格数据
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
// 更新卡片图表
updateStatCardCharts(stats);
// 尝试从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}`;
}
}
if (document.getElementById('cpu-usage')) {
// 保留两位小数并添加单位
const cpuUsage = stats.cpuUsage ? stats.cpuUsage.toFixed(2) + '%' : '---';
document.getElementById('cpu-usage').textContent = cpuUsage;
// 设置CPU状态颜色
const cpuStatusElem = document.getElementById('cpu-status');
if (cpuStatusElem) {
if (stats.cpuUsage !== undefined && stats.cpuUsage !== null) {
if (stats.cpuUsage > 80) {
cpuStatusElem.textContent = '警告';
cpuStatusElem.className = 'text-danger text-sm flex items-center';
} else if (stats.cpuUsage > 60) {
cpuStatusElem.textContent = '较高';
cpuStatusElem.className = 'text-warning text-sm flex items-center';
} else {
cpuStatusElem.textContent = '正常';
cpuStatusElem.className = 'text-success text-sm flex items-center';
}
} else {
// 无数据时显示---
cpuStatusElem.textContent = '---';
cpuStatusElem.className = 'text-gray-400 text-sm flex items-center';
}
}
}
// 更新表格
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
// 更新图表
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
// 更新统计卡片折线图
updateStatCardCharts(stats);
// 确保响应时间图表使用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();
} 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');
// 添加淡入淡出动画
element.style.opacity = '0';
element.style.transition = 'opacity 200ms ease-out';
setTimeout(() => {
element.textContent = formattedNewValue;
element.style.opacity = '1';
// 添加光晕效果
// 根据父级卡片类型确定光晕颜色
const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50');
if (card) {
// 设置光晕颜色类
if (card.classList.contains('bg-blue-50') || card.id.includes('total')) {
element.classList.add('number-glow-blue');
} else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) {
element.classList.add('number-glow-red');
} else if (card.classList.contains('bg-green-50') || card.id.includes('allowed')) {
element.classList.add('number-glow-green');
} else if (card.classList.contains('bg-yellow-50') || card.id.includes('error')) {
element.classList.add('number-glow-yellow');
}
} else {
// 默认光晕效果
element.classList.add('number-glow');
}
// 2秒后移除光晕效果
setTimeout(() => {
element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow');
}, 2000);
}, 200);
}
// 更新百分比元素的函数
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: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' },
{ name: '---', count: '---' }
];
console.log('使用示例数据填充Top屏蔽域名表格');
}
let html = '';
for (const domain of tableData) {
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name}</td>
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
</tr>
`;
}
tableBody.innerHTML = html;
}
// 更新最近屏蔽域名表格
function updateRecentBlockedTable(domains) {
console.log('更新最近屏蔽域名表格,收到数据:', domains);
const tableBody = document.getElementById('recent-blocked-table');
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.name || item.domain || item[0] || '未知',
timestamp: item.timestamp || item.time || Date.now()
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ name: '---', timestamp: now - 5 * 60 * 1000 },
{ name: '---', timestamp: now - 15 * 60 * 1000 },
{ name: '---', timestamp: now - 30 * 60 * 1000 },
{ name: '---', timestamp: now - 45 * 60 * 1000 },
{ name: '---', timestamp: now - 60 * 60 * 1000 }
];
console.log('使用示例数据填充最近屏蔽域名表格');
}
let html = '';
for (const domain of tableData) {
const time = formatTime(domain.timestamp);
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name}</td>
<td class="py-3 px-4 text-sm text-right text-gray-500">${time}</td>
</tr>
`;
}
tableBody.innerHTML = html;
}
// 当前选中的时间范围
let currentTimeRange = '24h'; // 默认为24小时
let isMixedView = false; // 是否为混合视图
let lastSelectedIndex = 0; // 最后选中的按钮索引
// 初始化时间范围切换
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); // 添加选中时的浅色悬停
// 获取并更新当前时间范围
const rangeValue = button.dataset.range || button.textContent.trim().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');
btn.classList.remove(...btnStyle.active);
btn.classList.add(...btnStyle.normal);
btn.classList.add(...btnStyle.hover);
});
// 然后设置第一个按钮为激活状态
firstButton.classList.remove(...firstStyle.normal);
firstButton.classList.remove(...firstStyle.hover);
firstButton.classList.add('active');
firstButton.classList.add(...firstStyle.active);
console.log('默认选中第一个按钮:', firstButton.textContent.trim());
}
}
// 初始化图表
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: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
},
cutout: '70%'
}
});
// 初始化解析类型统计饼图
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: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
},
tooltip: {
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: '70%'
}
});
} else {
console.warn('未找到解析类型统计图表元素');
}
// 初始化DNS请求统计图表
drawDNSRequestsChart();
}
// 绘制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);
// 返回模拟数据
return {
labels: generateTimeLabels(config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30)),
data: generateMockData(config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30), 100, 1000)
};
})
)).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();
} else {
dnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: results[0].labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
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函数
switch (currentTimeRange) {
case '7d':
apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] }));
break;
case '30d':
apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
break;
default: // 24h
apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
}
// 获取统计数据
apiFunction().then(data => {
// 创建或更新图表
if (dnsRequestsChart) {
dnsRequestsChart.data.labels = data.labels;
dnsRequestsChart.data.datasets = [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}];
dnsRequestsChart.options.plugins.legend.display = false;
dnsRequestsChart.update();
} else {
dnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
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 mockData = {
labels: generateTimeLabels(currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30)),
data: generateMockData(currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30), 100, 1000)
};
if (dnsRequestsChart) {
dnsRequestsChart.data.labels = mockData.labels;
dnsRequestsChart.data.datasets[0].data = mockData.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();
}
// 更新解析类型统计饼图
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();
}
}
// 更新统计卡片折线图
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();
}
// 从统计数据中获取规则数
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,
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 (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// 更新运行状态
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 = `
<div class="flex items-center">
<i class="fa ${icon} mr-3"></i>
<span>${message}</span>
</div>
`;
// 添加到页面
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');
menuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const targetId = item.getAttribute('href').substring(1);
// 隐藏所有内容
document.querySelectorAll('[id$="-content"]').forEach(content => {
content.classList.add('hidden');
});
// 显示目标内容
document.getElementById(`${targetId}-content`).classList.remove('hidden');
// 更新页面标题
document.getElementById('page-title').textContent = item.querySelector('span').textContent;
// 更新活动菜单项
menuItems.forEach(menuItem => {
menuItem.classList.remove('sidebar-item-active');
});
item.classList.add('sidebar-item-active');
// 侧边栏切换(移动端)
if (window.innerWidth < 1024) {
toggleSidebar();
}
});
});
}
// 侧边栏切换
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
}
// 响应式处理
function handleResponsive() {
const toggleBtn = document.getElementById('toggle-sidebar');
toggleBtn.addEventListener('click', toggleSidebar);
// 初始状态处理
if (window.innerWidth < 1024) {
document.getElementById('sidebar').classList.add('-translate-x-full');
}
// 窗口大小改变时处理
window.addEventListener('resize', () => {
const sidebar = document.getElementById('sidebar');
if (window.innerWidth >= 1024) {
sidebar.classList.remove('-translate-x-full');
}
});
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
// 初始化页面切换
handlePageSwitch();
// 初始化响应式
handleResponsive();
// 初始化仪表盘
initDashboard();
// 页面卸载时清理定时器
window.addEventListener('beforeunload', () => {
if (intervalId) {
clearInterval(intervalId);
}
});
});