555 lines
18 KiB
JavaScript
555 lines
18 KiB
JavaScript
// 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);
|
||
}
|
||
});
|
||
}); |