更新beta2
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
// dashboard.js - 仪表盘页面功能
|
||||
// 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 [statsData, topBlockedData, recentBlockedData, hourlyStatsData] = await Promise.all([
|
||||
// 并行请求所有数据
|
||||
const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([
|
||||
api.getStats(),
|
||||
api.getTopBlockedDomains(),
|
||||
api.getRecentBlockedDomains(),
|
||||
@@ -12,195 +34,161 @@ async function loadDashboardData() {
|
||||
]);
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatsCards(statsData);
|
||||
updateStatsCards(stats);
|
||||
|
||||
// 更新表格数据
|
||||
updateTopBlockedTable(topBlockedData);
|
||||
updateRecentBlockedTable(recentBlockedData);
|
||||
// 更新数据表格
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
|
||||
// 更新图表
|
||||
updateQueryTrendChart(hourlyStatsData);
|
||||
updateRatioChart(statsData);
|
||||
updateCharts(stats, hourlyStats);
|
||||
|
||||
// 更新运行状态
|
||||
updateUptime();
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
showErrorMessage('数据加载失败,请刷新页面重试');
|
||||
// 静默失败,不显示通知以免打扰用户
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
function updateStatsCards(data) {
|
||||
const dnsStats = data.dns;
|
||||
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(dnsStats.Queries);
|
||||
// 更新数量显示
|
||||
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);
|
||||
|
||||
// 更新屏蔽数量
|
||||
document.getElementById('blocked-queries').textContent = formatNumber(dnsStats.Blocked);
|
||||
|
||||
// 更新正常解析数量
|
||||
document.getElementById('allowed-queries').textContent = formatNumber(dnsStats.Allowed);
|
||||
|
||||
// 更新错误数量
|
||||
document.getElementById('error-queries').textContent = formatNumber(dnsStats.Errors);
|
||||
|
||||
// 计算百分比(简化计算,实际可能需要与历史数据比较)
|
||||
if (dnsStats.Queries > 0) {
|
||||
const blockedPercent = Math.round((dnsStats.Blocked / dnsStats.Queries) * 100);
|
||||
const allowedPercent = Math.round((dnsStats.Allowed / dnsStats.Queries) * 100);
|
||||
const errorPercent = Math.round((dnsStats.Errors / dnsStats.Queries) * 100);
|
||||
|
||||
document.getElementById('blocked-percent').textContent = `${blockedPercent}%`;
|
||||
document.getElementById('allowed-percent').textContent = `${allowedPercent}%`;
|
||||
document.getElementById('error-percent').textContent = `${errorPercent}%`;
|
||||
document.getElementById('queries-percent').textContent = '100%';
|
||||
}
|
||||
// 更新百分比(模拟数据,实际应该从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%';
|
||||
}
|
||||
|
||||
// 更新最常屏蔽域名表格
|
||||
function updateTopBlockedTable(data) {
|
||||
// 更新Top屏蔽域名表格
|
||||
function updateTopBlockedTable(domains) {
|
||||
const tableBody = document.getElementById('top-blocked-table');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>`;
|
||||
tableBody.appendChild(row);
|
||||
if (!domains || domains.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-100 hover:bg-gray-50';
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4 text-sm text-gray-800">${item.domain}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-800 text-right">${formatNumber(item.count)}</td>
|
||||
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.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新最近屏蔽域名表格
|
||||
function updateRecentBlockedTable(data) {
|
||||
function updateRecentBlockedTable(domains) {
|
||||
const tableBody = document.getElementById('recent-blocked-table');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>`;
|
||||
tableBody.appendChild(row);
|
||||
if (!domains || domains.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="2" class="py-4 text-center text-gray-500">暂无数据</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-100 hover:bg-gray-50';
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4 text-sm text-gray-800">${item.domain}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-500 text-right">${item.time}</td>
|
||||
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.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新查询趋势图表
|
||||
function updateQueryTrendChart(data) {
|
||||
const ctx = document.getElementById('query-trend-chart').getContext('2d');
|
||||
|
||||
// 创建图表
|
||||
new Chart(ctx, {
|
||||
// 初始化图表
|
||||
function initCharts() {
|
||||
// 初始化查询趋势图表
|
||||
const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d');
|
||||
queryTrendChart = new Chart(queryTrendCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: '查询数量',
|
||||
data: data.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)',
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointBackgroundColor: '#FFFFFF',
|
||||
pointBorderColor: '#165DFF',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6
|
||||
}]
|
||||
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: {
|
||||
display: false
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
weight: 'bold'
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13
|
||||
}
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 12
|
||||
},
|
||||
callback: function(value) {
|
||||
return formatNumber(value);
|
||||
}
|
||||
}
|
||||
drawBorder: false
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新比例图表
|
||||
function updateRatioChart(data) {
|
||||
const dnsStats = data.dns;
|
||||
const ctx = document.getElementById('ratio-chart').getContext('2d');
|
||||
|
||||
// 准备数据
|
||||
const chartData = [dnsStats.Allowed, dnsStats.Blocked, dnsStats.Errors];
|
||||
const chartColors = ['#00B42A', '#F53F3F', '#FF7D00'];
|
||||
const chartLabels = ['正常解析', '屏蔽', '错误'];
|
||||
|
||||
// 创建图表
|
||||
new Chart(ctx, {
|
||||
// 初始化比例图表
|
||||
const ratioCtx = document.getElementById('ratio-chart').getContext('2d');
|
||||
ratioChart = new Chart(ratioCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: chartLabels,
|
||||
labels: ['正常解析', '被屏蔽', '错误'],
|
||||
datasets: [{
|
||||
data: chartData,
|
||||
backgroundColor: chartColors,
|
||||
borderWidth: 0,
|
||||
hoverOffset: 10
|
||||
data: [70, 25, 5],
|
||||
backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
@@ -208,26 +196,7 @@ function updateRatioChart(data) {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',',\,
|
||||
labels: {
|
||||
padding: 20,
|
||||
font: {
|
||||
size: 13
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const value = context.parsed;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||
return `${context.label}: ${formatNumber(value)} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
position: 'bottom',
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
@@ -235,36 +204,205 @@ function updateRatioChart(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
// 更新图表数据
|
||||
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();
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showErrorMessage(message) {
|
||||
// 创建错误消息元素
|
||||
const errorElement = document.createElement('div');
|
||||
errorElement.className = 'fixed bottom-4 right-4 bg-danger text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center';
|
||||
errorElement.innerHTML = `
|
||||
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||
// 更新运行状态
|
||||
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(errorElement);
|
||||
// 添加到页面
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3秒后自动移除
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
errorElement.classList.add('opacity-0', 'transition-opacity', 'duration-300');
|
||||
notification.classList.remove('translate-y-0', 'opacity-0');
|
||||
notification.classList.add('-translate-y-2', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// 自动关闭
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(errorElement);
|
||||
notification.classList.add('translate-y-0', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 定期刷新数据
|
||||
setInterval(loadDashboardData, 30000); // 每30秒刷新一次
|
||||
// 页面切换处理
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user