Files
dns-server/static/js/dashboard.js
2025-11-25 15:35:53 +08:00

555 lines
18 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 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;
}
// 更新表格
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 += `
<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)}</td>
</tr>
`;
}
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 += `
<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;
}
// 当前选中的时间范围
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 = `
<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);
}
});
});