暗黑主题修复

This commit is contained in:
Alex Yang
2026-01-29 18:57:20 +08:00
parent 00f635ebec
commit ed686b21bb
7 changed files with 1668 additions and 404 deletions

View 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,6 +129,7 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
// 处理请求URL和参数
let url = `${API_BASE_URL}${endpoint}`;
let cacheKey = null;
if (data) {
if (method === 'GET') {
@@ -138,10 +139,26 @@ 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;
@@ -159,7 +176,14 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
// 使用.text()先获取响应文本处理可能的JSON解析错误
return response.text().then(text => {
try {
return JSON.parse(text);
const responseData = JSON.parse(text);
// 缓存GET请求的响应
if (method === 'GET' && cacheKey && window.memoryManager) {
memoryManager.addCacheItem('apiResponses', cacheKey, responseData);
}
return responseData;
} catch (e) {
console.error('JSON解析错误:', e, '响应文本:', text);
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试

View File

@@ -366,8 +366,13 @@ function showNotification(message, type = 'info') {
// 初始化GFWList管理页面
function initGFWListPage() {
loadGFWListConfig();
// 加载配置但不显示,因为功能未开发
// loadGFWListConfig();
setupGFWListEventListeners();
// 提示功能待开发
setTimeout(() => {
showNotification('GFWList管理功能正在开发中敬请期待', 'info');
}, 500);
}
// 加载GFWList配置
@@ -415,21 +420,8 @@ function populateGFWListForm(config) {
// 保存GFWList配置
async function handleSaveGFWListConfig() {
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 || '未知错误'));
}
// 提示功能待开发
showNotification('GFWList管理功能正在开发中敬请期待', 'info');
}
// 收集GFWList表单数据
@@ -521,14 +513,9 @@ function setupGFWListEventListeners() {
const toggleBtns = document.querySelectorAll('.toggle-btn');
toggleBtns.forEach(btn => {
btn.addEventListener('click', function() {
// 切换按钮状态
const currentState = this.classList.contains('bg-success');
setElementValue(this.id, !currentState);
// 如果是GFWList启用开关更新通行网站部分的显示效果
if (this.id === 'gfwlist-enabled') {
updateAllowedSitesSection(!currentState);
}
// 提示功能待开发
showNotification('GFWList管理功能正在开发中敬请期待', 'info');
// 不执行实际操作
});
});
}

View File

@@ -18,6 +18,38 @@ 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秒节流间隔
@@ -34,6 +66,10 @@ let errorQueries = 0;
// 初始化仪表盘
async function initDashboard() {
try {
// 初始化内存管理器
if (window.memoryManager && typeof window.memoryManager.init === 'function') {
window.memoryManager.init();
}
// 优先加载初始数据,确保页面显示最新信息
@@ -63,6 +99,21 @@ 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);
@@ -70,12 +121,6 @@ function connectWebSocket() {
// 连接打开事件
dashboardWsConnection.onopen = function() {
showNotification('数据更新成功', 'success');
// 清除重连计时器
if (dashboardWsReconnectTimer) {
clearTimeout(dashboardWsReconnectTimer);
dashboardWsReconnectTimer = null;
}
};
// 接收消息事件
@@ -408,7 +453,11 @@ function fallbackToIntervalRefresh() {
function cleanupResources() {
// 清除WebSocket连接
if (dashboardWsConnection) {
dashboardWsConnection.close();
try {
dashboardWsConnection.close();
} catch (e) {
console.error('关闭WebSocket连接失败:', e);
}
dashboardWsConnection = null;
}
@@ -425,36 +474,392 @@ function cleanupResources() {
}
// 清除图表实例,释放内存
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];
}
}
cleanupChartInstances();
// 清除事件监听器
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) {
@@ -644,8 +1049,8 @@ function updateStatsCards(stats) {
// 标记动画正在进行
animationInProgress[elementId] = true;
// 动画配置
const duration = Math.min(800, Math.abs(targetValue - currentValue) * 2); // 根据数值变化大小调整动画持续时间
// 动画配置 - 优化:使用固定的动画持续时间,减少计算开销
const duration = 300; // 固定300ms动画时间足够流畅且减少计算
const startTime = performance.now();
const startValue = currentValue;
@@ -668,6 +1073,8 @@ function updateStatsCards(stats) {
element.textContent = formatNumber(targetValue);
// 标记动画完成
delete animationInProgress[elementId];
// 清理内存
element = null;
}
}
@@ -839,7 +1246,7 @@ function updateStatsCards(stats) {
blockedQueries: 0,
allowedQueries: 0,
errorQueries: 0,
avgResponseTime: 0
prevResponseTime: null
};
// 计算百分比并更新箭头
@@ -867,19 +1274,46 @@ function updateStatsCards(stats) {
// 更新平均响应时间的百分比和箭头,使用与其他统计卡片相同的逻辑
if (avgResponseTime !== undefined && avgResponseTime !== null) {
// 计算变化百分比
let responsePercent = '0.0%';
const prevResponseTime = window.dashboardHistoryData.avgResponseTime || 0;
// 获取历史响应时间
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
const currentResponseTime = avgResponseTime;
if (prevResponseTime > 0) {
const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100;
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
// 查找箭头元素
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');
}
}
// 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
// updatePercentWithArrow函数内部已添加响应时间的特殊处理
updatePercentWithArrow('response-time-percent', responsePercent, prevResponseTime, currentResponseTime);
// 首次加载时初始化历史数据,不计算趋势
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);
}
} else {
updatePercentage('response-time-percent', '---');
}
@@ -890,9 +1324,9 @@ function updateStatsCards(stats) {
window.dashboardHistoryData.blockedQueries = blockedQueries;
window.dashboardHistoryData.allowedQueries = allowedQueries;
window.dashboardHistoryData.errorQueries = errorQueries;
// 只在avgResponseTime不为0时更新历史数据保留上一次不为0的状态
if (avgResponseTime > 0) {
window.dashboardHistoryData.avgResponseTime = avgResponseTime;
// 只有当prevResponseTime不为null时才更新避免覆盖首次加载时设置的值
if (window.dashboardHistoryData.prevResponseTime !== null) {
window.dashboardHistoryData.prevResponseTime = avgResponseTime;
}
@@ -1280,30 +1714,38 @@ 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);
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>
// 创建容器元素
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>
</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 = html;
// 清空表格并添加新内容
tableBody.innerHTML = '';
tableBody.appendChild(fragment);
}
// 更新请求域名排行表格
@@ -1348,59 +1790,66 @@ async function updateTopDomainsTable(domains) {
return sum + (typeof domain.count === 'number' ? domain.count : 0);
}, 0);
let html = '';
// 使用DocumentFragment批量处理DOM操作
const fragment = document.createDocumentFragment();
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';
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="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="w-full bg-gray-200 rounded-full h-2.5">
<div class="bg-success h-2.5 rounded-full" style="width: ${percentage}%"></div>
// 创建容器元素
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>
<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}
</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="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 = html;
// 清空表格并添加新内容
tableBody.innerHTML = '';
tableBody.appendChild(fragment);
// 添加跟踪器图标悬停事件
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
@@ -1585,6 +2034,13 @@ function initCharts() {
console.error('未找到比例图表元素');
return;
}
// 销毁现有图表
if (ratioChart) {
ratioChart.destroy();
ratioChart = null;
}
const ratioCtx = ratioChartElement.getContext('2d');
ratioChart = new Chart(ratioCtx, {
type: 'doughnut',
@@ -1700,6 +2156,12 @@ 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'];

File diff suppressed because it is too large Load Diff

392
static/js/memory-manager.js Normal file
View File

@@ -0,0 +1,392 @@
// memory-manager.js - 全局内存管理模块
// 全局内存管理对象
const memoryManager = {
// 缓存管理
caches: {
ipGeolocation: {
data: new Map(),
maxSize: 1000,
order: []
},
domainInfo: {
data: new Map(),
maxSize: 500
},
apiResponses: {
data: new Map(),
maxSize: 100,
ttl: 60000 // 1分钟过期
}
},
// 资源管理
resources: {
timers: [],
eventListeners: [],
webSockets: [],
intervals: []
},
// 内存监控
monitoring: {
enabled: true,
history: [],
maxHistory: 50,
threshold: 80 // 内存使用阈值(%
},
// 初始化
init() {
console.log('内存管理器初始化');
this.startMonitoring();
this.setupGlobalListeners();
},
// 启动内存监控
startMonitoring() {
if (this.monitoring.enabled && performance && performance.memory) {
setInterval(() => {
this.checkMemoryUsage();
}, 30000); // 每30秒检查一次
}
},
// 检查内存使用情况
checkMemoryUsage() {
if (!performance || !performance.memory) return;
const memory = performance.memory;
const usage = {
timestamp: Date.now(),
used: Math.round(memory.usedJSHeapSize / 1024 / 1024 * 100) / 100, // MB
total: Math.round(memory.totalJSHeapSize / 1024 / 1024 * 100) / 100, // MB
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024 * 100) / 100, // MB
usagePercent: Math.round((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100 * 100) / 100 // %
};
this.monitoring.history.push(usage);
// 限制历史记录大小
if (this.monitoring.history.length > this.monitoring.maxHistory) {
this.monitoring.history.shift();
}
// 内存使用过高时的处理
if (usage.usagePercent > this.monitoring.threshold) {
console.warn('内存使用过高:', usage);
this.triggerMemoryCleanup();
}
console.log('内存使用情况:', usage);
},
// 触发内存清理
triggerMemoryCleanup() {
console.log('触发内存清理...');
// 清理缓存
this.cleanupCaches();
// 清理未使用的资源
this.cleanupUnusedResources();
},
// 清理缓存
cleanupCaches() {
// 清理IP地理位置缓存
this.cleanupCache('ipGeolocation');
// 清理域名信息缓存
this.cleanupCache('domainInfo');
// 清理API响应缓存
this.cleanupCache('apiResponses');
},
// 清理特定缓存
cleanupCache(cacheName) {
const cache = this.caches[cacheName];
if (!cache) return;
console.log(`清理${cacheName}缓存 - 当前大小: ${cache.data.size}`);
// 清理超出大小限制的缓存
if (cache.data.size > cache.maxSize) {
if (cache.order && cache.order.length > 0) {
// 使用LRU策略
while (cache.data.size > cache.maxSize && cache.order.length > 0) {
const oldestKey = cache.order.shift();
if (oldestKey) {
cache.data.delete(oldestKey);
}
}
} else {
// 简单清理适用于有TTL的缓存
const now = Date.now();
for (const [key, value] of cache.data.entries()) {
if (cache.data.size <= cache.maxSize) break;
// 检查TTL
if (cache.ttl && value.timestamp && (now - value.timestamp) > cache.ttl) {
cache.data.delete(key);
}
}
// 如果仍然超出大小限制,删除最旧的项
if (cache.data.size > cache.maxSize) {
const keys = Array.from(cache.data.keys());
while (cache.data.size > cache.maxSize && keys.length > 0) {
const oldestKey = keys.shift();
cache.data.delete(oldestKey);
}
}
}
}
console.log(`清理后${cacheName}缓存大小: ${cache.data.size}`);
},
// 清理未使用的资源
cleanupUnusedResources() {
// 清理定时器(这里主要是记录,实际清理需要在具体组件中进行)
console.log(`当前活动定时器数量: ${this.resources.timers.length}`);
console.log(`当前活动事件监听器数量: ${this.resources.eventListeners.length}`);
console.log(`当前活动WebSocket连接数量: ${this.resources.webSockets.length}`);
console.log(`当前活动间隔定时器数量: ${this.resources.intervals.length}`);
},
// 添加缓存项
addCacheItem(cacheName, key, value) {
const cache = this.caches[cacheName];
if (!cache) return;
// 检查缓存大小
if (cache.data.size >= cache.maxSize) {
this.cleanupCache(cacheName);
}
// 添加到缓存
if (cache.ttl) {
cache.data.set(key, {
value,
timestamp: Date.now()
});
} else {
cache.data.set(key, value);
}
// 更新访问顺序用于LRU
if (cache.order) {
// 移除现有的位置
const index = cache.order.indexOf(key);
if (index > -1) {
cache.order.splice(index, 1);
}
// 添加到末尾
cache.order.push(key);
}
},
// 获取缓存项
getCacheItem(cacheName, key) {
const cache = this.caches[cacheName];
if (!cache) return null;
const item = cache.data.get(key);
if (!item) return null;
// 检查TTL
if (cache.ttl && item.timestamp && (Date.now() - item.timestamp) > cache.ttl) {
cache.data.delete(key);
return null;
}
// 更新访问顺序用于LRU
if (cache.order) {
// 移除现有的位置
const index = cache.order.indexOf(key);
if (index > -1) {
cache.order.splice(index, 1);
}
// 添加到末尾
cache.order.push(key);
}
return cache.ttl ? item.value : item;
},
// 注册定时器
registerTimer(timerId) {
if (timerId) {
this.resources.timers.push(timerId);
}
},
// 注销定时器
unregisterTimer(timerId) {
const index = this.resources.timers.indexOf(timerId);
if (index > -1) {
clearTimeout(timerId);
this.resources.timers.splice(index, 1);
}
},
// 注册事件监听器
registerEventListener(element, event, handler) {
if (element && event && handler) {
this.resources.eventListeners.push({ element, event, handler });
}
},
// 注销事件监听器
unregisterEventListener(element, event, handler) {
const index = this.resources.eventListeners.findIndex(item =>
item.element === element && item.event === event && item.handler === handler
);
if (index > -1) {
element.removeEventListener(event, handler);
this.resources.eventListeners.splice(index, 1);
}
},
// 注册WebSocket连接
registerWebSocket(ws) {
if (ws) {
this.resources.webSockets.push(ws);
}
},
// 注销WebSocket连接
unregisterWebSocket(ws) {
const index = this.resources.webSockets.indexOf(ws);
if (index > -1) {
try {
ws.close();
} catch (error) {
console.error('关闭WebSocket连接失败:', error);
}
this.resources.webSockets.splice(index, 1);
}
},
// 注册间隔定时器
registerInterval(intervalId) {
if (intervalId) {
this.resources.intervals.push(intervalId);
}
},
// 注销间隔定时器
unregisterInterval(intervalId) {
const index = this.resources.intervals.indexOf(intervalId);
if (index > -1) {
clearInterval(intervalId);
this.resources.intervals.splice(index, 1);
}
},
// 清理所有资源
cleanupAllResources() {
console.log('清理所有资源...');
// 清理定时器
this.resources.timers.forEach(timerId => {
clearTimeout(timerId);
});
this.resources.timers = [];
// 清理事件监听器
this.resources.eventListeners.forEach(({ element, event, handler }) => {
try {
element.removeEventListener(event, handler);
} catch (error) {
console.error('移除事件监听器失败:', error);
}
});
this.resources.eventListeners = [];
// 清理WebSocket连接
this.resources.webSockets.forEach(ws => {
try {
ws.close();
} catch (error) {
console.error('关闭WebSocket连接失败:', error);
}
});
this.resources.webSockets = [];
// 清理间隔定时器
this.resources.intervals.forEach(intervalId => {
clearInterval(intervalId);
});
this.resources.intervals = [];
// 清理缓存
this.cleanupCaches();
console.log('所有资源已清理');
},
// 设置全局监听器
setupGlobalListeners() {
// 页面卸载时清理所有资源
window.addEventListener('beforeunload', () => {
this.cleanupAllResources();
});
// 页面可见性变化时的处理
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面隐藏时清理一些资源
console.log('页面隐藏,清理资源...');
this.cleanupCaches();
}
});
},
// 获取内存使用统计
getStats() {
if (this.monitoring.history.length === 0) {
return null;
}
const recent = this.monitoring.history[this.monitoring.history.length - 1];
const avg = this.monitoring.history.reduce((sum, item) => sum + item.used, 0) / this.monitoring.history.length;
const max = Math.max(...this.monitoring.history.map(item => item.used));
const min = Math.min(...this.monitoring.history.map(item => item.used));
return {
recent,
avg: Math.round(avg * 100) / 100,
max: Math.round(max * 100) / 100,
min: Math.round(min * 100) / 100,
history: this.monitoring.history,
caches: {
ipGeolocation: this.caches.ipGeolocation.data.size,
domainInfo: this.caches.domainInfo.data.size,
apiResponses: this.caches.apiResponses.data.size
},
resources: {
timers: this.resources.timers.length,
eventListeners: this.resources.eventListeners.length,
webSockets: this.resources.webSockets.length,
intervals: this.resources.intervals.length
}
};
}
};
// 导出内存管理器
if (typeof module !== 'undefined' && module.exports) {
module.exports = memoryManager;
} else {
window.memoryManager = memoryManager;
}
// 自动初始化
if (typeof window !== 'undefined') {
window.addEventListener('DOMContentLoaded', () => {
memoryManager.init();
});
}