Files
dns-server/static/js/dashboard.js
2025-11-28 02:15:42 +08:00

2868 lines
111 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 wsConnection = null;
let wsReconnectTimer = null;
// 存储统计卡片图表实例
let statCardCharts = {};
// 存储统计卡片历史数据
let statCardHistoryData = {};
// 引入颜色配置文件
const COLOR_CONFIG = window.COLOR_CONFIG || {};
// 初始化仪表盘
async function initDashboard() {
try {
console.log('页面打开时强制刷新数据...');
// 优先加载初始数据,确保页面显示最新信息
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`;
console.log('正在连接WebSocket:', wsUrl);
// 创建WebSocket连接
wsConnection = new WebSocket(wsUrl);
// 连接打开事件
wsConnection.onopen = function() {
console.log('WebSocket连接已建立');
showNotification('数据更新成功', 'success');
// 清除重连计时器
if (wsReconnectTimer) {
clearTimeout(wsReconnectTimer);
wsReconnectTimer = null;
}
};
// 接收消息事件
wsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'initial_data' || data.type === 'stats_update') {
console.log('收到实时数据更新');
processRealTimeData(data.data);
}
} catch (error) {
console.error('处理WebSocket消息失败:', error);
}
};
// 连接关闭事件
wsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭代码:', event.code);
wsConnection = null;
// 设置重连
setupReconnect();
};
// 连接错误事件
wsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
// 如果WebSocket连接失败回退到定时刷新
fallbackToIntervalRefresh();
}
}
// 设置重连逻辑
function setupReconnect() {
if (wsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
wsReconnectTimer = setTimeout(() => {
connectWebSocket();
}, reconnectDelay);
}
// 处理实时数据更新
function processRealTimeData(stats) {
try {
// 更新统计卡片 - 这会更新所有统计卡片包括CPU使用率卡片
updateStatsCards(stats);
// 获取查询类型统计数据
let queryTypeStats = null;
if (stats.dns && stats.dns.QueryTypes) {
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
type,
count
}));
}
// 更新图表数据
updateCharts(stats, queryTypeStats);
// 尝试从stats中获取总查询数等信息
if (stats.dns) {
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
blockedQueries = stats.dns.Blocked;
errorQueries = stats.dns.Errors || 0;
allowedQueries = stats.dns.Allowed;
} else {
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
errorQueries = stats.errorQueries || 0;
allowedQueries = stats.allowedQueries || 0;
}
// 更新新卡片数据
if (document.getElementById('avg-response-time')) {
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
// 计算响应时间趋势
let responsePercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
// 存储当前值用于下次计算趋势
const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
// 计算变化百分比
if (prevResponseTime > 0) {
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色
if (changePercent > 0) {
trendIcon = '↓';
trendClass = 'text-danger';
} else if (changePercent < 0) {
trendIcon = '↑';
trendClass = 'text-success';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
}
document.getElementById('avg-response-time').textContent = responseTime;
const responseTimePercentElem = document.getElementById('response-time-percent');
if (responseTimePercentElem) {
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
}
}
if (document.getElementById('top-query-type')) {
const queryType = stats.topQueryType || '---';
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
queryPercentElem.textContent = '• ---';
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
}
document.getElementById('top-query-type').textContent = queryType;
}
if (document.getElementById('active-ips')) {
const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---';
// 计算活跃IP趋势
let ipsPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.activeIPs !== undefined) {
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs;
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-primary';
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-secondary';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
}
document.getElementById('active-ips').textContent = activeIPs;
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
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 {
// 获取最新的TOP客户端数据
const clientsData = await api.getTopClients();
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: '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 }
];
updateTopClientsTable(mockClients);
}
}
// 获取最新的TOP域名数据
const domainsData = await api.getTopDomains();
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);
}
}
} catch (error) {
console.error('更新TOP数据失败:', error);
// 出错时不做处理,保持原有数据
}
}
// 回退到定时刷新
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 (wsConnection) {
wsConnection.close();
wsConnection = null;
}
// 清除重连计时器
if (wsReconnectTimer) {
clearTimeout(wsReconnectTimer);
wsReconnectTimer = null;
}
// 清除定时刷新
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
// 加载仪表盘数据
async function loadDashboardData() {
console.log('开始加载仪表盘数据');
try {
// 获取基本统计数据
const stats = await api.getStats();
console.log('统计数据:', stats);
// 获取查询类型统计数据
let queryTypeStats = null;
try {
queryTypeStats = await api.getQueryTypeStats();
console.log('查询类型统计数据:', queryTypeStats);
} catch (error) {
console.warn('获取查询类型统计失败:', error);
// 如果API调用失败尝试从stats中提取查询类型数据
if (stats && stats.dns && stats.dns.QueryTypes) {
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
type,
count
}));
console.log('从stats中提取的查询类型统计:', queryTypeStats);
}
}
// 尝试获取TOP被屏蔽域名如果失败则提供模拟数据
let topBlockedDomains = [];
try {
topBlockedDomains = await api.getTopBlockedDomains();
console.log('TOP被屏蔽域名:', topBlockedDomains);
// 确保返回的数据是数组
if (!Array.isArray(topBlockedDomains)) {
console.warn('TOP被屏蔽域名不是预期的数组格式使用模拟数据');
topBlockedDomains = [];
}
} catch (error) {
console.warn('获取TOP被屏蔽域名失败:', error);
// 提供模拟数据
topBlockedDomains = [
{ domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() },
{ domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() },
{ domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() }
];
}
// 尝试获取最近屏蔽域名,如果失败则提供模拟数据
let recentBlockedDomains = [];
try {
recentBlockedDomains = await api.getRecentBlockedDomains();
console.log('最近屏蔽域名:', recentBlockedDomains);
// 确保返回的数据是数组
if (!Array.isArray(recentBlockedDomains)) {
console.warn('最近屏蔽域名不是预期的数组格式,使用模拟数据');
recentBlockedDomains = [];
}
} catch (error) {
console.warn('获取最近屏蔽域名失败:', error);
// 提供模拟数据
recentBlockedDomains = [
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() },
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }
];
}
// 实现数据加载状态管理
function showLoading(elementId) {
const loadingElement = document.getElementById(elementId + '-loading');
const errorElement = document.getElementById(elementId + '-error');
if (loadingElement) loadingElement.classList.remove('hidden');
if (errorElement) errorElement.classList.add('hidden');
}
function hideLoading(elementId) {
const loadingElement = document.getElementById(elementId + '-loading');
if (loadingElement) loadingElement.classList.add('hidden');
}
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 = [];
showLoading('top-clients');
try {
const clientsData = await api.getTopClients();
console.log('TOP客户端:', clientsData);
// 检查数据是否有效
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
// 使用真实数据
topClients = clientsData;
} else if (clientsData && clientsData.error) {
// API返回错误
console.warn('获取TOP客户端失败:', clientsData.error);
// 使用模拟数据
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');
} 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');
}
} catch (error) {
console.warn('获取TOP客户端失败:', error);
// 使用模拟数据
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');
} finally {
hideLoading('top-clients');
}
// 尝试获取TOP域名优先使用真实数据失败时使用模拟数据
let topDomains = [];
showLoading('top-domains');
try {
const domainsData = await api.getTopDomains();
console.log('TOP域名:', domainsData);
// 检查数据是否有效
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
// 使用真实数据
topDomains = domainsData;
} else if (domainsData && domainsData.error) {
// API返回错误
console.warn('获取TOP域名失败:', domainsData.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');
} 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');
} finally {
hideLoading('top-domains');
}
// 更新统计卡片
updateStatsCards(stats);
// 更新图表数据,传入查询类型统计
updateCharts(stats, queryTypeStats);
// 更新表格数据
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
updateTopClientsTable(topClients);
updateTopDomainsTable(topDomains);
// 尝试从stats中获取总查询数等信息
if (stats.dns) {
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
blockedQueries = stats.dns.Blocked;
errorQueries = stats.dns.Errors || 0;
allowedQueries = stats.dns.Allowed;
} else {
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
errorQueries = stats.errorQueries || 0;
allowedQueries = stats.allowedQueries || 0;
}
// 全局历史数据对象,用于存储趋势计算所需的上一次值
window.dashboardHistoryData = window.dashboardHistoryData || {};
// 更新新卡片数据 - 使用API返回的真实数据
if (document.getElementById('avg-response-time')) {
// 保留两位小数并添加单位
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
// 计算响应时间趋势
let responsePercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
// 存储当前值用于下次计算趋势
const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
// 计算变化百分比
if (prevResponseTime > 0) {
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
if (changePercent > 0) {
trendIcon = '↓';
trendClass = 'text-danger';
} else if (changePercent < 0) {
trendIcon = '↑';
trendClass = 'text-success';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
}
document.getElementById('avg-response-time').textContent = responseTime;
const responseTimePercentElem = document.getElementById('response-time-percent');
if (responseTimePercentElem) {
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
}
}
if (document.getElementById('top-query-type')) {
// 直接使用API返回的查询类型
const queryType = stats.topQueryType || '---';
// 设置默认趋势显示
const queryPercentElem = document.getElementById('query-type-percentage');
if (queryPercentElem) {
queryPercentElem.textContent = '• ---';
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
}
document.getElementById('top-query-type').textContent = queryType;
}
if (document.getElementById('active-ips')) {
// 直接使用API返回的活跃IP数
const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---';
// 计算活跃IP趋势
let ipsPercent = '---';
let trendClass = 'text-gray-400';
let trendIcon = '---';
if (stats.activeIPs !== undefined && stats.activeIPs !== null) {
// 存储当前值用于下次计算趋势
const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs;
window.dashboardHistoryData.prevActiveIPs = stats.activeIPs;
// 计算变化百分比
if (prevActiveIPs > 0) {
const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100;
ipsPercent = Math.abs(changePercent).toFixed(1) + '%';
// 设置趋势图标和颜色
if (changePercent > 0) {
trendIcon = '↑';
trendClass = 'text-success';
} else if (changePercent < 0) {
trendIcon = '↓';
trendClass = 'text-danger';
} else {
trendIcon = '•';
trendClass = 'text-gray-500';
}
}
}
document.getElementById('active-ips').textContent = activeIPs;
const activeIpsPercentElem = document.getElementById('active-ips-percent');
if (activeIpsPercentElem) {
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`;
}
}
// 更新表格
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
updateTopClientsTable(topClients);
updateTopDomainsTable(topDomains);
// 更新图表
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
// 确保响应时间图表使用API实时数据
if (document.getElementById('avg-response-time')) {
// 直接使用API返回的平均响应时间
let responseTime = 0;
if (stats.dns && stats.dns.AvgResponseTime) {
responseTime = stats.dns.AvgResponseTime;
} else if (stats.avgResponseTime !== undefined) {
responseTime = stats.avgResponseTime;
} else if (stats.responseTime) {
responseTime = stats.responseTime;
}
if (responseTime > 0 && statCardCharts['response-time-chart']) {
// 限制小数位数为两位并更新图表
updateChartData('response-time-chart', parseFloat(responseTime).toFixed(2));
}
}
// 更新运行状态
updateUptime();
} catch (error) {
console.error('加载仪表盘数据失败:', error);
// 静默失败,不显示通知以免打扰用户
}
}
// 更新统计卡片
function updateStatsCards(stats) {
console.log('更新统计卡片,收到数据:', stats);
// 适配不同的数据结构
let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0;
let topQueryType = 'A', queryTypePercentage = 0;
let activeIPs = 0, activeIPsPercentage = 0;
// 检查数据结构,兼容可能的不同格式
if (stats) {
// 优先使用顶层字段
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
allowedQueries = stats.allowedQueries || 0;
errorQueries = stats.errorQueries || 0;
topQueryType = stats.topQueryType || 'A';
queryTypePercentage = stats.queryTypePercentage || 0;
activeIPs = stats.activeIPs || 0;
activeIPsPercentage = stats.activeIPsPercentage || 0;
// 如果dns对象存在优先使用其中的数据
if (stats.dns) {
totalQueries = stats.dns.Queries || totalQueries;
blockedQueries = stats.dns.Blocked || blockedQueries;
allowedQueries = stats.dns.Allowed || allowedQueries;
errorQueries = stats.dns.Errors || errorQueries;
// 计算最常用查询类型的百分比
if (stats.dns.QueryTypes && stats.dns.Queries > 0) {
const topTypeCount = stats.dns.QueryTypes[topQueryType] || 0;
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
}
// 计算活跃IP百分比基于已有的活跃IP数
if (activeIPs > 0 && stats.dns.SourceIPs) {
activeIPsPercentage = activeIPs / Object.keys(stats.dns.SourceIPs).length * 100;
}
}
} else if (Array.isArray(stats) && stats.length > 0) {
// 可能的数据结构3: 数组形式
totalQueries = stats[0].total || 0;
blockedQueries = stats[0].blocked || 0;
allowedQueries = stats[0].allowed || 0;
errorQueries = stats[0].error || 0;
topQueryType = stats[0].topQueryType || 'A';
queryTypePercentage = stats[0].queryTypePercentage || 0;
activeIPs = stats[0].activeIPs || 0;
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
}
// 为数字元素添加翻页滚动特效
function animateValue(elementId, newValue) {
const element = document.getElementById(elementId);
if (!element) return;
const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0;
const formattedNewValue = formatNumber(newValue);
// 如果值没有变化,不执行动画
if (oldValue === newValue && element.textContent === formattedNewValue) {
return;
}
// 先移除可能存在的光晕效果类
element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow');
// 保存原始样式和内容
const originalStyle = element.getAttribute('style') || '';
const originalContent = element.innerHTML;
// 配置翻页容器样式
const containerStyle = `
position: relative;
display: inline-block;
overflow: hidden;
height: ${element.offsetHeight}px;
width: ${element.offsetWidth}px;
`;
// 创建翻页容器
const flipContainer = document.createElement('div');
flipContainer.style.cssText = containerStyle;
flipContainer.className = 'number-flip-container';
// 创建旧值元素
const oldValueElement = document.createElement('div');
oldValueElement.textContent = originalContent;
oldValueElement.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 400ms ease-in-out;
transform-origin: center;
`;
// 创建新值元素
const newValueElement = document.createElement('div');
newValueElement.textContent = formattedNewValue;
newValueElement.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 400ms ease-in-out;
transform-origin: center;
transform: translateY(100%);
`;
// 复制原始元素的样式到新元素
const computedStyle = getComputedStyle(element);
[oldValueElement, newValueElement].forEach(el => {
el.style.fontSize = computedStyle.fontSize;
el.style.fontWeight = computedStyle.fontWeight;
el.style.color = computedStyle.color;
el.style.fontFamily = computedStyle.fontFamily;
el.style.textAlign = computedStyle.textAlign;
el.style.lineHeight = computedStyle.lineHeight;
});
// 替换原始元素的内容
element.textContent = '';
flipContainer.appendChild(oldValueElement);
flipContainer.appendChild(newValueElement);
element.appendChild(flipContainer);
// 启动翻页动画
setTimeout(() => {
oldValueElement.style.transform = 'translateY(-100%)';
newValueElement.style.transform = 'translateY(0)';
}, 50);
// 动画结束后,恢复原始元素
setTimeout(() => {
// 清理并设置最终值
element.innerHTML = formattedNewValue;
if (originalStyle) {
element.setAttribute('style', originalStyle);
} else {
element.removeAttribute('style');
}
// 添加当前卡片颜色的深色光晕效果
// 根据父级卡片类型确定光晕颜色
const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50');
let glowColorClass = '';
// 使用更精准的卡片颜色检测
if (card) {
// 根据卡片类名确定深色光晕颜色
if (card.classList.contains('bg-blue-50') || card.id.includes('total') || card.id.includes('response')) {
// 蓝色卡片 - 深蓝色光晕
glowColorClass = 'number-glow-dark-blue';
} else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) {
// 红色卡片 - 深红色光晕
glowColorClass = 'number-glow-dark-red';
} else if (card.classList.contains('bg-green-50') || card.id.includes('allowed') || card.id.includes('active')) {
// 绿色卡片 - 深绿色光晕
glowColorClass = 'number-glow-dark-green';
} else if (card.classList.contains('bg-yellow-50') || card.id.includes('error') || card.id.includes('cpu')) {
// 黄色卡片 - 深黄色光晕
glowColorClass = 'number-glow-dark-yellow';
}
}
// 如果确定了光晕颜色类,则添加它
if (glowColorClass) {
element.classList.add(glowColorClass);
// 2秒后移除光晕效果
setTimeout(() => {
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
}, 2000);
}
}, 450);
}
// 更新百分比元素的函数
function updatePercentage(elementId, value) {
const element = document.getElementById(elementId);
if (!element) return;
element.style.opacity = '0';
element.style.transition = 'opacity 200ms ease-out';
setTimeout(() => {
element.textContent = value;
element.style.opacity = '1';
}, 200);
}
// 平滑更新数量显示
animateValue('total-queries', totalQueries);
animateValue('blocked-queries', blockedQueries);
animateValue('allowed-queries', allowedQueries);
animateValue('error-queries', errorQueries);
animateValue('active-ips', activeIPs);
// 平滑更新文本和百分比
updatePercentage('top-query-type', topQueryType);
updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`);
updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`);
// 计算并平滑更新百分比
if (totalQueries > 0) {
updatePercentage('blocked-percent', `${Math.round((blockedQueries / totalQueries) * 100)}%`);
updatePercentage('allowed-percent', `${Math.round((allowedQueries / totalQueries) * 100)}%`);
updatePercentage('error-percent', `${Math.round((errorQueries / totalQueries) * 100)}%`);
updatePercentage('queries-percent', '100%');
} else {
updatePercentage('queries-percent', '---');
updatePercentage('blocked-percent', '---');
updatePercentage('allowed-percent', '---');
updatePercentage('error-percent', '---');
}
}
// 更新Top屏蔽域名表格
function updateTopBlockedTable(domains) {
console.log('更新Top屏蔽域名表格收到数据:', domains);
const tableBody = document.getElementById('top-blocked-table');
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.name || item.domain || item[0] || '未知',
count: item.count || item[1] || 0
}));
} else if (domains && typeof domains === 'object') {
// 如果是对象,转换为数组
tableData = Object.entries(domains).map(([domain, count]) => ({
name: domain,
count: count || 0
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ name: 'example1.com', count: 150 },
{ name: 'example2.com', count: 130 },
{ name: 'example3.com', count: 120 },
{ name: 'example4.com', count: 110 },
{ name: 'example5.com', count: 100 }
];
console.log('使用示例数据填充Top屏蔽域名表格');
}
let html = '';
for (let i = 0; i < tableData.length && i < 5; i++) {
const domain = tableData[i];
html += `
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<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>
<span class="font-medium truncate">${domain.name}</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
</div>
`;
}
tableBody.innerHTML = html;
}
// 更新最近屏蔽域名表格
function updateRecentBlockedTable(domains) {
console.log('更新最近屏蔽域名表格,收到数据:', domains);
const tableBody = document.getElementById('recent-blocked-table');
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
name: item.name || item.domain || item[0] || '未知',
timestamp: item.timestamp || item.time || Date.now(),
type: item.type || '广告'
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ name: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' },
{ name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' },
{ name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' },
{ name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' },
{ name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' }
];
console.log('使用示例数据填充最近屏蔽域名表格');
}
let html = '';
for (let i = 0; i < tableData.length && i < 5; 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;
}
// 更新TOP客户端表格
function updateTopClientsTable(clients) {
console.log('更新TOP客户端表格收到数据:', clients);
const tableBody = document.getElementById('top-clients-table');
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: '---' }
];
console.log('使用示例数据填充TOP客户端表格');
}
let html = '';
for (let i = 0; i < tableData.length && i < 5; i++) {
const client = tableData[i];
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>
<span class="font-medium truncate">${client.ip}</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
</div>
`;
}
tableBody.innerHTML = html;
}
// 更新TOP域名表格
function updateTopDomainsTable(domains) {
console.log('更新TOP域名表格收到数据:', domains);
const tableBody = document.getElementById('top-domains-table');
let tableData = [];
// 适配不同的数据结构
if (Array.isArray(domains)) {
tableData = domains.map(item => ({
domain: 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]) => ({
domain,
count: count || 0
}));
}
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ domain: '---', count: '---' },
{ domain: '---', count: '---' },
{ domain: '---', count: '---' },
{ domain: '---', count: '---' },
{ domain: '---', count: '---' }
];
console.log('使用示例数据填充TOP域名表格');
}
let html = '';
for (const domain of tableData) {
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.domain}</td>
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
</tr>
`;
}
tableBody.innerHTML = html;
}
// 当前选中的时间范围
let currentTimeRange = '24h'; // 默认为24小时
let isMixedView = true; // 是否为混合视图 - 默认显示混合视图
let lastSelectedIndex = 0; // 最后选中的按钮索引
// 详细图表专用变量
let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围
let detailedIsMixedView = false; // 详细图表是否为混合视图
// 初始化时间范围切换
function initTimeRangeToggle() {
console.log('初始化时间范围切换');
// 查找所有可能的时间范围按钮类名
const timeRangeButtons = document.querySelectorAll('.time-range-btn, .time-range-button, .timerange-btn, button[data-range]');
console.log('找到时间范围按钮数量:', timeRangeButtons.length);
if (timeRangeButtons.length === 0) {
console.warn('未找到时间范围按钮请检查HTML中的类名');
return;
}
// 定义三个按钮的不同样式配置增加activeHover属性
const buttonStyles = [
{ // 24小时按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-blue-100'],
active: ['bg-blue-500', 'text-white'],
activeHover: ['hover:bg-blue-400'] // 选中时的浅色悬停
},
{ // 7天按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-green-100'],
active: ['bg-green-500', 'text-white'],
activeHover: ['hover:bg-green-400'] // 选中时的浅色悬停
},
{ // 30天按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-purple-100'],
active: ['bg-purple-500', 'text-white'],
activeHover: ['hover:bg-purple-400'] // 选中时的浅色悬停
},
{ // 混合视图按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-gray-200'],
active: ['bg-gray-500', 'text-white'],
activeHover: ['hover:bg-gray-400'] // 选中时的浅色悬停
}
];
// 为所有按钮设置初始样式和事件
timeRangeButtons.forEach((button, index) => {
// 使用相应的样式配置
const styleConfig = buttonStyles[index % buttonStyles.length];
// 移除所有按钮的初始样式
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
// 设置非选中状态样式
button.classList.add('transition-colors', 'duration-200');
button.classList.add(...styleConfig.normal);
button.classList.add(...styleConfig.hover);
// 移除鼠标悬停提示
console.log('为按钮设置初始样式:', button.textContent.trim(), '索引:', index, '类名:', Array.from(button.classList).join(', '));
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
console.log('点击按钮:', button.textContent.trim(), '索引:', index);
// 检查是否是再次点击已选中的按钮
const isActive = button.classList.contains('active');
// 重置所有按钮为非选中状态
timeRangeButtons.forEach((btn, btnIndex) => {
const btnStyle = buttonStyles[btnIndex % buttonStyles.length];
// 移除所有可能的激活状态类
btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', 'bg-gray-500');
btn.classList.remove(...btnStyle.active);
btn.classList.remove(...btnStyle.activeHover);
// 添加非选中状态类
btn.classList.add(...btnStyle.normal);
btn.classList.add(...btnStyle.hover);
});
if (isActive && index < 3) { // 再次点击已选中的时间范围按钮
// 切换到混合视图
isMixedView = true;
currentTimeRange = 'mixed';
console.log('切换到混合视图');
// 设置当前按钮为特殊混合视图状态(保持原按钮选中但添加混合视图标记)
button.classList.remove(...styleConfig.normal);
button.classList.remove(...styleConfig.hover);
button.classList.add('active', 'mixed-view-active');
button.classList.add(...styleConfig.active);
button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停
} else {
// 普通选中模式
isMixedView = false;
lastSelectedIndex = index;
// 设置当前按钮为激活状态
button.classList.remove(...styleConfig.normal);
button.classList.remove(...styleConfig.hover);
button.classList.add('active');
button.classList.add(...styleConfig.active);
button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停
// 获取并更新当前时间范围
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;
console.log('更新时间范围为:', currentTimeRange);
}
// 重新加载数据
loadDashboardData();
// 更新DNS请求图表
drawDNSRequestsChart();
});
// 移除自定义鼠标悬停提示效果
});
// 确保默认选中第一个按钮并显示混合内容
if (timeRangeButtons.length > 0) {
const firstButton = timeRangeButtons[0];
const firstStyle = buttonStyles[0];
// 先重置所有按钮
timeRangeButtons.forEach((btn, index) => {
const btnStyle = buttonStyles[index % buttonStyles.length];
btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', '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);
});
// 然后设置第一个按钮为激活状态,并标记为混合视图
firstButton.classList.remove(...firstStyle.normal);
firstButton.classList.remove(...firstStyle.hover);
firstButton.classList.add('active', 'mixed-view-active');
firstButton.classList.add(...firstStyle.active);
firstButton.classList.add(...firstStyle.activeHover);
console.log('默认选中第一个按钮并显示混合内容:', firstButton.textContent.trim());
// 设置默认显示混合内容
isMixedView = true;
currentTimeRange = 'mixed';
}
}
// 注意这个函数已被后面的实现覆盖请使用后面的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: ['---', '---', '---'],
backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'],
borderWidth: 2, // 添加边框宽度,增强区块分隔
borderColor: '#fff', // 白色边框,使各个扇区更清晰
hoverOffset: 10, // 添加悬停偏移效果,增强交互体验
hoverBorderWidth: 3 // 悬停时增加边框宽度
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
animation: {
duration: 500, // 延长动画时间,使过渡更平滑
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12, // 减小图例框的宽度
font: {
size: 11 // 减小字体大小
},
padding: 10 // 减小内边距
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 10,
titleFont: {
size: 12
},
bodyFont: {
size: 11
},
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '65%', // 减小中心空白区域比例,增大扇形区域以更好显示线段指示
// 添加线段指示相关配置
elements: {
arc: {
// 确保圆弧绘制时有足够的精度
borderAlign: 'center',
tension: 0.1 // 添加轻微的张力,使圆弧更平滑
}
}
}
});
// 初始化解析类型统计饼图
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: 2, // 添加边框宽度,增强区块分隔
borderColor: '#fff', // 白色边框,使各个扇区更清晰
hoverOffset: 10, // 添加悬停偏移效果,增强交互体验
hoverBorderWidth: 3 // 悬停时增加边框宽度
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
animation: {
duration: 300,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12, // 减小图例框的宽度
font: {
size: 11 // 减小字体大小
},
padding: 10 // 减小内边距
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 10,
titleFont: {
size: 12
},
bodyFont: {
size: 11
},
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '65%', // 减小中心空白区域比例,增大扇形区域以更好显示线段指示
// 添加线段指示相关配置
elements: {
arc: {
// 确保圆弧绘制时有足够的精度
borderAlign: 'center',
tension: 0.1 // 添加轻微的张力,使圆弧更平滑
}
}
}
});
} 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匹配
// 添加调试日志
console.log('初始化展开按钮功能:', { expandBtn, chartModal, closeModalBtn });
if (expandBtn && chartModal && closeModalBtn) {
// 展开按钮点击事件
expandBtn.addEventListener('click', () => {
console.log('展开按钮被点击');
// 显示浮窗
chartModal.classList.remove('hidden');
// 初始化或更新详细图表
drawDetailedDNSRequestsChart();
// 初始化浮窗中的时间范围切换
initDetailedTimeRangeToggle();
// 延迟更新图表大小,确保容器大小已计算
setTimeout(() => {
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.resize();
}
}, 100);
});
// 关闭按钮点击事件
closeModalBtn.addEventListener('click', () => {
console.log('关闭按钮被点击');
chartModal.classList.add('hidden');
});
// 点击遮罩层关闭浮窗使用chartModal作为遮罩层
chartModal.addEventListener('click', (e) => {
// 检查点击目标是否是遮罩层本身即最外层div
if (e.target === chartModal) {
console.log('点击遮罩层关闭');
chartModal.classList.add('hidden');
}
});
// ESC键关闭浮窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !chartModal.classList.contains('hidden')) {
console.log('ESC键关闭浮窗');
chartModal.classList.add('hidden');
}
});
} else {
console.error('无法找到必要的DOM元素');
}
}
// 初始化详细图表的时间范围切换
function initDetailedTimeRangeToggle() {
// 只选择图表模态框内的时间范围按钮,避免与主视图冲突
const chartModal = document.getElementById('chart-modal');
const detailedTimeRangeButtons = chartModal ? chartModal.querySelectorAll('.time-range-btn') : [];
console.log('初始化详细图表时间范围切换,找到按钮数量:', detailedTimeRangeButtons.length);
// 初始化详细图表的默认状态,与主图表保持一致
detailedCurrentTimeRange = currentTimeRange;
detailedIsMixedView = isMixedView;
// 定义按钮样式配置,与主视图保持一致
const buttonStyles = [
{ // 24小时按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-blue-100'],
active: ['bg-blue-500', 'text-white'],
activeHover: ['hover:bg-blue-400']
},
{ // 7天按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-green-100'],
active: ['bg-green-500', 'text-white'],
activeHover: ['hover:bg-green-400']
},
{ // 30天按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-purple-100'],
active: ['bg-purple-500', 'text-white'],
activeHover: ['hover:bg-purple-400']
},
{ // 混合视图按钮
normal: ['bg-gray-100', 'text-gray-700'],
hover: ['hover:bg-gray-200'],
active: ['bg-gray-500', 'text-white'],
activeHover: ['hover:bg-gray-400']
}
];
// 设置初始按钮状态
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);
// 如果是第一个按钮且当前是混合视图,设置为混合视图激活状态
if (index === 0 && detailedIsMixedView) {
button.classList.remove(...styleConfig.normal);
button.classList.remove(...styleConfig.hover);
button.classList.add('active', 'mixed-view-active');
button.classList.add(...styleConfig.active);
button.classList.add(...styleConfig.activeHover);
}
});
detailedTimeRangeButtons.forEach((button, index) => {
button.addEventListener('click', () => {
const styleConfig = buttonStyles[index % buttonStyles.length];
// 检查是否是再次点击已选中的按钮
const isActive = button.classList.contains('active');
// 重置所有按钮为非选中状态
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);
});
if (isActive && index < 3) { // 再次点击已选中的时间范围按钮
// 切换到混合视图
detailedIsMixedView = true;
detailedCurrentTimeRange = 'mixed';
console.log('详细图表切换到混合视图');
// 设置当前按钮为特殊混合视图状态
button.classList.remove(...styleConfig.normal);
button.classList.remove(...styleConfig.hover);
button.classList.add('active', 'mixed-view-active');
button.classList.add(...styleConfig.active);
button.classList.add(...styleConfig.activeHover);
} else {
// 普通选中模式
detailedIsMixedView = false;
// 设置当前按钮为激活状态
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;
console.log('详细图表更新时间范围为:', detailedCurrentTimeRange);
}
// 重新绘制详细图表
drawDetailedDNSRequestsChart();
});
});
}
// 绘制详细的DNS请求趋势图表
function drawDetailedDNSRequestsChart() {
console.log('绘制详细DNS请求趋势图表时间范围:', detailedCurrentTimeRange, '混合视图:', detailedIsMixedView);
const ctx = document.getElementById('detailed-dns-requests-chart');
if (!ctx) {
console.error('未找到详细DNS请求图表元素');
return;
}
const chartContext = ctx.getContext('2d');
// 混合视图配置
const datasetsConfig = [
{ label: '24小时', api: (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#3b82f6', fillColor: 'rgba(59, 130, 246, 0.1)' },
{ label: '7天', api: (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.1)' },
{ label: '30天', api: (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#a855f7', fillColor: 'rgba(168, 85, 247, 0.1)' }
];
// 检查是否为混合视图
if (detailedIsMixedView || detailedCurrentTimeRange === 'mixed') {
console.log('渲染混合视图详细图表');
// 显示图例
const showLegend = true;
// 获取所有时间范围的数据
Promise.all(datasetsConfig.map(config =>
config.api().catch(error => {
console.error(`获取${config.label}数据失败:`, error);
// 返回空数据
const count = config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30);
return {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
})
)).then(results => {
// 创建数据集
const datasets = results.map((data, index) => ({
label: datasetsConfig[index].label,
data: data.data,
borderColor: datasetsConfig[index].color,
backgroundColor: datasetsConfig[index].fillColor,
tension: 0.4,
fill: false,
borderWidth: 2
}));
// 创建或更新图表
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = results[0].labels;
detailedDnsRequestsChart.data.datasets = datasets;
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: results[0].labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: showLegend,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.1)'
}
},
x: {
grid: {
display: false
}
}
}
}
});
}
}).catch(error => {
console.error('绘制混合视图详细图表失败:', error);
});
} else {
// 普通视图
// 根据详细视图时间范围选择API函数和对应的颜色
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();
}
});
}
}
// 绘制DNS请求统计图表
function drawDNSRequestsChart() {
const ctx = document.getElementById('dns-requests-chart');
if (!ctx) {
console.error('未找到DNS请求图表元素');
return;
}
const chartContext = ctx.getContext('2d');
// 混合视图配置
const datasetsConfig = [
{ label: '24小时', api: (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#3b82f6', fillColor: 'rgba(59, 130, 246, 0.1)' },
{ label: '7天', api: (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.1)' },
{ label: '30天', api: (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#a855f7', fillColor: 'rgba(168, 85, 247, 0.1)' }
];
// 检查是否为混合视图
if (isMixedView || currentTimeRange === 'mixed') {
console.log('渲染混合视图图表');
// 显示图例
const showLegend = true;
// 获取所有时间范围的数据
Promise.all(datasetsConfig.map(config =>
config.api().catch(error => {
console.error(`获取${config.label}数据失败:`, error);
// 返回空数据而不是模拟数据
const count = config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30);
return {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
})
)).then(results => {
// 创建数据集
const datasets = results.map((data, index) => ({
label: datasetsConfig[index].label,
data: data.data,
borderColor: datasetsConfig[index].color,
backgroundColor: datasetsConfig[index].fillColor,
tension: 0.4,
fill: false, // 混合视图不填充
borderWidth: 2
}));
// 创建或更新图表
if (dnsRequestsChart) {
// 使用第一个数据集的标签,但确保每个数据集使用自己的数据
dnsRequestsChart.data.labels = results[0].labels;
dnsRequestsChart.data.datasets = datasets;
dnsRequestsChart.options.plugins.legend.display = showLegend;
// 使用平滑过渡动画更新图表
dnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
dnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: results[0].labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: showLegend,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.1)'
}
},
x: {
grid: {
display: false
}
}
}
}
});
}
}).catch(error => {
console.error('绘制混合视图图表失败:', error);
});
} else {
// 普通视图
// 根据当前时间范围选择API函数和对应的颜色
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();
}
});
}
}
// 更新图表数据
function updateCharts(stats, queryTypeStats) {
console.log('更新图表,收到统计数据:', stats);
console.log('查询类型统计数据:', queryTypeStats);
// 空值检查
if (!stats) {
console.error('更新图表失败: 未提供统计数据');
return;
}
// 更新比例图表
if (ratioChart) {
let allowed = '---', blocked = '---', error = '---';
// 尝试从stats数据中提取
if (stats.dns) {
allowed = stats.dns.Allowed || allowed;
blocked = stats.dns.Blocked || blocked;
error = stats.dns.Errors || error;
} else if (stats.totalQueries !== undefined) {
allowed = stats.allowedQueries || allowed;
blocked = stats.blockedQueries || blocked;
error = stats.errorQueries || error;
}
ratioChart.data.datasets[0].data = [allowed, blocked, error];
// 使用自定义动画配置更新图表,确保平滑过渡
ratioChart.update({
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 (document.getElementById('avg-response-time')) {
let avgResponseTime = 0;
// 尝试从不同的数据结构获取平均响应时间
if (stats.dns && stats.dns.AvgResponseTime) {
avgResponseTime = stats.dns.AvgResponseTime;
} else if (stats.avgResponseTime !== undefined) {
avgResponseTime = stats.avgResponseTime;
} else if (stats.responseTime) {
avgResponseTime = stats.responseTime;
}
document.getElementById('avg-response-time').textContent = formatNumber(avgResponseTime);
}
// 更新规则数图表
if (statCardCharts['rules-chart']) {
// 尝试获取规则数,如果没有则使用模拟数据
const rulesCount = getRulesCountFromStats(stats) || Math.floor(Math.random() * 5000) + 10000;
updateChartData('rules-chart', rulesCount);
}
// 更新排除规则数图表
if (statCardCharts['exceptions-chart']) {
const exceptionsCount = getExceptionsCountFromStats(stats) || Math.floor(Math.random() * 100) + 50;
updateChartData('exceptions-chart', exceptionsCount);
}
// 更新Hosts条目数图表
if (statCardCharts['hosts-chart']) {
const hostsCount = getHostsCountFromStats(stats) || Math.floor(Math.random() * 1000) + 2000;
updateChartData('hosts-chart', hostsCount);
}
}
// 更新单个图表的数据
function updateChartData(chartId, newValue) {
const chart = statCardCharts[chartId];
const historyData = statCardHistoryData[chartId];
if (!chart || !historyData) {
return;
}
// 添加新数据,移除最旧的数据
historyData.push(newValue);
if (historyData.length > 12) {
historyData.shift();
}
// 更新图表数据
chart.data.datasets[0].data = historyData;
chart.data.labels = generateTimeLabels(historyData.length);
// 使用自定义动画配置更新图表,确保平滑过渡,避免空白区域
chart.update({
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() {
console.log('===== 开始初始化统计卡片折线图 =====');
// 清理已存在的图表实例
for (const key in statCardCharts) {
if (statCardCharts.hasOwnProperty(key)) {
statCardCharts[key].destroy();
}
}
statCardCharts = {};
statCardHistoryData = {};
// 检查Chart.js是否加载
console.log('Chart.js是否可用:', typeof Chart !== 'undefined');
// 统计卡片配置信息
const cardConfigs = [
{ id: 'query-chart', color: '#9b59b6', label: '查询总量' },
{ id: 'blocked-chart', color: '#e74c3c', label: '屏蔽数量' },
{ id: 'allowed-chart', color: '#2ecc71', label: '正常解析' },
{ id: 'error-chart', color: '#f39c12', label: '错误数量' },
{ id: 'response-time-chart', color: '#3498db', label: '响应时间' },
{ id: 'ips-chart', color: '#1abc9c', label: '活跃IP' },
{ id: 'cpu-chart', color: '#e67e22', label: 'CPU使用率' },
{ id: 'rules-chart', color: '#95a5a6', label: '屏蔽规则数' },
{ id: 'exceptions-chart', color: '#34495e', label: '排除规则数' },
{ id: 'hosts-chart', color: '#16a085', label: 'Hosts条目数' }
];
console.log('图表配置:', cardConfigs);
cardConfigs.forEach(config => {
const canvas = document.getElementById(config.id);
if (!canvas) {
console.warn(`未找到统计卡片图表元素: ${config.id}`);
return;
}
const ctx = canvas.getContext('2d');
// 为不同类型的卡片生成更合适的初始数据
let initialData;
if (config.id === 'response-time-chart') {
// 响应时间图表使用空数组将通过API实时数据更新
initialData = Array(12).fill(null);
} else if (config.id === 'cpu-chart') {
initialData = generateMockData(12, 0, 10);
} else {
initialData = generateMockData(12, 0, 100);
}
// 初始化历史数据数组
statCardHistoryData[config.id] = [...initialData];
// 创建图表
statCardCharts[config.id] = new Chart(ctx, {
type: 'line',
data: {
labels: generateTimeLabels(12),
datasets: [{
label: config.label,
data: initialData,
borderColor: config.color,
backgroundColor: `${config.color}20`, // 透明度20%
borderWidth: 2,
tension: 0.4,
fill: true,
pointRadius: 0, // 隐藏数据点
pointHoverRadius: 4, // 鼠标悬停时显示数据点
pointBackgroundColor: config.color
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
// 添加动画配置,确保平滑过渡
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) {
// 显示完整数字的最大长度阈值
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();
}
}
// 处理hash变化
function handleHashChange() {
let hash = window.location.hash;
// 如果没有hash默认设置为#dashboard
if (!hash) {
hash = '#dashboard';
window.location.hash = hash;
return;
}
const targetId = hash.substring(1);
// 查找对应的菜单项
let targetMenuItem = null;
menuItems.forEach(item => {
if (item.getAttribute('href') === hash) {
targetMenuItem = item;
}
});
// 如果找到了对应的菜单项,切换页面
if (targetMenuItem) {
switchPage(targetId, targetMenuItem);
} else {
// 如果没有找到对应的菜单项,尝试显示对应的内容
const contentElement = document.getElementById(`${targetId}-content`);
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 (item.getAttribute('href') === hash) {
item.classList.add('sidebar-item-active');
// 更新页面标题
document.getElementById('page-title').textContent = item.querySelector('span').textContent;
}
});
} else {
// 如果没有找到对应的内容默认显示dashboard
window.location.hash = '#dashboard';
}
}
}
// 初始化hash路由
function initHashRoute() {
handleHashChange();
}
// 监听hash变化事件
window.addEventListener('hashchange', handleHashChange);
menuItems.forEach(item => {
item.addEventListener('click', (e) => {
// 允许默认的hash变化
// 页面切换会由hashchange事件处理
});
});
// 初始化hash路由
initHashRoute();
}
// 侧边栏切换
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-translate-x-full');
}
// 响应式处理
function handleResponsive() {
const toggleBtn = document.getElementById('toggle-sidebar');
const sidebar = document.getElementById('sidebar');
toggleBtn.addEventListener('click', toggleSidebar);
// 初始状态处理
function updateSidebarState() {
if (window.innerWidth < 1024) {
sidebar.classList.add('-translate-x-full');
} else {
sidebar.classList.remove('-translate-x-full');
}
}
updateSidebarState();
// 窗口大小改变时处理
window.addEventListener('resize', () => {
updateSidebarState();
// 更新所有图表大小
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 () => {
console.log('重试获取TOP客户端数据');
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 () => {
console.log('重试获取TOP域名数据');
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);
}
});
});