增加威胁域名审计

This commit is contained in:
Alex Yang
2026-04-03 10:04:07 +08:00
parent 170cdb3537
commit f8e222aaf6
41 changed files with 81016 additions and 4672993 deletions
+417 -239
View File
@@ -3,7 +3,8 @@
// 全局变量
let currentPage = 1;
let totalPages = 1;
let logsPerPage = 30; // 默认显示30条记录
let totalRecords = 0; // 记录
let logsPerPage = 10; // 默认显示 10 条记录
let currentFilter = '';
let currentSearch = '';
let logsChart = null;
@@ -18,6 +19,12 @@ if (typeof ipGeolocationCache === 'undefined') {
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期 24 小时
}
// 跟踪器检查缓存(优化性能,避免重复检查)
const trackerCache = new Map();
const trackerCacheTimestamp = new Map();
const TRACKER_CHECK_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期 24 小时
const TRACKER_CHECK_CACHE_MAX_SIZE = 1000; // 最大缓存 1000 条记录
// LRU 缓存辅助函数:更新缓存顺序,将指定 IP 移到最后(最近使用)
function updateCacheOrder(ip) {
const index = ipGeolocationCacheOrder.indexOf(ip);
@@ -269,22 +276,45 @@ async function getDomainInfoFromAPI(domain) {
}
}
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息(带缓存优化)
async function isDomainInTrackerDatabase(domain) {
// 检查缓存
const cached = trackerCache.get(domain);
const timestamp = trackerCacheTimestamp.get(domain);
if (cached !== undefined && timestamp && (Date.now() - timestamp) < TRACKER_CHECK_CACHE_EXPIRY) {
return cached;
}
// 确保数据库已加载
if (!trackersDatabase || !trackersLoaded) {
await loadTrackersDatabase();
}
if (!trackersDatabase || !trackersDatabase.trackers) {
// 缓存空结果
trackerCache.set(domain, null);
trackerCacheTimestamp.set(domain, Date.now());
return null;
}
// 检查域名是否直接作为跟踪器键存在
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
return trackersDatabase.trackers[domain];
const result = trackersDatabase.trackers[domain];
// 缓存结果
trackerCache.set(domain, result);
trackerCacheTimestamp.set(domain, Date.now());
// 限制缓存大小
if (trackerCache.size > TRACKER_CHECK_CACHE_MAX_SIZE) {
const oldestKey = trackerCache.keys().next().value;
trackerCache.delete(oldestKey);
trackerCacheTimestamp.delete(oldestKey);
}
return result;
}
// 检查域名是否在跟踪器URL中
// 检查域名是否在跟踪器 URL
let result = null;
for (const trackerKey in trackersDatabase.trackers) {
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
const tracker = trackersDatabase.trackers[trackerKey];
@@ -292,16 +322,28 @@ async function isDomainInTrackerDatabase(domain) {
try {
const trackerUrl = new URL(tracker.url);
if (trackerUrl.hostname === domain) {
return tracker;
result = tracker;
break;
}
} catch (e) {
// 忽略无效URL
// 忽略无效 URL
}
}
}
}
return null;
// 缓存结果
trackerCache.set(domain, result);
trackerCacheTimestamp.set(domain, Date.now());
// 限制缓存大小
if (trackerCache.size > TRACKER_CHECK_CACHE_MAX_SIZE) {
const oldestKey = trackerCache.keys().next().value;
trackerCache.delete(oldestKey);
trackerCacheTimestamp.delete(oldestKey);
}
return result;
}
// 根据域名查找对应的网站信息(使用 API)
@@ -662,6 +704,12 @@ function initResizableColumns() {
// 初始化查询日志页面
function initLogsPage() {
// 【优化】预加载跟踪器数据库(不阻塞页面初始化)
if (typeof loadTrackersDatabase === 'function') {
loadTrackersDatabase();
console.log('📦 [优化] 跟踪器数据库预加载已启动');
}
// 加载日志统计数据
loadLogsStats();
@@ -748,15 +796,30 @@ function bindLogsEvents() {
});
}
// 自定义记录数量
const perPageSelect = document.getElementById('logs-per-page');
if (perPageSelect) {
perPageSelect.addEventListener('change', () => {
logsPerPage = parseInt(perPageSelect.value);
currentPage = 1;
loadLogs();
});
}
// 自定义记录数量(顶部和底部选择器同步)
const perPageSelectTop = document.getElementById('logs-per-page');
const perPageSelectBottom = document.getElementById('logs-per-page-bottom');
// 同步两个选择器的事件
[perPageSelectTop, perPageSelectBottom].forEach(select => {
if (select) {
select.addEventListener('change', () => {
const newValue = parseInt(select.value);
console.log('每页数量改变:', logsPerPage, '->', newValue);
logsPerPage = newValue;
currentPage = 1;
// 同步两个选择器的值
if (perPageSelectTop && perPageSelectTop !== select) {
perPageSelectTop.value = newValue;
}
if (perPageSelectBottom && perPageSelectBottom !== select) {
perPageSelectBottom.value = newValue;
}
console.log('开始加载日志,limit:', logsPerPage);
loadLogs();
});
}
});
// 分页按钮
const prevBtn = document.getElementById('logs-prev-page');
@@ -916,18 +979,60 @@ function loadLogsStats() {
});
}
// 更新日志统计数据UI
// 更新日志统计数据 UI
function updateLogsStatsUI(data) {
if (!data) return;
// 更新统计卡片
document.getElementById('logs-total-queries').textContent = data.totalQueries;
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
document.getElementById('logs-active-ips').textContent = data.activeIPs;
// 计算屏蔽率
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
document.getElementById('logs-block-rate').textContent = blockRate + '%';
if (!data) return;
// 更新统计卡片
document.getElementById('logs-total-queries').textContent = data.totalQueries;
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
document.getElementById('logs-active-ips').textContent = data.activeIPs;
// 计算屏蔽率
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
document.getElementById('logs-block-rate').textContent = blockRate + '%';
// 更新归档统计信息(如果存在)
if (data.archiveCount !== undefined) {
const archiveInfoEl = document.getElementById('logs-archive-info');
if (archiveInfoEl) {
const archiveCount = data.archiveCount || 0;
const archiveTotalRecords = data.archiveTotalRecords || 0;
const archiveTotalSize = data.archiveTotalCompressedSize || 0;
const grandTotal = data.grandTotalRecords || data.totalQueries;
if (archiveCount > 0) {
archiveInfoEl.innerHTML = `
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
<i class="fa fa-archive text-primary mr-2"></i>
<span>已包含 <strong>${archiveCount}</strong> 个归档文件,共 <strong>${formatNumber(archiveTotalRecords)}</strong> 条历史数据</span>
<span class="mx-2">|</span>
<span>主库:<strong>${formatNumber(data.totalQueries)}</strong> 条</span>
<span class="mx-2">|</span>
<span>总计:<strong>${formatNumber(grandTotal)}</strong> 条</span>
<span class="mx-2">|</span>
<span>归档大小:<strong>${formatFileSize(archiveTotalSize)}</strong></span>
<button id="view-archives-btn" class="ml-4 px-3 py-1 bg-primary text-white rounded-md hover:bg-blue-600 transition-colors text-xs">
<i class="fa fa-list mr-1"></i>查看归档
</button>
</div>
`;
// 绑定查看归档按钮事件
const viewArchivesBtn = document.getElementById('view-archives-btn');
if (viewArchivesBtn) {
viewArchivesBtn.addEventListener('click', showArchiveListModal);
}
} else {
archiveInfoEl.innerHTML = `
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<i class="fa fa-archive mr-2"></i>
<span>暂无归档文件</span>
</div>
`;
}
}
}
}
// 加载日志详情
@@ -961,8 +1066,8 @@ async function loadLogs() {
}
*/
// 构建请求URL
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
// 构建请求 URL,使用 page 参数代替 offset
let endpoint = `/logs/query?page=${currentPage}&limit=${logsPerPage}`;
// 添加过滤条件
if (currentFilter) {
@@ -980,7 +1085,7 @@ async function loadLogs() {
}
try {
// 使用封装的apiRequest函数进行API调用
// 使用封装的 apiRequest 函数进行 API 调用
const logsData = await apiRequest(endpoint);
if (logsData && logsData.error) {
@@ -992,19 +1097,37 @@ async function loadLogs() {
return;
}
// 加载日志总数
const [logs, countData] = await Promise.all([
Promise.resolve(logsData || []), // 确保logsData是数组
apiRequest('/logs/count')
]);
// 处理新的返回格式:{logs: [...], total: 123, page: 1, limit: 30, totalPages: 5}
let logsArray = [];
// 确保logs是数组
const logsArray = Array.isArray(logs) ? logs : [];
// 确保countData是有效的
const totalLogs = countData && countData.count ? countData.count : logsArray.length;
if (logsData && typeof logsData === 'object') {
// 如果是对象格式,提取所有字段
logsArray = Array.isArray(logsData.logs) ? logsData.logs : [];
totalRecords = logsData.total || 0;
// 直接使用后端返回的分页信息
if (logsData.totalPages) {
totalPages = logsData.totalPages;
} else {
// 如果没有返回 totalPages,则自己计算
totalPages = totalRecords > 0 ? Math.ceil(totalRecords / logsPerPage) : 1;
}
// 验证当前页码是否有效
if (logsData.page && logsData.page !== currentPage) {
console.log('后端返回的页码:', logsData.page, '当前页码:', currentPage);
}
} else if (Array.isArray(logsData)) {
// 如果是数组格式(旧格式),直接使用
logsArray = logsData;
totalRecords = logsArray.length;
totalPages = totalRecords > 0 ? Math.ceil(totalRecords / logsPerPage) : 1;
}
// 计算总页数
totalPages = Math.ceil(totalLogs / logsPerPage);
// 确保 logsArray 是数组
if (!Array.isArray(logsArray)) {
logsArray = [];
}
// 禁用缓存存储,每次都从服务器获取最新数据
/*
@@ -1055,233 +1178,174 @@ async function loadLogs() {
}
}
// 更新日志表格
// 更新日志表格(优化版:并行处理 + 文档片段)
async function updateLogsTable(logs) {
const startTime = performance.now();
console.log('🚀 [性能] 开始渲染日志表格...');
const tableBody = document.getElementById('logs-table-body');
if (!tableBody) return;
// 清空表格
tableBody.innerHTML = '';
if (logs.length === 0) {
// 显示空状态
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = `
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
tableBody.innerHTML = `
<tr>
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
</tr>
`;
tableBody.appendChild(emptyRow);
return;
}
// 检测是否为移动设备
const isMobile = window.innerWidth <= 768;
// 填充表格
for (const log of logs) {
const trackerCheckStartTime = performance.now();
const trackerChecks = logs.map(log => isDomainInTrackerDatabase(log.domain));
const trackerResults = await Promise.all(trackerChecks);
const trackerCheckEndTime = performance.now();
console.log(`✅ [性能] 跟踪器检查完成,耗时:${(trackerCheckEndTime - trackerCheckStartTime).toFixed(2)}ms`);
const fragment = document.createDocumentFragment();
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
const trackerInfo = trackerResults[i];
const isTracker = trackerInfo !== null;
const row = document.createElement('tr');
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
row.className = `border-b border-gray-100 hover:bg-gray-50 transition-colors ${log.result === 'blocked' ? 'bg-red-50' : ''}`;
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
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'
});
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 = '';
}
let statusText = '', statusClass = '';
if (log.result === 'blocked') { statusText = '被屏蔽'; statusClass = 'text-danger'; }
else if (log.result === 'allowed') { statusText = '允许'; statusClass = 'text-success'; }
else if (log.result === 'error') { statusText = '错误'; statusClass = 'text-warning'; }
// 添加行背景色
if (rowClass) {
row.classList.add(rowClass);
}
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
const isBlocked = log.result === 'blocked';
// 添加被屏蔽或允许显示,并增加颜色
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 trackerTooltip = isTracker ? `
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
<div class="font-semibold mb-2">已知跟踪器</div>
<div class="mb-1"><strong>名称:</strong> ${trackerInfo.name || '未知'}</div>
<div class="mb-1"><strong>类别:</strong> ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}</div>
${trackerInfo.url ? `<div class="mb-1"><strong>URL:</strong> <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
${trackerInfo.source ? `<div class="mb-1"><strong>源:</strong> ${trackerInfo.source}</div>` : ''}
</div>
` : '';
// 检查域名是否在跟踪器数据库中
const 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 ? `
<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>` : ''}
if (isMobile) {
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>
</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>' : ''}
<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>'}
${trackerTooltip}
</div>
${log.domain}
</div>
` : '';
if (isMobile) {
// 移动设备只显示时间和请求信息
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>
</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>' : ''}
<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>'}
${trackerTooltip}
</div>
${log.domain}
</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>
</td>
`;
} else {
// 桌面设备显示完整信息
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>
</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>
</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>' : ''}
<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>'}
${trackerTooltip}
</div>
<img src="images/whois.svg" alt="WHOIS" class="w-4 h-4 mr-1 inline-block cursor-pointer hover:opacity-75" onclick="event.stopPropagation(); window.location.href = window.location.pathname + '#whois'; setTimeout(() => { const input = document.getElementById('whois-domain-input'); if(input && '${log.domain}') { input.value = '${log.domain}'; if(typeof searchDomainInfo === 'function') { searchDomainInfo(); } } }, 200);" title="查看域名信息" />
${log.domain}
</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>
</td>
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
<td class="py-3 px-4 text-sm text-center">
${isBlocked ?
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.domain}">放行</button>` :
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.domain}">拦截</button>`
}
</td>
`;
// 更新IP地理位置信息
const locationElement = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
if (locationElement) {
// 调用getIpGeolocation函数获取地理位置
getIpGeolocation(log.clientIP).then(location => {
locationElement.textContent = location;
});
}
}
// 添加跟踪器图标悬停事件
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');
});
}
}
<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>
</td>
`;
} else {
const domainEscaped = log.domain.replace(/'/g, "\\'");
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>
</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>
</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>' : ''}
<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>'}
${trackerTooltip}
</div>
<img src="images/whois.svg" alt="WHOIS" class="w-4 h-4 mr-1 inline-block cursor-pointer hover:opacity-75" onclick="event.stopPropagation(); window.location.href = window.location.pathname + '#whois'; setTimeout(() => { const input = document.getElementById('whois-domain-input'); if(input && '${domainEscaped}') { input.value = '${domainEscaped}'; if(typeof searchDomainInfo === 'function') { searchDomainInfo(); } } }, 200);" title="查看域名信息" />
${log.domain}
</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>
</td>
<td class="py-3 px-4 text-sm">${log.responseTime}ms</td>
<td class="py-3 px-4 text-sm text-center">
${isBlocked ? `<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${domainEscaped}">放行</button>` : `<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${domainEscaped}">拦截</button>`}
</td>
`;
// 绑定按钮事件
const blockBtn = row.querySelector('.block-btn');
if (blockBtn) {
blockBtn.addEventListener('click', (e) => {
e.preventDefault();
const domain = e.currentTarget.dataset.domain;
blockDomain(domain);
if (!isPrivateIP(log.clientIP)) {
getIpGeolocation(log.clientIP).then(location => {
const locationEl = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
if (locationEl) locationEl.textContent = location;
});
}
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 blockBtn = row.querySelector('.block-btn');
if (blockBtn) blockBtn.addEventListener('click', e => { e.preventDefault(); blockDomain(e.currentTarget.dataset.domain); });
const unblockBtn = row.querySelector('.unblock-btn');
if (unblockBtn) unblockBtn.addEventListener('click', e => { e.preventDefault(); unblockDomain(e.currentTarget.dataset.domain); });
row.addEventListener('click', e => { if (e.target.closest('button')) return; showLogDetailModal(log); });
if (isTracker) {
const iconContainer = row.querySelector('.tracker-icon-container');
const tooltip = iconContainer && iconContainer.querySelector('.tracker-tooltip');
if (iconContainer && tooltip) {
tooltip.removeAttribute('style');
iconContainer.addEventListener('mouseenter', () => tooltip.classList.add('visible'));
iconContainer.addEventListener('mouseleave', () => tooltip.classList.remove('visible'));
}
}
fragment.appendChild(row);
}
tableBody.appendChild(fragment);
const endTime = performance.now();
console.log(`✅ [性能] 日志表格渲染完成,总耗时:${(endTime - startTime).toFixed(2)}ms`);
}
// 更新分页信息
function updateLogsPagination() {
// 更新页码显示
document.getElementById('logs-current-page').textContent = currentPage;
document.getElementById('logs-total-pages').textContent = totalPages;
// 更新总记录数显示
const totalCountEl = document.getElementById('logs-total-count');
if (totalCountEl) {
totalCountEl.textContent = totalRecords;
}
// 更新页码显示(顶部和底部同步)
const currentPageEl = document.getElementById('logs-current-page');
const totalPagesEl = document.getElementById('logs-total-pages');
const totalPagesBottomEl = document.getElementById('logs-total-pages-bottom');
if (currentPageEl) {
currentPageEl.textContent = currentPage;
}
if (totalPagesEl) {
totalPagesEl.textContent = totalPages;
}
if (totalPagesBottomEl) {
totalPagesBottomEl.textContent = totalPages;
}
// 更新页码输入框
const pageInput = document.getElementById('logs-page-input');
@@ -1920,7 +1984,7 @@ async function showLogDetailModal(log) {
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
recordsDiv.innerHTML = `
<div class="text-xs text-gray-500 dark:text-gray-400">解析记录</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-all bg-gray-50 dark:bg-gray-700 p-3 rounded-md border border-gray-200 dark:border-gray-600">${dnsRecords}</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words font-mono bg-gray-50 dark:bg-gray-700 p-3 rounded-md border border-gray-200 dark:border-gray-600 overflow-x-auto">${dnsRecords}</div>
`;
// DNS服务器
@@ -2205,3 +2269,117 @@ function initLogDetailModal() {
}
// 格式化数字(添加千分位分隔符)
function formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === null || bytes === undefined || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// 显示归档列表弹窗
async function showArchiveListModal() {
try {
const archives = await apiRequest('/logs/archives');
if (!archives || archives.error) {
const errorMsg = archives && archives.error ? archives.error : '未知错误';
showNotification('获取归档列表失败:' + errorMsg, 'danger');
return;
}
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.style.zIndex = '9999';
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-4xl max-h-[90vh] overflow-y-auto animate-slide-in';
const header = document.createElement('div');
header.className = 'sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-between items-center';
const title = document.createElement('h3');
title.className = 'text-xl font-semibold text-gray-900 dark:text-gray-100';
title.textContent = '归档文件列表';
const closeButton = document.createElement('button');
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
closeButton.className = 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition-colors';
closeButton.onclick = () => closeModal();
header.appendChild(title);
header.appendChild(closeButton);
const content = document.createElement('div');
content.className = 'p-6';
if (archives.length === 0) {
content.innerHTML = '<div class="text-center py-12"><i class="fa fa-archive text-6xl text-gray-300 mb-4"></i><p class="text-gray-500 dark:text-gray-400">暂无归档文件</p></div>';
} else {
const table = document.createElement('table');
table.className = 'w-full text-sm text-left text-gray-500 dark:text-gray-400';
const thead = document.createElement('thead');
thead.className = 'text-xs text-gray-700 dark:text-gray-400 uppercase bg-gray-50 dark:bg-gray-700';
thead.innerHTML = '<tr><th class="px-6 py-3">归档日期</th><th class="px-6 py-3">月份</th><th class="px-6 py-3">记录数</th><th class="px-6 py-3">原始大小</th><th class="px-6 py-3">压缩后大小</th><th class="px-6 py-3">压缩比</th><th class="px-6 py-3">时间范围</th></tr>';
table.appendChild(thead);
const tbody = document.createElement('tbody');
archives.forEach(archive => {
const row = document.createElement('tr');
row.className = 'bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700';
const archiveDate = new Date(archive.archiveDate);
const startTime = new Date(archive.startTime);
const endTime = new Date(archive.endTime);
const compressionRatio = archive.originalSize > 0 ? (archive.originalSize / archive.compressedSize).toFixed(2) : 'N/A';
row.innerHTML = '<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">' + archiveDate.toLocaleDateString('zh-CN') + '</td><td class="px-6 py-4">' + archive.month + '</td><td class="px-6 py-4">' + formatNumber(archive.recordCount) + '</td><td class="px-6 py-4">' + formatFileSize(archive.originalSize) + '</td><td class="px-6 py-4">' + formatFileSize(archive.compressedSize) + '</td><td class="px-6 py-4 text-green-600 dark:text-green-400">' + compressionRatio + 'x</td><td class="px-6 py-4"><div class="text-xs">' + startTime.toLocaleDateString('zh-CN') + '</div><div class="text-xs text-gray-400">' + startTime.toLocaleTimeString('zh-CN') + ' - ' + endTime.toLocaleTimeString('zh-CN') + '</div></td>';
tbody.appendChild(row);
});
table.appendChild(tbody);
content.appendChild(table);
}
modalContent.appendChild(header);
modalContent.appendChild(content);
modalContainer.appendChild(modalContent);
document.body.appendChild(modalContainer);
function closeModal() {
modalContainer.classList.add('animate-fade-out');
modalContent.classList.add('animate-slide-out');
setTimeout(() => {
document.body.removeChild(modalContainer);
}, 300);
}
modalContainer.addEventListener('click', (e) => {
if (e.target === modalContainer) {
closeModal();
}
});
const handleEsc = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
} catch (error) {
console.error('显示归档列表失败:', error);
showNotification('显示归档列表失败:' + error.message, 'danger');
}
}