// dashboard.js - 仪表盘功能实现 // 全局变量 let ratioChart = null; let dnsRequestsChart = null; let queryTypeChart = null; // 解析类型统计饼图 let intervalId = null; // 存储统计卡片图表实例 let statCardCharts = {}; // 存储统计卡片历史数据 let statCardHistoryData = {}; // 引入颜色配置文件 const COLOR_CONFIG = window.COLOR_CONFIG || {}; // 初始化仪表盘 async function initDashboard() { try { // 加载初始数据 await loadDashboardData(); // 初始化图表 initCharts(); // 初始化统计卡片折线图 initStatCardCharts(); // 初始化时间范围切换 initTimeRangeToggle(); // 设置定时更新 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 } catch (error) { console.error('初始化仪表盘失败:', error); showNotification('初始化失败: ' + error.message, 'error'); } } // 加载仪表盘数据 async function loadDashboardData() { console.log('开始加载仪表盘数据'); try { // 获取基本统计数据 const stats = await api.getStats(); console.log('统计数据:', stats); // 获取查询类型统计数据 let queryTypeStats = null; try { queryTypeStats = await api.getQueryTypeStats(); console.log('查询类型统计数据:', queryTypeStats); } catch (error) { console.warn('获取查询类型统计失败:', error); // 如果API调用失败,尝试从stats中提取查询类型数据 if (stats && stats.dns && stats.dns.QueryTypes) { queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({ type, count })); console.log('从stats中提取的查询类型统计:', queryTypeStats); } } // 尝试获取TOP被屏蔽域名,如果失败则提供模拟数据 let topBlockedDomains = []; try { topBlockedDomains = await api.getTopBlockedDomains(); console.log('TOP被屏蔽域名:', topBlockedDomains); // 确保返回的数据是数组 if (!Array.isArray(topBlockedDomains)) { console.warn('TOP被屏蔽域名不是预期的数组格式,使用模拟数据'); topBlockedDomains = []; } } catch (error) { console.warn('获取TOP被屏蔽域名失败:', error); // 提供模拟数据 topBlockedDomains = [ { domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() }, { domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() }, { domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() } ]; } // 尝试获取最近屏蔽域名,如果失败则提供模拟数据 let recentBlockedDomains = []; try { recentBlockedDomains = await api.getRecentBlockedDomains(); console.log('最近屏蔽域名:', recentBlockedDomains); // 确保返回的数据是数组 if (!Array.isArray(recentBlockedDomains)) { console.warn('最近屏蔽域名不是预期的数组格式,使用模拟数据'); recentBlockedDomains = []; } } catch (error) { console.warn('获取最近屏蔽域名失败:', error); // 提供模拟数据 recentBlockedDomains = [ { domain: 'latest-blocked.com', ip: '192.168.1.1', timestamp: new Date().toISOString() }, { domain: 'recent-ads.org', ip: '192.168.1.2', timestamp: new Date().toISOString() } ]; } // 更新统计卡片 updateStatsCards(stats); // 更新图表数据,传入查询类型统计 updateCharts(stats, queryTypeStats); // 更新表格数据 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); // 更新卡片图表 updateStatCardCharts(stats); // 尝试从stats中获取总查询数等信息 if (stats.dns) { totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0); blockedQueries = stats.dns.Blocked; errorQueries = stats.dns.Errors || 0; allowedQueries = stats.dns.Allowed; } else { totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; errorQueries = stats.errorQueries || 0; allowedQueries = stats.allowedQueries || 0; } // 全局历史数据对象,用于存储趋势计算所需的上一次值 window.dashboardHistoryData = window.dashboardHistoryData || {}; // 更新新卡片数据 - 使用API返回的真实数据 if (document.getElementById('avg-response-time')) { // 保留两位小数并添加单位 const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---'; // 计算响应时间趋势 let responsePercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) { // 存储当前值用于下次计算趋势 const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime; window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime; // 计算变化百分比 if (prevResponseTime > 0) { const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100; responsePercent = Math.abs(changePercent).toFixed(1) + '%'; // 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的) if (changePercent > 0) { trendIcon = '↓'; trendClass = 'text-danger'; } else if (changePercent < 0) { trendIcon = '↑'; trendClass = 'text-success'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('avg-response-time').textContent = responseTime; const responseTimePercentElem = document.getElementById('response-time-percent'); if (responseTimePercentElem) { responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent; responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`; } } if (document.getElementById('top-query-type')) { // 直接使用API返回的查询类型 const queryType = stats.topQueryType || '---'; // 设置默认趋势显示 const queryPercentElem = document.getElementById('query-type-percentage'); if (queryPercentElem) { queryPercentElem.textContent = '• ---'; queryPercentElem.className = 'text-sm flex items-center text-gray-500'; } document.getElementById('top-query-type').textContent = queryType; } if (document.getElementById('active-ips')) { // 直接使用API返回的活跃IP数 const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---'; // 计算活跃IP趋势 let ipsPercent = '---'; let trendClass = 'text-gray-400'; let trendIcon = '---'; if (stats.activeIPs !== undefined && stats.activeIPs !== null) { // 存储当前值用于下次计算趋势 const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs; window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; // 计算变化百分比 if (prevActiveIPs > 0) { const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100; ipsPercent = Math.abs(changePercent).toFixed(1) + '%'; // 设置趋势图标和颜色 if (changePercent > 0) { trendIcon = '↑'; trendClass = 'text-success'; } else if (changePercent < 0) { trendIcon = '↓'; trendClass = 'text-danger'; } else { trendIcon = '•'; trendClass = 'text-gray-500'; } } } document.getElementById('active-ips').textContent = activeIPs; const activeIpsPercentElem = document.getElementById('active-ips-percent'); if (activeIpsPercentElem) { activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent; activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`; } } if (document.getElementById('cpu-usage')) { // 保留两位小数并添加单位 const cpuUsage = stats.cpuUsage ? stats.cpuUsage.toFixed(2) + '%' : '---'; document.getElementById('cpu-usage').textContent = cpuUsage; // 设置CPU状态颜色 const cpuStatusElem = document.getElementById('cpu-status'); if (cpuStatusElem) { if (stats.cpuUsage !== undefined && stats.cpuUsage !== null) { if (stats.cpuUsage > 80) { cpuStatusElem.textContent = '警告'; cpuStatusElem.className = 'text-danger text-sm flex items-center'; } else if (stats.cpuUsage > 60) { cpuStatusElem.textContent = '较高'; cpuStatusElem.className = 'text-warning text-sm flex items-center'; } else { cpuStatusElem.textContent = '正常'; cpuStatusElem.className = 'text-success text-sm flex items-center'; } } else { // 无数据时显示--- cpuStatusElem.textContent = '---'; cpuStatusElem.className = 'text-gray-400 text-sm flex items-center'; } } } // 更新表格 updateTopBlockedTable(topBlockedDomains); updateRecentBlockedTable(recentBlockedDomains); // 更新图表 updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries}); // 更新统计卡片折线图 updateStatCardCharts(stats); // 确保响应时间图表使用API实时数据 if (document.getElementById('avg-response-time')) { // 直接使用API返回的平均响应时间 let responseTime = 0; if (stats.dns && stats.dns.AvgResponseTime) { responseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { responseTime = stats.avgResponseTime; } else if (stats.responseTime) { responseTime = stats.responseTime; } if (responseTime > 0 && statCardCharts['response-time-chart']) { // 限制小数位数为两位并更新图表 updateChartData('response-time-chart', parseFloat(responseTime).toFixed(2)); } } // 更新运行状态 updateUptime(); } catch (error) { console.error('加载仪表盘数据失败:', error); // 静默失败,不显示通知以免打扰用户 } } // 更新统计卡片 function updateStatsCards(stats) { console.log('更新统计卡片,收到数据:', stats); // 适配不同的数据结构 let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0; let topQueryType = 'A', queryTypePercentage = 0; let activeIPs = 0, activeIPsPercentage = 0; // 检查数据结构,兼容可能的不同格式 if (stats) { // 优先使用顶层字段 totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; allowedQueries = stats.allowedQueries || 0; errorQueries = stats.errorQueries || 0; topQueryType = stats.topQueryType || 'A'; queryTypePercentage = stats.queryTypePercentage || 0; activeIPs = stats.activeIPs || 0; activeIPsPercentage = stats.activeIPsPercentage || 0; // 如果dns对象存在,优先使用其中的数据 if (stats.dns) { totalQueries = stats.dns.Queries || totalQueries; blockedQueries = stats.dns.Blocked || blockedQueries; allowedQueries = stats.dns.Allowed || allowedQueries; errorQueries = stats.dns.Errors || errorQueries; // 计算最常用查询类型的百分比 if (stats.dns.QueryTypes && stats.dns.Queries > 0) { const topTypeCount = stats.dns.QueryTypes[topQueryType] || 0; queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100; } // 计算活跃IP百分比(基于已有的活跃IP数) if (activeIPs > 0 && stats.dns.SourceIPs) { activeIPsPercentage = activeIPs / Object.keys(stats.dns.SourceIPs).length * 100; } } } else if (Array.isArray(stats) && stats.length > 0) { // 可能的数据结构3: 数组形式 totalQueries = stats[0].total || 0; blockedQueries = stats[0].blocked || 0; allowedQueries = stats[0].allowed || 0; errorQueries = stats[0].error || 0; topQueryType = stats[0].topQueryType || 'A'; queryTypePercentage = stats[0].queryTypePercentage || 0; activeIPs = stats[0].activeIPs || 0; activeIPsPercentage = stats[0].activeIPsPercentage || 0; } // 更新数量显示 document.getElementById('total-queries').textContent = formatNumber(totalQueries); document.getElementById('blocked-queries').textContent = formatNumber(blockedQueries); document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries); document.getElementById('error-queries').textContent = formatNumber(errorQueries); document.getElementById('active-ips').textContent = formatNumber(activeIPs); // 更新最常查询类型 document.getElementById('top-query-type').textContent = topQueryType; document.getElementById('query-type-percentage').textContent = `${Math.round(queryTypePercentage)}%`; // 更新活跃来源IP百分比 document.getElementById('active-ips-percent').textContent = `${Math.round(activeIPsPercentage)}%`; // 计算并更新百分比 if (totalQueries > 0) { document.getElementById('blocked-percent').textContent = `${Math.round((blockedQueries / totalQueries) * 100)}%`; document.getElementById('allowed-percent').textContent = `${Math.round((allowedQueries / totalQueries) * 100)}%`; document.getElementById('error-percent').textContent = `${Math.round((errorQueries / totalQueries) * 100)}%`; document.getElementById('queries-percent').textContent = `100%`; } else { document.getElementById('queries-percent').textContent = '---'; document.getElementById('blocked-percent').textContent = '---'; document.getElementById('allowed-percent').textContent = '---'; document.getElementById('error-percent').textContent = '---'; } } // 更新Top屏蔽域名表格 function updateTopBlockedTable(domains) { console.log('更新Top屏蔽域名表格,收到数据:', domains); const tableBody = document.getElementById('top-blocked-table'); let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.name || item.domain || item[0] || '未知', count: item.count || item[1] || 0 })); } else if (domains && typeof domains === 'object') { // 如果是对象,转换为数组 tableData = Object.entries(domains).map(([domain, count]) => ({ name: domain, count: count || 0 })); } // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { tableData = [ { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' }, { name: '---', count: '---' } ]; console.log('使用示例数据填充Top屏蔽域名表格'); } let html = ''; for (const domain of tableData) { html += ` ${domain.name} ${formatNumber(domain.count)} `; } tableBody.innerHTML = html; } // 更新最近屏蔽域名表格 function updateRecentBlockedTable(domains) { console.log('更新最近屏蔽域名表格,收到数据:', domains); const tableBody = document.getElementById('recent-blocked-table'); let tableData = []; // 适配不同的数据结构 if (Array.isArray(domains)) { tableData = domains.map(item => ({ name: item.name || item.domain || item[0] || '未知', timestamp: item.timestamp || item.time || Date.now() })); } // 如果没有有效数据,提供示例数据 if (tableData.length === 0) { const now = Date.now(); tableData = [ { name: '---', timestamp: now - 5 * 60 * 1000 }, { name: '---', timestamp: now - 15 * 60 * 1000 }, { name: '---', timestamp: now - 30 * 60 * 1000 }, { name: '---', timestamp: now - 45 * 60 * 1000 }, { name: '---', timestamp: now - 60 * 60 * 1000 } ]; console.log('使用示例数据填充最近屏蔽域名表格'); } let html = ''; for (const domain of tableData) { const time = formatTime(domain.timestamp); html += ` ${domain.name} ${time} `; } tableBody.innerHTML = html; } // 当前选中的时间范围 let currentTimeRange = '24h'; // 默认为24小时 // 初始化时间范围切换 function initTimeRangeToggle() { const timeRangeButtons = document.querySelectorAll('.time-range-btn'); timeRangeButtons.forEach(button => { button.addEventListener('click', () => { // 移除所有按钮的激活状态 timeRangeButtons.forEach(btn => btn.classList.remove('active')); // 添加当前按钮的激活状态 button.classList.add('active'); // 更新当前时间范围 currentTimeRange = button.dataset.range; // 重新加载数据 loadDashboardData(); // 更新DNS请求图表 drawDNSRequestsChart(); }); }); } // 初始化图表 function initCharts() { // 初始化比例图表 const ratioChartElement = document.getElementById('ratio-chart'); if (!ratioChartElement) { console.error('未找到比例图表元素'); return; } const ratioCtx = ratioChartElement.getContext('2d'); ratioChart = new Chart(ratioCtx, { type: 'doughnut', data: { labels: ['正常解析', '被屏蔽', '错误'], datasets: [{ data: ['---', '---', '---'], backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', } }, cutout: '70%' } }); // 初始化解析类型统计饼图 const queryTypeChartElement = document.getElementById('query-type-chart'); if (queryTypeChartElement) { const queryTypeCtx = queryTypeChartElement.getContext('2d'); // 预定义的颜色数组,用于解析类型 const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e']; queryTypeChart = new Chart(queryTypeCtx, { type: 'doughnut', data: { labels: ['暂无数据'], datasets: [{ data: [1], backgroundColor: [queryTypeColors[0]], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; } } } }, cutout: '70%' } }); } else { console.warn('未找到解析类型统计图表元素'); } // 初始化DNS请求统计图表 drawDNSRequestsChart(); } // 绘制DNS请求统计图表 function drawDNSRequestsChart() { const ctx = document.getElementById('dns-requests-chart'); if (!ctx) { console.error('未找到DNS请求图表元素'); return; } const chartContext = ctx.getContext('2d'); let apiFunction; // 根据当前时间范围选择API函数 switch (currentTimeRange) { case '7d': apiFunction = api.getDailyStats; break; case '30d': apiFunction = api.getMonthlyStats; break; default: // 24h apiFunction = api.getHourlyStats; } // 获取统计数据 apiFunction().then(data => { // 创建或更新图表 if (dnsRequestsChart) { dnsRequestsChart.data.labels = data.labels; dnsRequestsChart.data.datasets[0].data = data.data; dnsRequestsChart.update(); } else { dnsRequestsChart = new Chart(chartContext, { type: 'line', data: { labels: data.labels, datasets: [{ label: 'DNS请求数量', data: data.data, borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.1)' } }, x: { grid: { display: false } } } } }); } }).catch(error => { console.error('绘制DNS请求图表失败:', error); }); } // 更新图表数据 function updateCharts(stats, queryTypeStats) { console.log('更新图表,收到统计数据:', stats); console.log('查询类型统计数据:', queryTypeStats); // 空值检查 if (!stats) { console.error('更新图表失败: 未提供统计数据'); return; } // 更新比例图表 if (ratioChart) { let allowed = '---', blocked = '---', error = '---'; // 尝试从stats数据中提取 if (stats.dns) { allowed = stats.dns.Allowed || allowed; blocked = stats.dns.Blocked || blocked; error = stats.dns.Errors || error; } else if (stats.totalQueries !== undefined) { allowed = stats.allowedQueries || allowed; blocked = stats.blockedQueries || blocked; error = stats.errorQueries || error; } ratioChart.data.datasets[0].data = [allowed, blocked, error]; ratioChart.update(); } // 更新解析类型统计饼图 if (queryTypeChart && queryTypeStats && Array.isArray(queryTypeStats)) { const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e']; // 检查是否有有效的数据项 const validData = queryTypeStats.filter(item => item && item.count > 0); if (validData.length > 0) { // 准备标签和数据 const labels = validData.map(item => item.type); const data = validData.map(item => item.count); // 为每个解析类型分配颜色 const colors = labels.map((_, index) => queryTypeColors[index % queryTypeColors.length]); // 更新图表数据 queryTypeChart.data.labels = labels; queryTypeChart.data.datasets[0].data = data; queryTypeChart.data.datasets[0].backgroundColor = colors; } else { // 如果没有数据,显示默认值 queryTypeChart.data.labels = ['暂无数据']; queryTypeChart.data.datasets[0].data = [1]; queryTypeChart.data.datasets[0].backgroundColor = [queryTypeColors[0]]; } queryTypeChart.update(); } } // 更新统计卡片折线图 function updateStatCardCharts(stats) { if (!stats || Object.keys(statCardCharts).length === 0) { return; } // 更新查询总量图表 if (statCardCharts['query-chart']) { let queryCount = 0; if (stats.dns) { queryCount = stats.dns.Queries || 0; } else if (stats.totalQueries !== undefined) { queryCount = stats.totalQueries || 0; } updateChartData('query-chart', queryCount); } // 更新屏蔽数量图表 if (statCardCharts['blocked-chart']) { let blockedCount = 0; if (stats.dns) { blockedCount = stats.dns.Blocked || 0; } else if (stats.blockedQueries !== undefined) { blockedCount = stats.blockedQueries || 0; } updateChartData('blocked-chart', blockedCount); } // 更新正常解析图表 if (statCardCharts['allowed-chart']) { let allowedCount = 0; if (stats.dns) { allowedCount = stats.dns.Allowed || 0; } else if (stats.allowedQueries !== undefined) { allowedCount = stats.allowedQueries || 0; } else if (stats.dns && stats.dns.Queries && stats.dns.Blocked) { allowedCount = stats.dns.Queries - stats.dns.Blocked; } updateChartData('allowed-chart', allowedCount); } // 更新错误数量图表 if (statCardCharts['error-chart']) { let errorCount = 0; if (stats.dns) { errorCount = stats.dns.Errors || 0; } else if (stats.errorQueries !== undefined) { errorCount = stats.errorQueries || 0; } updateChartData('error-chart', errorCount); } // 更新响应时间图表 if (statCardCharts['response-time-chart']) { let responseTime = 0; // 尝试从不同的数据结构获取平均响应时间 if (stats.dns && stats.dns.AvgResponseTime) { responseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { responseTime = stats.avgResponseTime; } else if (stats.responseTime) { responseTime = stats.responseTime; } // 限制小数位数为两位 responseTime = parseFloat(responseTime).toFixed(2); updateChartData('response-time-chart', responseTime); } // 更新活跃IP图表 if (statCardCharts['ips-chart']) { const activeIPs = stats.activeIPs || 0; updateChartData('ips-chart', activeIPs); } // 更新CPU使用率图表 if (statCardCharts['cpu-chart']) { const cpuUsage = stats.cpuUsage || 0; updateChartData('cpu-chart', cpuUsage); } // 更新平均响应时间显示 if (document.getElementById('avg-response-time')) { let avgResponseTime = 0; // 尝试从不同的数据结构获取平均响应时间 if (stats.dns && stats.dns.AvgResponseTime) { avgResponseTime = stats.dns.AvgResponseTime; } else if (stats.avgResponseTime !== undefined) { avgResponseTime = stats.avgResponseTime; } else if (stats.responseTime) { avgResponseTime = stats.responseTime; } document.getElementById('avg-response-time').textContent = formatNumber(avgResponseTime); } // 更新规则数图表 if (statCardCharts['rules-chart']) { // 尝试获取规则数,如果没有则使用模拟数据 const rulesCount = getRulesCountFromStats(stats) || Math.floor(Math.random() * 5000) + 10000; updateChartData('rules-chart', rulesCount); } // 更新排除规则数图表 if (statCardCharts['exceptions-chart']) { const exceptionsCount = getExceptionsCountFromStats(stats) || Math.floor(Math.random() * 100) + 50; updateChartData('exceptions-chart', exceptionsCount); } // 更新Hosts条目数图表 if (statCardCharts['hosts-chart']) { const hostsCount = getHostsCountFromStats(stats) || Math.floor(Math.random() * 1000) + 2000; updateChartData('hosts-chart', hostsCount); } } // 更新单个图表的数据 function updateChartData(chartId, newValue) { const chart = statCardCharts[chartId]; const historyData = statCardHistoryData[chartId]; if (!chart || !historyData) { return; } // 添加新数据,移除最旧的数据 historyData.push(newValue); if (historyData.length > 12) { historyData.shift(); } // 更新图表数据 chart.data.datasets[0].data = historyData; chart.data.labels = generateTimeLabels(historyData.length); chart.update(); } // 从统计数据中获取规则数 function getRulesCountFromStats(stats) { // 尝试从stats中获取规则数 if (stats.shield && stats.shield.rules) { return stats.shield.rules; } return null; } // 从统计数据中获取排除规则数 function getExceptionsCountFromStats(stats) { // 尝试从stats中获取排除规则数 if (stats.shield && stats.shield.exceptions) { return stats.shield.exceptions; } return null; } // 从统计数据中获取Hosts条目数 function getHostsCountFromStats(stats) { // 尝试从stats中获取Hosts条目数 if (stats.shield && stats.shield.hosts) { return stats.shield.hosts; } return null; } // 初始化统计卡片折线图 function initStatCardCharts() { console.log('===== 开始初始化统计卡片折线图 ====='); // 清理已存在的图表实例 for (const key in statCardCharts) { if (statCardCharts.hasOwnProperty(key)) { statCardCharts[key].destroy(); } } statCardCharts = {}; statCardHistoryData = {}; // 检查Chart.js是否加载 console.log('Chart.js是否可用:', typeof Chart !== 'undefined'); // 统计卡片配置信息 const cardConfigs = [ { id: 'query-chart', color: '#9b59b6', label: '查询总量' }, { id: 'blocked-chart', color: '#e74c3c', label: '屏蔽数量' }, { id: 'allowed-chart', color: '#2ecc71', label: '正常解析' }, { id: 'error-chart', color: '#f39c12', label: '错误数量' }, { id: 'response-time-chart', color: '#3498db', label: '响应时间' }, { id: 'ips-chart', color: '#1abc9c', label: '活跃IP' }, { id: 'cpu-chart', color: '#e67e22', label: 'CPU使用率' }, { id: 'rules-chart', color: '#95a5a6', label: '屏蔽规则数' }, { id: 'exceptions-chart', color: '#34495e', label: '排除规则数' }, { id: 'hosts-chart', color: '#16a085', label: 'Hosts条目数' } ]; console.log('图表配置:', cardConfigs); cardConfigs.forEach(config => { const canvas = document.getElementById(config.id); if (!canvas) { console.warn(`未找到统计卡片图表元素: ${config.id}`); return; } const ctx = canvas.getContext('2d'); // 为不同类型的卡片生成更合适的初始数据 let initialData; if (config.id === 'response-time-chart') { // 响应时间图表使用空数组,将通过API实时数据更新 initialData = Array(12).fill(null); } else if (config.id === 'cpu-chart') { initialData = generateMockData(12, 0, 10); } else { initialData = generateMockData(12, 0, 100); } // 初始化历史数据数组 statCardHistoryData[config.id] = [...initialData]; // 创建图表 statCardCharts[config.id] = new Chart(ctx, { type: 'line', data: { labels: generateTimeLabels(12), datasets: [{ label: config.label, data: initialData, borderColor: config.color, backgroundColor: `${config.color}20`, // 透明度20% borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0, // 隐藏数据点 pointHoverRadius: 4, // 鼠标悬停时显示数据点 pointBackgroundColor: config.color }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, backgroundColor: 'rgba(0, 0, 0, 0.9)', titleColor: '#fff', bodyColor: '#fff', borderColor: config.color, borderWidth: 1, padding: 8, displayColors: false, cornerRadius: 4, titleFont: { size: 12, weight: 'normal' }, bodyFont: { size: 11 }, // 确保HTML渲染正确 useHTML: true, filter: function(tooltipItem) { return tooltipItem.datasetIndex === 0; }, callbacks: { title: function(tooltipItems) { // 简化时间显示格式 return tooltipItems[0].label; }, label: function(context) { const value = context.parsed.y; // 格式化大数字 const formattedValue = formatNumber(value); // 使用CSS类显示变化趋势 let trendInfo = ''; const data = context.dataset.data; const currentIndex = context.dataIndex; if (currentIndex > 0) { const prevValue = data[currentIndex - 1]; const change = value - prevValue; if (change !== 0) { const changeSymbol = change > 0 ? '↑' : '↓'; // 取消颜色显示,简化显示 trendInfo = (changeSymbol + Math.abs(change)); } } // 简化标签格式 return `${config.label}: ${formattedValue}${trendInfo}`; }, // 移除平均值显示 afterLabel: function(context) { return ''; } } } }, scales: { x: { display: false // 隐藏X轴 }, y: { display: false, // 隐藏Y轴 beginAtZero: true } }, interaction: { intersect: false, mode: 'index' } } }); }); } // 生成模拟数据 function generateMockData(count, min, max) { const data = []; for (let i = 0; i < count; i++) { data.push(Math.floor(Math.random() * (max - min + 1)) + min); } return data; } // 生成时间标签 function generateTimeLabels(count) { const labels = []; const now = new Date(); for (let i = count - 1; i >= 0; i--) { const time = new Date(now.getTime() - i * 5 * 60 * 1000); // 每5分钟一个点 labels.push(`${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`); } return labels; } // 格式化数字显示(使用K/M后缀) function formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } // 更新运行状态 function updateUptime() { // 实现更新运行时间的逻辑 // 这里应该调用API获取当前运行时间并更新到UI // 由于API暂时没有提供运行时间,我们先使用模拟数据 const uptimeElement = document.getElementById('uptime'); if (uptimeElement) { uptimeElement.textContent = '---'; } } // 格式化数字(添加千位分隔符) function formatWithCommas(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // 格式化时间 function formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; // 如果是今天,显示时间 if (date.toDateString() === now.toDateString()) { return date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'}); } // 否则显示日期和时间 return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } // 根据颜色代码获取对应的CSS类名(兼容方式) function getColorClassName(colorCode) { // 优先使用配置文件中的颜色处理 if (COLOR_CONFIG.getColorClassName) { return COLOR_CONFIG.getColorClassName(colorCode); } // 备用颜色映射 const colorMap = { '#1890ff': 'blue', '#52c41a': 'green', '#fa8c16': 'orange', '#f5222d': 'red', '#722ed1': 'purple', '#13c2c2': 'cyan', '#36cfc9': 'teal' }; // 返回映射的类名,如果没有找到则返回默认的blue return colorMap[colorCode] || 'blue'; } // 显示通知 function showNotification(message, type = 'info') { // 移除已存在的通知 const existingNotification = document.getElementById('notification'); if (existingNotification) { existingNotification.remove(); } // 创建通知元素 const notification = document.createElement('div'); notification.id = 'notification'; notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-0 opacity-0`; // 设置样式和内容 let bgColor, textColor, icon; switch (type) { case 'success': bgColor = 'bg-success'; textColor = 'text-white'; icon = 'fa-check-circle'; break; case 'error': bgColor = 'bg-danger'; textColor = 'text-white'; icon = 'fa-exclamation-circle'; break; case 'warning': bgColor = 'bg-warning'; textColor = 'text-white'; icon = 'fa-exclamation-triangle'; break; default: bgColor = 'bg-primary'; textColor = 'text-white'; icon = 'fa-info-circle'; } notification.className += ` ${bgColor} ${textColor}`; notification.innerHTML = `
${message}
`; // 添加到页面 document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.classList.remove('translate-y-0', 'opacity-0'); notification.classList.add('-translate-y-2', 'opacity-100'); }, 10); // 自动关闭 setTimeout(() => { notification.classList.add('translate-y-0', 'opacity-0'); setTimeout(() => { notification.remove(); }, 300); }, 3000); } // 页面切换处理 function handlePageSwitch() { const menuItems = document.querySelectorAll('nav a'); menuItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const targetId = item.getAttribute('href').substring(1); // 隐藏所有内容 document.querySelectorAll('[id$="-content"]').forEach(content => { content.classList.add('hidden'); }); // 显示目标内容 document.getElementById(`${targetId}-content`).classList.remove('hidden'); // 更新页面标题 document.getElementById('page-title').textContent = item.querySelector('span').textContent; // 更新活动菜单项 menuItems.forEach(menuItem => { menuItem.classList.remove('sidebar-item-active'); }); item.classList.add('sidebar-item-active'); // 侧边栏切换(移动端) if (window.innerWidth < 1024) { toggleSidebar(); } }); }); } // 侧边栏切换 function toggleSidebar() { const sidebar = document.getElementById('sidebar'); sidebar.classList.toggle('-translate-x-full'); } // 响应式处理 function handleResponsive() { const toggleBtn = document.getElementById('toggle-sidebar'); toggleBtn.addEventListener('click', toggleSidebar); // 初始状态处理 if (window.innerWidth < 1024) { document.getElementById('sidebar').classList.add('-translate-x-full'); } // 窗口大小改变时处理 window.addEventListener('resize', () => { const sidebar = document.getElementById('sidebar'); if (window.innerWidth >= 1024) { sidebar.classList.remove('-translate-x-full'); } }); } // 页面加载完成后初始化 window.addEventListener('DOMContentLoaded', () => { // 初始化页面切换 handlePageSwitch(); // 初始化响应式 handleResponsive(); // 初始化仪表盘 initDashboard(); // 页面卸载时清理定时器 window.addEventListener('beforeunload', () => { if (intervalId) { clearInterval(intervalId); } }); });