修复主题
This commit is contained in:
0
static/js/about.js
Normal file
0
static/js/about.js
Normal file
@@ -115,7 +115,7 @@ function fetchHostsCount() {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
}
|
||||
|
||||
// 通用API请求函数 - 添加错误处理、重试机制和缓存
|
||||
// 通用API请求函数 - 添加错误处理和重试机制
|
||||
function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -129,7 +129,6 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
|
||||
// 处理请求URL和参数
|
||||
let url = `${API_BASE_URL}${endpoint}`;
|
||||
let cacheKey = null;
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
@@ -139,26 +138,10 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
params.append(key, data[key]);
|
||||
});
|
||||
url += `?${params.toString()}`;
|
||||
// 生成缓存键
|
||||
cacheKey = `${endpoint}_${params.toString()}`;
|
||||
} else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
||||
// 为其他方法设置body
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
} else {
|
||||
// 无参数的GET请求
|
||||
if (method === 'GET') {
|
||||
cacheKey = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从缓存获取响应(仅GET请求)
|
||||
if (method === 'GET' && cacheKey && window.memoryManager) {
|
||||
const cachedResponse = memoryManager.getCacheItem('apiResponses', cacheKey);
|
||||
if (cachedResponse) {
|
||||
console.log('从缓存获取API响应:', cacheKey);
|
||||
return Promise.resolve(cachedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
let retries = 0;
|
||||
@@ -176,14 +159,7 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
// 使用.text()先获取响应文本,处理可能的JSON解析错误
|
||||
return response.text().then(text => {
|
||||
try {
|
||||
const responseData = JSON.parse(text);
|
||||
|
||||
// 缓存GET请求的响应
|
||||
if (method === 'GET' && cacheKey && window.memoryManager) {
|
||||
memoryManager.addCacheItem('apiResponses', cacheKey, responseData);
|
||||
}
|
||||
|
||||
return responseData;
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('JSON解析错误:', e, '响应文本:', text);
|
||||
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试
|
||||
|
||||
@@ -366,13 +366,31 @@ function showNotification(message, type = 'info') {
|
||||
|
||||
// 初始化GFWList管理页面
|
||||
function initGFWListPage() {
|
||||
// 加载配置但不显示,因为功能未开发
|
||||
// loadGFWListConfig();
|
||||
// 显示"正在开发中"的提示
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待', 'info');
|
||||
// 禁用所有按钮和输入框
|
||||
disableGFWListElements();
|
||||
setupGFWListEventListeners();
|
||||
// 提示功能待开发
|
||||
setTimeout(() => {
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待!', 'info');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 禁用GFWList页面的所有元素
|
||||
function disableGFWListElements() {
|
||||
const gfwlistContent = document.getElementById('gfwlist-content');
|
||||
if (!gfwlistContent) return;
|
||||
|
||||
// 禁用所有按钮
|
||||
const buttons = gfwlistContent.querySelectorAll('button');
|
||||
buttons.forEach(button => {
|
||||
button.disabled = true;
|
||||
button.classList.add('cursor-not-allowed', 'opacity-50');
|
||||
});
|
||||
|
||||
// 禁用所有输入框
|
||||
const inputs = gfwlistContent.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.disabled = true;
|
||||
input.classList.add('opacity-50');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载GFWList配置
|
||||
@@ -420,8 +438,21 @@ function populateGFWListForm(config) {
|
||||
|
||||
// 保存GFWList配置
|
||||
async function handleSaveGFWListConfig() {
|
||||
// 提示功能待开发
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待!', 'info');
|
||||
const formData = collectGFWListFormData();
|
||||
if (!formData) return;
|
||||
|
||||
try {
|
||||
const result = await api.saveConfig(formData);
|
||||
|
||||
if (result && result.error) {
|
||||
showErrorMessage('保存配置失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage('配置保存成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 收集GFWList表单数据
|
||||
@@ -506,16 +537,24 @@ async function handleRestartGFWListService() {
|
||||
function setupGFWListEventListeners() {
|
||||
const saveBtn = getElement('gfwlist-save-btn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', handleSaveGFWListConfig);
|
||||
saveBtn.addEventListener('click', function() {
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// 为所有按钮式开关添加点击事件监听器
|
||||
const toggleBtns = document.querySelectorAll('.toggle-btn');
|
||||
toggleBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// 提示功能待开发
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待!', 'info');
|
||||
// 不执行实际操作
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待', 'info');
|
||||
});
|
||||
});
|
||||
|
||||
// 为所有输入框添加点击事件监听器
|
||||
const inputs = document.querySelectorAll('#gfwlist-content input');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('click', function() {
|
||||
showNotification('GFWList管理功能正在开发中,敬请期待', 'info');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,38 +18,6 @@ window.dashboardHistoryData = window.dashboardHistoryData || {
|
||||
prevActiveIPs: null,
|
||||
prevTopQueryTypeCount: null
|
||||
};
|
||||
|
||||
// 图表实例管理
|
||||
function cleanupChartInstances() {
|
||||
// 销毁现有图表实例
|
||||
if (ratioChart) {
|
||||
ratioChart.destroy();
|
||||
ratioChart = null;
|
||||
}
|
||||
if (dnsRequestsChart) {
|
||||
dnsRequestsChart.destroy();
|
||||
dnsRequestsChart = null;
|
||||
}
|
||||
if (detailedDnsRequestsChart) {
|
||||
detailedDnsRequestsChart.destroy();
|
||||
detailedDnsRequestsChart = null;
|
||||
}
|
||||
if (queryTypeChart) {
|
||||
queryTypeChart.destroy();
|
||||
queryTypeChart = null;
|
||||
}
|
||||
|
||||
// 销毁统计卡片图表实例
|
||||
for (const key in statCardCharts) {
|
||||
if (statCardCharts[key]) {
|
||||
statCardCharts[key].destroy();
|
||||
delete statCardCharts[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 清空统计卡片历史数据
|
||||
statCardHistoryData = {};
|
||||
}
|
||||
// 节流相关变量
|
||||
let lastProcessedTime = 0;
|
||||
const PROCESS_THROTTLE_INTERVAL = 1000; // 1秒节流间隔
|
||||
@@ -66,10 +34,6 @@ let errorQueries = 0;
|
||||
// 初始化仪表盘
|
||||
async function initDashboard() {
|
||||
try {
|
||||
// 初始化内存管理器
|
||||
if (window.memoryManager && typeof window.memoryManager.init === 'function') {
|
||||
window.memoryManager.init();
|
||||
}
|
||||
|
||||
|
||||
// 优先加载初始数据,确保页面显示最新信息
|
||||
@@ -99,21 +63,6 @@ function connectWebSocket() {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
|
||||
|
||||
// 确保先关闭现有连接
|
||||
if (dashboardWsConnection) {
|
||||
try {
|
||||
dashboardWsConnection.close();
|
||||
} catch (e) {
|
||||
console.error('关闭现有WebSocket连接失败:', e);
|
||||
}
|
||||
dashboardWsConnection = null;
|
||||
}
|
||||
|
||||
// 清除现有重连计时器
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
}
|
||||
|
||||
// 创建WebSocket连接
|
||||
dashboardWsConnection = new WebSocket(wsUrl);
|
||||
@@ -121,6 +70,12 @@ function connectWebSocket() {
|
||||
// 连接打开事件
|
||||
dashboardWsConnection.onopen = function() {
|
||||
showNotification('数据更新成功', 'success');
|
||||
|
||||
// 清除重连计时器
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 接收消息事件
|
||||
@@ -453,11 +408,7 @@ function fallbackToIntervalRefresh() {
|
||||
function cleanupResources() {
|
||||
// 清除WebSocket连接
|
||||
if (dashboardWsConnection) {
|
||||
try {
|
||||
dashboardWsConnection.close();
|
||||
} catch (e) {
|
||||
console.error('关闭WebSocket连接失败:', e);
|
||||
}
|
||||
dashboardWsConnection.close();
|
||||
dashboardWsConnection = null;
|
||||
}
|
||||
|
||||
@@ -474,392 +425,36 @@ function cleanupResources() {
|
||||
}
|
||||
|
||||
// 清除图表实例,释放内存
|
||||
cleanupChartInstances();
|
||||
if (ratioChart) {
|
||||
ratioChart.destroy();
|
||||
ratioChart = null;
|
||||
}
|
||||
if (dnsRequestsChart) {
|
||||
dnsRequestsChart.destroy();
|
||||
dnsRequestsChart = null;
|
||||
}
|
||||
if (detailedDnsRequestsChart) {
|
||||
detailedDnsRequestsChart.destroy();
|
||||
detailedDnsRequestsChart = null;
|
||||
}
|
||||
if (queryTypeChart) {
|
||||
queryTypeChart.destroy();
|
||||
queryTypeChart = null;
|
||||
}
|
||||
|
||||
// 清除统计卡片图表实例
|
||||
for (const key in statCardCharts) {
|
||||
if (statCardCharts[key]) {
|
||||
statCardCharts[key].destroy();
|
||||
delete statCardCharts[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 清除事件监听器
|
||||
window.removeEventListener('beforeunload', cleanupResources);
|
||||
}
|
||||
|
||||
// 加载仪表盘数据
|
||||
async function loadDashboardData() {
|
||||
console.log('开始加载仪表盘数据');
|
||||
try {
|
||||
// 并行获取所有数据,提高加载效率
|
||||
const [stats, queryTypeStatsResult, topBlockedDomainsResult, recentBlockedDomainsResult, topClientsResult] = await Promise.all([
|
||||
// 获取基本统计数据
|
||||
api.getStats().catch(error => {
|
||||
console.error('获取基本统计数据失败:', error);
|
||||
return null;
|
||||
}),
|
||||
// 获取查询类型统计数据
|
||||
api.getQueryTypeStats().catch(() => null),
|
||||
// 获取TOP被屏蔽域名
|
||||
api.getTopBlockedDomains().catch(() => null),
|
||||
// 获取最近屏蔽域名
|
||||
api.getRecentBlockedDomains().catch(() => null),
|
||||
// 获取TOP客户端
|
||||
api.getTopClients().catch(() => null)
|
||||
]);
|
||||
|
||||
// 检查内存使用情况
|
||||
if (window.memoryManager && typeof window.memoryManager.checkMemoryUsage === 'function') {
|
||||
window.memoryManager.checkMemoryUsage();
|
||||
}
|
||||
|
||||
// 确保stats是有效的对象
|
||||
if (!stats || typeof stats !== 'object') {
|
||||
console.error('无效的统计数据:', stats);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('统计数据:', stats);
|
||||
|
||||
// 处理查询类型统计数据
|
||||
let queryTypeStats = null;
|
||||
if (queryTypeStatsResult) {
|
||||
console.log('查询类型统计数据:', queryTypeStatsResult);
|
||||
queryTypeStats = queryTypeStatsResult;
|
||||
} else if (stats.dns && stats.dns.QueryTypes) {
|
||||
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
|
||||
type,
|
||||
count
|
||||
}));
|
||||
console.log('从stats中提取的查询类型统计:', queryTypeStats);
|
||||
}
|
||||
|
||||
// 处理TOP被屏蔽域名
|
||||
let topBlockedDomains = [];
|
||||
if (topBlockedDomainsResult && Array.isArray(topBlockedDomainsResult)) {
|
||||
topBlockedDomains = topBlockedDomainsResult;
|
||||
console.log('TOP被屏蔽域名:', topBlockedDomains);
|
||||
} else {
|
||||
topBlockedDomains = [
|
||||
{ domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() },
|
||||
{ domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() },
|
||||
{ domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() }
|
||||
];
|
||||
}
|
||||
|
||||
// 处理最近屏蔽域名
|
||||
let recentBlockedDomains = [];
|
||||
if (recentBlockedDomainsResult && Array.isArray(recentBlockedDomainsResult)) {
|
||||
recentBlockedDomains = recentBlockedDomainsResult;
|
||||
console.log('最近屏蔽域名:', recentBlockedDomains);
|
||||
} else {
|
||||
recentBlockedDomains = [
|
||||
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() },
|
||||
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }
|
||||
];
|
||||
}
|
||||
|
||||
// 处理TOP客户端
|
||||
let topClients = [];
|
||||
if (topClientsResult && Array.isArray(topClientsResult)) {
|
||||
topClients = topClientsResult;
|
||||
console.log('TOP客户端:', topClients);
|
||||
} else {
|
||||
topClients = [
|
||||
{ ip: '192.168.1.100', count: 100 },
|
||||
{ ip: '192.168.1.101', count: 80 },
|
||||
{ ip: '192.168.1.102', count: 60 }
|
||||
];
|
||||
}
|
||||
|
||||
// 处理TOP域名
|
||||
let topDomains = [];
|
||||
try {
|
||||
const topDomainsResult = await api.getTopDomains().catch(() => null);
|
||||
if (topDomainsResult && Array.isArray(topDomainsResult)) {
|
||||
topDomains = topDomainsResult;
|
||||
console.log('TOP域名:', topDomains);
|
||||
} else {
|
||||
topDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 }
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取TOP域名失败:', error);
|
||||
topDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 }
|
||||
];
|
||||
}
|
||||
|
||||
// 更新主页面的统计卡片数据
|
||||
updateStatsCards(stats);
|
||||
|
||||
// 更新TOP客户端表格
|
||||
updateTopClientsTable(topClients);
|
||||
|
||||
// 更新TOP域名表格
|
||||
updateTopDomainsTable(topDomains);
|
||||
|
||||
// 更新TOP被屏蔽域名表格
|
||||
if (typeof updateTopBlockedDomainsTable === 'function') {
|
||||
updateTopBlockedDomainsTable(topBlockedDomains);
|
||||
}
|
||||
|
||||
// 更新最近屏蔽域名表格
|
||||
if (typeof updateRecentBlockedDomainsTable === 'function') {
|
||||
updateRecentBlockedDomainsTable(recentBlockedDomains);
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
updateCharts(stats, queryTypeStats);
|
||||
|
||||
// 初始化或更新查询类型统计饼图
|
||||
if (queryTypeStats) {
|
||||
drawQueryTypeChart(queryTypeStats);
|
||||
}
|
||||
|
||||
// 更新查询类型统计信息
|
||||
if (document.getElementById('top-query-type')) {
|
||||
const topQueryTypeElement = document.getElementById('top-query-type');
|
||||
const topQueryTypeCountElement = document.getElementById('top-query-type-count');
|
||||
|
||||
// 从stats中获取查询类型统计数据
|
||||
if (stats.dns && stats.dns.QueryTypes) {
|
||||
const queryTypes = stats.dns.QueryTypes;
|
||||
|
||||
// 找出数量最多的查询类型
|
||||
let maxCount = 0;
|
||||
let topType = 'A';
|
||||
|
||||
for (const [type, count] of Object.entries(queryTypes)) {
|
||||
const numCount = Number(count) || 0;
|
||||
if (numCount > maxCount) {
|
||||
maxCount = numCount;
|
||||
topType = type;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新DOM
|
||||
if (topQueryTypeElement) {
|
||||
topQueryTypeElement.textContent = topType;
|
||||
}
|
||||
|
||||
if (topQueryTypeCountElement) {
|
||||
topQueryTypeCountElement.textContent = formatNumber(maxCount);
|
||||
}
|
||||
|
||||
// 保存到历史数据,用于计算趋势
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = maxCount;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新活跃IP信息
|
||||
if (document.getElementById('active-ips')) {
|
||||
const activeIPsElement = document.getElementById('active-ips');
|
||||
|
||||
// 从stats中获取活跃IP数
|
||||
let activeIPs = 0;
|
||||
if (stats.activeIPs !== undefined) {
|
||||
activeIPs = Number(stats.activeIPs) || 0;
|
||||
} else if (stats.dns && stats.dns.ActiveIPs !== undefined) {
|
||||
activeIPs = Number(stats.dns.ActiveIPs) || 0;
|
||||
} else if (stats.dns && stats.dns.SourceIPs) {
|
||||
activeIPs = Object.keys(stats.dns.SourceIPs).length;
|
||||
}
|
||||
|
||||
// 更新DOM
|
||||
if (activeIPsElement) {
|
||||
activeIPsElement.textContent = formatNumber(activeIPs);
|
||||
}
|
||||
|
||||
// 保存到历史数据,用于计算趋势
|
||||
window.dashboardHistoryData.prevActiveIPs = activeIPs;
|
||||
}
|
||||
|
||||
// 更新平均响应时间
|
||||
if (document.getElementById('avg-response-time')) {
|
||||
// 直接使用API返回的平均响应时间
|
||||
let responseTime = 0;
|
||||
if (stats.avgResponseTime !== undefined) {
|
||||
responseTime = Number(stats.avgResponseTime) || 0;
|
||||
} else if (stats.dns && stats.dns.AvgResponseTime !== undefined) {
|
||||
responseTime = Number(stats.dns.AvgResponseTime) || 0;
|
||||
}
|
||||
|
||||
const responseTimeElement = document.getElementById('avg-response-time');
|
||||
if (responseTimeElement) {
|
||||
responseTimeElement.textContent = responseTime.toFixed(2) + 'ms';
|
||||
}
|
||||
|
||||
// 保存到历史数据,用于计算趋势
|
||||
window.dashboardHistoryData.prevResponseTime = responseTime;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
// 显示错误通知
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('加载仪表盘数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制查询类型统计饼图
|
||||
function drawQueryTypeChart(queryTypeStats) {
|
||||
const ctx = document.getElementById('query-type-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// 销毁现有图表
|
||||
if (queryTypeChart) {
|
||||
queryTypeChart.destroy();
|
||||
queryTypeChart = null;
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
const labels = queryTypeStats.map(item => item.type);
|
||||
const data = queryTypeStats.map(item => Number(item.count) || 0);
|
||||
|
||||
// 生成颜色
|
||||
const backgroundColors = [
|
||||
'#3b82f6', // 蓝色
|
||||
'#ef4444', // 红色
|
||||
'#10b981', // 绿色
|
||||
'#f59e0b', // 橙色
|
||||
'#8b5cf6', // 紫色
|
||||
'#ec4899', // 粉色
|
||||
'#6366f1', // 靛蓝色
|
||||
'#14b8a6', // 青色
|
||||
'#f97316', // 橙红色
|
||||
'#84cc16' // 黄绿
|
||||
];
|
||||
|
||||
// 生成悬停时的深色效果
|
||||
const hoverBackgroundColor = backgroundColors.map(color => {
|
||||
const hex = color.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`;
|
||||
});
|
||||
|
||||
// 检查是否为深色模式
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const legendTextColor = isDarkMode ? '#e2e8f0' : '#4B5563';
|
||||
|
||||
// 创建新图表
|
||||
queryTypeChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 0, // 移除边框宽度
|
||||
hoverOffset: 15, // 增加悬停偏移效果,增强交互体验
|
||||
hoverBorderWidth: 0, // 移除悬停时的边框宽度
|
||||
hoverBackgroundColor: hoverBackgroundColor, // 悬停时的深色效果
|
||||
borderRadius: 10, // 添加圆角效果,增强现代感
|
||||
borderSkipped: false // 显示所有边框
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// 简化动画,提高性能
|
||||
animation: {
|
||||
duration: 300, // 缩短动画时间
|
||||
easing: 'easeOutQuart', // 简化缓动函数
|
||||
animateRotate: true, // 仅保留旋转动画
|
||||
animateScale: false // 禁用缩放动画
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
align: 'center',
|
||||
labels: {
|
||||
boxWidth: 12, // 调整图例框的宽度
|
||||
font: {
|
||||
size: 11, // 调整字体大小
|
||||
family: 'Inter, system-ui, sans-serif', // 使用现代字体
|
||||
weight: 500 // 字体粗细
|
||||
},
|
||||
padding: 12, // 调整内边距
|
||||
lineHeight: 1.5, // 调整行高
|
||||
usePointStyle: true, // 使用点样式代替方形图例,节省空间
|
||||
pointStyle: 'circle', // 使用圆形点样式
|
||||
color: legendTextColor, // 根据主题设置图例文本颜色
|
||||
// 启用图例点击交互
|
||||
onClick: function(event, legendItem, legend) {
|
||||
// 切换对应数据的显示
|
||||
const index = legendItem.index;
|
||||
const ci = legend.chart;
|
||||
ci.toggleDataVisibility(index);
|
||||
ci.update();
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(17, 24, 39, 0.9)', // 深背景,增强可读性
|
||||
padding: 12, // 增加内边距
|
||||
titleFont: {
|
||||
size: 13, // 标题字体大小
|
||||
family: 'Inter, system-ui, sans-serif',
|
||||
weight: 600
|
||||
},
|
||||
bodyFont: {
|
||||
size: 12, // 正文字体大小
|
||||
family: 'Inter, system-ui, sans-serif'
|
||||
},
|
||||
bodySpacing: 6, // 正文行间距
|
||||
displayColors: true, // 显示颜色指示器
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.raw || 0;
|
||||
const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
|
||||
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
},
|
||||
cornerRadius: 8, // 圆角
|
||||
boxPadding: 6, // 盒子内边距
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)', // 边框颜色
|
||||
borderWidth: 1 // 边框宽度
|
||||
},
|
||||
title: {
|
||||
display: false // 不显示标题,由HTML标题代替
|
||||
}
|
||||
},
|
||||
cutout: '50%', // 调整中心空白区域比例,使环更粗
|
||||
// 增强元素配置
|
||||
elements: {
|
||||
arc: {
|
||||
borderAlign: 'center',
|
||||
tension: 0.1, // 添加轻微的张力,使圆弧更平滑
|
||||
borderWidth: 3 // 统一边框宽度
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 20, // 增加顶部内边距
|
||||
right: 20,
|
||||
bottom: 30, // 增加底部内边距,为图例预留更多空间
|
||||
left: 20
|
||||
}
|
||||
},
|
||||
// 添加交互配置
|
||||
interaction: {
|
||||
mode: 'nearest', // 交互模式
|
||||
axis: 'x', // 交互轴
|
||||
intersect: false // 不要求精确相交
|
||||
},
|
||||
// 增强悬停效果
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: true,
|
||||
animationDuration: 300 // 悬停动画持续时间
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
function updateStatsCards(stats) {
|
||||
@@ -1049,8 +644,8 @@ function updateStatsCards(stats) {
|
||||
// 标记动画正在进行
|
||||
animationInProgress[elementId] = true;
|
||||
|
||||
// 动画配置 - 优化:使用固定的动画持续时间,减少计算开销
|
||||
const duration = 300; // 固定300ms动画时间,足够流畅且减少计算
|
||||
// 动画配置
|
||||
const duration = Math.min(800, Math.abs(targetValue - currentValue) * 2); // 根据数值变化大小调整动画持续时间
|
||||
const startTime = performance.now();
|
||||
const startValue = currentValue;
|
||||
|
||||
@@ -1073,8 +668,6 @@ function updateStatsCards(stats) {
|
||||
element.textContent = formatNumber(targetValue);
|
||||
// 标记动画完成
|
||||
delete animationInProgress[elementId];
|
||||
// 清理内存
|
||||
element = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1246,7 +839,7 @@ function updateStatsCards(stats) {
|
||||
blockedQueries: 0,
|
||||
allowedQueries: 0,
|
||||
errorQueries: 0,
|
||||
prevResponseTime: null
|
||||
avgResponseTime: 0
|
||||
};
|
||||
|
||||
// 计算百分比并更新箭头
|
||||
@@ -1274,46 +867,19 @@ function updateStatsCards(stats) {
|
||||
|
||||
// 更新平均响应时间的百分比和箭头,使用与其他统计卡片相同的逻辑
|
||||
if (avgResponseTime !== undefined && avgResponseTime !== null) {
|
||||
// 获取历史响应时间
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||
// 计算变化百分比
|
||||
let responsePercent = '0.0%';
|
||||
const prevResponseTime = window.dashboardHistoryData.avgResponseTime || 0;
|
||||
const currentResponseTime = avgResponseTime;
|
||||
|
||||
// 查找箭头元素
|
||||
const responseTimePercentElem = document.getElementById('response-time-percent');
|
||||
let parent = null;
|
||||
let arrowIcon = null;
|
||||
|
||||
if (responseTimePercentElem) {
|
||||
parent = responseTimePercentElem.parentElement;
|
||||
if (parent) {
|
||||
arrowIcon = parent.querySelector('.fa-arrow-up, .fa-arrow-down, .fa-circle');
|
||||
}
|
||||
if (prevResponseTime > 0) {
|
||||
const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// 首次加载时初始化历史数据,不计算趋势
|
||||
if (prevResponseTime === null) {
|
||||
window.dashboardHistoryData.prevResponseTime = currentResponseTime;
|
||||
if (responseTimePercentElem) {
|
||||
responseTimePercentElem.textContent = '0.0%';
|
||||
responseTimePercentElem.className = 'text-sm flex items-center text-gray-500';
|
||||
}
|
||||
if (arrowIcon) {
|
||||
arrowIcon.className = 'fa fa-circle mr-1 text-xs';
|
||||
parent.className = 'text-gray-500 text-sm flex items-center';
|
||||
}
|
||||
} else {
|
||||
// 计算变化百分比
|
||||
let responsePercent = '0.0%';
|
||||
|
||||
if (prevResponseTime > 0) {
|
||||
const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
|
||||
// updatePercentWithArrow函数内部已添加响应时间的特殊处理
|
||||
updatePercentWithArrow('response-time-percent', responsePercent, prevResponseTime, currentResponseTime);
|
||||
}
|
||||
// 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
|
||||
// updatePercentWithArrow函数内部已添加响应时间的特殊处理
|
||||
updatePercentWithArrow('response-time-percent', responsePercent, prevResponseTime, currentResponseTime);
|
||||
} else {
|
||||
updatePercentage('response-time-percent', '---');
|
||||
}
|
||||
@@ -1324,9 +890,9 @@ function updateStatsCards(stats) {
|
||||
window.dashboardHistoryData.blockedQueries = blockedQueries;
|
||||
window.dashboardHistoryData.allowedQueries = allowedQueries;
|
||||
window.dashboardHistoryData.errorQueries = errorQueries;
|
||||
// 只有当prevResponseTime不为null时才更新,避免覆盖首次加载时设置的值
|
||||
if (window.dashboardHistoryData.prevResponseTime !== null) {
|
||||
window.dashboardHistoryData.prevResponseTime = avgResponseTime;
|
||||
// 只在avgResponseTime不为0时更新历史数据,保留上一次不为0的状态
|
||||
if (avgResponseTime > 0) {
|
||||
window.dashboardHistoryData.avgResponseTime = avgResponseTime;
|
||||
}
|
||||
|
||||
|
||||
@@ -1714,38 +1280,30 @@ async function updateTopClientsTable(clients) {
|
||||
];
|
||||
}
|
||||
|
||||
// 使用DocumentFragment批量处理DOM操作
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const client = tableData[i];
|
||||
// 获取IP地理信息
|
||||
const location = await getIpGeolocation(client.ip);
|
||||
|
||||
// 创建容器元素
|
||||
const container = document.createElement('div');
|
||||
container.className = 'flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary';
|
||||
|
||||
// 创建内容
|
||||
container.innerHTML = `
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium truncate">${client.ip}</span>
|
||||
<span class="text-xs text-gray-500">${location}</span>
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium truncate">${client.ip}</span>
|
||||
<span class="text-xs text-gray-500">${location}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||
`;
|
||||
|
||||
fragment.appendChild(container);
|
||||
}
|
||||
|
||||
// 清空表格并添加新内容
|
||||
tableBody.innerHTML = '';
|
||||
tableBody.appendChild(fragment);
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新请求域名排行表格
|
||||
@@ -1790,66 +1348,59 @@ async function updateTopDomainsTable(domains) {
|
||||
return sum + (typeof domain.count === 'number' ? domain.count : 0);
|
||||
}, 0);
|
||||
|
||||
// 使用DocumentFragment批量处理DOM操作
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const domain = tableData[i];
|
||||
// 检查域名是否是跟踪器
|
||||
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
const trackerTooltip = isTracker ? `
|
||||
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||
<div class="font-semibold mb-2">已知跟踪器</div>
|
||||
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
|
||||
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
|
||||
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// 计算百分比
|
||||
const percentage = totalCount > 0 && typeof domain.count === 'number'
|
||||
? ((domain.count / totalCount) * 100).toFixed(2)
|
||||
: '0.00';
|
||||
|
||||
// 创建容器元素
|
||||
const container = document.createElement('div');
|
||||
container.className = 'p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success';
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
const trackerContent = isTracker ? `
|
||||
<div class="tracker-icon-container relative ml-2">
|
||||
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm hidden">
|
||||
<div class="font-semibold mb-2">已知跟踪器</div>
|
||||
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
|
||||
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
|
||||
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// 创建内容
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
||||
html += `
|
||||
<div class="p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
||||
${trackerContent}
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
||||
${isTracker ? `
|
||||
<div class="tracker-icon-container relative ml-2">
|
||||
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center space-x-2">
|
||||
<span class="flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
||||
<span class="text-xs text-gray-500">${percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex items-center space-x-2">
|
||||
<span class="flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
||||
<span class="text-xs text-gray-500">${percentage}%</span>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-success h-2.5 rounded-full" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-success h-2.5 rounded-full" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fragment.appendChild(container);
|
||||
}
|
||||
|
||||
// 清空表格并添加新内容
|
||||
tableBody.innerHTML = '';
|
||||
tableBody.appendChild(fragment);
|
||||
tableBody.innerHTML = html;
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
|
||||
@@ -2034,13 +1585,6 @@ function initCharts() {
|
||||
console.error('未找到比例图表元素');
|
||||
return;
|
||||
}
|
||||
|
||||
// 销毁现有图表
|
||||
if (ratioChart) {
|
||||
ratioChart.destroy();
|
||||
ratioChart = null;
|
||||
}
|
||||
|
||||
const ratioCtx = ratioChartElement.getContext('2d');
|
||||
ratioChart = new Chart(ratioCtx, {
|
||||
type: 'doughnut',
|
||||
@@ -2156,12 +1700,6 @@ function initCharts() {
|
||||
// 初始化解析类型统计饼图
|
||||
const queryTypeChartElement = document.getElementById('query-type-chart');
|
||||
if (queryTypeChartElement) {
|
||||
// 销毁现有图表
|
||||
if (queryTypeChart) {
|
||||
queryTypeChart.destroy();
|
||||
queryTypeChart = null;
|
||||
}
|
||||
|
||||
const queryTypeCtx = queryTypeChartElement.getContext('2d');
|
||||
// 预定义的颜色数组,用于解析类型
|
||||
const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e'];
|
||||
@@ -3490,6 +3028,15 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
});// 重写loadDashboardData函数,修复语法错误
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
// 检查是否有有效的缓存数据
|
||||
const cachedData = window.pageDataCache && window.pageDataCache.getCache('dashboard');
|
||||
if (cachedData) {
|
||||
console.log('使用缓存的仪表盘数据');
|
||||
// 处理缓存数据
|
||||
processDashboardData(cachedData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 并行获取所有数据,提高加载效率
|
||||
const [stats, queryTypeStatsResult, topBlockedDomainsResult, recentBlockedDomainsResult, topClientsResult] = await Promise.all([
|
||||
// 获取基本统计数据
|
||||
@@ -3712,12 +3259,143 @@ async function loadDashboardData() {
|
||||
|
||||
// 确保TOP域名数据被正确加载
|
||||
updateTopData();
|
||||
|
||||
// 存储数据到缓存
|
||||
const dashboardData = {
|
||||
stats,
|
||||
queryTypeStats,
|
||||
topClients,
|
||||
topDomains,
|
||||
topBlockedDomains,
|
||||
recentBlockedDomains
|
||||
};
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache('dashboard', dashboardData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
// 静默失败,不显示通知以免打扰用户
|
||||
}
|
||||
}
|
||||
|
||||
// 处理缓存的仪表盘数据
|
||||
function processDashboardData(cachedData) {
|
||||
try {
|
||||
const { stats, queryTypeStats, topClients, topDomains, topBlockedDomains, recentBlockedDomains } = cachedData;
|
||||
|
||||
// 确保stats是有效的对象
|
||||
if (!stats || typeof stats !== 'object') {
|
||||
console.error('无效的缓存统计数据:', stats);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新主页面的统计卡片数据
|
||||
updateStatsCards(stats);
|
||||
|
||||
// 更新TOP客户端表格
|
||||
updateTopClientsTable(topClients);
|
||||
|
||||
// 更新TOP域名表格
|
||||
updateTopDomainsTable(topDomains);
|
||||
|
||||
// 更新TOP被屏蔽域名表格
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
|
||||
// 更新最近屏蔽域名表格
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
|
||||
// 更新图表
|
||||
updateCharts(stats, queryTypeStats);
|
||||
|
||||
// 初始化或更新查询类型统计饼图
|
||||
if (queryTypeStats) {
|
||||
drawQueryTypeChart(queryTypeStats);
|
||||
}
|
||||
|
||||
// 更新查询类型统计信息
|
||||
if (document.getElementById('top-query-type')) {
|
||||
const topQueryTypeElement = document.getElementById('top-query-type');
|
||||
const topQueryTypeCountElement = document.getElementById('top-query-type-count');
|
||||
|
||||
// 从stats中获取查询类型统计数据
|
||||
if (stats.dns && stats.dns.QueryTypes) {
|
||||
const queryTypes = stats.dns.QueryTypes;
|
||||
|
||||
// 找出数量最多的查询类型
|
||||
let maxCount = 0;
|
||||
let topType = 'A';
|
||||
|
||||
for (const [type, count] of Object.entries(queryTypes)) {
|
||||
const numCount = Number(count) || 0;
|
||||
if (numCount > maxCount) {
|
||||
maxCount = numCount;
|
||||
topType = type;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新DOM
|
||||
if (topQueryTypeElement) {
|
||||
topQueryTypeElement.textContent = topType;
|
||||
}
|
||||
|
||||
if (topQueryTypeCountElement) {
|
||||
topQueryTypeCountElement.textContent = formatNumber(maxCount);
|
||||
}
|
||||
|
||||
// 保存到历史数据,用于计算趋势
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = maxCount;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新活跃IP信息
|
||||
if (document.getElementById('active-ips')) {
|
||||
const activeIPsElement = document.getElementById('active-ips');
|
||||
|
||||
// 从stats中获取活跃IP数
|
||||
let activeIPs = 0;
|
||||
if (stats.dns && stats.dns.ActiveIPs) {
|
||||
activeIPs = stats.dns.ActiveIPs;
|
||||
} else if (stats.activeIPs !== undefined) {
|
||||
activeIPs = stats.activeIPs;
|
||||
}
|
||||
|
||||
// 更新DOM
|
||||
if (activeIPsElement) {
|
||||
activeIPsElement.textContent = formatNumber(activeIPs);
|
||||
}
|
||||
|
||||
// 保存到历史数据,用于计算趋势
|
||||
window.dashboardHistoryData.prevActiveIPs = activeIPs;
|
||||
}
|
||||
|
||||
// 更新平均响应时间
|
||||
if (document.getElementById('avg-response-time')) {
|
||||
// 直接使用缓存的平均响应时间
|
||||
let responseTime = 0;
|
||||
if (stats.dns && stats.dns.AvgResponseTime) {
|
||||
responseTime = stats.dns.AvgResponseTime;
|
||||
} else if (stats.avgResponseTime !== undefined) {
|
||||
responseTime = stats.avgResponseTime;
|
||||
} else if (stats.responseTime) {
|
||||
responseTime = stats.responseTime;
|
||||
}
|
||||
|
||||
if (responseTime > 0 && statCardCharts['response-time-chart']) {
|
||||
// 限制小数位数为两位并更新图表
|
||||
updateChartData('response-time-chart', parseFloat(responseTime).toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新运行状态
|
||||
updateUptime();
|
||||
|
||||
// 确保TOP域名数据被正确加载
|
||||
updateTopData();
|
||||
} catch (error) {
|
||||
console.error('处理缓存仪表盘数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 主题切换功能
|
||||
function initThemeToggle() {
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
1007
static/js/logs.js
1007
static/js/logs.js
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,84 @@
|
||||
// main.js - 主脚本文件
|
||||
|
||||
// 页面数据缓存对象
|
||||
const pageDataCache = {
|
||||
// 页面初始化状态
|
||||
initializedPages: {},
|
||||
// 页面数据缓存
|
||||
data: {
|
||||
dashboard: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 5 * 60 * 1000 // 5分钟过期
|
||||
},
|
||||
logs: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 5 * 60 * 1000 // 5分钟过期
|
||||
},
|
||||
shield: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
},
|
||||
hosts: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
},
|
||||
gfwlist: {
|
||||
timestamp: 0,
|
||||
data: null,
|
||||
expiry: 10 * 60 * 1000 // 10分钟过期
|
||||
}
|
||||
},
|
||||
|
||||
// 检查缓存是否有效
|
||||
isCacheValid: function(page) {
|
||||
const cache = this.data[page];
|
||||
if (!cache || !cache.data) return false;
|
||||
const now = Date.now();
|
||||
return (now - cache.timestamp) < cache.expiry;
|
||||
},
|
||||
|
||||
// 获取缓存数据
|
||||
getCache: function(page) {
|
||||
if (this.isCacheValid(page)) {
|
||||
return this.data[page].data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// 设置缓存数据
|
||||
setCache: function(page, data) {
|
||||
if (this.data[page]) {
|
||||
this.data[page].data = data;
|
||||
this.data[page].timestamp = Date.now();
|
||||
}
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
clearCache: function(page) {
|
||||
if (this.data[page]) {
|
||||
this.data[page].data = null;
|
||||
this.data[page].timestamp = 0;
|
||||
}
|
||||
},
|
||||
|
||||
// 标记页面已初始化
|
||||
markPageInitialized: function(page) {
|
||||
this.initializedPages[page] = true;
|
||||
},
|
||||
|
||||
// 检查页面是否已初始化
|
||||
isPageInitialized: function(page) {
|
||||
return this.initializedPages[page] || false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面可见性状态
|
||||
let isPageVisible = true;
|
||||
|
||||
// 页面初始化函数 - 根据当前hash值初始化对应页面
|
||||
function initPageByHash() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
@@ -46,31 +125,51 @@ function initPageByHash() {
|
||||
if (hash === 'shield') {
|
||||
setTimeout(() => {
|
||||
if (typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
// 检查页面是否已经初始化
|
||||
if (!pageDataCache.isPageInitialized('shield') || !pageDataCache.isCacheValid('shield')) {
|
||||
initShieldPage();
|
||||
pageDataCache.markPageInitialized('shield');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'hosts') {
|
||||
setTimeout(() => {
|
||||
if (typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
// 检查页面是否已经初始化
|
||||
if (!pageDataCache.isPageInitialized('hosts') || !pageDataCache.isCacheValid('hosts')) {
|
||||
initHostsPage();
|
||||
pageDataCache.markPageInitialized('hosts');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'gfwlist') {
|
||||
setTimeout(() => {
|
||||
if (typeof initGFWListPage === 'function') {
|
||||
initGFWListPage();
|
||||
// 检查页面是否已经初始化
|
||||
if (!pageDataCache.isPageInitialized('gfwlist') || !pageDataCache.isCacheValid('gfwlist')) {
|
||||
initGFWListPage();
|
||||
pageDataCache.markPageInitialized('gfwlist');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'logs') {
|
||||
setTimeout(() => {
|
||||
if (typeof initLogsPage === 'function') {
|
||||
initLogsPage();
|
||||
// 检查页面是否已经初始化
|
||||
if (!pageDataCache.isPageInitialized('logs') || !pageDataCache.isCacheValid('logs')) {
|
||||
initLogsPage();
|
||||
pageDataCache.markPageInitialized('logs');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'dashboard') {
|
||||
setTimeout(() => {
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
// 检查页面是否已经初始化
|
||||
if (!pageDataCache.isPageInitialized('dashboard') || !pageDataCache.isCacheValid('dashboard')) {
|
||||
loadDashboardData();
|
||||
pageDataCache.markPageInitialized('dashboard');
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -407,6 +506,39 @@ function updateThemeIcon(toggleElement, isDark) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
isPageVisible = true;
|
||||
console.log('页面变为可见');
|
||||
|
||||
// 当页面重新可见时,检查当前页面是否需要刷新数据
|
||||
const hash = window.location.hash.substring(1);
|
||||
|
||||
// 只有当缓存过期时才重新加载数据
|
||||
if (hash && !pageDataCache.isCacheValid(hash)) {
|
||||
console.log(`缓存已过期,重新加载${hash}页面数据`);
|
||||
|
||||
// 根据当前页面类型重新加载数据
|
||||
if (hash === 'dashboard' && typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
} else if (hash === 'logs' && typeof loadLogs === 'function') {
|
||||
loadLogs();
|
||||
} else if (hash === 'shield' && typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
} else if (hash === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统状态
|
||||
updateSystemStatus();
|
||||
} else {
|
||||
isPageVisible = false;
|
||||
console.log('页面变为隐藏');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
@@ -424,6 +556,9 @@ function init() {
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,14 @@ function animateCounter(element, target, duration = 1000) {
|
||||
|
||||
// 加载屏蔽规则统计信息
|
||||
async function loadShieldStats() {
|
||||
// 检查是否有有效的缓存数据
|
||||
const cachedStats = window.pageDataCache && window.pageDataCache.getCache('shield_stats');
|
||||
if (cachedStats) {
|
||||
console.log('使用缓存的屏蔽规则统计信息');
|
||||
updateShieldStatsUI(cachedStats);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取屏蔽规则统计信息
|
||||
const shieldResponse = await fetch('/api/shield');
|
||||
@@ -203,27 +211,18 @@ async function loadShieldStats() {
|
||||
const blacklists = await blacklistsResponse.json();
|
||||
const disabledBlacklistCount = blacklists.filter(blacklist => !blacklist.enabled).length;
|
||||
|
||||
// 组合统计数据
|
||||
const shieldStats = {
|
||||
...stats,
|
||||
disabledBlacklistCount
|
||||
};
|
||||
|
||||
// 更新统计信息
|
||||
const elements = [
|
||||
{ id: 'domain-rules-count', value: stats.domainRulesCount },
|
||||
{ id: 'domain-exceptions-count', value: stats.domainExceptionsCount },
|
||||
{ id: 'regex-rules-count', value: stats.regexRulesCount },
|
||||
{ id: 'regex-exceptions-count', value: stats.regexExceptionsCount },
|
||||
{ id: 'hosts-rules-count', value: stats.hostsRulesCount },
|
||||
{ id: 'blacklist-count', value: stats.blacklistCount }
|
||||
];
|
||||
updateShieldStatsUI(shieldStats);
|
||||
|
||||
elements.forEach(item => {
|
||||
const element = document.getElementById(item.id);
|
||||
if (element) {
|
||||
animateCounter(element, item.value || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新禁用黑名单数量
|
||||
const disabledBlacklistElement = document.getElementById('blacklist-disabled-count');
|
||||
if (disabledBlacklistElement) {
|
||||
animateCounter(disabledBlacklistElement, disabledBlacklistCount);
|
||||
// 存储数据到缓存
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache('shield_stats', shieldStats);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载屏蔽规则统计信息失败:', error);
|
||||
@@ -231,8 +230,44 @@ async function loadShieldStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新屏蔽规则统计信息UI
|
||||
function updateShieldStatsUI(stats) {
|
||||
if (!stats) return;
|
||||
|
||||
// 更新统计信息
|
||||
const elements = [
|
||||
{ id: 'domain-rules-count', value: stats.domainRulesCount },
|
||||
{ id: 'domain-exceptions-count', value: stats.domainExceptionsCount },
|
||||
{ id: 'regex-rules-count', value: stats.regexRulesCount },
|
||||
{ id: 'regex-exceptions-count', value: stats.regexExceptionsCount },
|
||||
{ id: 'hosts-rules-count', value: stats.hostsRulesCount },
|
||||
{ id: 'blacklist-count', value: stats.blacklistCount }
|
||||
];
|
||||
|
||||
elements.forEach(item => {
|
||||
const element = document.getElementById(item.id);
|
||||
if (element) {
|
||||
animateCounter(element, item.value || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新禁用黑名单数量
|
||||
const disabledBlacklistElement = document.getElementById('blacklist-disabled-count');
|
||||
if (disabledBlacklistElement) {
|
||||
animateCounter(disabledBlacklistElement, stats.disabledBlacklistCount || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载自定义规则
|
||||
async function loadLocalRules() {
|
||||
// 检查是否有有效的缓存数据
|
||||
const cachedRules = window.pageDataCache && window.pageDataCache.getCache('local_rules');
|
||||
if (cachedRules) {
|
||||
console.log('使用缓存的自定义规则');
|
||||
updateLocalRulesUI(cachedRules);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/shield/localrules');
|
||||
|
||||
@@ -243,41 +278,62 @@ async function loadLocalRules() {
|
||||
const data = await response.json();
|
||||
|
||||
// 更新自定义规则数量显示
|
||||
if (document.getElementById('local-rules-count')) {
|
||||
document.getElementById('local-rules-count').textContent = data.localRulesCount || 0;
|
||||
}
|
||||
updateLocalRulesUI(data);
|
||||
|
||||
// 设置当前规则类型
|
||||
currentRulesType = 'local';
|
||||
|
||||
// 合并所有自定义规则
|
||||
let rules = [];
|
||||
// 添加域名规则
|
||||
if (Array.isArray(data.domainRules)) {
|
||||
rules = rules.concat(data.domainRules);
|
||||
// 存储数据到缓存
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache('local_rules', data);
|
||||
}
|
||||
// 添加域名排除规则
|
||||
if (Array.isArray(data.domainExceptions)) {
|
||||
rules = rules.concat(data.domainExceptions);
|
||||
}
|
||||
// 添加正则规则
|
||||
if (Array.isArray(data.regexRules)) {
|
||||
rules = rules.concat(data.regexRules);
|
||||
}
|
||||
// 添加正则排除规则
|
||||
if (Array.isArray(data.regexExceptions)) {
|
||||
rules = rules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
updateRulesTable(rules);
|
||||
} catch (error) {
|
||||
console.error('加载自定义规则失败:', error);
|
||||
showNotification('加载自定义规则失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新自定义规则UI
|
||||
function updateLocalRulesUI(data) {
|
||||
if (!data) return;
|
||||
|
||||
// 更新自定义规则数量显示
|
||||
if (document.getElementById('local-rules-count')) {
|
||||
document.getElementById('local-rules-count').textContent = data.localRulesCount || 0;
|
||||
}
|
||||
|
||||
// 设置当前规则类型
|
||||
currentRulesType = 'local';
|
||||
|
||||
// 合并所有自定义规则
|
||||
let rules = [];
|
||||
// 添加域名规则
|
||||
if (Array.isArray(data.domainRules)) {
|
||||
rules = rules.concat(data.domainRules);
|
||||
}
|
||||
// 添加域名排除规则
|
||||
if (Array.isArray(data.domainExceptions)) {
|
||||
rules = rules.concat(data.domainExceptions);
|
||||
}
|
||||
// 添加正则规则
|
||||
if (Array.isArray(data.regexRules)) {
|
||||
rules = rules.concat(data.regexRules);
|
||||
}
|
||||
// 添加正则排除规则
|
||||
if (Array.isArray(data.regexExceptions)) {
|
||||
rules = rules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
updateRulesTable(rules);
|
||||
}
|
||||
|
||||
// 加载远程规则
|
||||
async function loadRemoteRules() {
|
||||
// 检查是否有有效的缓存数据
|
||||
const cachedRules = window.pageDataCache && window.pageDataCache.getCache('remote_rules');
|
||||
if (cachedRules) {
|
||||
console.log('使用缓存的远程规则');
|
||||
updateRemoteRulesUI(cachedRules);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置当前规则类型
|
||||
currentRulesType = 'remote';
|
||||
@@ -290,36 +346,52 @@ async function loadRemoteRules() {
|
||||
const data = await response.json();
|
||||
|
||||
// 更新远程规则数量显示
|
||||
if (document.getElementById('remote-rules-count')) {
|
||||
document.getElementById('remote-rules-count').textContent = data.remoteRulesCount || 0;
|
||||
}
|
||||
updateRemoteRulesUI(data);
|
||||
|
||||
// 合并所有远程规则
|
||||
let rules = [];
|
||||
// 添加域名规则
|
||||
if (Array.isArray(data.domainRules)) {
|
||||
rules = rules.concat(data.domainRules);
|
||||
// 存储数据到缓存
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache('remote_rules', data);
|
||||
}
|
||||
// 添加域名排除规则
|
||||
if (Array.isArray(data.domainExceptions)) {
|
||||
rules = rules.concat(data.domainExceptions);
|
||||
}
|
||||
// 添加正则规则
|
||||
if (Array.isArray(data.regexRules)) {
|
||||
rules = rules.concat(data.regexRules);
|
||||
}
|
||||
// 添加正则排除规则
|
||||
if (Array.isArray(data.regexExceptions)) {
|
||||
rules = rules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
updateRulesTable(rules);
|
||||
} catch (error) {
|
||||
console.error('加载远程规则失败:', error);
|
||||
showNotification('加载远程规则失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新远程规则UI
|
||||
function updateRemoteRulesUI(data) {
|
||||
if (!data) return;
|
||||
|
||||
// 设置当前规则类型
|
||||
currentRulesType = 'remote';
|
||||
|
||||
// 更新远程规则数量显示
|
||||
if (document.getElementById('remote-rules-count')) {
|
||||
document.getElementById('remote-rules-count').textContent = data.remoteRulesCount || 0;
|
||||
}
|
||||
|
||||
// 合并所有远程规则
|
||||
let rules = [];
|
||||
// 添加域名规则
|
||||
if (Array.isArray(data.domainRules)) {
|
||||
rules = rules.concat(data.domainRules);
|
||||
}
|
||||
// 添加域名排除规则
|
||||
if (Array.isArray(data.domainExceptions)) {
|
||||
rules = rules.concat(data.domainExceptions);
|
||||
}
|
||||
// 添加正则规则
|
||||
if (Array.isArray(data.regexRules)) {
|
||||
rules = rules.concat(data.regexRules);
|
||||
}
|
||||
// 添加正则排除规则
|
||||
if (Array.isArray(data.regexExceptions)) {
|
||||
rules = rules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
updateRulesTable(rules);
|
||||
}
|
||||
|
||||
// 更新规则表格
|
||||
function updateRulesTable(rules) {
|
||||
const tbody = document.getElementById('rules-table-body');
|
||||
@@ -600,6 +672,14 @@ async function handleAddRule() {
|
||||
|
||||
// 加载远程黑名单
|
||||
async function loadRemoteBlacklists() {
|
||||
// 检查是否有有效的缓存数据
|
||||
const cachedBlacklists = window.pageDataCache && window.pageDataCache.getCache('remote_blacklists');
|
||||
if (cachedBlacklists) {
|
||||
console.log('使用缓存的远程黑名单');
|
||||
updateBlacklistsTable(cachedBlacklists);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/shield/blacklists');
|
||||
|
||||
@@ -612,6 +692,11 @@ async function loadRemoteBlacklists() {
|
||||
// 确保blacklists是数组
|
||||
const blacklistArray = Array.isArray(blacklists) ? blacklists : [];
|
||||
updateBlacklistsTable(blacklistArray);
|
||||
|
||||
// 存储数据到缓存
|
||||
if (window.pageDataCache) {
|
||||
window.pageDataCache.setCache('remote_blacklists', blacklistArray);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载远程黑名单失败:', error);
|
||||
showNotification('加载远程黑名单失败', 'error');
|
||||
|
||||
Reference in New Issue
Block a user