Files
dns-server/static/js/dashboard.js
2026-01-25 16:13:52 +08:00

3254 lines
124 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 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 ? `
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
<div class="font-semibold mb-2">已知跟踪器</div>
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
</div>
` : '';
// 计算百分比
const percentage = totalCount > 0 && typeof domain.count === 'number'
? ((domain.count / totalCount) * 100).toFixed(2)
: '0.00';
html += `
<div class="p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex items-center justify-between mb-2">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
<div class="flex items-center">
<span class="font-medium truncate">${domain.name}</span>
${isTracker ? `
<div class="tracker-icon-container relative ml-2">
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
${trackerTooltip}
</div>
` : ''}
</div>
</div>
</div>
<div class="ml-4 flex items-center space-x-2">
<span class="flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
<span class="text-xs text-gray-500">${percentage}%</span>
</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div class="bg-danger h-2.5 rounded-full" style="width: ${percentage}%"></div>
</div>
</div>
`;
}
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 += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-warning">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">${domain.name}</div>
<div class="text-sm text-gray-500 mt-1">${time}</div>
</div>
<span class="ml-4 flex-shrink-0 text-sm text-gray-500">${domain.type}</span>
</div>
`;
}
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 += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
<div class="flex flex-col">
<span class="font-medium truncate">${client.ip}</span>
<span class="text-xs text-gray-500">${location}</span>
</div>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
</div>
`;
}
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 ? `
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
<div class="font-semibold mb-2">已知跟踪器</div>
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
</div>
` : '';
// 计算百分比
const percentage = totalCount > 0 && typeof domain.count === 'number'
? ((domain.count / totalCount) * 100).toFixed(2)
: '0.00';
html += `
<div class="p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
<div class="flex items-center justify-between mb-2">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
<div class="flex items-center">
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
${isTracker ? `
<div class="tracker-icon-container relative ml-2">
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
${trackerTooltip}
</div>
` : ''}
</div>
</div>
</div>
<div class="ml-4 flex items-center space-x-2">
<span class="flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
<span class="text-xs text-gray-500">${percentage}%</span>
</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div class="bg-success h-2.5 rounded-full" style="width: ${percentage}%"></div>
</div>
</div>
`;
}
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 = `
<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');
// 页面切换逻辑
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);
// 静默失败,不显示通知以免打扰用户
}
}