// logs.js - 查询日志页面功能 // 全局变量 let currentPage = 1; let totalPages = 1; let logsPerPage = 30; // 默认显示30条记录 let currentFilter = ''; let currentSearch = ''; let logsChart = null; let currentSortField = ''; let currentSortDirection = 'desc'; // 默认降序 // IP地理位置缓存(检查是否已经存在,避免重复声明) if (typeof ipGeolocationCache === 'undefined') { var ipGeolocationCache = {}; var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 } // 获取IP地理位置信息 async function getIpGeolocation(ip) { // 检查是否为内网IP if (isPrivateIP(ip)) { return "内网 内网"; } // 检查缓存 const now = Date.now(); if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) { 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; } // 保存到缓存 ipGeolocationCache[ip] = { location: location, timestamp: now }; 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; } // 域名信息数据库缓存 let domainInfoDatabase = null; let domainInfoLoaded = false; let domainInfoLoading = false; // 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; } } // 加载域名信息数据库 async function loadDomainInfoDatabase() { console.log('开始加载域名信息数据库'); if (domainInfoLoaded) { console.log('域名信息数据库已加载,直接返回'); return domainInfoDatabase; } if (domainInfoLoading) { console.log('域名信息数据库正在加载中,等待完成'); // 等待正在进行的加载完成 while (domainInfoLoading) { await new Promise(resolve => setTimeout(resolve, 100)); } return domainInfoDatabase; } domainInfoLoading = true; try { console.log('发起请求获取域名信息数据库'); const response = await fetch('domain-info/domains/domain-info.json'); if (!response.ok) { console.error('加载域名信息数据库失败,HTTP状态:', response.status, response.statusText); console.error('请求URL:', response.url); domainInfoDatabase = { domains: {}, categories: {} }; return domainInfoDatabase; } console.log('域名信息数据库请求成功,开始解析JSON'); domainInfoDatabase = await response.json(); console.log('域名信息数据库解析成功,包含', Object.keys(domainInfoDatabase.domains || {}).length, '个公司'); domainInfoLoaded = true; return domainInfoDatabase; } catch (error) { console.error('加载域名信息数据库失败,错误信息:', error.message); console.error('错误堆栈:', error.stack); domainInfoDatabase = { domains: {}, categories: {} }; return domainInfoDatabase; } finally { domainInfoLoading = false; console.log('域名信息数据库加载完成'); } } // 检查域名是否在跟踪器数据库中,并返回跟踪器信息 async function isDomainInTrackerDatabase(domain) { if (!trackersDatabase || !trackersLoaded) { await loadTrackersDatabase(); } if (!trackersDatabase || !trackersDatabase.trackers) { return null; } // 检查域名是否直接作为跟踪器键存在 if (trackersDatabase.trackers.hasOwnProperty(domain)) { return trackersDatabase.trackers[domain]; } // 检查域名是否在跟踪器URL中 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) { return tracker; } } catch (e) { // 忽略无效URL } } } } return null; } // 根据域名查找对应的网站信息 async function getDomainInfo(domain) { console.log('开始查找域名信息,域名:', domain); if (!domainInfoDatabase || !domainInfoLoaded) { console.log('域名信息数据库未加载,调用loadDomainInfoDatabase'); await loadDomainInfoDatabase(); } if (!domainInfoDatabase || !domainInfoDatabase.domains) { console.error('域名信息数据库无效或为空'); return null; } // 规范化域名,移除可能的端口号 const normalizedDomain = domain.replace(/:\d+$/, '').toLowerCase(); console.log('规范化后的域名:', normalizedDomain); // 遍历所有公司 console.log('开始遍历公司,总公司数:', Object.keys(domainInfoDatabase.domains).length); for (const companyKey in domainInfoDatabase.domains) { if (domainInfoDatabase.domains.hasOwnProperty(companyKey)) { console.log('检查公司:', companyKey); const companyData = domainInfoDatabase.domains[companyKey]; const companyName = companyData.company || companyKey; // 遍历公司下的所有网站和类别 for (const websiteKey in companyData) { if (companyData.hasOwnProperty(websiteKey) && websiteKey !== 'company') { console.log(' 检查网站/类别:', websiteKey); const website = companyData[websiteKey]; // 如果有URL属性,直接检查域名 if (website.url) { // 处理字符串类型的URL if (typeof website.url === 'string') { console.log(' 检查字符串URL:', website.url); if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: website.name, icon: website.icon, categoryId: website.categoryId, categoryName: domainInfoDatabase.categories[website.categoryId] || '未知', company: website.company || companyName }; } } // 处理对象类型的URL else if (typeof website.url === 'object') { console.log(' 检查对象类型URL,包含', Object.keys(website.url).length, '个URL'); for (const urlKey in website.url) { if (website.url.hasOwnProperty(urlKey)) { const urlValue = website.url[urlKey]; console.log(' 检查URL', urlKey, ':', urlValue); if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: website.name, icon: website.icon, categoryId: website.categoryId, categoryName: domainInfoDatabase.categories[website.categoryId] || '未知', company: website.company || companyName }; } } } } } else if (typeof website === 'object' && website !== null) { // 没有URL属性,可能是嵌套的类别 console.log(' 发现嵌套类别,进一步检查'); for (const nestedWebsiteKey in website) { if (website.hasOwnProperty(nestedWebsiteKey) && nestedWebsiteKey !== 'company') { console.log(' 检查嵌套网站/类别:', nestedWebsiteKey); const nestedWebsite = website[nestedWebsiteKey]; if (nestedWebsite.url) { // 处理字符串类型的URL if (typeof nestedWebsite.url === 'string') { console.log(' 检查字符串URL:', nestedWebsite.url); if (isDomainMatch(nestedWebsite.url, normalizedDomain, nestedWebsite.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: nestedWebsite.name, icon: nestedWebsite.icon, categoryId: nestedWebsite.categoryId, categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知', company: nestedWebsite.company || companyName }; } } // 处理对象类型的URL else if (typeof nestedWebsite.url === 'object') { console.log(' 检查对象类型URL,包含', Object.keys(nestedWebsite.url).length, '个URL'); for (const urlKey in nestedWebsite.url) { if (nestedWebsite.url.hasOwnProperty(urlKey)) { const urlValue = nestedWebsite.url[urlKey]; console.log(' 检查URL', urlKey, ':', urlValue); if (isDomainMatch(urlValue, normalizedDomain, nestedWebsite.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: nestedWebsite.name, icon: nestedWebsite.icon, categoryId: nestedWebsite.categoryId, categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知', company: nestedWebsite.company || companyName }; } } } } } else if (typeof nestedWebsite === 'object' && nestedWebsite !== null) { // 嵌套类别中的嵌套类别,递归检查 console.log(' 发现二级嵌套类别,进一步检查'); for (const secondNestedWebsiteKey in nestedWebsite) { if (nestedWebsite.hasOwnProperty(secondNestedWebsiteKey) && secondNestedWebsiteKey !== 'company') { console.log(' 检查二级嵌套网站:', secondNestedWebsiteKey); const secondNestedWebsite = nestedWebsite[secondNestedWebsiteKey]; if (secondNestedWebsite.url) { // 处理字符串类型的URL if (typeof secondNestedWebsite.url === 'string') { console.log(' 检查字符串URL:', secondNestedWebsite.url); if (isDomainMatch(secondNestedWebsite.url, normalizedDomain, secondNestedWebsite.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: secondNestedWebsite.name, icon: secondNestedWebsite.icon, categoryId: secondNestedWebsite.categoryId, categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知', company: secondNestedWebsite.company || companyName }; } } // 处理对象类型的URL else if (typeof secondNestedWebsite.url === 'object') { console.log(' 检查对象类型URL,包含', Object.keys(secondNestedWebsite.url).length, '个URL'); for (const urlKey in secondNestedWebsite.url) { if (secondNestedWebsite.url.hasOwnProperty(urlKey)) { const urlValue = secondNestedWebsite.url[urlKey]; console.log(' 检查URL', urlKey, ':', urlValue); if (isDomainMatch(urlValue, normalizedDomain, secondNestedWebsite.categoryId)) { console.log(' 匹配成功,返回网站信息'); return { name: secondNestedWebsite.name, icon: secondNestedWebsite.icon, categoryId: secondNestedWebsite.categoryId, categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知', company: secondNestedWebsite.company || companyName }; } } } } } } } } else { console.log(' 嵌套网站没有URL属性且不是对象类型'); } } } } else { console.log(' 网站没有URL属性'); } } } } } console.log('未找到匹配的域名信息'); return null; } // 检查域名是否匹配 function isDomainMatch(urlValue, targetDomain, categoryId) { console.log(' 开始匹配URL:', urlValue, '目标域名:', targetDomain, '类别ID:', categoryId); // 规范化目标域名,去除末尾的点 const normalizedTargetDomain = targetDomain.replace(/\.$/, '').toLowerCase(); try { // 尝试将URL值解析为完整URL console.log(' 尝试解析URL为完整URL'); const url = new URL(urlValue); let hostname = url.hostname.toLowerCase(); // 规范化主机名,去除末尾的点 hostname = hostname.replace(/\.$/, ''); console.log(' 解析成功,主机名:', hostname, '规范化目标域名:', normalizedTargetDomain); // 根据类别ID选择匹配方式 if (categoryId === 2) { // CDN类别,使用域名后缀匹配 if (normalizedTargetDomain.endsWith('.' + hostname) || normalizedTargetDomain === hostname) { console.log(' CDN域名后缀匹配成功'); return true; } else { console.log(' CDN域名后缀不匹配'); return false; } } else { // 其他类别,使用完整域名匹配 if (hostname === normalizedTargetDomain) { console.log(' 完整域名匹配成功'); return true; } else { console.log(' 完整域名不匹配'); return false; } } } catch (e) { console.log(' 解析URL失败,将其视为纯域名处理,错误信息:', e.message); // 如果是纯域名而不是完整URL let urlDomain = urlValue.toLowerCase(); // 规范化纯域名,去除末尾的点 urlDomain = urlDomain.replace(/\.$/, ''); console.log(' 处理为纯域名:', urlDomain, '规范化目标域名:', normalizedTargetDomain); // 根据类别ID选择匹配方式 if (categoryId === 2) { // CDN类别,使用域名后缀匹配 if (normalizedTargetDomain.endsWith('.' + urlDomain) || normalizedTargetDomain === urlDomain) { console.log(' CDN域名后缀匹配成功'); return true; } else { console.log(' CDN域名后缀不匹配'); return false; } } else { // 其他类别,使用完整域名匹配 if (urlDomain === normalizedTargetDomain) { console.log(' 完整域名匹配成功'); return true; } else { console.log(' 完整域名不匹配'); return false; } } } } // 提取主域名 function extractPrimaryDomain(domain) { console.log(' 开始提取主域名,原始域名:', domain); const parts = domain.split('.'); console.log(' 域名分割为:', parts); if (parts.length <= 2) { console.log(' 域名长度小于等于2,直接返回:', domain); return domain; } // 处理常见的三级域名 const commonSubdomains = ['www', 'mail', 'news', 'map', 'image', 'video', 'cdn', 'api', 'blog', 'shop', 'cloud', 'docs', 'help', 'support', 'dev', 'test', 'staging']; console.log(' 检查是否为常见三级域名'); if (commonSubdomains.includes(parts[0])) { const result = parts.slice(1).join('.'); console.log(' 是常见三级域名,返回:', result); 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']; console.log(' 检查是否为特殊国家TLD'); for (const tld of countryTLDs) { if (domain.endsWith('.' + tld)) { const mainParts = domain.split('.'); const result = mainParts.slice(-tld.split('.').length - 1).join('.'); console.log(' 是特殊国家TLD,返回:', result); return result; } } // 默认情况:返回最后两个部分 const result = parts.slice(-2).join('.'); console.log(' 默认情况,返回最后两个部分:', result); 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() { 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 perPageSelect = document.getElementById('logs-per-page'); if (perPageSelect) { perPageSelect.addEventListener('change', () => { logsPerPage = parseInt(perPageSelect.value); currentPage = 1; 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(); }); }); } // 更新排序图标 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() { // 使用封装的apiRequest函数进行API调用 apiRequest('/logs/stats') .then(data => { if (data && data.error) { console.error('加载日志统计数据失败:', data.error); 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 + '%'; }) .catch(error => { console.error('加载日志统计数据失败:', error); }); } // 加载日志详情 async function loadLogs() { // 显示加载状态 const loadingEl = document.getElementById('logs-loading'); if (loadingEl) { loadingEl.classList.remove('hidden'); } // 构建请求URL let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * 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; } // 加载日志总数 const [logs, countData] = await Promise.all([ Promise.resolve(logsData || []), // 确保logsData是数组 apiRequest('/logs/count') ]); // 确保logs是数组 const logsArray = Array.isArray(logs) ? logs : []; // 确保countData是有效的 const totalLogs = countData && countData.count ? countData.count : logsArray.length; // 计算总页数 totalPages = Math.ceil(totalLogs / logsPerPage); // 更新日志表格 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 tableBody = document.getElementById('logs-table-body'); if (!tableBody) return; // 清空表格 tableBody.innerHTML = ''; if (logs.length === 0) { // 显示空状态 const emptyRow = document.createElement('tr'); emptyRow.innerHTML = `
暂无查询日志
`; tableBody.appendChild(emptyRow); return; } // 检测是否为移动设备 const isMobile = window.innerWidth <= 768; // 填充表格 for (const log of logs) { const row = document.createElement('tr'); row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors'; // 格式化时间 - 两行显示,第一行显示时间,第二行显示日期 const time = new Date(log.timestamp); const formattedDate = time.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); const formattedTime = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); // 根据结果添加不同的背景色 let rowClass = ''; switch (log.result) { case 'blocked': rowClass = 'bg-red-50'; // 淡红色填充 break; case 'allowed': // 检查是否是规则允许项目 if (log.blockRule && log.blockRule.includes('allow')) { rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充 } else { rowClass = ''; // 允许的不填充 } break; default: rowClass = ''; } // 添加行背景色 if (rowClass) { row.classList.add(rowClass); } // 添加被屏蔽或允许显示,并增加颜色 let statusText = ''; let statusClass = ''; switch (log.result) { case 'blocked': statusText = '被屏蔽'; statusClass = 'text-danger'; break; case 'allowed': statusText = '允许'; statusClass = 'text-success'; break; case 'error': statusText = '错误'; statusClass = 'text-warning'; break; default: statusText = ''; statusClass = ''; } // 检查域名是否在跟踪器数据库中 const trackerInfo = await isDomainInTrackerDatabase(log.domain); const isTracker = trackerInfo !== null; // 构建行内容 - 根据设备类型决定显示内容 // 添加缓存状态显示 const cacheStatusClass = log.fromCache ? 'text-primary' : 'text-gray-500'; const cacheStatusText = log.fromCache ? '缓存' : '非缓存'; // 检查域名是否被拦截 const isBlocked = log.result === 'blocked'; // 构建跟踪器浮窗内容 const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name}
类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
${trackerInfo.url ? `
URL: ${trackerInfo.url}
` : ''} ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : ''; if (isMobile) { // 移动设备只显示时间和请求信息 row.innerHTML = `
${formattedTime}
${formattedDate}
${log.dnssec ? '' : ''}
${isTracker ? '' : ''} ${trackerTooltip}
${log.domain}
类型: ${log.queryType}, ${statusText}
客户端: ${log.clientIP}
`; } else { // 桌面设备显示完整信息 row.innerHTML = `
${formattedTime}
${formattedDate}
${log.clientIP}
${log.location || '未知 未知'}
${log.dnssec ? '' : ''}
${isTracker ? '' : ''} ${trackerTooltip}
${log.domain}
类型: ${log.queryType}, ${statusText}, ${log.fromCache ? '缓存' : '非缓存'}${log.dnssec ? ', DNSSEC' : ''}${log.edns ? ', EDNS' : ''}
DNS 服务器: ${log.dnsServer || '无'}, DNSSEC专用: ${log.dnssecServer || '无'}
${log.responseTime}ms ${isBlocked ? `` : `` } `; // 更新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) { tooltip.style.display = 'none'; iconContainer.addEventListener('mouseenter', () => { tooltip.style.display = 'block'; }); iconContainer.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); } } // 绑定按钮事件 const blockBtn = row.querySelector('.block-btn'); if (blockBtn) { blockBtn.addEventListener('click', (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; blockDomain(domain); }); } const unblockBtn = row.querySelector('.unblock-btn'); if (unblockBtn) { unblockBtn.addEventListener('click', (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; unblockDomain(domain); }); } // 绑定日志详情点击事件 row.addEventListener('click', (e) => { // 如果点击的是按钮,不触发详情弹窗 if (e.target.closest('button')) { return; } console.log('Row clicked, log object:', log); showLogDetailModal(log); }); tableBody.appendChild(row); } } // 更新分页信息 function updateLogsPagination() { // 更新页码显示 document.getElementById('logs-current-page').textContent = currentPage; document.getElementById('logs-total-pages').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`; console.log('正在连接WebSocket:', wsUrl); // 创建WebSocket连接 logsWsConnection = new WebSocket(wsUrl); // 连接打开事件 logsWsConnection.onopen = function() { console.log('WebSocket连接已建立'); }; // 接收消息事件 logsWsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === 'initial_data' || data.type === 'stats_update') { console.log('收到实时数据更新'); // 只更新统计数据,不更新日志详情 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秒后重连 console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`); logsWsReconnectTimer = setTimeout(() => { connectLogsWebSocket(); }, reconnectDelay); } // 从WebSocket更新日志统计数据 function updateLogsStatsFromWebSocket(stats) { 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 { console.log(`开始拦截域名: ${domain}`); // 创建拦截规则,使用AdBlock Plus格式 const blockRule = `||${domain}^`; console.log(`创建的拦截规则: ${blockRule}`); // 调用API添加拦截规则 console.log(`调用API添加拦截规则,路径: /shield, 方法: POST`); const response = await apiRequest('/shield', 'POST', { rule: blockRule }); console.log(`API响应:`, response); // 处理不同的响应格式 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 { console.log(`开始放行域名: ${domain}`); // 创建放行规则,使用AdBlock Plus格式 const allowRule = `@@||${domain}^`; console.log(`创建的放行规则: ${allowRule}`); // 调用API添加放行规则 console.log(`调用API添加放行规则,路径: /shield, 方法: POST`); const response = await apiRequest('/shield', 'POST', { rule: allowRule }); console.log(`API响应:`, response); // 处理不同的响应格式 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) { console.log('showLogDetailModal called with log:', JSON.stringify(log, null, 2)); // 输出完整的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 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 border-b border-gray-200 px-6 py-4 flex justify-between items-center'; const title = document.createElement('h3'); title.className = 'text-xl font-semibold text-gray-900'; title.textContent = '日志详情'; const closeButton = document.createElement('button'); closeButton.innerHTML = ''; closeButton.className = 'text-gray-500 hover:text-gray-700 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 uppercase tracking-wider'; basicInfoTitle.textContent = '基本信息'; const basicInfoGrid = document.createElement('div'); basicInfoGrid.className = 'grid grid-cols-1 md:grid-cols-2 gap-4'; // 添加基本信息项 basicInfoGrid.innerHTML = `
日期
${dateStr}
时间
${timeStr}
状态
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
域名
${domain}
类型
${queryType}
`; // 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.name || '未知'}
类别: ${domainInfo.categoryName || '未知'}
所属单位/公司: ${domainInfo.company || '未知'}
` : '无'}
`; // 跟踪器信息 const trackerDiv = document.createElement('div'); trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1'; trackerDiv.innerHTML = `
跟踪器信息
${isTracker ? `
${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})
` : '无'}
`; // 解析记录 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'; const responseDetailsTitle = document.createElement('h4'); responseDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider'; responseDetailsTitle.textContent = '响应细节'; // 准备响应细节内容,根据条件添加规则信息 let responseDetailsHTML = `
响应时间
${responseTime}毫秒
响应代码
${getResponseCodeText(log.responseCode)}
缓存状态
${fromCache ? '缓存' : '非缓存'}
`; // 只有被屏蔽时才显示规则信息 if (result === 'blocked') { responseDetailsHTML += `
规则
${blockRule || '-'}
`; } 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'; const clientDetailsTitle = document.createElement('h4'); clientDetailsTitle.className = 'text-sm font-medium text-gray-700 uppercase tracking-wider'; clientDetailsTitle.textContent = '客户端详情'; // 创建客户端IP容器,为后续更新地理位置做准备 const clientIPContainer = document.createElement('div'); clientIPContainer.className = 'text-sm font-medium text-gray-900'; 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 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 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(); } }); } // 定期更新日志统计数据(备用方案) setInterval(() => { // 只有在查询日志页面时才更新 if (window.location.hash === '#logs') { loadLogsStats(); // 不自动更新日志详情,只更新统计数据 } }, 30000); // 每30秒更新一次