// dashboard.js - 仪表盘功能实现
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
let intervalId = null;
// 初始化仪表盘
async function initDashboard() {
try {
// 加载初始数据
await loadDashboardData();
// 初始化图表
initCharts();
// 初始化时间范围切换
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);
// 获取TOP被屏蔽域名
const topBlockedDomains = await api.getTopBlockedDomains();
console.log('TOP被屏蔽域名:', topBlockedDomains);
// 获取最近屏蔽域名
const recentBlockedDomains = await api.getRecentBlockedDomains();
console.log('最近屏蔽域名:', recentBlockedDomains);
// 原并行请求方式(保留以备后续恢复)
// const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
// api.getStats(),
// api.getTopBlockedDomains(),
// api.getRecentBlockedDomains()
// ]);
// 更新统计卡片
updateStatsCards(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;
}
// 更新新卡片数据 - 使用API返回的真实数据
if (document.getElementById('avg-response-time')) {
// 保留两位小数并添加单位
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
const responsePercent = stats.responseTimePercent !== undefined ? stats.responseTimePercent + '%' : '---';
document.getElementById('avg-response-time').textContent = responseTime;
document.getElementById('response-time-percent').textContent = responsePercent;
}
if (document.getElementById('top-query-type')) {
// 直接使用API返回的查询类型
const queryType = stats.topQueryType || '---';
const queryPercent = stats.queryTypePercentage !== undefined ? stats.queryTypePercentage + '%' : '---';
document.getElementById('top-query-type').textContent = queryType;
document.getElementById('query-type-percentage').textContent = queryPercent;
}
if (document.getElementById('active-ips')) {
// 直接使用API返回的活跃IP数
const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---';
const ipsPercent = stats.activeIPsPercent !== undefined ? stats.activeIPsPercent + '%' : '---';
document.getElementById('active-ips').textContent = activeIPs;
document.getElementById('active-ips-percent').textContent = ipsPercent;
}
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});
// 更新运行状态
updateUptime();
} catch (error) {
console.error('加载仪表盘数据失败:', error);
// 静默失败,不显示通知以免打扰用户
}
}
// 更新统计卡片
function updateStatsCards(stats) {
console.log('更新统计卡片,收到数据:', stats);
// 适配不同的数据结构
let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0;
// 检查数据结构,兼容可能的不同格式
if (stats.dns) {
// 可能的数据结构1: stats.dns.Queries等
totalQueries = stats.dns.Queries || 0;
blockedQueries = stats.dns.Blocked || 0;
allowedQueries = stats.dns.Allowed || 0;
errorQueries = stats.dns.Errors || 0;
} else if (stats.totalQueries !== undefined) {
// 可能的数据结构2: stats.totalQueries等
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
allowedQueries = stats.allowedQueries || 0;
errorQueries = stats.errorQueries || 0;
} 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;
}
// 更新数量显示
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);
// 计算并更新百分比
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%'
}
});
// 初始化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) {
console.log('更新图表,收到统计数据:', stats);
// 空值检查
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();
}
}
// 更新运行状态
function updateUptime() {
// 这里应该从API获取真实的运行时间
const uptimeElement = document.getElementById('uptime');
uptimeElement.textContent = '正常运行中';
uptimeElement.className = 'mt-1 text-success';
}
// 格式化数字(添加千位分隔符)
function formatNumber(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'
});
}
// 显示通知
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);
}
});
});