-
+ // 创建容器元素
+ 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 ? `
+
+
+
+
已知跟踪器
+
名称: ${trackerInfo.name || '未知'}
+
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
+ ${trackerInfo.url ? `
` : ''}
+ ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
+ ` : '';
+
+ // 创建内容
+ container.innerHTML = `
+
+
+
+
${i + 1}
+
+ ${domain.name}${domain.dnssec ? ' ' : ''}
+ ${trackerContent}
+
+
+
+
+ ${formatNumber(domain.count)}
+ ${percentage}%
+
+
+
`;
+
+ 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'];
diff --git a/static/js/logs.js b/static/js/logs.js
index d151f5c..1c46f7b 100644
--- a/static/js/logs.js
+++ b/static/js/logs.js
@@ -10,10 +10,148 @@ let logsChart = null;
let currentSortField = 'timestamp'; // 默认按时间排序,显示最新记录
let currentSortDirection = 'desc'; // 默认降序
+// 内存使用监控
+let memoryMonitor = {
+ enabled: true,
+ interval: null,
+ history: [],
+ maxHistory: 50,
+
+ // 开始监控
+ start() {
+ if (this.enabled && !this.interval) {
+ this.interval = setInterval(() => {
+ this.checkMemoryUsage();
+ }, 30000); // 每30秒检查一次
+ }
+ },
+
+ // 停止监控
+ stop() {
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+
+ // 检查内存使用情况
+ checkMemoryUsage() {
+ if (performance && performance.memory) {
+ 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.history.push(usage);
+
+ // 限制历史记录大小
+ if (this.history.length > this.maxHistory) {
+ this.history.shift();
+ }
+
+ // 内存使用过高时的处理
+ if (usage.usagePercent > 80) {
+ console.warn('内存使用过高:', usage);
+ // 可以在这里添加自动清理机制
+ this.triggerMemoryCleanup();
+ }
+
+ console.log('内存使用情况:', usage);
+ }
+ },
+
+ // 触发内存清理
+ triggerMemoryCleanup() {
+ console.log('触发内存清理...');
+
+ // 清理IP地理位置缓存
+ if (ipGeolocationCache && typeof ipGeolocationCache === 'object') {
+ const cacheSize = Object.keys(ipGeolocationCache).length;
+ console.log('清理前IP地理位置缓存大小:', cacheSize);
+
+ // 清理超出大小限制的缓存
+ if (GEOLOCATION_CACHE_ORDER && GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
+ while (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
+ const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
+ if (oldestIp) {
+ delete ipGeolocationCache[oldestIp];
+ }
+ }
+ console.log('清理后IP地理位置缓存大小:', Object.keys(ipGeolocationCache).length);
+ }
+ }
+
+ // 清理域名信息缓存
+ if (domainInfoCache && domainInfoCache.size > 0) {
+ const cacheSize = domainInfoCache.size;
+ console.log('清理前域名信息缓存大小:', cacheSize);
+
+ // 清理超出大小限制的缓存
+ if (domainInfoCache.size > DOMAIN_INFO_CACHE_MAX_SIZE) {
+ while (domainInfoCache.size > DOMAIN_INFO_CACHE_MAX_SIZE) {
+ const firstKey = domainInfoCache.keys().next().value;
+ domainInfoCache.delete(firstKey);
+ }
+ console.log('清理后域名信息缓存大小:', domainInfoCache.size);
+ }
+ }
+ },
+
+ // 获取内存使用统计
+ getStats() {
+ if (this.history.length === 0) {
+ return null;
+ }
+
+ const recent = this.history[this.history.length - 1];
+ const avg = this.history.reduce((sum, item) => sum + item.used, 0) / this.history.length;
+ const max = Math.max(...this.history.map(item => item.used));
+ const min = Math.min(...this.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.history
+ };
+ }
+};
+
// IP地理位置缓存(检查是否已经存在,避免重复声明)
if (typeof ipGeolocationCache === 'undefined') {
var ipGeolocationCache = {};
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
+ var GEOLOCATION_CACHE_MAX_SIZE = 1000; // 缓存最大大小
+ var GEOLOCATION_CACHE_ORDER = []; // 用于LRU策略的访问顺序
+}
+
+// 清理IP地理位置缓存,保持在最大大小以内
+function cleanupGeolocationCache() {
+ if (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
+ // 移除最旧的缓存项
+ const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
+ if (oldestIp) {
+ delete ipGeolocationCache[oldestIp];
+ }
+ }
+}
+
+// 更新缓存访问顺序(用于LRU策略)
+function updateCacheAccessOrder(ip) {
+ // 移除现有的位置
+ const index = GEOLOCATION_CACHE_ORDER.indexOf(ip);
+ if (index > -1) {
+ GEOLOCATION_CACHE_ORDER.splice(index, 1);
+ }
+ // 添加到末尾(表示最近访问)
+ GEOLOCATION_CACHE_ORDER.push(ip);
+ // 清理超出大小限制的缓存
+ cleanupGeolocationCache();
}
// 获取IP地理位置信息
@@ -26,6 +164,8 @@ async function getIpGeolocation(ip) {
// 检查缓存
const now = Date.now();
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
+ // 更新缓存访问顺序
+ updateCacheAccessOrder(ip);
return ipGeolocationCache[ip].location;
}
@@ -57,6 +197,9 @@ async function getIpGeolocation(ip) {
timestamp: now
};
+ // 更新缓存访问顺序
+ updateCacheAccessOrder(ip);
+
return location;
} catch (error) {
console.error('获取IP地理位置失败:', error);
@@ -126,6 +269,10 @@ let domainInfoDatabase = null;
let domainInfoLoaded = false;
let domainInfoLoading = false;
+// 域名信息查询缓存
+let domainInfoCache = new Map();
+let DOMAIN_INFO_CACHE_MAX_SIZE = 500; // 缓存最大大小
+
// WebSocket连接和重连计时器
let logsWsConnection = null;
let logsWsReconnectTimer = null;
@@ -240,6 +387,13 @@ async function isDomainInTrackerDatabase(domain) {
// 根据域名查找对应的网站信息
async function getDomainInfo(domain) {
+ // 规范化域名,移除可能的端口号
+ const normalizedDomain = domain.replace(/:\d+$/, '').toLowerCase();
+
+ // 检查缓存
+ if (domainInfoCache.has(normalizedDomain)) {
+ return domainInfoCache.get(normalizedDomain);
+ }
if (!domainInfoDatabase || !domainInfoLoaded) {
await loadDomainInfoDatabase();
@@ -250,9 +404,6 @@ async function getDomainInfo(domain) {
return null;
}
- // 规范化域名,移除可能的端口号
- const normalizedDomain = domain.replace(/:\d+$/, '').toLowerCase();
-
// 遍历所有公司
for (const companyKey in domainInfoDatabase.domains) {
if (domainInfoDatabase.domains.hasOwnProperty(companyKey)) {
@@ -269,13 +420,16 @@ async function getDomainInfo(domain) {
// 处理字符串类型的URL
if (typeof website.url === 'string') {
if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) {
- return {
+ const result = {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
// 处理对象类型的URL
@@ -284,13 +438,16 @@ async function getDomainInfo(domain) {
if (website.url.hasOwnProperty(urlKey)) {
const urlValue = website.url[urlKey];
if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) {
- return {
+ const result = {
name: website.name,
icon: website.icon,
categoryId: website.categoryId,
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
company: website.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
}
@@ -305,13 +462,16 @@ async function getDomainInfo(domain) {
// 处理字符串类型的URL
if (typeof nestedWebsite.url === 'string') {
if (isDomainMatch(nestedWebsite.url, normalizedDomain, nestedWebsite.categoryId)) {
- return {
+ const result = {
name: nestedWebsite.name,
icon: nestedWebsite.icon,
categoryId: nestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
company: nestedWebsite.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
// 处理对象类型的URL
@@ -320,13 +480,16 @@ async function getDomainInfo(domain) {
if (nestedWebsite.url.hasOwnProperty(urlKey)) {
const urlValue = nestedWebsite.url[urlKey];
if (isDomainMatch(urlValue, normalizedDomain, nestedWebsite.categoryId)) {
- return {
+ const result = {
name: nestedWebsite.name,
icon: nestedWebsite.icon,
categoryId: nestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
company: nestedWebsite.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
}
@@ -341,13 +504,16 @@ async function getDomainInfo(domain) {
// 处理字符串类型的URL
if (typeof secondNestedWebsite.url === 'string') {
if (isDomainMatch(secondNestedWebsite.url, normalizedDomain, secondNestedWebsite.categoryId)) {
- return {
+ const result = {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
// 处理对象类型的URL
@@ -356,13 +522,16 @@ async function getDomainInfo(domain) {
if (secondNestedWebsite.url.hasOwnProperty(urlKey)) {
const urlValue = secondNestedWebsite.url[urlKey];
if (isDomainMatch(urlValue, normalizedDomain, secondNestedWebsite.categoryId)) {
- return {
+ const result = {
name: secondNestedWebsite.name,
icon: secondNestedWebsite.icon,
categoryId: secondNestedWebsite.categoryId,
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
company: secondNestedWebsite.company || companyName
};
+ // 存入缓存
+ addToDomainInfoCache(normalizedDomain, result);
+ return result;
}
}
}
@@ -370,11 +539,9 @@ async function getDomainInfo(domain) {
}
}
}
- } else {
}
}
}
- } else {
}
}
}
@@ -384,6 +551,18 @@ async function getDomainInfo(domain) {
return null;
}
+// 添加到域名信息缓存
+function addToDomainInfoCache(domain, info) {
+ // 检查缓存大小
+ if (domainInfoCache.size >= DOMAIN_INFO_CACHE_MAX_SIZE) {
+ // 移除最早的缓存项
+ const firstKey = domainInfoCache.keys().next().value;
+ domainInfoCache.delete(firstKey);
+ }
+ // 添加新的缓存项
+ domainInfoCache.set(domain, info);
+}
+
// 检查域名是否匹配
function isDomainMatch(urlValue, targetDomain, categoryId) {
@@ -536,13 +715,16 @@ function initResizableColumns() {
// 获取表头宽度
let maxWidth = header.offsetWidth;
- // 遍历所有数据行,找到该列的最大宽度
- rows.forEach(row => {
+ // 遍历部分数据行(最多前20行),找到该列的最大宽度
+ // 这样可以在保证准确性的同时提高性能
+ const maxRowsToCheck = Math.min(20, rows.length);
+ for (let i = 0; i < maxRowsToCheck; i++) {
+ const row = rows[i];
const cell = row.children[index];
if (cell) {
maxWidth = Math.max(maxWidth, cell.offsetWidth);
}
- });
+ }
// 添加一些 padding
maxWidth += 20;
@@ -557,16 +739,6 @@ function initResizableColumns() {
header.style.width = width;
header.style.minWidth = width;
header.style.maxWidth = width;
-
- // 找到对应的数据列并设置宽度
- rows.forEach(row => {
- const cell = row.children[index];
- if (cell) {
- cell.style.width = width;
- cell.style.minWidth = width;
- cell.style.maxWidth = width;
- }
- });
});
// 恢复表格布局
@@ -735,9 +907,28 @@ function initResizableColumns() {
}
}
+// 执行搜索操作
+function performSearch(searchTerm) {
+ // 获取搜索文本框
+ const searchInput = document.getElementById('logs-search');
+ if (searchInput) {
+ // 设置搜索内容
+ searchInput.value = searchTerm;
+ // 更新currentSearch变量
+ currentSearch = searchTerm;
+ // 重置页码到第一页
+ currentPage = 1;
+ // 重新加载日志
+ loadLogs();
+ }
+}
+
// 初始化查询日志页面
function initLogsPage() {
+ // 启动内存监控
+ memoryMonitor.start();
+
// 加载日志统计数据
loadLogsStats();
@@ -788,26 +979,121 @@ function cleanupLogsResources() {
// 清除窗口大小改变事件监听器
window.removeEventListener('resize', handleWindowResize);
+
+ // 清除图表实例
+ if (logsChart) {
+ logsChart.destroy();
+ logsChart = null;
+ }
+
+ // 清除表格事件委托
+ const tableBody = document.getElementById('logs-table-body');
+ if (tableBody) {
+ tableBody.removeEventListener('click', handleTableClick);
+ }
+
+ // 清理缓存数据(保留但限制大小)
+ if (ipGeolocationCache && typeof ipGeolocationCache === 'object') {
+ // 清理超出大小限制的缓存
+ if (GEOLOCATION_CACHE_ORDER && GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
+ while (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
+ const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
+ if (oldestIp) {
+ delete ipGeolocationCache[oldestIp];
+ }
+ }
+ }
+ }
+
+ // 清理跟踪器和域名信息数据库(可选,根据内存使用情况决定)
+ // 注意:如果这些数据在其他地方也使用,不要在这里清理
+ // if (trackersDatabase) {
+ // trackersDatabase = null;
+ // trackersLoaded = false;
+ // }
+ // if (domainInfoDatabase) {
+ // domainInfoDatabase = null;
+ // domainInfoLoaded = false;
+ // }
+
+ // 清除模态框和工具提示
+ const modals = document.querySelectorAll('.fixed.inset-0.bg-black.bg-opacity-50');
+ modals.forEach(modal => {
+ modal.remove();
+ });
+
+ // 清除事件监听器(如果有其他全局事件监听器)
+ window.removeEventListener('beforeunload', cleanupLogsResources);
+
+ // 停止内存监控
+ memoryMonitor.stop();
+
+ console.log('Resources cleaned up successfully');
+
+ // 输出最终内存使用统计
+ if (memoryMonitor && memoryMonitor.getStats) {
+ const stats = memoryMonitor.getStats();
+ if (stats) {
+ console.log('最终内存使用统计:', {
+ avg: stats.avg,
+ max: stats.max,
+ min: stats.min,
+ recent: stats.recent
+ });
+ }
+ }
}
// 绑定事件
function bindLogsEvents() {
// 搜索按钮
const searchBtn = document.getElementById('logs-search-btn');
+ const searchInput = document.getElementById('logs-search');
+ const clearSearchBtn = document.getElementById('logs-clear-search');
+
+ // 显示或隐藏清除按钮
+ function toggleClearSearchBtn() {
+ if (searchInput && clearSearchBtn) {
+ if (searchInput.value.trim() !== '') {
+ clearSearchBtn.classList.remove('hidden');
+ } else {
+ clearSearchBtn.classList.add('hidden');
+ }
+ }
+ }
+
if (searchBtn) {
searchBtn.addEventListener('click', () => {
- currentSearch = document.getElementById('logs-search').value;
+ currentSearch = document.getElementById('logs-search').value.trim();
+ toggleClearSearchBtn();
currentPage = 1;
loadLogs();
});
}
- // 搜索框回车事件
- const searchInput = document.getElementById('logs-search');
+ // 搜索框事件
if (searchInput) {
+ // 搜索框回车事件
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
- currentSearch = searchInput.value;
+ currentSearch = searchInput.value.trim();
+ toggleClearSearchBtn();
+ currentPage = 1;
+ loadLogs();
+ }
+ });
+
+ // 搜索框输入事件,用于显示/隐藏清除按钮
+ searchInput.addEventListener('input', toggleClearSearchBtn);
+ }
+
+ // 清除搜索按钮事件
+ if (clearSearchBtn) {
+ clearSearchBtn.addEventListener('click', () => {
+ if (searchInput) {
+ searchInput.value = '';
+ currentSearch = '';
+ toggleClearSearchBtn();
currentPage = 1;
loadLogs();
}
@@ -826,9 +1112,33 @@ function bindLogsEvents() {
// 自定义记录数量
const perPageSelect = document.getElementById('logs-per-page');
+ const perPageSelectBottom = document.getElementById('logs-per-page-bottom');
+
+ // 同步两个下拉选择框的值
+ function syncPerPageSelects(value) {
+ if (perPageSelect) {
+ perPageSelect.value = value;
+ }
+ if (perPageSelectBottom) {
+ perPageSelectBottom.value = value;
+ }
+ }
+
+ // 为顶部下拉选择框添加事件处理
if (perPageSelect) {
perPageSelect.addEventListener('change', () => {
logsPerPage = parseInt(perPageSelect.value);
+ syncPerPageSelects(logsPerPage);
+ currentPage = 1;
+ loadLogs();
+ });
+ }
+
+ // 为底部下拉选择框添加事件处理
+ if (perPageSelectBottom) {
+ perPageSelectBottom.addEventListener('change', () => {
+ logsPerPage = parseInt(perPageSelectBottom.value);
+ syncPerPageSelects(logsPerPage);
currentPage = 1;
loadLogs();
});
@@ -1082,8 +1392,8 @@ async function updateLogsTable(logs) {
const tableBody = document.getElementById('logs-table-body');
if (!tableBody) return;
- // 清空表格
- tableBody.innerHTML = '';
+ // 创建DocumentFragment来批量处理DOM操作
+ const fragment = document.createDocumentFragment();
if (logs.length === 0) {
// 显示空状态
@@ -1094,207 +1404,286 @@ async function updateLogsTable(logs) {
暂无查询日志
`;
- tableBody.appendChild(emptyRow);
- return;
- }
-
- // 检测是否为移动设备
- const isMobile = window.innerWidth <= 768;
-
- // 填充表格
- for (const log of logs) {
- const row = document.createElement('tr');
- row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
+ fragment.appendChild(emptyRow);
+ } else {
+ // 检测是否为移动设备
+ const isMobile = window.innerWidth <= 768;
- // 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
- const time = new Date(log.timestamp);
- const formattedDate = time.toLocaleDateString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit'
- });
- const formattedTime = time.toLocaleTimeString('zh-CN', {
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
-
- // 根据结果添加不同的背景色
- let rowClass = '';
- switch (log.result) {
- case 'blocked':
- rowClass = 'bg-red-50'; // 淡红色填充
- break;
- case 'allowed':
- // 检查是否是规则允许项目
- if (log.blockRule && log.blockRule.includes('allow')) {
- rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
- } else {
- rowClass = ''; // 允许的不填充
- }
- break;
- default:
- rowClass = '';
- }
-
- // 添加行背景色
- if (rowClass) {
- row.classList.add(rowClass);
- }
-
- // 添加被屏蔽或允许显示,并增加颜色
- let statusText = '';
- let statusClass = '';
- switch (log.result) {
- case 'blocked':
- statusText = '被屏蔽';
- statusClass = 'text-danger';
- break;
- case 'allowed':
- statusText = '允许';
- statusClass = 'text-success';
- break;
- case 'error':
- statusText = '错误';
- statusClass = 'text-warning';
- break;
- default:
- statusText = '';
- statusClass = '';
- }
-
- // 检查域名是否在跟踪器数据库中
+ // 填充表格
+ for (const log of logs) {
+ const row = document.createElement('tr');
+ row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
+ row.dataset.logId = log.id || Math.random().toString(36).substr(2, 9); // 添加唯一标识
+ row.dataset.log = JSON.stringify(log); // 存储日志数据
+
+ // 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
+ const time = new Date(log.timestamp);
+ const formattedDate = time.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ });
+ const formattedTime = time.toLocaleTimeString('zh-CN', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
+
+ // 根据结果添加不同的背景色
+ let rowClass = '';
+ switch (log.result) {
+ case 'blocked':
+ rowClass = 'bg-red-50'; // 淡红色填充
+ break;
+ case 'allowed':
+ // 检查是否是规则允许项目
+ if (log.blockRule && log.blockRule.includes('allow')) {
+ rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
+ } else {
+ rowClass = ''; // 允许的不填充
+ }
+ break;
+ default:
+ rowClass = '';
+ }
+
+ // 添加行背景色
+ if (rowClass) {
+ row.classList.add(rowClass);
+ }
+
+ // 添加被屏蔽或允许显示,并增加颜色
+ let statusText = '';
+ let statusClass = '';
+ switch (log.result) {
+ case 'blocked':
+ statusText = '被屏蔽';
+ statusClass = 'text-danger';
+ break;
+ case 'allowed':
+ statusText = '允许';
+ statusClass = 'text-success';
+ break;
+ case 'error':
+ statusText = '错误';
+ statusClass = 'text-warning';
+ break;
+ default:
+ statusText = '';
+ statusClass = '';
+ }
+
+ // 检查域名是否在跟踪器数据库中
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
const isTracker = trackerInfo !== null;
// 构建行内容 - 根据设备类型决定显示内容
- // 添加缓存状态显示
- const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
- const cacheStatusText = log.fromCache ? '缓存' : '非缓存';
-
- // 检查域名是否被拦截
- const isBlocked = log.result === 'blocked';
-
- // 构建跟踪器浮窗内容
- const trackerTooltip = isTracker ? `
-
-
已知跟踪器
-
名称: ${trackerInfo.name || '未知'}
-
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
- ${trackerInfo.url ? `
` : ''}
- ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
+ // 添加缓存状态显示
+ const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
+ const cacheStatusText = log.fromCache ? '缓存' : '非缓存';
+
+ // 检查域名是否被拦截
+ const isBlocked = log.result === 'blocked';
+
+ // 构建跟踪器浮窗内容
+ // HTML转义函数,防止注入攻击
+ function escapeHtml(text) {
+ return text.replace(/[&<>'"]/g, function (match) {
+ return {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ "'": ''',
+ '"': '"'
+ }[match];
+ });
+ }
+
+ const escapedName = escapeHtml(trackerInfo && trackerInfo.name ? trackerInfo.name : '未知');
+ const escapedCategory = escapeHtml(trackerInfo && trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知');
+ const escapedUrl = trackerInfo && trackerInfo.url ? escapeHtml(trackerInfo.url) : '';
+ const escapedSource = trackerInfo && trackerInfo.source ? escapeHtml(trackerInfo.source) : '';
+
+ const trackerTooltip = isTracker ? `
+
+
- ` : '';
-
- if (isMobile) {
- // 移动设备只显示时间和请求信息
- row.innerHTML = `
-
- ${formattedTime}
- ${formattedDate}
- |
-
-
- ${log.dnssec ? ' ' : ''}
-
- ${isTracker ? '' : ''}
- ${trackerTooltip}
-
- ${log.domain}
+
+
+ 名称:
+ ${escapedName}
+
+
+ 类别:
+ ${escapedCategory}
+
+ ${trackerInfo.url ? `
+
+ ` : ''}
+ ${trackerInfo.source ? `
+
+ 源:
+ ${escapedSource}
+
+ ` : ''}
+
+
+ ` : '';
+
+ if (isMobile) {
+ // 移动设备只显示时间和请求信息
+ row.innerHTML = `
+ |
+ ${formattedTime}
+ ${formattedDate}
+ |
+
+
+ ${log.dnssec ? ' ' : ''}
+
+ ${isTracker ? '' : ''}
+ ${trackerTooltip}
- 类型: ${log.queryType}, ${statusText}
- 客户端: ${log.clientIP}
- |
- `;
- } else {
- // 桌面设备显示完整信息
- row.innerHTML = `
-
- ${formattedTime}
- ${formattedDate}
- |
-
- ${log.clientIP}
- ${log.location || '未知 未知'}
- |
-
-
- ${log.dnssec ? ' ' : ''}
-
- ${isTracker ? '' : ''}
- ${trackerTooltip}
-
- ${log.domain}
-
- 类型: ${log.queryType}, ${statusText}, ${log.fromCache ? '缓存' : '非缓存'}${log.dnssec ? ', DNSSEC' : ''}${log.edns ? ', EDNS' : ''}
- DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}
- |
-
${log.responseTime}ms |
-
- ${isBlocked ?
- `` :
- ``
- }
- |
- `;
-
- // 更新IP地理位置信息
- const locationElement = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
- if (locationElement) {
- // 调用getIpGeolocation函数获取地理位置
- getIpGeolocation(log.clientIP).then(location => {
- locationElement.textContent = location;
- });
- }
- }
+
${log.domain}
+
+
类型: ${log.queryType}, ${statusText}
+
客户端: ${log.clientIP}
+
+ `;
+ } else {
+ // 桌面设备显示完整信息
+ row.innerHTML = `
+
+ ${formattedTime}
+ ${formattedDate}
+ |
+
+ ${log.clientIP}
+ ${log.location || '未知 未知'}
+ |
+
+
+ ${log.dnssec ? ' ' : ''}
+
+ ${isTracker ? '' : ''}
+ ${trackerTooltip}
+
+ ${log.domain}
+
+ 类型: ${log.queryType}, ${statusText}, ${log.fromCache ? '缓存' : '非缓存'}${log.dnssec ? ', DNSSEC' : ''}${log.edns ? ', EDNS' : ''}
+ DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}
+ |
+
${log.responseTime}ms |
+
+ ${isBlocked ?
+ `` :
+ ``
+ }
+ |
+ `;
- // 添加跟踪器图标悬停事件
- if (isTracker) {
- const iconContainer = row.querySelector('.tracker-icon-container');
- const tooltip = iconContainer.querySelector('.tracker-tooltip');
- if (iconContainer && tooltip) {
- // 移除内联样式,使用CSS类控制显示
- tooltip.removeAttribute('style');
-
- iconContainer.addEventListener('mouseenter', () => {
- tooltip.classList.add('visible');
- });
-
- iconContainer.addEventListener('mouseleave', () => {
- tooltip.classList.remove('visible');
- });
- }
+ // 更新IP地理位置信息
+ const locationElement = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
+ if (locationElement) {
+ // 调用getIpGeolocation函数获取地理位置
+ getIpGeolocation(log.clientIP).then(location => {
+ locationElement.textContent = location;
+ });
}
-
- // 绑定按钮事件
- const blockBtn = row.querySelector('.block-btn');
- if (blockBtn) {
- blockBtn.addEventListener('click', (e) => {
- e.preventDefault();
- const domain = e.currentTarget.dataset.domain;
- blockDomain(domain);
- });
}
- const unblockBtn = row.querySelector('.unblock-btn');
- if (unblockBtn) {
- unblockBtn.addEventListener('click', (e) => {
- e.preventDefault();
- const domain = e.currentTarget.dataset.domain;
- unblockDomain(domain);
- });
+ // 添加跟踪器图标悬停事件
+ if (isTracker) {
+ const iconContainer = row.querySelector('.tracker-icon-container');
+ const tooltip = iconContainer.querySelector('.tracker-tooltip');
+ if (iconContainer && tooltip) {
+ // 确保容器是相对定位,作为浮窗的定位基准
+ iconContainer.style.position = 'relative';
+
+ // 移除内联样式,使用CSS类控制显示
+ tooltip.removeAttribute('style');
+
+ // 鼠标进入事件
+ iconContainer.addEventListener('mouseenter', () => {
+ tooltip.classList.remove('invisible', 'opacity-0', '-translate-y-2');
+ tooltip.classList.add('opacity-100', 'translate-y-0');
+ });
+
+ // 鼠标离开事件
+ iconContainer.addEventListener('mouseleave', () => {
+ tooltip.classList.add('opacity-0', '-translate-y-2');
+ tooltip.classList.remove('opacity-100', 'translate-y-0');
+ // 延迟移除invisible类,等待过渡动画完成
+ setTimeout(() => {
+ tooltip.classList.add('invisible');
+ }, 200);
+ });
+ }
}
- // 绑定日志详情点击事件
- row.addEventListener('click', (e) => {
- // 如果点击的是按钮,不触发详情弹窗
- if (e.target.closest('button')) {
- return;
- }
+ fragment.appendChild(row);
+ }
+ }
+
+ // 一次性清空表格并添加所有行
+ tableBody.innerHTML = '';
+ tableBody.appendChild(fragment);
+
+ // 添加事件委托
+ setupTableEventDelegation();
+}
+
+// 设置表格事件委托
+function setupTableEventDelegation() {
+ const tableBody = document.getElementById('logs-table-body');
+ if (!tableBody) return;
+
+ // 移除现有的事件监听器
+ tableBody.removeEventListener('click', handleTableClick);
+
+ // 添加新的事件监听器
+ tableBody.addEventListener('click', handleTableClick);
+}
+
+// 处理表格点击事件
+function handleTableClick(e) {
+ // 处理拦截按钮点击
+ const blockBtn = e.target.closest('.block-btn');
+ if (blockBtn) {
+ e.preventDefault();
+ const domain = blockBtn.dataset.domain;
+ blockDomain(domain);
+ return;
+ }
+
+ // 处理放行按钮点击
+ const unblockBtn = e.target.closest('.unblock-btn');
+ if (unblockBtn) {
+ e.preventDefault();
+ const domain = unblockBtn.dataset.domain;
+ unblockDomain(domain);
+ return;
+ }
+
+ // 处理行点击,显示详情
+ const row = e.target.closest('tr');
+ if (row && !e.target.closest('button')) {
+ const logData = row.dataset.log;
+ if (logData) {
+ try {
+ const log = JSON.parse(logData);
showLogDetailModal(log);
- });
-
- tableBody.appendChild(row);
+ } catch (error) {
+ console.error('解析日志数据失败:', error);
+ }
+ }
}
}
@@ -1337,6 +1726,12 @@ function initLogsChart() {
return;
}
+ // 销毁现有图表
+ if (logsChart) {
+ logsChart.destroy();
+ logsChart = null;
+ }
+
// 创建图表
logsChart = new Chart(ctx, {
type: 'line',
@@ -1543,27 +1938,11 @@ async function blockDomain(domain) {
}
}
-// 绑定操作按钮事件
+// 绑定操作按钮事件(已被事件委托替代,保留此函数以保持兼容性)
function bindActionButtonsEvents() {
- // 绑定拦截按钮事件
- const blockBtns = document.querySelectorAll('.block-btn');
- blockBtns.forEach(btn => {
- btn.addEventListener('click', async (e) => {
- e.preventDefault();
- const domain = e.currentTarget.dataset.domain;
- await blockDomain(domain);
- });
- });
-
- // 绑定放行按钮事件
- const unblockBtns = document.querySelectorAll('.unblock-btn');
- unblockBtns.forEach(btn => {
- btn.addEventListener('click', async (e) => {
- e.preventDefault();
- const domain = e.currentTarget.dataset.domain;
- await unblockDomain(domain);
- });
- });
+ // 此函数已被 setupTableEventDelegation 中的事件委托替代
+ // 保留此函数以保持与现有代码的兼容性
+ console.log('Action buttons events are now handled by event delegation');
}
// 刷新规则列表
@@ -1777,12 +2156,17 @@ async function showLogDetailModal(log) {
// 创建模态框容器
const modalContainer = document.createElement('div');
- modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
+ modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
modalContainer.style.zIndex = '9999';
+ modalContainer.style.opacity = '0';
+ modalContainer.style.transition = 'opacity 0.3s ease-in-out';
// 创建模态框内容
const modalContent = document.createElement('div');
- modalContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto animate-slide-in';
+ modalContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto';
+ modalContent.style.transform = 'scale(0.9) translateY(20px)';
+ modalContent.style.transition = 'transform 0.3s ease-in-out';
+ modalContent.style.opacity = '0';
// 创建标题栏
const header = document.createElement('div');
@@ -1878,17 +2262,6 @@ async function showLogDetailModal(log) {
`;
- // 构建跟踪器浮窗内容
- const trackerTooltip = isTracker ? `
-
-
已知跟踪器
-
名称: ${trackerInfo.name || '未知'}
-
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
- ${trackerInfo.url ? `
` : ''}
- ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
-
- ` : '';
-
// 跟踪器信息
const trackerDiv = document.createElement('div');
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
@@ -1897,34 +2270,13 @@ async function showLogDetailModal(log) {
${isTracker ? `
-
-
- ${trackerTooltip}
-
+
${trackerInfo.name} (${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'})
` : '无'}
`;
- // 添加跟踪器图标悬停事件
- if (isTracker) {
- const iconContainer = trackerDiv.querySelector('.tracker-icon-container');
- const tooltip = iconContainer.querySelector('.tracker-tooltip');
- if (iconContainer && tooltip) {
- // 移除内联样式,使用CSS类控制显示
- tooltip.removeAttribute('style');
-
- iconContainer.addEventListener('mouseenter', () => {
- tooltip.classList.add('visible');
- });
-
- iconContainer.addEventListener('mouseleave', () => {
- tooltip.classList.remove('visible');
- });
- }
- }
-
// 解析记录
const recordsDiv = document.createElement('div');
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
@@ -2089,10 +2441,18 @@ async function showLogDetailModal(log) {
// 添加到页面
document.body.appendChild(modalContainer);
+ // 触发动画效果,使其平滑显示
+ setTimeout(() => {
+ modalContainer.style.opacity = '1';
+ modalContent.style.transform = 'scale(1) translateY(0)';
+ modalContent.style.opacity = '1';
+ }, 10);
+
// 关闭模态框函数
function closeModal() {
- modalContainer.classList.add('animate-fade-out');
- modalContent.classList.add('animate-slide-out');
+ modalContainer.style.opacity = '0';
+ modalContent.style.transform = 'scale(0.9) translateY(20px)';
+ modalContent.style.opacity = '0';
// 等待动画结束后移除元素
setTimeout(() => {
@@ -2121,11 +2481,16 @@ async function showLogDetailModal(log) {
// 显示错误提示
const errorModal = document.createElement('div');
- errorModal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4 animate-fade-in';
+ errorModal.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4';
errorModal.style.zIndex = '9999';
+ errorModal.style.opacity = '0';
+ errorModal.style.transition = 'opacity 0.3s ease-in-out';
const errorContent = document.createElement('div');
- errorContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl p-6 w-full max-w-md animate-slide-in';
+ errorContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl p-6 w-full max-w-md';
+ errorContent.style.transform = 'scale(0.9) translateY(20px)';
+ errorContent.style.transition = 'transform 0.3s ease-in-out';
+ errorContent.style.opacity = '0';
errorContent.innerHTML = `
@@ -2142,10 +2507,18 @@ async function showLogDetailModal(log) {
errorModal.appendChild(errorContent);
document.body.appendChild(errorModal);
+ // 触发动画效果,使其平滑显示
+ setTimeout(() => {
+ errorModal.style.opacity = '1';
+ errorContent.style.transform = 'scale(1) translateY(0)';
+ errorContent.style.opacity = '1';
+ }, 10);
+
// 关闭错误模态框函数
function closeErrorModal() {
- errorModal.classList.add('animate-fade-out');
- errorContent.classList.add('animate-slide-out');
+ errorModal.style.opacity = '0';
+ errorContent.style.transform = 'scale(0.9) translateY(20px)';
+ errorContent.style.opacity = '0';
// 等待动画结束后移除元素
setTimeout(() => {
diff --git a/static/js/memory-manager.js b/static/js/memory-manager.js
new file mode 100644
index 0000000..ac4e71c
--- /dev/null
+++ b/static/js/memory-manager.js
@@ -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();
+ });
+}