Files
dns-server/static/js/dashboard.js
2025-11-25 01:34:50 +08:00

408 lines
12 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 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 = `
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>
</tr>
`;
return;
}
let html = '';
for (const domain of domains) {
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name || '未知'}</td>
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count || 0)}</td>
</tr>
`;
}
tableBody.innerHTML = html;
}
// 更新最近屏蔽域名表格
function updateRecentBlockedTable(domains) {
const tableBody = document.getElementById('recent-blocked-table');
if (!domains || domains.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>
</tr>
`;
return;
}
let html = '';
for (const domain of domains) {
const time = formatTime(domain.timestamp || Date.now());
html += `
<tr class="border-b border-gray-200 hover:bg-gray-50">
<td class="py-3 px-4 text-sm">${domain.name || '未知'}</td>
<td class="py-3 px-4 text-sm text-right text-gray-500">${time}</td>
</tr>
`;
}
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 = `
<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');
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);
}
});
});