// dashboard.js - 仪表盘功能实现
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
let dashboardWsConnection = null;
let dashboardWsReconnectTimer = null;
// 存储统计卡片图表实例
let statCardCharts = {};
// 存储统计卡片历史数据
let statCardHistoryData = {};
// 存储仪表盘历史数据,用于计算趋势
window.dashboardHistoryData = window.dashboardHistoryData || {
prevResponseTime: null,
prevActiveIPs: null,
prevTopQueryTypeCount: null
};
// 节流相关变量
let lastProcessedTime = 0;
const PROCESS_THROTTLE_INTERVAL = 1000; // 1秒节流间隔
// 引入颜色配置文件
const COLOR_CONFIG = window.COLOR_CONFIG || {};
// 全局统计变量
let totalQueries = 0;
let blockedQueries = 0;
let allowedQueries = 0;
let errorQueries = 0;
// 初始化仪表盘
async function initDashboard() {
try {
// 优先加载初始数据,确保页面显示最新信息
await loadDashboardData();
// 初始化图表
initCharts();
// 初始化时间范围切换
initTimeRangeToggle();
// 建立WebSocket连接
connectWebSocket();
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupResources);
} catch (error) {
console.error('初始化仪表盘失败:', error);
showNotification('初始化失败: ' + error.message, 'error');
}
}
// 建立WebSocket连接
function connectWebSocket() {
try {
// 构建WebSocket URL
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
// 创建WebSocket连接
dashboardWsConnection = new WebSocket(wsUrl);
// 连接打开事件
dashboardWsConnection.onopen = function() {
showNotification('数据更新成功', 'success');
// 清除重连计时器
if (dashboardWsReconnectTimer) {
clearTimeout(dashboardWsReconnectTimer);
dashboardWsReconnectTimer = null;
}
};
// 接收消息事件
dashboardWsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'initial_data' || data.type === 'stats_update') {
processRealTimeData(data.data);
}
} catch (error) {
console.error('处理WebSocket消息失败:', error);
}
};
// 连接关闭事件
dashboardWsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭,代码:', event.code);
dashboardWsConnection = null;
// 设置重连
setupReconnect();
};
// 连接错误事件
dashboardWsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
// 如果WebSocket连接失败,回退到定时刷新
fallbackToIntervalRefresh();
}
}
// 设置重连逻辑
function setupReconnect() {
if (dashboardWsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
dashboardWsReconnectTimer = setTimeout(() => {
connectWebSocket();
}, reconnectDelay);
}
// 处理实时数据更新 - 添加节流机制
function processRealTimeData(stats) {
// 节流处理,限制执行频率
const now = Date.now();
if (now - lastProcessedTime < PROCESS_THROTTLE_INTERVAL) {
return; // 跳过执行
}
lastProcessedTime = now;
try {
// 确保stats是有效的对象
if (!stats || typeof stats !== 'object') {
console.error('无效的实时数据:', stats);
return;
}
// 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片
updateStatsCards(stats);
// 获取查询类型统计数据
let queryTypeStats = null;
if (stats.dns && stats.dns.QueryTypes) {
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
type,
count: Number(count) || 0
}));
}
// 更新图表数据
updateCharts(stats, queryTypeStats);
// 尝试从stats中获取总查询数等信息,并更新全局变量
// 确保使用数字类型
if (stats.dns) {
const allowed = Number(stats.dns.Allowed) || 0;
const blocked = Number(stats.dns.Blocked) || 0;
const errors = Number(stats.dns.Errors || 0);
totalQueries = allowed + blocked + errors;
blockedQueries = blocked;
errorQueries = errors;
allowedQueries = allowed;
} else {
totalQueries = Number(stats.totalQueries) || 0;
blockedQueries = Number(stats.blockedQueries) || 0;
errorQueries = Number(stats.errorQueries) || 0;
allowedQueries = Number(stats.allowedQueries) || 0;
}
if (document.getElementById('top-query-type')) {
const queryType = stats.topQueryType || '---';
document.getElementById('top-query-type').textContent = queryType;
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
// 计算最常用查询类型的百分比
let queryTypePercentage = 0;
if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
}
queryPercentElem.textContent = `${Math.round(queryTypePercentage)}%`;
queryPercentElem.className = 'text-sm flex items-center text-primary';
}
}
if (document.getElementById('active-ips')) {
const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---';
// 计算活跃IP趋势
let ipsPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '•';
// 查找箭头元素
const activeIpsPercentElem = document.getElementById('active-ips-percent');
let parent = null;
let arrowIcon = null;
if (activeIpsPercentElem) {
parent = activeIpsPercentElem.parentElement;
if (parent) {
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
}
}
if (stats.activeIPs !== undefined) {
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs;
// 首次加载时初始化历史数据,不计算趋势
if (prevActiveIPs === null) {
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
ipsPercent = '0.0%';
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
} else {
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
// 处理-0.0%的情况
if (ipsPercent === '-0.0%') {
ipsPercent = '0.0%';
}
// 根据用户要求:数量下降显示红色箭头,上升显示绿色箭头
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-success';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
}
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-danger';
if (arrowIcon) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
}
} else {
// 趋势为0时,显示圆点图标
trendIcon = '•';
trendClass = 'text-gray-500';
if (arrowIcon) {
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
}
// 更新历史数据
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
}
}
document.getElementById('active-ips').textContent = activeIPs;
if (activeIpsPercentElem) {
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`;
}
}
// 实时更新TOP客户端和TOP域名数据
updateTopData();
} catch (error) {
console.error('处理实时数据失败:', error);
}
}
// 实时更新TOP客户端和TOP域名数据
async function updateTopData() {
try {
// 隐藏所有加载中状态
const clientsLoadingElement = document.getElementById('top-clients-loading');
if (clientsLoadingElement) {
clientsLoadingElement.classList.add('hidden');
}
const domainsLoadingElement = document.getElementById('top-domains-loading');
if (domainsLoadingElement) {
domainsLoadingElement.classList.add('hidden');
}
// 获取最新的TOP客户端数据
let clientsData = [];
try {
clientsData = await api.getTopClients();
} catch (error) {
console.error('获取TOP客户端数据失败:', error);
}
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
if (clientsData.length > 0) {
// 使用真实数据
updateTopClientsTable(clientsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-clients-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 数据为空,使用模拟数据
const mockClients = [
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' }
];
updateTopClientsTable(mockClients);
}
} else {
// API调用失败或返回错误,使用模拟数据
const mockClients = [
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' },
{ ip: '---.---.---.---', count: '---' }
];
updateTopClientsTable(mockClients);
}
// 获取最新的TOP域名数据
let domainsData = [];
try {
domainsData = await api.getTopDomains();
} catch (error) {
console.error('获取TOP域名数据失败:', error);
}
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
if (domainsData.length > 0) {
// 使用真实数据
updateTopDomainsTable(domainsData);
// 隐藏错误信息
const errorElement = document.getElementById('top-domains-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 数据为空,使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
updateTopDomainsTable(mockDomains);
}
} else {
// API调用失败或返回错误,使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
updateTopDomainsTable(mockDomains);
}
} catch (error) {
console.error('更新TOP数据失败:', error);
// 出错时使用模拟数据
const mockDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
updateTopDomainsTable(mockDomains);
}
}
// 回退到定时刷新
function fallbackToIntervalRefresh() {
console.warn('回退到定时刷新模式');
showNotification('实时更新连接失败,已切换到定时刷新模式', 'warning');
// 如果已经有定时器,先清除
if (intervalId) {
clearInterval(intervalId);
}
// 设置新的定时器
intervalId = setInterval(async () => {
try {
await loadDashboardData();
} catch (error) {
console.error('定时刷新失败:', error);
}
}, 5000); // 每5秒更新一次
}
// 清理资源
function cleanupResources() {
// 清除WebSocket连接
if (dashboardWsConnection) {
dashboardWsConnection.close();
dashboardWsConnection = null;
}
// 清除重连计时器
if (dashboardWsReconnectTimer) {
clearTimeout(dashboardWsReconnectTimer);
dashboardWsReconnectTimer = null;
}
// 清除定时刷新
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
// 清除图表实例,释放内存
if (ratioChart) {
ratioChart.destroy();
ratioChart = null;
}
if (dnsRequestsChart) {
dnsRequestsChart.destroy();
dnsRequestsChart = null;
}
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.destroy();
detailedDnsRequestsChart = null;
}
if (queryTypeChart) {
queryTypeChart.destroy();
queryTypeChart = null;
}
// 清除统计卡片图表实例
for (const key in statCardCharts) {
if (statCardCharts[key]) {
statCardCharts[key].destroy();
delete statCardCharts[key];
}
}
// 清除事件监听器
window.removeEventListener('beforeunload', cleanupResources);
}
// 加载仪表盘数据
// 更新统计卡片
function updateStatsCards(stats) {
// 适配不同的数据结构
// 保存当前显示的值,用于在数据缺失时保留
let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0;
let topQueryType = 'A', queryTypePercentage = 0;
let activeIPs = 0, activeIPsPercentage = 0;
let avgResponseTime = 0;
// 优先从API数据中获取值,仅在API数据不可用时使用DOM中的当前值
// 解析当前显示的值,作为备用默认值
const getCurrentValue = (elem) => {
if (!elem) return 0;
const text = elem.textContent.replace(/,/g, '').replace(/[^0-9]/g, '');
return parseInt(text) || 0;
};
// 检查数据结构,兼容可能的不同格式
if (stats) {
// 优先使用顶层字段,只有当值存在时才更新
if (stats.totalQueries !== undefined) {
totalQueries = Number(stats.totalQueries) || 0;
}
if (stats.blockedQueries !== undefined) {
blockedQueries = Number(stats.blockedQueries) || 0;
}
if (stats.allowedQueries !== undefined) {
allowedQueries = Number(stats.allowedQueries) || 0;
}
if (stats.errorQueries !== undefined) {
errorQueries = Number(stats.errorQueries) || 0;
}
if (stats.topQueryType !== undefined) {
topQueryType = stats.topQueryType;
}
if (stats.queryTypePercentage !== undefined) {
queryTypePercentage = stats.queryTypePercentage;
}
if (stats.activeIPs !== undefined) {
activeIPs = Number(stats.activeIPs) || 0;
}
if (stats.activeIPsPercentage !== undefined) {
activeIPsPercentage = stats.activeIPsPercentage;
}
if (stats.avgResponseTime !== undefined) {
avgResponseTime = Number(stats.avgResponseTime) || 0;
}
if (stats.responseTime !== undefined) {
avgResponseTime = Number(stats.responseTime) || 0;
}
// 如果dns对象存在,优先使用其中的数据
if (stats.dns) {
// 计算总查询数,确保准确性
if (stats.dns.Allowed !== undefined && stats.dns.Blocked !== undefined) {
const allowed = Number(stats.dns.Allowed) || 0;
const blocked = Number(stats.dns.Blocked) || 0;
const errors = Number(stats.dns.Errors || 0);
totalQueries = allowed + blocked + errors;
allowedQueries = allowed;
blockedQueries = blocked;
errorQueries = errors;
} else if (stats.dns.Queries !== undefined) {
totalQueries = Number(stats.dns.Queries) || 0;
}
// 确保使用dns对象中的具体数值
if (stats.dns.Blocked !== undefined) {
blockedQueries = Number(stats.dns.Blocked) || 0;
}
if (stats.dns.Allowed !== undefined) {
allowedQueries = Number(stats.dns.Allowed) || 0;
}
if (stats.dns.Errors !== undefined) {
errorQueries = Number(stats.dns.Errors) || 0;
}
// 计算最常用查询类型的百分比
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;
}
// 检查并更新平均响应时间
if (stats.dns.AvgResponseTime !== undefined) {
avgResponseTime = Number(stats.dns.AvgResponseTime) || 0;
}
}
} else if (Array.isArray(stats) && stats.length > 0) {
// 可能的数据结构3: 数组形式
if (stats[0].total !== undefined) {
totalQueries = Number(stats[0].total) || 0;
}
if (stats[0].blocked !== undefined) {
blockedQueries = Number(stats[0].blocked) || 0;
}
if (stats[0].allowed !== undefined) {
allowedQueries = Number(stats[0].allowed) || 0;
}
if (stats[0].error !== undefined) {
errorQueries = Number(stats[0].error) || 0;
}
if (stats[0].topQueryType !== undefined) {
topQueryType = stats[0].topQueryType;
}
if (stats[0].queryTypePercentage !== undefined) {
queryTypePercentage = stats[0].queryTypePercentage;
}
if (stats[0].activeIPs !== undefined) {
activeIPs = Number(stats[0].activeIPs) || 0;
}
if (stats[0].activeIPsPercentage !== undefined) {
activeIPsPercentage = stats[0].activeIPsPercentage;
}
if (stats[0].avgResponseTime !== undefined) {
avgResponseTime = Number(stats[0].avgResponseTime) || 0;
}
if (stats[0].responseTime !== undefined) {
avgResponseTime = Number(stats[0].responseTime) || 0;
}
} else {
// 仅在API数据完全不可用时,才从DOM中获取当前值作为默认值
const totalQueriesElem = document.getElementById('total-queries');
const blockedQueriesElem = document.getElementById('blocked-queries');
const allowedQueriesElem = document.getElementById('allowed-queries');
const errorQueriesElem = document.getElementById('error-queries');
const activeIPsElem = document.getElementById('active-ips');
totalQueries = getCurrentValue(totalQueriesElem);
blockedQueries = getCurrentValue(blockedQueriesElem);
allowedQueries = getCurrentValue(allowedQueriesElem);
errorQueries = getCurrentValue(errorQueriesElem);
activeIPs = getCurrentValue(activeIPsElem);
}
// 存储正在进行的动画状态,避免动画重叠
const animationInProgress = {};
// 为数字元素添加翻页滚动特效
function animateValue(elementId, newValue) {
const element = document.getElementById(elementId);
if (!element) return;
const formattedNewValue = formatNumber(newValue);
const currentValue = element.textContent;
// 如果值没有变化,不执行任何操作
if (currentValue === formattedNewValue) {
return;
}
// 简化动画:使用CSS opacity过渡实现平滑更新
element.style.transition = 'opacity 0.3s ease-in-out';
element.style.opacity = '0';
// 使用requestAnimationFrame确保平滑过渡
requestAnimationFrame(() => {
element.textContent = formattedNewValue;
element.style.opacity = '1';
// 移除transition样式,避免影响后续更新
setTimeout(() => {
element.style.transition = '';
}, 300);
});
}
// 更新百分比元素的函数
function updatePercentage(elementId, value) {
const element = document.getElementById(elementId);
if (!element) return;
// 检查是否有正在进行的动画
if (animationInProgress[elementId + '_percent']) {
clearTimeout(animationInProgress[elementId + '_percent']);
}
try {
element.style.opacity = '0';
element.style.transition = 'opacity 200ms ease-out';
// 保存定时器ID,便于后续可能的取消
animationInProgress[elementId + '_percent'] = setTimeout(() => {
try {
element.textContent = value;
element.style.opacity = '1';
} catch (e) {
console.error('更新百分比元素失败:', e);
} finally {
// 清除动画状态标记
delete animationInProgress[elementId + '_percent'];
}
}, 200);
} catch (e) {
console.error('设置百分比动画失败:', e);
// 出错时直接设置值
try {
element.textContent = value;
element.style.opacity = '1';
} catch (e2) {
console.error('直接更新百分比元素也失败:', e2);
}
}
}
// 平滑更新数量显示
animateValue('total-queries', totalQueries);
animateValue('blocked-queries', blockedQueries);
animateValue('allowed-queries', allowedQueries);
animateValue('error-queries', errorQueries);
animateValue('active-ips', activeIPs);
// DNSSEC相关数据
// 优先从DOM中获取当前显示的值,作为默认值
const dnssecSuccessElem = document.getElementById('dnssec-success');
const dnssecFailedElem = document.getElementById('dnssec-failed');
const dnssecQueriesElem = document.getElementById('dnssec-queries');
// 从当前显示值初始化,确保数据刷新前保留前一次结果
let dnssecEnabled = false;
let dnssecQueries = getCurrentValue(dnssecQueriesElem);
let dnssecSuccess = getCurrentValue(dnssecSuccessElem);
let dnssecFailed = getCurrentValue(dnssecFailedElem);
let dnssecUsage = 0;
// 检查DNSSEC数据
if (stats) {
// 优先使用顶层字段,只有当值存在时才更新
if (stats.dnssecEnabled !== undefined) dnssecEnabled = stats.dnssecEnabled;
if (stats.dnssecQueries !== undefined) dnssecQueries = stats.dnssecQueries;
if (stats.dnssecSuccess !== undefined) dnssecSuccess = stats.dnssecSuccess;
if (stats.dnssecFailed !== undefined) dnssecFailed = stats.dnssecFailed;
if (stats.dnssecUsage !== undefined) dnssecUsage = stats.dnssecUsage;
// 如果dns对象存在,优先使用其中的数据
if (stats.dns) {
if (stats.dns.DNSSECEnabled !== undefined) dnssecEnabled = stats.dns.DNSSECEnabled;
if (stats.dns.DNSSECQueries !== undefined) dnssecQueries = stats.dns.DNSSECQueries;
if (stats.dns.DNSSECSuccess !== undefined) dnssecSuccess = stats.dns.DNSSECSuccess;
if (stats.dns.DNSSECFailed !== undefined) dnssecFailed = stats.dns.DNSSECFailed;
}
// 如果没有直接提供使用率,计算使用率
if (dnssecUsage === 0 && totalQueries > 0) {
dnssecUsage = (dnssecQueries / totalQueries) * 100;
}
}
// 更新DNSSEC统计卡片
const dnssecUsageElement = document.getElementById('dnssec-usage');
const dnssecStatusElement = document.getElementById('dnssec-status');
const dnssecSuccessElement = document.getElementById('dnssec-success');
const dnssecFailedElement = document.getElementById('dnssec-failed');
const dnssecQueriesElement = document.getElementById('dnssec-queries');
if (dnssecUsageElement) {
dnssecUsageElement.textContent = `${Math.round(dnssecUsage)}%`;
}
if (dnssecStatusElement) {
dnssecStatusElement.textContent = dnssecEnabled ? '已启用' : '已禁用';
dnssecStatusElement.className = `text-sm flex items-center ${dnssecEnabled ? 'text-success' : 'text-danger'}`;
}
if (dnssecSuccessElement) {
dnssecSuccessElement.textContent = formatNumber(dnssecSuccess);
}
if (dnssecFailedElement) {
dnssecFailedElement.textContent = formatNumber(dnssecFailed);
}
if (dnssecQueriesElement) {
dnssecQueriesElement.textContent = formatNumber(dnssecQueries);
}
// 直接更新文本和百分比,移除动画效果
const topQueryTypeElement = document.getElementById('top-query-type');
const queryTypePercentageElement = document.getElementById('query-type-percentage');
const activeIpsPercentElement = document.getElementById('active-ips-percent');
if (topQueryTypeElement) topQueryTypeElement.textContent = topQueryType;
if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`;
if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`;
// 计算并平滑更新百分比,同时更新箭头颜色和方向
function updatePercentWithArrow(elementId, percentage, prevValue, currentValue) {
const element = document.getElementById(elementId);
if (!element) return;
// 更新百分比数值
updatePercentage(elementId, percentage);
// 查找父元素,获取箭头图标
const parent = element.parentElement;
if (!parent) return;
let arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
if (!arrowIcon) return;
// 计算变化趋势
let isIncrease = currentValue > prevValue;
let isDecrease = currentValue < prevValue;
let isNoChange = currentValue === prevValue;
// 处理百分比显示,避免-0.0%的情况
let formattedPercentage = percentage;
if (percentage === '-0.0%') {
formattedPercentage = '0.0%';
updatePercentage(elementId, formattedPercentage);
}
// 响应时间特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
if (elementId === 'response-time-percent') {
// 反转箭头逻辑
[isIncrease, isDecrease] = [isDecrease, isIncrease];
}
// 更新箭头图标和颜色
if (isIncrease) {
arrowIcon.className = 'fa fa-arrow-up mr-1';
parent.className = 'text-success text-sm flex items-center';
} else if (isDecrease) {
arrowIcon.className = 'fa fa-arrow-down mr-1';
parent.className = 'text-danger text-sm flex items-center';
} else if (isNoChange) {
// 趋势为0时,显示圆点图标
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
parent.className = 'text-gray-500 text-sm flex items-center';
}
}
// 保存历史数据,用于计算趋势
window.dashboardHistoryData = window.dashboardHistoryData || {
totalQueries: 0,
blockedQueries: 0,
allowedQueries: 0,
errorQueries: 0,
avgResponseTime: 0
};
// 计算百分比并更新箭头
if (totalQueries > 0) {
const queriesPercent = '100%';
const blockedPercent = `${Math.round((blockedQueries / totalQueries) * 100)}%`;
const allowedPercent = `${Math.round((allowedQueries / totalQueries) * 100)}%`;
const errorPercent = `${Math.round((errorQueries / totalQueries) * 100)}%`;
updatePercentWithArrow('queries-percent', queriesPercent, window.dashboardHistoryData.totalQueries, totalQueries);
updatePercentWithArrow('blocked-percent', blockedPercent, window.dashboardHistoryData.blockedQueries, blockedQueries);
updatePercentWithArrow('allowed-percent', allowedPercent, window.dashboardHistoryData.allowedQueries, allowedQueries);
updatePercentWithArrow('error-percent', errorPercent, window.dashboardHistoryData.errorQueries, errorQueries);
} else {
updatePercentage('queries-percent', '---');
updatePercentage('blocked-percent', '---');
updatePercentage('allowed-percent', '---');
updatePercentage('error-percent', '---');
}
// 更新平均响应时间卡片
if (document.getElementById('avg-response-time')) {
const responseTime = avgResponseTime ? avgResponseTime.toFixed(2) + 'ms' : '---';
document.getElementById('avg-response-time').textContent = responseTime;
// 更新平均响应时间的百分比和箭头,使用与其他统计卡片相同的逻辑
if (avgResponseTime !== undefined && avgResponseTime !== null) {
// 计算变化百分比
let responsePercent = '0.0%';
const prevResponseTime = window.dashboardHistoryData.avgResponseTime || 0;
const currentResponseTime = avgResponseTime;
if (prevResponseTime > 0) {
const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
}
// 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
// updatePercentWithArrow函数内部已添加响应时间的特殊处理
updatePercentWithArrow('response-time-percent', responsePercent, prevResponseTime, currentResponseTime);
} else {
updatePercentage('response-time-percent', '---');
}
}
// 更新历史数据
window.dashboardHistoryData.totalQueries = totalQueries;
window.dashboardHistoryData.blockedQueries = blockedQueries;
window.dashboardHistoryData.allowedQueries = allowedQueries;
window.dashboardHistoryData.errorQueries = errorQueries;
// 只在avgResponseTime不为0时更新历史数据,保留上一次不为0的状态
if (avgResponseTime > 0) {
window.dashboardHistoryData.avgResponseTime = avgResponseTime;
}
}
// 更新Top屏蔽域名表格
async function updateTopBlockedTable(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: '---' }
];
}
// 计算总拦截次数
const totalCount = tableData.reduce((sum, domain) => {
return sum + (typeof domain.count === 'number' ? domain.count : 0);
}, 0);
let html = '';
for (let i = 0; i < tableData.length; i++) {
const domain = tableData[i];
// 检查域名是否是跟踪器
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
const isTracker = trackerInfo !== null;
// 构建跟踪器浮窗内容
const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
` : ''}
${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : '';
// 计算百分比
const percentage = totalCount > 0 && typeof domain.count === 'number'
? ((domain.count / totalCount) * 100).toFixed(2)
: '0.00';
html += `
${i + 1}
${domain.name}
${isTracker ? `
${trackerTooltip}
` : ''}
${formatNumber(domain.count)}
${percentage}%
`;
}
tableBody.innerHTML = html;
// 添加跟踪器图标悬停事件
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
trackerIconContainers.forEach(container => {
const tooltip = container.querySelector('.tracker-tooltip');
if (tooltip) {
// 移除内联样式,使用CSS类控制显示
tooltip.removeAttribute('style');
container.addEventListener('mouseenter', () => {
tooltip.classList.add('visible');
});
container.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
});
}
});
}
// 更新最近屏蔽域名表格
function updateRecentBlockedTable(domains) {
const tableBody = document.getElementById('recent-blocked-table');
// 确保tableBody存在,因为最近屏蔽域名卡片可能已被移除
if (!tableBody) {
return;
}
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.name || item.domain || item[0] || '未知',
timestamp: item.timestamp || item.time || Date.now(),
type: item.type || '广告'
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ name: '---.---.---', timestamp: now - 5 * 60 * 1000, type: '广告' },
{ name: '---.---.---', timestamp: now - 15 * 60 * 1000, type: '恶意' },
{ name: '---.---.---', timestamp: now - 30 * 60 * 1000, type: '广告' },
{ name: '---.---.---', timestamp: now - 45 * 60 * 1000, type: '追踪' },
{ name: '---.---.---', timestamp: now - 60 * 60 * 1000, type: '恶意' }
];
}
let html = '';
for (let i = 0; i < tableData.length; i++) {
const domain = tableData[i];
const time = formatTime(domain.timestamp);
html += `
`;
}
tableBody.innerHTML = html;
}
// IP地理位置缓存(检查是否已经存在,避免重复声明)
if (typeof ipGeolocationCache === 'undefined') {
var ipGeolocationCache = {};
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
}
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
if (typeof trackersDatabase === 'undefined') {
var trackersDatabase = null;
var trackersLoaded = false;
var trackersLoading = false;
}
// 加载跟踪器数据库
async function loadTrackersDatabase() {
if (trackersLoaded) return trackersDatabase;
if (trackersLoading) {
// 等待正在进行的加载完成
while (trackersLoading) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return trackersDatabase;
}
trackersLoading = true;
try {
const response = await fetch('domain-info/tracker/trackers.json');
if (!response.ok) {
console.error('加载跟踪器数据库失败:', response.statusText);
trackersDatabase = { trackers: {} };
return trackersDatabase;
}
trackersDatabase = await response.json();
trackersLoaded = true;
return trackersDatabase;
} catch (error) {
console.error('加载跟踪器数据库失败:', error);
trackersDatabase = { trackers: {} };
return trackersDatabase;
} finally {
trackersLoading = false;
}
}
// 检查域名是否在跟踪器数据库中
async function isDomainInTrackerDatabase(domain) {
if (!trackersDatabase || !trackersLoaded) {
await loadTrackersDatabase();
}
if (!trackersDatabase || !trackersDatabase.trackers) {
return null;
}
// 检查域名是否直接作为跟踪器键存在
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
return trackersDatabase.trackers[domain];
}
// 检查域名是否在跟踪器URL中
for (const trackerKey in trackersDatabase.trackers) {
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
const tracker = trackersDatabase.trackers[trackerKey];
if (tracker && tracker.url) {
try {
const trackerUrl = new URL(tracker.url);
if (trackerUrl.hostname === domain) {
return tracker;
}
} catch (e) {
// 忽略无效URL
}
}
}
}
return null;
}
// 获取IP地理位置信息
async function getIpGeolocation(ip) {
// 检查是否为内网IP
if (isPrivateIP(ip)) {
return "内网 内网";
}
// 检查缓存
const now = Date.now();
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
return ipGeolocationCache[ip].location;
}
try {
// 使用whois.pconline.com.cn API获取IP地理位置
const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`;
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析响应数据
const data = await response.json();
let location = "未知 未知";
if (data && data.addr) {
// 直接使用addr字段作为完整的地理位置信息
location = data.addr;
}
// 保存到缓存
ipGeolocationCache[ip] = {
location: location,
timestamp: now
};
return location;
} catch (error) {
console.error('获取IP地理位置失败:', error);
return "未知 未知";
}
}
// 检查是否为内网IP
function isPrivateIP(ip) {
const parts = ip.split('.');
// 检查IPv4内网地址
if (parts.length === 4) {
const first = parseInt(parts[0]);
const second = parseInt(parts[1]);
// 10.0.0.0/8
if (first === 10) {
return true;
}
// 172.16.0.0/12
if (first === 172 && second >= 16 && second <= 31) {
return true;
}
// 192.168.0.0/16
if (first === 192 && second === 168) {
return true;
}
// 127.0.0.0/8 (localhost)
if (first === 127) {
return true;
}
// 169.254.0.0/16 (link-local)
if (first === 169 && second === 254) {
return true;
}
}
// 检查IPv6内网地址
if (ip.includes(':')) {
// ::1/128 (localhost)
if (ip === '::1' || ip.startsWith('0:0:0:0:0:0:0:1')) {
return true;
}
// fc00::/7 (unique local address)
if (ip.startsWith('fc') || ip.startsWith('fd')) {
return true;
}
// fe80::/10 (link-local)
if (ip.startsWith('fe80:')) {
return true;
}
}
return false;
}
// 更新TOP客户端表格
async function updateTopClientsTable(clients) {
const tableBody = document.getElementById('top-clients-table');
// 确保tableBody存在
if (!tableBody) {
console.error('未找到top-clients-table元素');
return;
}
// 隐藏加载中状态
const loadingElement = document.getElementById('top-clients-loading');
if (loadingElement) {
loadingElement.classList.add('hidden');
}
// 显示数据区域
tableBody.classList.remove('hidden');
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(clients)) {
tableData = clients.map(item => ({
ip: item.ip || item[0] || '未知',
count: item.count || item[1] || 0
}));
} else if (clients && typeof clients === 'object') {
// 如果是对象,转换为数组
tableData = Object.entries(clients).map(([ip, count]) => ({
ip,
count: count || 0
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' }
];
}
let html = '';
for (let i = 0; i < tableData.length; i++) {
const client = tableData[i];
// 获取IP地理信息
const location = await getIpGeolocation(client.ip);
html += `
${i + 1}
${client.ip}
${location}
${formatNumber(client.count)}
`;
}
tableBody.innerHTML = html;
}
// 更新请求域名排行表格
async function updateTopDomainsTable(domains) {
const tableBody = document.getElementById('top-domains-table');
// 确保tableBody存在
if (!tableBody) {
console.error('未找到top-domains-table元素');
return;
}
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.domain || item.name || 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: 'example.com', count: 50 },
{ name: 'google.com', count: 45 },
{ name: 'facebook.com', count: 40 },
{ name: 'twitter.com', count: 35 },
{ name: 'youtube.com', count: 30 }
];
}
// 计算总请求次数
const totalCount = tableData.reduce((sum, domain) => {
return sum + (typeof domain.count === 'number' ? domain.count : 0);
}, 0);
let html = '';
for (let i = 0; i < tableData.length; i++) {
const domain = tableData[i];
// 检查域名是否是跟踪器
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
const isTracker = trackerInfo !== null;
// 构建跟踪器浮窗内容
const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
` : ''}
${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : '';
// 计算百分比
const percentage = totalCount > 0 && typeof domain.count === 'number'
? ((domain.count / totalCount) * 100).toFixed(2)
: '0.00';
html += `
${i + 1}
${domain.name}${domain.dnssec ? ' ' : ''}
${isTracker ? `
${trackerTooltip}
` : ''}
${formatNumber(domain.count)}
${percentage}%
`;
}
tableBody.innerHTML = html;
// 添加跟踪器图标悬停事件
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
trackerIconContainers.forEach(container => {
const tooltip = container.querySelector('.tracker-tooltip');
if (tooltip) {
// 移除内联样式,使用CSS类控制显示
tooltip.removeAttribute('style');
container.addEventListener('mouseenter', () => {
tooltip.classList.add('visible');
});
container.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
});
}
});
}
// 当前选中的时间范围
let currentTimeRange = '24h'; // 默认为24小时
let lastSelectedIndex = 0; // 最后选中的按钮索引,24小时是第一个按钮
// 详细图表专用变量
let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围
// 初始化时间范围切换
function initTimeRangeToggle() {
// 查找所有可能的时间范围按钮类名
const allTimeRangeButtons = document.querySelectorAll('.time-range-btn, .time-range-button, .timerange-btn, button[data-range]');
// 排除图表模态框内的按钮
const chartModal = document.getElementById('chart-modal');
const timeRangeButtons = Array.from(allTimeRangeButtons).filter(button => {
// 检查按钮是否是图表模态框的后代
return !chartModal || !chartModal.contains(button);
});
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'] // 选中时的浅色悬停
}
];
// 为所有按钮设置初始样式和事件
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);
// 移除鼠标悬停提示
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
// 重置所有按钮为非选中状态
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);
});
// 普通选中模式
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); // 添加选中时的浅色悬停
// 获取并更新当前时间范围
let rangeValue;
if (button.dataset.range) {
rangeValue = button.dataset.range;
} else {
const btnText = button.textContent.trim();
if (btnText.includes('24')) {
rangeValue = '24h';
} else if (btnText.includes('7')) {
rangeValue = '7d';
} else if (btnText.includes('30')) {
rangeValue = '30d';
} else {
rangeValue = btnText.replace(/[^0-9a-zA-Z]/g, '');
}
}
currentTimeRange = rangeValue;
// 重新加载数据
loadDashboardData();
// 更新DNS请求图表
drawDNSRequestsChart();
});
// 移除自定义鼠标悬停提示效果
});
// 确保默认选中24小时按钮
if (timeRangeButtons.length > 0) {
// 选择24小时按钮(索引为0),如果不存在则使用第一个按钮
const defaultButtonIndex = 0;
const defaultButton = timeRangeButtons[defaultButtonIndex] || timeRangeButtons[0];
const defaultStyle = buttonStyles[defaultButtonIndex % buttonStyles.length] || 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', 'bg-gray-500', 'mixed-view-active');
btn.classList.remove(...btnStyle.active);
btn.classList.remove(...btnStyle.activeHover);
btn.classList.add(...btnStyle.normal);
btn.classList.add(...btnStyle.hover);
});
// 然后设置24小时按钮为激活状态
defaultButton.classList.remove(...defaultStyle.normal);
defaultButton.classList.remove(...defaultStyle.hover);
defaultButton.classList.add('active');
defaultButton.classList.add(...defaultStyle.active);
defaultButton.classList.add(...defaultStyle.activeHover);
// 设置默认时间范围为24小时
currentTimeRange = '24h';
}
}
// 注意:这个函数已被后面的实现覆盖,请使用后面的drawDetailedDNSRequestsChart函数
// 初始化图表
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: [0, 0, 0],
backgroundColor: ['#34D399', '#EF4444', '#F59E0B'], // 优化的现代化配色
borderWidth: 3, // 增加边框宽度,增强区块分隔
borderColor: '#FFFFFF', // 白色边框,使各个扇区更清晰
hoverOffset: 15, // 增加悬停偏移效果,增强交互体验
hoverBorderWidth: 4, // 悬停时增加边框宽度
hoverBackgroundColor: ['#10B981', '#DC2626', '#D97706'], // 悬停时的深色效果
borderRadius: 10, // 添加圆角效果,增强现代感
borderSkipped: false // 显示所有边框
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// 简化动画,提高性能
animation: {
duration: 300, // 缩短动画时间
easing: 'easeOutQuart', // 简化缓动函数
animateRotate: true, // 仅保留旋转动画
animateScale: false // 禁用缩放动画
},
plugins: {
legend: {
position: 'bottom',
align: 'center',
labels: {
boxWidth: 12, // 调整图例框的宽度
font: {
size: 11, // 调整字体大小
family: 'Inter, system-ui, sans-serif', // 使用现代字体
weight: 500 // 字体粗细
},
padding: 12, // 调整内边距
lineHeight: 1.5, // 调整行高
usePointStyle: true, // 使用点样式代替方形图例,节省空间
pointStyle: 'circle', // 使用圆形点样式
color: '#4B5563', // 图例文本颜色
generateLabels: function(chart) {
// 自定义图例生成,添加更多样式控制
const data = chart.data;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const dataset = data.datasets[0];
return {
text: label,
fillStyle: dataset.backgroundColor[i],
strokeStyle: dataset.borderColor,
lineWidth: dataset.borderWidth,
pointStyle: 'circle',
hidden: !chart.isDatasetVisible(0),
index: i
};
});
}
return [];
}
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(17, 24, 39, 0.9)', // 深背景,增强可读性
padding: 12, // 增加内边距
titleFont: {
size: 13, // 标题字体大小
family: 'Inter, system-ui, sans-serif',
weight: 600
},
bodyFont: {
size: 12, // 正文字体大小
family: 'Inter, system-ui, sans-serif'
},
bodySpacing: 6, // 正文行间距
displayColors: true, // 显示颜色指示器
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
},
afterLabel: function(context) {
// 可以添加额外的信息
}
},
cornerRadius: 8, // 圆角
boxPadding: 6, // 盒子内边距
borderColor: 'rgba(255, 255, 255, 0.2)', // 边框颜色
borderWidth: 1 // 边框宽度
},
title: {
display: false // 不显示标题,由HTML标题代替
}
},
cutout: '70%', // 调整中心空白区域比例,增强现代感
// 增强元素配置
elements: {
arc: {
borderAlign: 'center',
tension: 0.1, // 添加轻微的张力,使圆弧更平滑
borderWidth: 3 // 统一边框宽度
}
},
layout: {
padding: {
top: 20, // 增加顶部内边距
right: 20,
bottom: 30, // 增加底部内边距,为图例预留更多空间
left: 20
}
},
// 添加交互配置
interaction: {
mode: 'nearest', // 交互模式
axis: 'x', // 交互轴
intersect: false // 不要求精确相交
},
// 增强悬停效果
hover: {
mode: 'nearest',
intersect: true,
animationDuration: 300 // 悬停动画持续时间
}
}
});
// 初始化解析类型统计饼图
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: 3, // 增加边框宽度,增强区块分隔
borderColor: '#fff', // 白色边框,使各个扇区更清晰
hoverOffset: 15, // 增加悬停偏移效果,增强交互体验
hoverBorderWidth: 4, // 悬停时增加边框宽度
hoverBackgroundColor: queryTypeColors.map(color => {
// 生成悬停时的深色效果
const hex = color.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`;
}),
borderRadius: 10, // 添加圆角效果,增强现代感
borderSkipped: false // 显示所有边框
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// 简化动画,提高性能
animation: {
duration: 300, // 缩短动画时间
easing: 'easeOutQuart', // 简化缓动函数
animateRotate: true, // 仅保留旋转动画
animateScale: false // 禁用缩放动画
},
plugins: {
legend: {
position: 'bottom',
align: 'center',
labels: {
boxWidth: 12, // 调整图例框的宽度
font: {
size: 11, // 调整字体大小
family: 'Inter, system-ui, sans-serif', // 使用现代字体
weight: 500 // 字体粗细
},
padding: 12, // 调整内边距
lineHeight: 1.5, // 调整行高
usePointStyle: true, // 使用点样式代替方形图例,节省空间
pointStyle: 'circle', // 使用圆形点样式
color: '#4B5563', // 图例文本颜色
generateLabels: function(chart) {
// 自定义图例生成,添加更多样式控制
const data = chart.data;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const dataset = data.datasets[0];
return {
text: label,
fillStyle: dataset.backgroundColor[i],
strokeStyle: dataset.borderColor,
lineWidth: dataset.borderWidth,
pointStyle: 'circle',
hidden: !chart.isDatasetVisible(0),
index: i
};
});
}
return [];
},
// 启用图例点击交互
onClick: function(event, legendItem, legend) {
// 切换对应数据的显示
const index = legendItem.index;
const ci = legend.chart;
ci.toggleDataVisibility(index);
ci.update();
},
// 图例悬停样式
fontColor: '#4B5563',
usePointStyle: true,
pointStyle: 'circle'
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(17, 24, 39, 0.9)', // 深背景,增强可读性
padding: 12, // 增加内边距
titleFont: {
size: 13, // 标题字体大小
family: 'Inter, system-ui, sans-serif',
weight: 600
},
bodyFont: {
size: 12, // 正文字体大小
family: 'Inter, system-ui, sans-serif'
},
bodySpacing: 6, // 正文行间距
displayColors: true, // 显示颜色指示器
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
},
afterLabel: function(context) {
// 可以添加额外的信息
}
},
cornerRadius: 8, // 圆角
boxPadding: 6, // 盒子内边距
borderColor: 'rgba(255, 255, 255, 0.2)', // 边框颜色
borderWidth: 1 // 边框宽度
},
title: {
display: false // 不显示标题,由HTML标题代替
}
},
cutout: '70%', // 调整中心空白区域比例,增强现代感
// 增强元素配置
elements: {
arc: {
borderAlign: 'center',
tension: 0.1, // 添加轻微的张力,使圆弧更平滑
borderWidth: 3 // 统一边框宽度
}
},
layout: {
padding: {
top: 20, // 增加顶部内边距
right: 20,
bottom: 30, // 增加底部内边距,为图例预留更多空间
left: 20
}
},
// 添加交互配置
interaction: {
mode: 'nearest', // 交互模式
axis: 'x', // 交互轴
intersect: false // 不要求精确相交
},
// 增强悬停效果
hover: {
mode: 'nearest',
intersect: true,
animationDuration: 300 // 悬停动画持续时间
}
}
});
} else {
console.warn('未找到解析类型统计图表元素');
}
// 初始化DNS请求统计图表
drawDNSRequestsChart();
// 初始化展开按钮功能
initExpandButton();
}
// 初始化展开按钮事件
function initExpandButton() {
const expandBtn = document.getElementById('expand-chart-btn');
const chartModal = document.getElementById('chart-modal');
const closeModalBtn = document.getElementById('close-modal-btn'); // 修复ID匹配
// 添加调试日志
if (expandBtn && chartModal && closeModalBtn) {
// 展开按钮点击事件
expandBtn.addEventListener('click', () => {
// 显示浮窗
chartModal.classList.remove('hidden');
// 初始化或更新详细图表
drawDetailedDNSRequestsChart();
// 初始化浮窗中的时间范围切换
initDetailedTimeRangeToggle();
// 延迟更新图表大小,确保容器大小已计算
setTimeout(() => {
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.resize();
}
}, 100);
});
// 关闭按钮点击事件
closeModalBtn.addEventListener('click', () => {
chartModal.classList.add('hidden');
});
// 点击遮罩层关闭浮窗(使用chartModal作为遮罩层)
chartModal.addEventListener('click', (e) => {
// 检查点击目标是否是遮罩层本身(即最外层div)
if (e.target === chartModal) {
chartModal.classList.add('hidden');
}
});
// ESC键关闭浮窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !chartModal.classList.contains('hidden')) {
chartModal.classList.add('hidden');
}
});
} else {
console.error('无法找到必要的DOM元素');
}
}
// 初始化详细图表的时间范围切换
function initDetailedTimeRangeToggle() {
// 只选择图表模态框内的时间范围按钮,避免与主视图冲突
const chartModal = document.getElementById('chart-modal');
const detailedTimeRangeButtons = chartModal ? chartModal.querySelectorAll('.time-range-btn') : [];
// 初始化详细图表的默认状态,与主图表保持一致
detailedCurrentTimeRange = currentTimeRange;
// 定义按钮样式配置,与主视图保持一致
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']
}
];
// 设置初始按钮状态
detailedTimeRangeButtons.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', 'mixed-view-active');
// 设置非选中状态样式
button.classList.add('transition-colors', 'duration-200');
button.classList.add(...styleConfig.normal);
button.classList.add(...styleConfig.hover);
});
detailedTimeRangeButtons.forEach((button, index) => {
button.addEventListener('click', () => {
const styleConfig = buttonStyles[index % buttonStyles.length];
// 重置所有按钮为非选中状态
detailedTimeRangeButtons.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', 'mixed-view-active');
btn.classList.remove(...btnStyle.active);
btn.classList.remove(...btnStyle.activeHover);
// 添加非选中状态类
btn.classList.add(...btnStyle.normal);
btn.classList.add(...btnStyle.hover);
});
// 设置当前按钮为激活状态
button.classList.remove(...styleConfig.normal);
button.classList.remove(...styleConfig.hover);
button.classList.add('active');
button.classList.add(...styleConfig.active);
button.classList.add(...styleConfig.activeHover);
// 获取并更新当前时间范围
let rangeValue;
if (button.dataset.range) {
rangeValue = button.dataset.range;
} else {
const btnText = button.textContent.trim();
if (btnText.includes('24')) {
rangeValue = '24h';
} else if (btnText.includes('7')) {
rangeValue = '7d';
} else if (btnText.includes('30')) {
rangeValue = '30d';
} else {
rangeValue = btnText.replace(/[^0-9a-zA-Z]/g, '');
}
}
detailedCurrentTimeRange = rangeValue;
// 重新绘制详细图表
drawDetailedDNSRequestsChart();
});
});
}
// 绘制详细的DNS请求趋势图表
function drawDetailedDNSRequestsChart() {
const ctx = document.getElementById('detailed-dns-requests-chart');
if (!ctx) {
console.error('未找到详细DNS请求图表元素');
return;
}
const chartContext = ctx.getContext('2d');
// 根据详细视图时间范围选择API函数和对应的颜色
let apiFunction;
let chartColor;
let chartFillColor;
switch (detailedCurrentTimeRange) {
case '7d':
apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#22c55e'; // 绿色,与7天按钮颜色一致
chartFillColor = 'rgba(34, 197, 94, 0.1)';
break;
case '30d':
apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#a855f7'; // 紫色,与30天按钮颜色一致
chartFillColor = 'rgba(168, 85, 247, 0.1)';
break;
default: // 24h
apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#3b82f6'; // 蓝色,与24小时按钮颜色一致
chartFillColor = 'rgba(59, 130, 246, 0.1)';
}
// 获取统计数据
apiFunction().then(data => {
// 创建或更新图表
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = data.labels;
detailedDnsRequestsChart.data.datasets = [{
label: 'DNS请求数量',
data: data.data,
borderColor: chartColor,
backgroundColor: chartFillColor,
tension: 0.4,
fill: true
}];
detailedDnsRequestsChart.options.plugins.legend.display = false;
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'DNS请求数量',
data: data.data,
borderColor: chartColor,
backgroundColor: chartFillColor,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'DNS请求趋势',
font: {
size: 14
}
},
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 count = detailedCurrentTimeRange === '24h' ? 24 : (detailedCurrentTimeRange === '7d' ? 7 : 30);
const emptyData = {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = emptyData.labels;
detailedDnsRequestsChart.data.datasets[0].data = emptyData.data;
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
}
});
}
// 绘制DNS请求统计图表
function drawDNSRequestsChart() {
const ctx = document.getElementById('dns-requests-chart');
if (!ctx) {
console.error('未找到DNS请求图表元素');
return;
}
const chartContext = ctx.getContext('2d');
// 普通视图
// 根据当前时间范围选择API函数和对应的颜色
let apiFunction;
let chartColor;
let chartFillColor;
switch (currentTimeRange) {
case '7d':
apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#22c55e'; // 绿色,与7天按钮颜色一致
chartFillColor = 'rgba(34, 197, 94, 0.1)';
break;
case '30d':
apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#a855f7'; // 紫色,与30天按钮颜色一致
chartFillColor = 'rgba(168, 85, 247, 0.1)';
break;
default: // 24h
apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
chartColor = '#3b82f6'; // 蓝色,与24小时按钮颜色一致
chartFillColor = 'rgba(59, 130, 246, 0.1)';
}
// 获取统计数据
apiFunction().then(data => {
// 创建或更新图表
if (dnsRequestsChart) {
dnsRequestsChart.data.labels = data.labels;
dnsRequestsChart.data.datasets = [{
label: 'DNS请求数量',
data: data.data,
borderColor: chartColor,
backgroundColor: chartFillColor,
tension: 0.4,
fill: true
}];
dnsRequestsChart.options.plugins.legend.display = false;
// 使用平滑过渡动画更新图表
dnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
dnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'DNS请求数量',
data: data.data,
borderColor: chartColor,
backgroundColor: chartFillColor,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
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 count = currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30);
const emptyData = {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
if (dnsRequestsChart) {
dnsRequestsChart.data.labels = emptyData.labels;
dnsRequestsChart.data.datasets[0].data = emptyData.data;
dnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
}
});
}
// 更新图表数据
function updateCharts(stats, queryTypeStats) {
// 空值检查
if (!stats) {
console.error('更新图表失败: 未提供统计数据');
return;
}
// 更新比例图表
if (ratioChart) {
let allowed = 0, blocked = 0, error = 0;
// 尝试从stats数据中提取
if (stats.dns) {
allowed = parseInt(stats.dns.Allowed) || allowed;
blocked = parseInt(stats.dns.Blocked) || blocked;
error = parseInt(stats.dns.Errors) || error;
} else if (stats.totalQueries !== undefined) {
allowed = parseInt(stats.allowedQueries) || allowed;
blocked = parseInt(stats.blockedQueries) || blocked;
error = parseInt(stats.errorQueries) || error;
}
ratioChart.data.datasets[0].data = [allowed, blocked, error];
// 使用自定义动画配置更新图表,确保平滑过渡
ratioChart.update({
duration: 500,
easing: 'easeInOutQuart'
});
}
// 更新解析类型统计饼图
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({
duration: 500,
easing: 'easeInOutQuart'
});
}
}
// 更新统计卡片折线图
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 (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({
duration: 300, // 增加动画持续时间
easing: 'easeInOutQuart', // 使用平滑的缓动函数
transition: {
duration: 300,
easing: 'easeInOutQuart'
}
});
}
// 从统计数据中获取规则数
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() {
// 清理已存在的图表实例
for (const key in statCardCharts) {
if (statCardCharts.hasOwnProperty(key)) {
statCardCharts[key].destroy();
}
}
statCardCharts = {};
statCardHistoryData = {};
// 检查Chart.js是否加载
// 统计卡片配置信息
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条目数' }
];
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,
// 添加动画配置,确保平滑过渡
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
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 (isNaN(num) || num === '---') {
return num;
}
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return fullNumStr;
}
// 更新运行状态
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 = `
${message}
`;
// 添加到页面
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');
// 页面切换逻辑
function switchPage(targetId, menuItem) {
// 隐藏所有内容
document.querySelectorAll('[id$="-content"]').forEach(content => {
content.classList.add('hidden');
});
// 显示目标内容
document.getElementById(`${targetId}-content`).classList.remove('hidden');
// 更新页面标题
document.getElementById('page-title').textContent = menuItem.querySelector('span').textContent;
// 更新活动菜单项
menuItems.forEach(item => {
item.classList.remove('sidebar-item-active');
});
menuItem.classList.add('sidebar-item-active');
// 侧边栏切换(移动端)
if (window.innerWidth < 1024) {
toggleSidebar();
}
}
menuItems.forEach(item => {
item.addEventListener('click', (e) => {
// 允许默认的hash变化
// 页面切换会由hashchange事件处理
});
});
}
// 处理hash变化 - 全局函数,确保在页面加载时就能被调用
function handleHashChange() {
let hash = window.location.hash;
// 如果没有hash,默认设置为#dashboard
if (!hash) {
hash = '#dashboard';
window.location.hash = hash;
return;
}
const targetId = hash.substring(1);
const menuItems = document.querySelectorAll('nav a');
// 首先检查是否存在对应的内容元素
const contentElement = document.getElementById(`${targetId}-content`);
// 查找对应的菜单项
let targetMenuItem = null;
menuItems.forEach(item => {
if (item.getAttribute('href') === hash) {
targetMenuItem = item;
}
});
// 如果找到了对应的内容元素,直接显示
if (contentElement) {
// 隐藏所有内容
document.querySelectorAll('[id$="-content"]').forEach(content => {
content.classList.add('hidden');
});
// 显示目标内容
contentElement.classList.remove('hidden');
// 更新活动菜单项和页面标题
menuItems.forEach(item => {
item.classList.remove('sidebar-item-active');
});
if (targetMenuItem) {
targetMenuItem.classList.add('sidebar-item-active');
// 更新页面标题
const pageTitle = targetMenuItem.querySelector('span').textContent;
document.getElementById('page-title').textContent = pageTitle;
} else {
// 如果没有找到对应的菜单项,尝试根据hash更新页面标题
const titleElement = document.getElementById(`${targetId}-title`);
if (titleElement) {
document.getElementById('page-title').textContent = titleElement.textContent;
}
}
} else if (targetMenuItem) {
// 隐藏所有内容
document.querySelectorAll('[id$="-content"]').forEach(content => {
content.classList.add('hidden');
});
// 显示目标内容
document.getElementById(`${targetId}-content`).classList.remove('hidden');
// 更新页面标题
document.getElementById('page-title').textContent = targetMenuItem.querySelector('span').textContent;
// 更新活动菜单项
menuItems.forEach(item => {
item.classList.remove('sidebar-item-active');
});
targetMenuItem.classList.add('sidebar-item-active');
// 侧边栏切换(移动端)
if (window.innerWidth < 1024) {
toggleSidebar();
}
} else {
// 如果既没有找到内容元素也没有找到菜单项,默认显示dashboard
window.location.hash = '#dashboard';
}
}
// 初始化hash路由
function initHashRoute() {
handleHashChange();
}
// 监听hash变化事件 - 全局事件监听器
window.addEventListener('hashchange', handleHashChange);
// 初始化hash路由 - 确保在页面加载时就能被调用
initHashRoute();
// 响应式处理
function handleResponsive() {
// 窗口大小改变时处理
window.addEventListener('resize', () => {
// 更新所有图表大小
if (dnsRequestsChart) {
dnsRequestsChart.update();
}
if (ratioChart) {
ratioChart.update();
}
if (queryTypeChart) {
queryTypeChart.update();
}
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.update();
}
// 更新统计卡片图表
Object.values(statCardCharts).forEach(chart => {
if (chart) {
chart.update();
}
});
});
// 添加触摸事件支持,用于移动端
let touchStartX = 0;
let touchEndX = 0;
document.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, false);
document.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, false);
function handleSwipe() {
// 从左向右滑动,打开侧边栏
if (touchEndX - touchStartX > 50 && window.innerWidth < 1024) {
sidebar.classList.remove('-translate-x-full');
}
// 从右向左滑动,关闭侧边栏
if (touchStartX - touchEndX > 50 && window.innerWidth < 1024) {
sidebar.classList.add('-translate-x-full');
}
}
}
// 添加重试功能
function addRetryEventListeners() {
// TOP客户端重试按钮
const retryTopClientsBtn = document.getElementById('retry-top-clients');
if (retryTopClientsBtn) {
retryTopClientsBtn.addEventListener('click', async () => {
const clientsData = await api.getTopClients();
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
// 使用真实数据
updateTopClientsTable(clientsData);
hideLoading('top-clients');
const errorElement = document.getElementById('top-clients-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 重试失败,保持原有状态
console.warn('重试获取TOP客户端数据失败');
}
});
}
// TOP域名重试按钮
const retryTopDomainsBtn = document.getElementById('retry-top-domains');
if (retryTopDomainsBtn) {
retryTopDomainsBtn.addEventListener('click', async () => {
const domainsData = await api.getTopDomains();
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
// 使用真实数据
updateTopDomainsTable(domainsData);
hideLoading('top-domains');
const errorElement = document.getElementById('top-domains-error');
if (errorElement) errorElement.classList.add('hidden');
} else {
// 重试失败,保持原有状态
console.warn('重试获取TOP域名数据失败');
}
});
}
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', () => {
// 初始化页面切换
handlePageSwitch();
// 初始化响应式
handleResponsive();
// 初始化仪表盘
initDashboard();
// 添加重试事件监听器
addRetryEventListeners();
// 页面卸载时清理定时器
window.addEventListener('beforeunload', () => {
if (intervalId) {
clearInterval(intervalId);
}
});
});// 重写loadDashboardData函数,修复语法错误
async function loadDashboardData() {
try {
// 并行获取所有数据,提高加载效率
const [stats, queryTypeStatsResult, topBlockedDomainsResult, recentBlockedDomainsResult, topClientsResult] = await Promise.all([
// 获取基本统计数据
api.getStats().catch(error => {
console.error('获取基本统计数据失败:', error);
return null;
}),
// 获取查询类型统计数据
api.getQueryTypeStats().catch(() => null),
// 获取TOP被屏蔽域名
api.getTopBlockedDomains().catch(() => null),
// 获取最近屏蔽域名
api.getRecentBlockedDomains().catch(() => null),
// 获取TOP客户端
api.getTopClients().catch(() => null)
]);
// 确保stats是有效的对象
if (!stats || typeof stats !== 'object') {
console.error('无效的统计数据:', stats);
return;
}
// 处理查询类型统计数据
let queryTypeStats = null;
if (queryTypeStatsResult) {
queryTypeStats = queryTypeStatsResult;
} else if (stats.dns && stats.dns.QueryTypes) {
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
type,
count
}));
}
// 处理TOP被屏蔽域名
let topBlockedDomains = [];
if (topBlockedDomainsResult && Array.isArray(topBlockedDomainsResult)) {
topBlockedDomains = topBlockedDomainsResult;
} else {
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 = [];
if (recentBlockedDomainsResult && Array.isArray(recentBlockedDomainsResult)) {
recentBlockedDomains = recentBlockedDomainsResult;
} else {
recentBlockedDomains = [
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() },
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }
];
}
// 显示错误的辅助函数
function showError(elementId) {
const loadingElement = document.getElementById(elementId + '-loading');
const errorElement = document.getElementById(elementId + '-error');
if (loadingElement) loadingElement.classList.add('hidden');
if (errorElement) errorElement.classList.remove('hidden');
}
// 处理TOP客户端
let topClients = [];
if (topClientsResult && !topClientsResult.error && Array.isArray(topClientsResult) && topClientsResult.length > 0) {
topClients = topClientsResult;
} else {
console.warn('获取TOP客户端失败或数据无效,使用模拟数据');
topClients = [
{ ip: '192.168.1.100', count: 120 },
{ ip: '192.168.1.101', count: 95 },
{ ip: '192.168.1.102', count: 80 },
{ ip: '192.168.1.103', count: 65 },
{ ip: '192.168.1.104', count: 50 }
];
showError('top-clients');
}
// 处理TOP域名 - 注意:这个API调用不在Promise.all中,所以需要try-catch
let topDomains = [];
try {
const domainsData = await api.getTopDomains();
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
topDomains = domainsData;
} else {
console.warn('获取TOP域名失败或数据无效,使用模拟数据');
topDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
showError('top-domains');
}
} catch (error) {
console.warn('获取TOP域名失败:', error);
topDomains = [
{ domain: 'example.com', count: 50 },
{ domain: 'google.com', count: 45 },
{ domain: 'facebook.com', count: 40 },
{ domain: 'twitter.com', count: 35 },
{ domain: 'youtube.com', count: 30 }
];
showError('top-domains');
}
// 存储统计卡片历史数据,用于计算趋势
const getStatValue = (statPath, defaultValue = 0) => {
const path = statPath.split('.');
let value = stats;
for (const key of path) {
if (!value || typeof value !== 'object') {
return defaultValue;
}
value = value[key];
}
return value !== undefined ? value : defaultValue;
};
// 更新主页面的统计卡片数据
updateStatsCards(stats);
// 更新TOP客户端表格
updateTopClientsTable(topClients);
// 更新TOP域名表格
updateTopDomainsTable(topDomains);
// 更新TOP被屏蔽域名表格
updateTopBlockedTable(topBlockedDomains);
// 更新最近屏蔽域名表格
updateRecentBlockedTable(recentBlockedDomains);
// 更新图表
updateCharts(stats, queryTypeStats);
// 初始化或更新查询类型统计饼图
if (queryTypeStats) {
drawQueryTypeChart(queryTypeStats);
}
// 更新查询类型统计信息
if (document.getElementById('top-query-type')) {
const topQueryTypeElement = document.getElementById('top-query-type');
const topQueryTypeCountElement = document.getElementById('top-query-type-count');
// 从stats中获取查询类型统计数据
if (stats.dns && stats.dns.QueryTypes) {
const queryTypes = stats.dns.QueryTypes;
// 找出数量最多的查询类型
let maxCount = 0;
let topType = 'A';
for (const [type, count] of Object.entries(queryTypes)) {
const numCount = Number(count) || 0;
if (numCount > maxCount) {
maxCount = numCount;
topType = type;
}
}
// 更新DOM
if (topQueryTypeElement) {
topQueryTypeElement.textContent = topType;
}
if (topQueryTypeCountElement) {
topQueryTypeCountElement.textContent = formatNumber(maxCount);
}
// 保存到历史数据,用于计算趋势
window.dashboardHistoryData.prevTopQueryTypeCount = maxCount;
}
}
// 更新活跃IP信息
if (document.getElementById('active-ips')) {
const activeIPsElement = document.getElementById('active-ips');
// 从stats中获取活跃IP数
let activeIPs = getStatValue('dns.ActiveIPs', 0);
// 更新DOM
if (activeIPsElement) {
activeIPsElement.textContent = formatNumber(activeIPs);
}
// 保存到历史数据,用于计算趋势
window.dashboardHistoryData.prevActiveIPs = activeIPs;
}
// 更新平均响应时间
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();
// 确保TOP域名数据被正确加载
updateTopData();
} catch (error) {
console.error('加载仪表盘数据失败:', error);
// 静默失败,不显示通知以免打扰用户
}
}