暗黑主题修复
This commit is contained in:
@@ -1395,6 +1395,22 @@ tr:hover {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 深色主题下的跟踪器浮窗样式 */
|
||||
.dark .tracker-tooltip {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dark .tracker-tooltip::before {
|
||||
border-right-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.dark .tracker-tooltip::after {
|
||||
border-right-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* 浮窗标题 */
|
||||
.tracker-tooltip .font-semibold {
|
||||
font-weight: 600;
|
||||
@@ -1402,6 +1418,11 @@ tr:hover {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 深色主题下的浮窗标题 */
|
||||
.dark .tracker-tooltip .font-semibold {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 搜索框样式优化 */
|
||||
#logs-search {
|
||||
/* 确保搜索框在所有设备上都有合适的宽度 */
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
|
||||
<!-- Tailwind 配置 -->
|
||||
<script src="js/vendor/tailwind.js"></script>
|
||||
<!-- 内存管理模块 -->
|
||||
<script src="js/memory-manager.js"></script>
|
||||
<!-- 自定义工具类 -->
|
||||
<style type="text/tailwindcss" src="css/index.css"></style>
|
||||
</head>
|
||||
@@ -924,8 +926,11 @@
|
||||
<!-- 日志搜索和过滤 -->
|
||||
<div class="bg-white rounded-lg p-4 sm:p-6 card-shadow">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div class="sm:col-span-2">
|
||||
<input type="text" id="logs-search" placeholder="搜索域名或客户端IP" class="w-full px-3 sm:px-4 py-2 sm:py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
|
||||
<div class="sm:col-span-2 relative">
|
||||
<input type="text" id="logs-search" placeholder="搜索域名或客户端IP" class="w-full pl-3 pr-10 sm:pl-4 sm:pr-12 py-2 sm:py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
|
||||
<button id="logs-clear-search" class="absolute right-2 sm:right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none hidden">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<select id="logs-result-filter" class="w-full px-3 sm:px-4 py-2 sm:py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
|
||||
@@ -1026,7 +1031,7 @@
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">每页显示:</span>
|
||||
<select id="logs-per-page" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<select id="logs-per-page-bottom" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<option value="10">10条</option>
|
||||
<option value="20">20条</option>
|
||||
<option value="30" selected>30条</option>
|
||||
@@ -1056,7 +1061,7 @@
|
||||
|
||||
<div id="gfwlist-content" class="hidden">
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<h3 class="text-lg font-semibold mb-6">GFWList管理</h3>
|
||||
<h3 class="text-lg font-semibold mb-6">GFWList管理 <span class="inline-block px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 rounded-full">Dev</span></h3>
|
||||
|
||||
<form id="gfwlist-form">
|
||||
<div class="mb-8">
|
||||
|
||||
@@ -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错误进行重试
|
||||
|
||||
@@ -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');
|
||||
// 不执行实际操作
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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,10 +1274,36 @@ function updateStatsCards(stats) {
|
||||
|
||||
// 更新平均响应时间的百分比和箭头,使用与其他统计卡片相同的逻辑
|
||||
if (avgResponseTime !== undefined && avgResponseTime !== null) {
|
||||
// 获取历史响应时间
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||
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 === 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%';
|
||||
const prevResponseTime = window.dashboardHistoryData.avgResponseTime || 0;
|
||||
const currentResponseTime = avgResponseTime;
|
||||
|
||||
if (prevResponseTime > 0) {
|
||||
const changePercent = ((currentResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
@@ -880,6 +1313,7 @@ function updateStatsCards(stats) {
|
||||
// 响应时间趋势特殊处理:响应时间下降(性能提升)显示上升箭头,响应时间上升(性能下降)显示下降箭头
|
||||
// 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,15 +1714,20 @@ 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">
|
||||
|
||||
// 创建容器元素
|
||||
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>
|
||||
@@ -1299,11 +1738,14 @@ async function updateTopClientsTable(clients) {
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fragment.appendChild(container);
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
// 清空表格并添加新内容
|
||||
tableBody.innerHTML = '';
|
||||
tableBody.appendChild(fragment);
|
||||
}
|
||||
|
||||
// 更新请求域名排行表格
|
||||
@@ -1348,43 +1790,47 @@ 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 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 trackerTooltip = isTracker ? `
|
||||
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
|
||||
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>
|
||||
` : '';
|
||||
|
||||
// 计算百分比
|
||||
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">
|
||||
// 创建内容
|
||||
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>
|
||||
${isTracker ? `
|
||||
<div class="tracker-icon-container relative ml-2">
|
||||
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
` : ''}
|
||||
${trackerContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1396,11 +1842,14 @@ async function updateTopDomainsTable(domains) {
|
||||
<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>
|
||||
`;
|
||||
|
||||
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'];
|
||||
|
||||
@@ -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,10 +1404,8 @@ async function updateLogsTable(logs) {
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.appendChild(emptyRow);
|
||||
} else {
|
||||
// 检测是否为移动设备
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
@@ -1105,6 +1413,8 @@ async function updateLogsTable(logs) {
|
||||
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);
|
||||
@@ -1176,13 +1486,54 @@ async function updateLogsTable(logs) {
|
||||
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 ? `
|
||||
<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 class="tracker-tooltip absolute z-50 bg-white dark:bg-gray-800 shadow-lg rounded-lg border border-gray-200 dark:border-gray-700 p-4 min-w-72 text-sm text-gray-900 dark:text-gray-100 transition-all duration-200 invisible opacity-0 -translate-y-2 translate-x-4 w-72">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<i class="fa fa-eye text-red-500 dark:text-red-400 mr-2 mt-0.5"></i>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">已知跟踪器</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start">
|
||||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">名称:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedName}</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">类别:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedCategory}</span>
|
||||
</div>
|
||||
${trackerInfo.url ? `
|
||||
<div class="flex items-start">
|
||||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">URL:</span>
|
||||
<a href="${escapedUrl}" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline flex-grow break-all">${escapedUrl}</a>
|
||||
</div>
|
||||
` : ''}
|
||||
${trackerInfo.source ? `
|
||||
<div class="flex items-start">
|
||||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">源:</span>
|
||||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedSource}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
@@ -1191,19 +1542,19 @@ async function updateLogsTable(logs) {
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm" colspan="4">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 dark:text-green-400 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>' : '<i class="fa fa-eye-slash text-gray-300 dark:text-gray-600 mr-1" title="非跟踪器"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${log.domain}
|
||||
<span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.domain}')">${log.domain}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||
<div class="text-xs text-gray-500 mt-1">客户端: ${log.clientIP}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">客户端: <span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.clientIP}')">${log.clientIP}</span></div>
|
||||
</td>
|
||||
`;
|
||||
} else {
|
||||
@@ -1211,23 +1562,23 @@ async function updateLogsTable(logs) {
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4">
|
||||
<div class="text-sm font-medium">${formattedTime}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">${formattedDate}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium">${log.clientIP}</div>
|
||||
<div class="text-xs text-gray-500 mt-1 location-${log.clientIP.replace(/[.:]/g, '-')}">${log.location || '未知 未知'}</div>
|
||||
<div class="font-medium cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.clientIP}')">${log.clientIP}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 location-${log.clientIP.replace(/[.:]/g, '-')}">${log.location || '未知 未知'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<div class="font-medium flex items-center relative">
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
${log.dnssec ? '<i class="fa fa-lock text-green-500 dark:text-green-400 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||||
<div class="tracker-icon-container relative">
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
|
||||
${isTracker ? '<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>' : '<i class="fa fa-eye-slash text-gray-300 dark:text-gray-600 mr-1" title="非跟踪器"></i>'}
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
${log.domain}
|
||||
<span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.domain}')">${log.domain}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass} dark:text-blue-400">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500 dark:text-green-400"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500 dark:text-blue-400"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
|
||||
<td class="py-3 px-4 text-sm text-center">
|
||||
@@ -1253,48 +1604,86 @@ async function updateLogsTable(logs) {
|
||||
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.add('visible');
|
||||
tooltip.classList.remove('invisible', 'opacity-0', '-translate-y-2');
|
||||
tooltip.classList.add('opacity-100', 'translate-y-0');
|
||||
});
|
||||
|
||||
// 鼠标离开事件
|
||||
iconContainer.addEventListener('mouseleave', () => {
|
||||
tooltip.classList.remove('visible');
|
||||
tooltip.classList.add('opacity-0', '-translate-y-2');
|
||||
tooltip.classList.remove('opacity-100', 'translate-y-0');
|
||||
// 延迟移除invisible类,等待过渡动画完成
|
||||
setTimeout(() => {
|
||||
tooltip.classList.add('invisible');
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定按钮事件
|
||||
const blockBtn = row.querySelector('.block-btn');
|
||||
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) {
|
||||
blockBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const domain = e.currentTarget.dataset.domain;
|
||||
const domain = blockBtn.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);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定日志详情点击事件
|
||||
row.addEventListener('click', (e) => {
|
||||
// 如果点击的是按钮,不触发详情弹窗
|
||||
if (e.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
showLogDetailModal(log);
|
||||
});
|
||||
|
||||
tableBody.appendChild(row);
|
||||
// 处理放行按钮点击
|
||||
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);
|
||||
} 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) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
const trackerTooltip = isTracker ? `
|
||||
<div class="tracker-tooltip absolute z-50 bg-white dark:bg-gray-800 shadow-lg rounded-md border border-gray-200 dark:border-gray-700 p-3 min-w-64 text-sm text-gray-900 dark:text-gray-100">
|
||||
<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 dark:text-blue-400 hover:underline">${trackerInfo.url}</a></div>` : ''}
|
||||
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// 跟踪器信息
|
||||
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) {
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
${isTracker ? `
|
||||
<div class="flex items-center">
|
||||
<div class="tracker-icon-container relative">
|
||||
<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
<span>${trackerInfo.name} (${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'})</span>
|
||||
</div>
|
||||
` : '无'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
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 = `
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -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(() => {
|
||||
|
||||
392
static/js/memory-manager.js
Normal file
392
static/js/memory-manager.js
Normal 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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user