diff --git a/static/js/dashboard.js b/static/js/dashboard.js
index b056494..b9395d0 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -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 = `
暂无数据 | `;
- tableBody.appendChild(row);
+ if (!domains || domains.length === 0) {
+ tableBody.innerHTML = `
+
+ | 暂无数据 |
+
+ `;
return;
}
- data.forEach(item => {
- const row = document.createElement('tr');
- row.className = 'border-b border-gray-100 hover:bg-gray-50';
- row.innerHTML = `
- ${item.domain} |
- ${formatNumber(item.count)} |
+ let html = '';
+ for (const domain of domains) {
+ html += `
+
+ | ${domain.name || '未知'} |
+ ${formatNumber(domain.count || 0)} |
+
`;
- 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 = `暂无数据 | `;
- tableBody.appendChild(row);
+ if (!domains || domains.length === 0) {
+ tableBody.innerHTML = `
+
+ | 暂无数据 |
+
+ `;
return;
}
- data.forEach(item => {
- const row = document.createElement('tr');
- row.className = 'border-b border-gray-100 hover:bg-gray-50';
- row.innerHTML = `
- ${item.domain} |
- ${item.time} |
+ let html = '';
+ for (const domain of domains) {
+ const time = formatTime(domain.timestamp || Date.now());
+ html += `
+
+ | ${domain.name || '未知'} |
+ ${time} |
+
`;
- 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,
- 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
- }]
+ 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: {
- 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
+ }
+ },
+ x: {
+ grid: {
+ display: false
}
}
- },
- interaction: {
- intersect: false,
- mode: 'index'
}
}
});
-}
-
-// 更新比例图表
-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 = `
-
- ${message}
+// 更新运行状态
+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(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(() => {
+ notification.classList.add('translate-y-0', 'opacity-0');
setTimeout(() => {
- document.body.removeChild(errorElement);
+ notification.remove();
}, 300);
}, 3000);
}
-// 定期刷新数据
-setInterval(loadDashboardData, 30000); // 每30秒刷新一次
\ No newline at end of file
+// 页面切换处理
+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);
+ }
+ });
+});
\ No newline at end of file