// 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);
}
});
});