// logs.js - 查询日志页面功能
// 全局变量
let currentPage = 1;
let totalPages = 1;
let totalRecords = 0; // 总记录数
let logsPerPage = 10; // 默认显示 10 条记录
let currentFilter = '';
let currentSearch = '';
let logsChart = null;
let currentSortField = 'timestamp'; // 默认按时间排序,显示最新记录
let currentSortDirection = 'desc'; // 默认降序
// IP 地理位置缓存(检查是否已经存在,避免重复声明)
if (typeof ipGeolocationCache === 'undefined') {
var ipGeolocationCache = {};
var ipGeolocationCacheOrder = []; // 维护缓存插入顺序,用于 LRU
var GEOLOCATION_CACHE_MAX_SIZE = 1000; // 最大缓存 1000 条记录
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);
if (index > -1) {
// 已存在,移除旧位置
ipGeolocationCacheOrder.splice(index, 1);
}
// 添加到末尾(最近使用)
ipGeolocationCacheOrder.push(ip);
}
// LRU 缓存辅助函数:淘汰最少使用的缓存项
function evictLRUCache() {
while (ipGeolocationCacheOrder.length >= GEOLOCATION_CACHE_MAX_SIZE) {
// 淘汰最久未使用的(数组第一个)
const oldestIP = ipGeolocationCacheOrder.shift();
if (oldestIP) {
delete ipGeolocationCache[oldestIP];
}
}
}
// LRU 缓存辅助函数:清理过期缓存
function cleanupExpiredCache() {
const now = Date.now();
const expiredIPs = [];
// 找出所有过期的 IP
for (const ip of ipGeolocationCacheOrder) {
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) >= GEOLOCATION_CACHE_EXPIRY) {
expiredIPs.push(ip);
}
}
// 删除过期缓存
for (const ip of expiredIPs) {
delete ipGeolocationCache[ip];
const orderIndex = ipGeolocationCacheOrder.indexOf(ip);
if (orderIndex > -1) {
ipGeolocationCacheOrder.splice(orderIndex, 1);
}
}
}
// 获取 IP 地理位置信息(优化版,带 LRU 缓存管理)
async function getIpGeolocation(ip) {
// 检查是否为内网 IP
if (isPrivateIP(ip)) {
return "内网 内网";
}
// 定期清理过期缓存(每 100 次请求清理一次)
if (Math.random() < 0.01) {
cleanupExpiredCache();
}
// 检查缓存
const now = Date.now();
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
// 缓存命中,更新使用顺序
updateCacheOrder(ip);
return ipGeolocationCache[ip].location;
}
try {
// 使用 whois.pconline.com.cn API 获取 IP 地理位置
const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`;
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析响应数据
const data = await response.json();
let location = "未知 未知";
if (data && data.addr) {
// 直接使用 addr 字段作为完整的地理位置信息
location = data.addr;
}
// 如果缓存已满,先淘汰最少使用的项
if (Object.keys(ipGeolocationCache).length >= GEOLOCATION_CACHE_MAX_SIZE) {
evictLRUCache();
}
// 保存到缓存
ipGeolocationCache[ip] = {
location: location,
timestamp: now
};
updateCacheOrder(ip);
return location;
} catch (error) {
console.error('获取 IP 地理位置失败:', error);
return "未知 未知";
}
}
// 检查是否为内网IP
function isPrivateIP(ip) {
const parts = ip.split('.');
// 检查IPv4内网地址
if (parts.length === 4) {
const first = parseInt(parts[0]);
const second = parseInt(parts[1]);
// 10.0.0.0/8
if (first === 10) {
return true;
}
// 172.16.0.0/12
if (first === 172 && second >= 16 && second <= 31) {
return true;
}
// 192.168.0.0/16
if (first === 192 && second === 168) {
return true;
}
// 127.0.0.0/8 (localhost)
if (first === 127) {
return true;
}
// 169.254.0.0/16 (link-local)
if (first === 169 && second === 254) {
return true;
}
}
// 检查IPv6内网地址
if (ip.includes(':')) {
// ::1/128 (localhost)
if (ip === '::1' || ip.startsWith('0:0:0:0:0:0:0:1')) {
return true;
}
// fc00::/7 (unique local address)
if (ip.startsWith('fc') || ip.startsWith('fd')) {
return true;
}
// fe80::/10 (link-local)
if (ip.startsWith('fe80:')) {
return true;
}
}
return false;
}
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
if (typeof trackersDatabase === 'undefined') {
var trackersDatabase = null;
var trackersLoaded = false;
var trackersLoading = false;
}
// 域名信息缓存(用于 API 调用)
let domainInfoCache = {};
let domainInfoCacheLoading = {};
// WebSocket连接和重连计时器
let logsWsConnection = null;
let logsWsReconnectTimer = null;
// 加载跟踪器数据库
async function loadTrackersDatabase() {
if (trackersLoaded) return trackersDatabase;
if (trackersLoading) {
// 等待正在进行的加载完成
while (trackersLoading) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return trackersDatabase;
}
trackersLoading = true;
try {
const response = await fetch('domain-info/tracker/trackers.json');
if (!response.ok) {
console.error('加载跟踪器数据库失败:', response.statusText);
trackersDatabase = { trackers: {} };
return trackersDatabase;
}
trackersDatabase = await response.json();
trackersLoaded = true;
return trackersDatabase;
} catch (error) {
console.error('加载跟踪器数据库失败:', error);
trackersDatabase = { trackers: {} };
return trackersDatabase;
} finally {
trackersLoading = false;
}
}
// 从 API 获取域名信息
async function getDomainInfoFromAPI(domain) {
// 检查缓存
if (domainInfoCache[domain]) {
return domainInfoCache[domain];
}
// 检查是否正在加载中
if (domainInfoCacheLoading[domain]) {
while (domainInfoCacheLoading[domain]) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return domainInfoCache[domain] || null;
}
domainInfoCacheLoading[domain] = true;
try {
const response = await fetch(`/api/domain-info?domains=${encodeURIComponent(domain)}`);
if (!response.ok) {
console.error('获取域名信息失败:', response.statusText);
return null;
}
const data = await response.json();
// API 返回的是数组,取第一个匹配的结果
if (Array.isArray(data) && data.length > 0) {
const info = data[0];
// 缓存结果
domainInfoCache[domain] = {
name: info.name || '未知',
icon: info.icon || null,
category: info.category || '未知',
company: info.company || '未知'
};
return domainInfoCache[domain];
}
return null;
} catch (error) {
console.error('获取域名信息失败:', error);
return null;
} finally {
domainInfoCacheLoading[domain] = false;
}
}
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息(带缓存优化)
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)) {
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 中
let result = null;
for (const trackerKey in trackersDatabase.trackers) {
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
const tracker = trackersDatabase.trackers[trackerKey];
if (tracker && tracker.url) {
try {
const trackerUrl = new URL(tracker.url);
if (trackerUrl.hostname === domain) {
result = tracker;
break;
}
} catch (e) {
// 忽略无效 URL
}
}
}
}
// 缓存结果
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)
async function getDomainInfo(domain) {
return await getDomainInfoFromAPI(domain);
}
// 检查域名是否匹配
function isDomainMatch(urlValue, targetDomain, categoryId) {
// 规范化目标域名,去除末尾的点
const normalizedTargetDomain = targetDomain.replace(/\.$/, '').toLowerCase();
try {
// 尝试将URL值解析为完整URL
const url = new URL(urlValue);
let hostname = url.hostname.toLowerCase();
// 规范化主机名,去除末尾的点
hostname = hostname.replace(/\.$/, '');
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别,使用域名后缀匹配
if (normalizedTargetDomain.endsWith('.' + hostname) || normalizedTargetDomain === hostname) {
return true;
} else {
return false;
}
} else {
// 其他类别,使用完整域名匹配
if (hostname === normalizedTargetDomain) {
return true;
} else {
return false;
}
}
} catch (e) {
// 如果是纯域名而不是完整URL
let urlDomain = urlValue.toLowerCase();
// 规范化纯域名,去除末尾的点
urlDomain = urlDomain.replace(/\.$/, '');
// 根据类别ID选择匹配方式
if (categoryId === 2) {
// CDN类别,使用域名后缀匹配
if (normalizedTargetDomain.endsWith('.' + urlDomain) || normalizedTargetDomain === urlDomain) {
return true;
} else {
return false;
}
} else {
// 其他类别,使用完整域名匹配
if (urlDomain === normalizedTargetDomain) {
return true;
} else {
return false;
}
}
}
}
// 提取主域名
function extractPrimaryDomain(domain) {
const parts = domain.split('.');
if (parts.length <= 2) {
return domain;
}
// 处理常见的三级域名
const commonSubdomains = ['www', 'mail', 'news', 'map', 'image', 'video', 'cdn', 'api', 'blog', 'shop', 'cloud', 'docs', 'help', 'support', 'dev', 'test', 'staging'];
if (commonSubdomains.includes(parts[0])) {
const result = parts.slice(1).join('.');
return result;
}
// 处理特殊情况,如co.uk, co.jp等
const countryTLDs = ['co.uk', 'co.jp', 'co.kr', 'co.in', 'co.ca', 'co.au', 'co.nz', 'co.th', 'co.sg', 'co.my', 'co.id', 'co.za', 'com.cn', 'org.cn', 'net.cn', 'gov.cn', 'edu.cn'];
for (const tld of countryTLDs) {
if (domain.endsWith('.' + tld)) {
const mainParts = domain.split('.');
const result = mainParts.slice(-tld.split('.').length - 1).join('.');
return result;
}
}
// 默认情况:返回最后两个部分
const result = parts.slice(-2).join('.');
return result;
}
// 初始化列宽调节功能
function initResizableColumns() {
const table = document.querySelector('.resizable-table');
if (!table) return;
// 为每个表头添加调整手柄元素
function addResizeHandles() {
const headers = table.querySelectorAll('th');
headers.forEach(header => {
// 移除已存在的手柄
const existingHandle = header.querySelector('.resize-handle');
if (existingHandle) {
existingHandle.remove();
}
// 创建新的调整手柄
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
resizeHandle.style.cssText = `
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 100%;
cursor: col-resize;
background: rgba(59, 130, 246, 0.1);
z-index: 10;
transition: background-color 0.2s ease;
`;
// 添加悬停效果
resizeHandle.addEventListener('mouseenter', () => {
resizeHandle.style.background = 'rgba(59, 130, 246, 0.3)';
});
resizeHandle.addEventListener('mouseleave', () => {
if (!resizeHandle.classList.contains('dragging')) {
resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)';
}
});
header.style.position = 'relative';
header.appendChild(resizeHandle);
});
}
// 计算列宽并设置固定宽度
function calculateAndSetColumnWidths() {
// 确保表格可见
table.style.visibility = 'visible';
// 保存当前表格布局
const originalLayout = table.style.tableLayout;
table.style.tableLayout = 'auto';
// 获取所有表头和数据行
const headers = table.querySelectorAll('th');
const rows = table.querySelectorAll('tbody tr');
// 计算每列的最大宽度
const columnWidths = [];
headers.forEach((header, index) => {
// 获取表头宽度
let maxWidth = header.offsetWidth;
// 遍历所有数据行,找到该列的最大宽度
rows.forEach(row => {
const cell = row.children[index];
if (cell) {
maxWidth = Math.max(maxWidth, cell.offsetWidth);
}
});
// 添加一些 padding
maxWidth += 20;
// 保存最大宽度
columnWidths[index] = maxWidth;
});
// 设置每列的固定宽度
headers.forEach((header, index) => {
const width = `${columnWidths[index]}px`;
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;
}
});
});
// 恢复表格布局
table.style.tableLayout = 'fixed';
}
// 保存列宽设置的函数
function saveColumnWidths() {
const headers = table.querySelectorAll('th');
const columnWidths = {};
headers.forEach((header, index) => {
columnWidths[index] = header.style.width;
});
localStorage.setItem('logsTableColumnWidths', JSON.stringify(columnWidths));
}
// 恢复列宽设置的函数
function restoreColumnWidths() {
const headers = table.querySelectorAll('th');
const savedWidths = localStorage.getItem('logsTableColumnWidths');
if (savedWidths) {
const columnWidths = JSON.parse(savedWidths);
// 设置表格布局为fixed
table.style.tableLayout = 'fixed';
headers.forEach((header, index) => {
if (columnWidths[index]) {
const width = columnWidths[index];
header.style.width = width;
header.style.minWidth = width;
header.style.maxWidth = width;
// 找到对应的数据列并设置宽度
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cell = row.children[index];
if (cell) {
cell.style.width = width;
cell.style.minWidth = width;
cell.style.maxWidth = width;
}
});
}
});
} else {
// 没有保存的宽度,计算并设置列宽
calculateAndSetColumnWidths();
}
}
// 恢复保存的列宽设置或计算初始列宽
restoreColumnWidths();
// 添加调整手柄
addResizeHandles();
// 拖拽状态变量
let currentHeader = null;
let startX = 0;
let startWidth = 0;
let isDragging = false;
// 鼠标按下事件
table.addEventListener('mousedown', (e) => {
const resizeHandle = e.target.closest('.resize-handle');
if (resizeHandle) {
currentHeader = resizeHandle.parentElement;
startX = e.clientX;
startWidth = currentHeader.offsetWidth;
isDragging = true;
// 添加拖拽状态类
currentHeader.classList.add('dragging');
resizeHandle.classList.add('dragging');
// 改变拖拽手柄样式
resizeHandle.style.background = 'rgba(59, 130, 246, 0.6)';
// 阻止默认事件和冒泡
e.preventDefault();
e.stopPropagation();
// 阻止文本选择
document.addEventListener('selectstart', preventSelect, { capture: true });
document.addEventListener('copy', preventCopy, { capture: true });
// 添加全局事件监听器
document.addEventListener('mousemove', onMouseMove, { capture: true });
document.addEventListener('mouseup', onMouseUp, { capture: true });
}
});
// 鼠标移动事件处理函数
function onMouseMove(e) {
if (!currentHeader) return;
// 阻止默认事件
e.preventDefault();
e.stopPropagation();
// 计算新宽度
const deltaX = e.clientX - startX;
const newWidth = Math.max(50, startWidth + deltaX);
// 设置新宽度
const width = `${newWidth}px`;
currentHeader.style.width = width;
currentHeader.style.minWidth = width;
currentHeader.style.maxWidth = width;
// 找到对应的数据列并设置宽度
const headers = table.querySelectorAll('th');
const index = Array.from(headers).indexOf(currentHeader);
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cell = row.children[index];
if (cell) {
cell.style.width = width;
cell.style.minWidth = width;
cell.style.maxWidth = width;
}
});
}
// 鼠标释放事件处理函数
function onMouseUp(e) {
if (!currentHeader) return;
// 阻止默认事件
e.preventDefault();
e.stopPropagation();
// 获取调整手柄
const resizeHandle = currentHeader.querySelector('.resize-handle');
// 移除拖拽状态类
currentHeader.classList.remove('dragging');
resizeHandle.classList.remove('dragging');
// 恢复拖拽手柄样式
resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)';
// 保存列宽设置
saveColumnWidths();
// 重置状态
currentHeader = null;
isDragging = false;
// 移除事件监听器
document.removeEventListener('selectstart', preventSelect, { capture: true });
document.removeEventListener('copy', preventCopy, { capture: true });
document.removeEventListener('mousemove', onMouseMove, { capture: true });
document.removeEventListener('mouseup', onMouseUp, { capture: true });
}
// 阻止文本选择和复制
function preventSelect(e) {
e.preventDefault();
}
function preventCopy(e) {
e.preventDefault();
}
}
// 初始化查询日志页面
function initLogsPage() {
// 【优化】预加载跟踪器数据库(不阻塞页面初始化)
if (typeof loadTrackersDatabase === 'function') {
loadTrackersDatabase();
console.log('📦 [优化] 跟踪器数据库预加载已启动');
}
// 加载日志统计数据
loadLogsStats();
// 加载日志详情
loadLogs();
// 初始化图表
initLogsChart();
// 绑定事件
bindLogsEvents();
// 初始化日志详情弹窗
initLogDetailModal();
// 建立WebSocket连接,用于实时更新统计数据和图表
connectLogsWebSocket();
// 初始化列宽调节功能
initResizableColumns();
// 窗口大小改变时重新加载日志表格
window.addEventListener('resize', handleWindowResize);
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupLogsResources);
}
// 处理窗口大小改变
function handleWindowResize() {
// 重新加载日志表格,以适应新的屏幕尺寸
loadLogs();
}
// 清理资源
function cleanupLogsResources() {
// 清除WebSocket连接
if (logsWsConnection) {
logsWsConnection.close();
logsWsConnection = null;
}
// 清除重连计时器
if (logsWsReconnectTimer) {
clearTimeout(logsWsReconnectTimer);
logsWsReconnectTimer = null;
}
// 清除窗口大小改变事件监听器
window.removeEventListener('resize', handleWindowResize);
}
// 绑定事件
function bindLogsEvents() {
// 搜索按钮
const searchBtn = document.getElementById('logs-search-btn');
if (searchBtn) {
searchBtn.addEventListener('click', () => {
currentSearch = document.getElementById('logs-search').value;
currentPage = 1;
loadLogs();
});
}
// 搜索框回车事件
const searchInput = document.getElementById('logs-search');
if (searchInput) {
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
currentSearch = searchInput.value;
currentPage = 1;
loadLogs();
}
});
}
// 结果过滤
const resultFilter = document.getElementById('logs-result-filter');
if (resultFilter) {
resultFilter.addEventListener('change', () => {
currentFilter = resultFilter.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');
const nextBtn = document.getElementById('logs-next-page');
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
loadLogs();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
loadLogs();
}
});
}
// 页码跳转
const pageInput = document.getElementById('logs-page-input');
const goBtn = document.getElementById('logs-go-page');
if (pageInput) {
// 页码输入框回车事件
pageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const page = parseInt(pageInput.value);
if (page >= 1 && page <= totalPages) {
currentPage = page;
loadLogs();
}
}
});
}
if (goBtn) {
// 前往按钮点击事件
goBtn.addEventListener('click', () => {
const page = parseInt(pageInput.value);
if (page >= 1 && page <= totalPages) {
currentPage = page;
loadLogs();
}
});
}
// 时间范围切换
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
timeRangeBtns.forEach(btn => {
btn.addEventListener('click', () => {
// 更新按钮样式
timeRangeBtns.forEach(b => {
b.classList.remove('bg-primary', 'text-white');
b.classList.add('bg-gray-200', 'text-gray-700');
});
btn.classList.remove('bg-gray-200', 'text-gray-700');
btn.classList.add('bg-primary', 'text-white');
// 更新图表
const range = btn.getAttribute('data-range');
updateLogsChart(range);
});
});
// 刷新按钮事件
const refreshBtn = document.getElementById('logs-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
// 重新加载日志
currentPage = 1;
loadLogs();
});
}
// 排序按钮事件
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
header.addEventListener('click', () => {
const sortField = header.getAttribute('data-sort');
// 如果点击的是当前排序字段,则切换排序方向
if (sortField === currentSortField) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
// 否则,设置新的排序字段,默认降序
currentSortField = sortField;
currentSortDirection = 'desc';
}
// 更新排序图标
updateSortIcons();
// 重新加载日志
currentPage = 1;
loadLogs();
});
});
// 初始化排序图标
updateSortIcons();
}
// 更新排序图标
function updateSortIcons() {
const sortHeaders = document.querySelectorAll('th[data-sort]');
sortHeaders.forEach(header => {
const sortField = header.getAttribute('data-sort');
const icon = header.querySelector('i');
// 重置所有图标
icon.className = 'fa fa-sort ml-1 text-xs';
// 设置当前排序字段的图标
if (sortField === currentSortField) {
if (currentSortDirection === 'asc') {
icon.className = 'fa fa-sort-asc ml-1 text-xs';
} else {
icon.className = 'fa fa-sort-desc ml-1 text-xs';
}
}
});
}
// 加载日志统计数据
function loadLogsStats() {
// 检查是否有有效的缓存数据
const cachedStats = window.pageDataCache && window.pageDataCache.getCache('logs_stats');
if (cachedStats) {
console.log('使用缓存的日志统计数据');
updateLogsStatsUI(cachedStats);
return;
}
// 使用封装的apiRequest函数进行API调用
apiRequest('/logs/stats')
.then(data => {
if (data && data.error) {
console.error('加载日志统计数据失败:', data.error);
return;
}
// 更新统计卡片
updateLogsStatsUI(data);
// 存储数据到缓存
if (window.pageDataCache) {
window.pageDataCache.setCache('logs_stats', data);
}
})
.catch(error => {
console.error('加载日志统计数据失败:', error);
});
}
// 更新日志统计数据 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.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 = `
已包含 ${archiveCount} 个归档文件,共 ${formatNumber(archiveTotalRecords)} 条历史数据
|
主库:${formatNumber(data.totalQueries)} 条
|
总计:${formatNumber(grandTotal)} 条
|
归档大小:${formatFileSize(archiveTotalSize)}
`;
// 绑定查看归档按钮事件
const viewArchivesBtn = document.getElementById('view-archives-btn');
if (viewArchivesBtn) {
viewArchivesBtn.addEventListener('click', showArchiveListModal);
}
} else {
archiveInfoEl.innerHTML = `
暂无归档文件
`;
}
}
}
}
// 加载日志详情
async function loadLogs() {
// 显示加载状态
const loadingEl = document.getElementById('logs-loading');
if (loadingEl) {
loadingEl.classList.remove('hidden');
}
// 构建缓存键,包含所有查询参数
// 已禁用缓存,每次都从服务器获取最新数据
// const cacheKey = `logs_${logsPerPage}_${currentPage}_${currentFilter}_${currentSearch}_${currentSortField}_${currentSortDirection}`;
// 检查是否有有效的缓存数据(已禁用)
/*
const cachedLogs = window.pageDataCache && window.pageDataCache.getCache(cacheKey);
if (cachedLogs) {
console.log('使用缓存的日志数据');
const logsArray = Array.isArray(cachedLogs.logs) ? cachedLogs.logs : [];
const totalLogs = cachedLogs.totalLogs || logsArray.length;
totalPages = Math.ceil(totalLogs / logsPerPage);
await updateLogsTable(logsArray);
bindActionButtonsEvents();
updateLogsPagination();
initResizableColumns();
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
*/
// 构建请求 URL,使用 page 参数代替 offset
let endpoint = `/logs/query?page=${currentPage}&limit=${logsPerPage}`;
// 添加过滤条件
if (currentFilter) {
endpoint += `&result=${currentFilter}`;
}
// 添加搜索条件
if (currentSearch) {
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
}
// 添加排序条件
if (currentSortField) {
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
}
try {
// 使用封装的 apiRequest 函数进行 API 调用
const logsData = await apiRequest(endpoint);
if (logsData && logsData.error) {
console.error('加载日志详情失败:', logsData.error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
// 处理新的返回格式:{logs: [...], total: 123, page: 1, limit: 30, totalPages: 5}
let logsArray = [];
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;
}
// 确保 logsArray 是数组
if (!Array.isArray(logsArray)) {
logsArray = [];
}
// 禁用缓存存储,每次都从服务器获取最新数据
/*
if (window.pageDataCache) {
window.pageDataCache.setCache(cacheKey, {
logs: logsArray,
totalLogs: totalLogs
});
}
*/
// 更新日志表格
await updateLogsTable(logsArray);
// 绑定操作按钮事件
bindActionButtonsEvents();
// 更新分页信息
updateLogsPagination();
// 重新初始化列宽调节功能,确保新添加的行也能继承列宽设置
initResizableColumns();
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
} catch (error) {
console.error('加载日志详情失败:', error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
// 显示空状态
const tableBody = document.getElementById('logs-table-body');
if (tableBody) {
tableBody.innerHTML = `
|
暂无查询日志
|
`;
}
}
}
// 更新日志表格(优化版:并行处理 + 文档片段)
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) {
tableBody.innerHTML = `
|
暂无查询日志
|
`;
return;
}
const isMobile = window.innerWidth <= 768;
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 ${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' });
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'; }
const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500';
const isBlocked = log.result === 'blocked';
const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
` : ''}
${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : '';
if (isMobile) {
row.innerHTML = `
${formattedTime}
${formattedDate}
|
${log.dnssec ? ' ' : ''}
${isTracker ? '' : ''}
${trackerTooltip}
${log.domain}
类型:${log.queryType}, ${statusText}
客户端:${log.clientIP}
|
`;
} else {
const domainEscaped = log.domain.replace(/'/g, "\\'");
row.innerHTML = `
${formattedTime}
${formattedDate}
|
${log.clientIP}
${log.location || '未知 未知'}
|
${log.dnssec ? ' ' : ''}
${isTracker ? '' : ''}
${trackerTooltip}

${log.domain}
类型:${log.queryType}, ${statusText}, ${log.fromCache ? '缓存' : '非缓存'}${log.dnssec ? ', DNSSEC' : ''}${log.edns ? ', EDNS' : ''}
DNS 服务器:${log.dnsServer || '无'}, DNSSEC 专用:${log.dnssecServer || '无'}
|
${log.responseTime}ms |
${isBlocked ? `` : ``}
|
`;
if (!isPrivateIP(log.clientIP)) {
getIpGeolocation(log.clientIP).then(location => {
const locationEl = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
if (locationEl) locationEl.textContent = location;
});
}
}
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() {
// 更新总记录数显示
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');
if (pageInput) {
pageInput.max = totalPages;
pageInput.value = currentPage;
}
// 更新按钮状态
const prevBtn = document.getElementById('logs-prev-page');
const nextBtn = document.getElementById('logs-next-page');
if (prevBtn) {
prevBtn.disabled = currentPage === 1;
}
if (nextBtn) {
nextBtn.disabled = currentPage === totalPages;
}
}
// 初始化日志图表
function initLogsChart() {
const ctx = document.getElementById('logs-trend-chart');
if (!ctx) return;
// 获取24小时统计数据
apiRequest('/hourly-stats')
.then(data => {
if (data && data.error) {
console.error('初始化日志图表失败:', data.error);
return;
}
// 创建图表
logsChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: '查询数',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
})
.catch(error => {
console.error('初始化日志图表失败:', error);
});
}
// 更新日志图表
function updateLogsChart(range) {
if (!logsChart) return;
let endpoint = '';
switch (range) {
case '24h':
endpoint = '/hourly-stats';
break;
case '7d':
endpoint = '/daily-stats';
break;
case '30d':
endpoint = '/monthly-stats';
break;
default:
endpoint = '/hourly-stats';
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('更新日志图表失败:', data.error);
return;
}
// 更新图表数据
logsChart.data.labels = data.labels;
logsChart.data.datasets[0].data = data.data;
logsChart.update();
})
.catch(error => {
console.error('更新日志图表失败:', error);
});
}
// 建立WebSocket连接
function connectLogsWebSocket() {
try {
// 构建WebSocket URL
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
// 创建WebSocket连接
logsWsConnection = new WebSocket(wsUrl);
// 连接打开事件
logsWsConnection.onopen = function() {
};
// 接收消息事件
logsWsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'initial_data' || data.type === 'stats_update') {
// 只更新统计数据,不更新日志详情
updateLogsStatsFromWebSocket(data.data);
}
} catch (error) {
console.error('处理WebSocket消息失败:', error);
}
};
// 连接关闭事件
logsWsConnection.onclose = function(event) {
console.warn('WebSocket连接已关闭,代码:', event.code);
logsWsConnection = null;
// 设置重连
setupLogsReconnect();
};
// 连接错误事件
logsWsConnection.onerror = function(error) {
console.error('WebSocket连接错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
}
}
// 设置重连逻辑
function setupLogsReconnect() {
if (logsWsReconnectTimer) {
return; // 已经有重连计时器在运行
}
const reconnectDelay = 5000; // 5秒后重连
logsWsReconnectTimer = setTimeout(() => {
connectLogsWebSocket();
}, reconnectDelay);
}
// 从 WebSocket 更新日志统计数据 - 添加页面可见性检查
function updateLogsStatsFromWebSocket(stats) {
// 页面不可见时跳过处理,节省资源
if (document.hidden) {
return;
}
try {
// 更新统计卡片
if (stats.dns) {
// 适配不同的数据结构
const totalQueries = stats.dns.Queries || 0;
const blockedQueries = stats.dns.Blocked || 0;
const allowedQueries = stats.dns.Allowed || 0;
const errorQueries = stats.dns.Errors || 0;
const avgResponseTime = stats.dns.AvgResponseTime || 0;
const activeIPs = stats.activeIPs || Object.keys(stats.dns.SourceIPs || {}).length;
// 更新统计卡片
document.getElementById('logs-total-queries').textContent = totalQueries;
document.getElementById('logs-avg-response-time').textContent = avgResponseTime.toFixed(2) + 'ms';
document.getElementById('logs-active-ips').textContent = activeIPs;
// 计算屏蔽率
const blockRate = totalQueries > 0 ? (blockedQueries / totalQueries * 100).toFixed(1) : '0';
document.getElementById('logs-block-rate').textContent = blockRate + '%';
}
} catch (error) {
console.error('从 WebSocket 更新日志统计数据失败:', error);
}
}
// 拦截域名
async function blockDomain(domain) {
try {
// 创建拦截规则,使用AdBlock Plus格式
const blockRule = `||${domain}^`;
// 调用API添加拦截规则
const response = await apiRequest('/shield', 'POST', { rule: blockRule });
// 处理不同的响应格式
if (response && (response.success || response.status === 'success')) {
// 重新加载日志,显示更新后的状态
loadLogs();
// 刷新规则列表
refreshRulesList();
// 显示成功通知
if (typeof window.showNotification === 'function') {
window.showNotification(`已成功拦截域名: ${domain}`, 'success');
}
} else {
const errorMsg = response ? (response.message || '添加拦截规则失败') : '添加拦截规则失败: 无效的API响应';
console.error(`拦截域名失败: ${errorMsg}`);
throw new Error(errorMsg);
}
} catch (error) {
console.error('拦截域名失败:', error);
// 显示错误通知
if (typeof window.showNotification === 'function') {
window.showNotification(`拦截域名失败: ${error.message}`, 'danger');
}
}
}
// 绑定操作按钮事件
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);
});
});
}
// 刷新规则列表
async function refreshRulesList() {
try {
// 调用API重新加载规则
const response = await apiRequest('/shield', 'GET');
if (response) {
// 处理规则列表响应
let allRules = [];
if (response && typeof response === 'object') {
// 合并所有类型的规则到一个数组
if (Array.isArray(response.domainRules)) allRules = allRules.concat(response.domainRules);
if (Array.isArray(response.domainExceptions)) allRules = allRules.concat(response.domainExceptions);
if (Array.isArray(response.regexRules)) allRules = allRules.concat(response.regexRules);
if (Array.isArray(response.regexExceptions)) allRules = allRules.concat(response.regexExceptions);
}
// 更新规则列表
if (window.rules) {
rules = allRules;
filteredRules = [...rules];
// 更新规则数量统计
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
window.updateRulesCount(rules.length);
}
}
}
} catch (error) {
console.error('刷新规则列表失败:', error);
}
}
// 放行域名
async function unblockDomain(domain) {
try {
// 创建放行规则,使用AdBlock Plus格式
const allowRule = `@@||${domain}^`;
// 调用API添加放行规则
const response = await apiRequest('/shield', 'POST', { rule: allowRule });
// 处理不同的响应格式
if (response && (response.success || response.status === 'success')) {
// 重新加载日志,显示更新后的状态
loadLogs();
// 刷新规则列表
refreshRulesList();
// 显示成功通知
if (typeof window.showNotification === 'function') {
window.showNotification(`已成功放行域名: ${domain}`, 'success');
}
} else {
const errorMsg = response ? (response.message || '添加放行规则失败') : '添加放行规则失败: 无效的API响应';
console.error(`放行域名失败: ${errorMsg}`);
throw new Error(errorMsg);
}
} catch (error) {
console.error('放行域名失败:', error);
// 显示错误通知
if (typeof window.showNotification === 'function') {
window.showNotification(`放行域名失败: ${error.message}`, 'danger');
}
}
}
// 独立的DNS记录格式化函数
function formatDNSRecords(log, result) {
if (result === 'blocked') return '无';
let records = '';
const sources = [
log.answers,
log.answer,
log.Records,
log.records,
log.response
];
for (const source of sources) {
if (records) break;
if (!source || source === '无') continue;
// 处理数组类型
if (Array.isArray(source)) {
records = source.map(answer => {
const type = answer.type || answer.Type || '未知';
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
// 增强的记录值提取逻辑
if (typeof value === 'string') {
value = value.trim();
// 处理制表符分隔的格式
if (value.includes('\t') || value.includes('\\t')) {
const parts = value.replace(/\\t/g, '\t').split('\t');
if (parts.length >= 4) {
value = parts[parts.length - 1].trim();
}
}
// 处理JSON格式
else if (value.startsWith('{') && value.endsWith('}')) {
try {
const parsed = JSON.parse(value);
value = parsed.data || parsed.value || value;
} catch (e) {}
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
// 处理字符串类型
else if (typeof source === 'string') {
// 尝试解析为JSON数组
if (source.startsWith('[') && source.endsWith(']')) {
try {
const parsed = JSON.parse(source);
if (Array.isArray(parsed)) {
records = parsed.map(answer => {
const type = answer.type || answer.Type || '未知';
let value = answer.value || answer.Value || answer.data || answer.Data || '未知';
const ttl = answer.TTL || answer.ttl || answer.expires || '未知';
if (typeof value === 'string') {
value = value.trim();
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
} catch (e) {
// 解析失败,尝试直接格式化
records = formatDNSString(source);
}
} else {
// 直接格式化字符串
records = formatDNSString(source);
}
}
}
return records || '无解析记录';
}
// 格式化DNS字符串记录
function formatDNSString(str) {
// 处理可能的转义字符并分割行
const recordLines = str.split(/\r?\n/).map(line => line.replace(/^\s+/, '')).filter(line => line.trim() !== '');
return recordLines.map(line => {
// 检查是否已经是标准格式
if (line.includes(':') && line.includes('(')) {
return line;
}
// 尝试解析为标准DNS格式
const parts = line.split(/\s+/);
if (parts.length >= 5) {
const type = parts[3];
const value = parts.slice(4).join(' ');
const ttl = parts[1];
return `${type}: ${value} (ttl=${ttl})`;
}
// 无法解析,返回原始行但移除前导空格
return line.replace(/^\s+/, '');
}).join('\n');
}
// 显示日志详情弹窗
async function showLogDetailModal(log) {
if (!log) {
console.error('No log data provided!');
return;
}
try {
// 安全获取log属性,提供默认值
const timestamp = log.timestamp ? new Date(log.timestamp) : null;
const dateStr = timestamp ? timestamp.toLocaleDateString() : '未知';
const timeStr = timestamp ? timestamp.toLocaleTimeString() : '未知';
const domain = log.domain || '未知';
const queryType = log.queryType || '未知';
const result = log.result || '未知';
const responseTime = log.responseTime || '未知';
const clientIP = log.clientIP || '未知';
const location = log.location || '未知';
const fromCache = log.fromCache || false;
const dnssec = log.dnssec || false;
const edns = log.edns || false;
const dnsServer = log.dnsServer || '无';
const dnssecServer = log.dnssecServer || '无';
const blockRule = log.blockRule || '无';
// 检查域名是否在跟踪器数据库中
const trackerInfo = await isDomainInTrackerDatabase(log.domain);
const isTracker = trackerInfo !== null;
// 获取域名信息
const domainInfo = await getDomainInfo(domain);
// 格式化DNS解析记录
const dnsRecords = formatDNSRecords(log, result);
// 创建模态框容器
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-2xl 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 = '';
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 space-y-6';
// 基本信息部分
const basicInfo = document.createElement('div');
basicInfo.className = 'space-y-4';
const basicInfoTitle = document.createElement('h4');
basicInfoTitle.className = 'text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider';
basicInfoTitle.textContent = '基本信息';
const basicInfoGrid = document.createElement('div');
basicInfoGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
// 添加基本信息项
basicInfoGrid.innerHTML = `
状态
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
`;
// DNS特性
const dnsFeatures = document.createElement('div');
dnsFeatures.className = 'col-span-1 md:col-span-2 space-y-1';
dnsFeatures.innerHTML = `
DNS特性
${dnssec ? 'DNSSEC ' : ''}
${edns ? 'EDNS' : ''}
${!dnssec && !edns ? '无' : ''}
`;
// 域名信息
const domainInfoDiv = document.createElement('div');
domainInfoDiv.className = 'col-span-1 md:col-span-2 space-y-1';
domainInfoDiv.innerHTML = `
域名信息
${domainInfo ? `
${domainInfo.icon ? `

` : ''}
${domainInfo.name || '未知'}
类别:
${domainInfo.category || '未知'}
所属单位/公司:
${domainInfo.company || '未知'}
` : '无'}
`;
// 构建跟踪器浮窗内容
const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name || '未知'}
类别: ${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'}
${trackerInfo.url ? `
` : ''}
${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : '';
// 跟踪器信息
const trackerDiv = document.createElement('div');
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
trackerDiv.innerHTML = `
跟踪器信息
${isTracker ? `
${trackerTooltip}
${trackerInfo.name} (${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知'})
` : '无'}
`;
// 添加跟踪器图标悬停事件
if (isTracker) {
const iconContainer = trackerDiv.querySelector('.tracker-icon-container');
const tooltip = iconContainer.querySelector('.tracker-tooltip');
if (iconContainer && tooltip) {
// 移除内联样式,使用CSS类控制显示
tooltip.removeAttribute('style');
iconContainer.addEventListener('mouseenter', () => {
tooltip.classList.add('visible');
});
iconContainer.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
});
}
}
// 解析记录
const recordsDiv = document.createElement('div');
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
recordsDiv.innerHTML = `
解析记录
${dnsRecords}
`;
// DNS服务器
const dnsServerDiv = document.createElement('div');
dnsServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnsServerDiv.innerHTML = `
DNS服务器
${dnsServer}
`;
// DNSSEC专用服务器
const dnssecServerDiv = document.createElement('div');
dnssecServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnssecServerDiv.innerHTML = `
DNSSEC专用服务器
${dnssecServer}
`;
basicInfoGrid.appendChild(dnsFeatures);
basicInfoGrid.appendChild(domainInfoDiv);
basicInfoGrid.appendChild(trackerDiv);
basicInfoGrid.appendChild(recordsDiv);
basicInfoGrid.appendChild(dnsServerDiv);
basicInfoGrid.appendChild(dnssecServerDiv);
basicInfo.appendChild(basicInfoTitle);
basicInfo.appendChild(basicInfoGrid);
// 响应细节部分
const responseDetails = document.createElement('div');
responseDetails.className = 'space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700';
const responseDetailsTitle = document.createElement('h4');
responseDetailsTitle.className = 'text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider';
responseDetailsTitle.textContent = '响应细节';
// 准备响应细节内容,根据条件添加规则信息
let responseDetailsHTML = `
响应代码
${getResponseCodeText(log.responseCode)}
缓存状态
${fromCache ? '缓存' : '非缓存'}
`;
// 只有被屏蔽时才显示规则信息
if (result === 'blocked') {
responseDetailsHTML += `
`;
}
const responseGrid = document.createElement('div');
responseGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4';
responseGrid.innerHTML = responseDetailsHTML;
responseDetails.appendChild(responseDetailsTitle);
responseDetails.appendChild(responseGrid);
// 客户端详情部分
const clientDetails = document.createElement('div');
clientDetails.className = 'space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700';
const clientDetailsTitle = document.createElement('h4');
clientDetailsTitle.className = 'text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider';
clientDetailsTitle.textContent = '客户端详情';
// 创建客户端IP容器,为后续更新地理位置做准备
const clientIPContainer = document.createElement('div');
clientIPContainer.className = 'text-sm font-medium text-gray-900 dark:text-gray-100';
clientIPContainer.innerHTML = `${clientIP} (${location})`;
const clientIPDiv = document.createElement('div');
clientIPDiv.className = 'space-y-1';
clientIPDiv.innerHTML = `
IP地址
`;
clientIPDiv.appendChild(clientIPContainer);
clientDetails.appendChild(clientDetailsTitle);
clientDetails.appendChild(clientIPDiv);
// 动态更新地理位置信息
const locationElement = clientIPDiv.querySelector(`#modal-location-${clientIP.replace(/[.:]/g, '-')}`);
if (locationElement) {
getIpGeolocation(clientIP).then(location => {
locationElement.textContent = `(${location})`;
});
}
// 操作按钮区域
const actionButtons = document.createElement('div');
actionButtons.className = 'pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-2';
// 根据域名状态显示不同的操作按钮
if (result === 'blocked') {
// 被拦截时显示放行按钮
actionButtons.innerHTML = `
`;
} else {
// 未被拦截时显示拦截按钮
actionButtons.innerHTML = `
`;
}
// 组装内容
content.appendChild(basicInfo);
content.appendChild(responseDetails);
content.appendChild(clientDetails);
content.appendChild(actionButtons);
// 组装模态框
modalContent.appendChild(header);
modalContent.appendChild(content);
modalContainer.appendChild(modalContent);
// 绑定操作按钮事件
if (result === 'blocked') {
const unblockBtn = modalContent.querySelector('#unblock-domain-btn');
if (unblockBtn) {
unblockBtn.addEventListener('click', async () => {
await unblockDomain(domain);
closeModal();
loadLogs(); // 刷新日志列表
});
}
} else {
const blockBtn = modalContent.querySelector('#block-domain-btn');
if (blockBtn) {
blockBtn.addEventListener('click', async () => {
await blockDomain(domain);
closeModal();
loadLogs(); // 刷新日志列表
});
}
}
// 添加到页面
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();
}
});
// ESC键关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handleEsc);
}
};
document.addEventListener('keydown', handleEsc);
} catch (error) {
console.error('Error in showLogDetailModal:', error);
// 显示错误提示
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.style.zIndex = '9999';
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.innerHTML = `
错误
加载日志详情失败: ${error.message}
`;
errorModal.appendChild(errorContent);
document.body.appendChild(errorModal);
// 关闭错误模态框函数
function closeErrorModal() {
errorModal.classList.add('animate-fade-out');
errorContent.classList.add('animate-slide-out');
// 等待动画结束后移除元素
setTimeout(() => {
document.body.removeChild(errorModal);
}, 300);
}
// ESC键关闭错误模态框
const handleErrorEsc = (e) => {
if (e.key === 'Escape') {
closeErrorModal();
document.removeEventListener('keydown', handleErrorEsc);
}
};
document.addEventListener('keydown', handleErrorEsc);
}
}
// 关闭日志详情弹窗
// 获取响应代码文本
function getResponseCodeText(rcode) {
const rcodeMap = {
0: 'NOERROR',
1: 'FORMERR',
2: 'SERVFAIL',
3: 'NXDOMAIN',
4: 'NOTIMP',
5: 'REFUSED',
6: 'YXDOMAIN',
7: 'YXRRSET',
8: 'NXRRSET',
9: 'NOTAUTH',
10: 'NOTZONE'
};
return rcodeMap[rcode] || `UNKNOWN(${rcode})`;
}
function closeLogDetailModal() {
const modal = document.getElementById('log-detail-modal');
modal.classList.add('hidden');
}
// 初始化日志详情弹窗事件
function initLogDetailModal() {
// 关闭按钮事件
const closeBtn = document.getElementById('close-log-modal-btn');
if (closeBtn) {
closeBtn.addEventListener('click', closeLogDetailModal);
}
// 点击模态框外部关闭
const modal = document.getElementById('log-detail-modal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeLogDetailModal();
}
});
}
// ESC键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeLogDetailModal();
}
});
}
// 格式化数字(添加千分位分隔符)
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 = '';
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 = '';
} 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 = '| 归档日期 | 月份 | 记录数 | 原始大小 | 压缩后大小 | 压缩比 | 时间范围 |
';
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 = '' + archiveDate.toLocaleDateString('zh-CN') + ' | ' + archive.month + ' | ' + formatNumber(archive.recordCount) + ' | ' + formatFileSize(archive.originalSize) + ' | ' + formatFileSize(archive.compressedSize) + ' | ' + compressionRatio + 'x | ' + startTime.toLocaleDateString('zh-CN') + ' ' + startTime.toLocaleTimeString('zh-CN') + ' - ' + endTime.toLocaleTimeString('zh-CN') + ' | ';
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');
}
}