// dashboard.js - 仪表盘功能实现
// 全局变量
let queryTrendChart = null;
let ratioChart = null;
let intervalId = null;
// 初始化仪表盘
async function initDashboard() {
try {
// 加载初始数据
await loadDashboardData();
// 初始化图表
initCharts();
// 设置定时更新
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
} catch (error) {
console.error('初始化仪表盘失败:', error);
showNotification('初始化失败: ' + error.message, 'error');
}
}
// 加载仪表盘数据
async function loadDashboardData() {
try {
// 并行请求所有数据
const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([
api.getStats(),
api.getTopBlockedDomains(),
api.getRecentBlockedDomains(),
api.getHourlyStats()
]);
// 更新统计卡片
updateStatsCards(stats);
// 更新数据表格
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
// 更新图表
updateCharts(stats, hourlyStats);
// 更新运行状态
updateUptime();
} catch (error) {
console.error('加载仪表盘数据失败:', error);
// 静默失败,不显示通知以免打扰用户
}
}
// 更新统计卡片
function updateStatsCards(stats) {
const totalQueries = stats.totalQueries || 0;
const blockedQueries = stats.blockedQueries || 0;
const allowedQueries = stats.allowedQueries || 0;
const errorQueries = stats.errorQueries || 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);
// 更新百分比(模拟数据,实际应该从API获取)
document.getElementById('queries-percent').textContent = '12%';
document.getElementById('blocked-percent').textContent = '8%';
document.getElementById('allowed-percent').textContent = '15%';
document.getElementById('error-percent').textContent = '2%';
}
// 更新Top屏蔽域名表格
function updateTopBlockedTable(domains) {
const tableBody = document.getElementById('top-blocked-table');
if (!domains || domains.length === 0) {
tableBody.innerHTML = `
| 暂无数据 |
`;
return;
}
let html = '';
for (const domain of domains) {
html += `
| ${domain.name || '未知'} |
${formatNumber(domain.count || 0)} |
`;
}
tableBody.innerHTML = html;
}
// 更新最近屏蔽域名表格
function updateRecentBlockedTable(domains) {
const tableBody = document.getElementById('recent-blocked-table');
if (!domains || domains.length === 0) {
tableBody.innerHTML = `
| 暂无数据 |
`;
return;
}
let html = '';
for (const domain of domains) {
const time = formatTime(domain.timestamp || Date.now());
html += `
| ${domain.name || '未知'} |
${time} |
`;
}
tableBody.innerHTML = html;
}
// 初始化图表
function initCharts() {
// 初始化查询趋势图表
const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d');
queryTrendChart = new Chart(queryTrendCtx, {
type: 'line',
data: {
labels: Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`),
datasets: [
{
label: '总查询',
data: Array(24).fill(0),
borderColor: '#165DFF',
backgroundColor: 'rgba(22, 93, 255, 0.1)',
tension: 0.4,
fill: true
},
{
label: '屏蔽数量',
data: Array(24).fill(0),
borderColor: '#F53F3F',
backgroundColor: 'rgba(245, 63, 63, 0.1)',
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
drawBorder: false
}
},
x: {
grid: {
display: false
}
}
}
}
});
// 初始化比例图表
const ratioCtx = document.getElementById('ratio-chart').getContext('2d');
ratioChart = new Chart(ratioCtx, {
type: 'doughnut',
data: {
labels: ['正常解析', '被屏蔽', '错误'],
datasets: [{
data: [70, 25, 5],
backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
},
cutout: '70%'
}
});
}
// 更新图表数据
function updateCharts(stats, hourlyStats) {
// 更新比例图表
if (ratioChart) {
const total = (stats.totalQueries || 0);
const blocked = (stats.blockedQueries || 0);
const allowed = (stats.allowedQueries || 0);
const error = (stats.errorQueries || 0);
if (total > 0) {
ratioChart.data.datasets[0].data = [allowed, blocked, error];
ratioChart.update();
}
}
// 更新趋势图表
if (queryTrendChart && hourlyStats && hourlyStats.length > 0) {
const labels = hourlyStats.map(h => `${h.hour}:00`);
const totalData = hourlyStats.map(h => h.total || 0);
const blockedData = hourlyStats.map(h => h.blocked || 0);
queryTrendChart.data.labels = labels;
queryTrendChart.data.datasets[0].data = totalData;
queryTrendChart.data.datasets[1].data = blockedData;
queryTrendChart.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);
}
});
});