// 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地理位置缓存
let ipGeolocationCache = {};
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
// 跟踪器数据库缓存
let trackersDatabase = null;
let trackersLoaded = false;
let trackersLoading = 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('/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 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 || trackerUrl.hostname.includes(domain)) {
return tracker;
}
} catch (e) {
// 忽略无效URL
}
}
}
}
return null;
}
// 初始化查询日志页面
function initLogsPage() {
console.log('初始化查询日志页面');
// 加载日志统计数据
loadLogsStats();
// 加载日志详情
loadLogs();
// 初始化图表
initLogsChart();
// 绑定事件
bindLogsEvents();
// 初始化日志详情弹窗
initLogDetailModal();
// 建立WebSocket连接,用于实时更新统计数据和图表
connectLogsWebSocket();
// 在页面卸载时清理资源
window.addEventListener('beforeunload', cleanupLogsResources);
}
// 清理资源
function cleanupLogsResources() {
// 清除WebSocket连接
if (logsWsConnection) {
logsWsConnection.close();
logsWsConnection = null;
}
// 清除重连计时器
if (logsWsReconnectTimer) {
clearTimeout(logsWsReconnectTimer);
logsWsReconnectTimer = null;
}
}
// 绑定事件
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);
});
}
// 加载日志详情
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}`;
}
// 使用封装的apiRequest函数进行API调用
apiRequest(endpoint)
.then(data => {
if (data && data.error) {
console.error('加载日志详情失败:', data.error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
// 加载日志总数
return apiRequest('/logs/count').then(countData => {
return { logs: data, count: countData.count };
});
})
.then(result => {
if (!result || !result.logs) {
console.error('加载日志详情失败: 无效的响应数据');
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
return;
}
const logs = result.logs;
const totalLogs = result.count;
// 计算总页数
totalPages = Math.ceil(totalLogs / logsPerPage);
// 更新日志表格
updateLogsTable(logs);
// 绑定操作按钮事件
bindActionButtonsEvents();
// 更新分页信息
updateLogsPagination();
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
})
.catch(error => {
console.error('加载日志详情失败:', error);
// 隐藏加载状态
if (loadingEl) {
loadingEl.classList.add('hidden');
}
});
}
// 更新日志表格
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 ? `
` : ''}
${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 |
${log.BlockRule || '-'} |
${isBlocked ?
`` :
``
}
|
`;
}
// 添加跟踪器图标悬停事件
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');
}
}
}
// 显示日志详情弹窗
async function showLogDetailModal(log) {
console.log('showLogDetailModal called with log:', JSON.stringify(log, null, 2)); // 输出完整的log对象
// 确保log对象存在
if (!log) {
console.error('No log data provided!');
return;
}
try {
// 简化版本,直接创建一个新的模态框
const modalContainer = document.createElement('div');
modalContainer.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center';
modalContainer.style.zIndex = '9999'; // 确保z-index足够高
// 创建模态框内容
const modalContent = document.createElement('div');
modalContent.className = 'bg-white rounded-lg shadow-xl p-6 w-full max-w-md';
// 添加关闭按钮
const closeButton = document.createElement('button');
closeButton.innerHTML = '';
closeButton.className = 'text-gray-500 hover:text-gray-700 focus:outline-none ml-auto';
closeButton.onclick = function() {
document.body.removeChild(modalContainer);
};
// 创建标题栏
const titleBar = document.createElement('div');
titleBar.className = 'flex justify-between items-center mb-4';
// 添加标题
const title = document.createElement('h3');
title.className = 'text-xl font-semibold';
title.textContent = '日志详情';
// 将标题和关闭按钮添加到标题栏
titleBar.appendChild(title);
titleBar.appendChild(closeButton);
// 创建详情内容
const details = document.createElement('div');
details.className = 'space-y-4';
// 安全获取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;
// 获取DNS响应内容(如果有)
const dnsResponse = log.Response || '无';
// 添加调试信息,查看log对象结构
console.log('=== DNS日志对象结构 ===');
console.log('log对象:', log);
console.log('log字段列表:', Object.keys(log));
console.log('Answers字段值:', log.Answers);
console.log('answers字段值:', log.answers);
console.log('Response字段值:', log.Response);
console.log('Answer字段值:', log.Answer);
// 处理Answers字段,确保正确解析
let dnsAnswers = log.Answers || log.answers || [];
// 添加更多调试信息
console.log('=== 解析记录提取调试信息 ===');
console.log('日志对象:', log);
console.log('日志字段:', Object.keys(log));
// 检查所有可能的解析记录字段
const potentialFields = ['Answers', 'answers', 'Answer', 'answer', 'Records', 'records', 'Response'];
potentialFields.forEach(field => {
console.log(`${field}:`, log[field]);
});
// 关键修复:如果Answers是字符串类型的JSON数组,强制解析
if (typeof dnsAnswers === 'string') {
// 先检查是否是有效的JSON数组格式
if (dnsAnswers.startsWith('[') && dnsAnswers.endsWith(']')) {
try {
dnsAnswers = JSON.parse(dnsAnswers);
} catch (e) {
console.error('解析Answers JSON数组失败:', e);
dnsAnswers = [];
}
} else {
// 如果不是数组格式,尝试解析为单个对象
try {
dnsAnswers = JSON.parse(dnsAnswers);
// 如果解析后是单个对象,转换为数组
if (typeof dnsAnswers === 'object' && !Array.isArray(dnsAnswers)) {
dnsAnswers = [dnsAnswers];
}
} catch (e) {
console.error('解析Answers JSON失败:', e);
dnsAnswers = [];
}
}
}
console.log('处理后的dnsAnswers:', dnsAnswers);
// 添加基本信息,使用安全获取的值
details.innerHTML = `
基本信息
状态:
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
DNS特性:
${dnssec ? 'DNSSEC ' : ''}
${edns ? 'EDNS' : ''}
${!dnssec && !edns ? '无' : ''}
跟踪器信息:
${isTracker ? `
${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})
` : '无'}
解析记录:
${result === 'blocked' ? '无' : (() => {
// 尝试从不同字段获取解析记录
let records = '';
// 1. 尝试使用Answers数组 - 始终优先使用Answers
if (dnsAnswers && Array.isArray(dnsAnswers) && dnsAnswers.length > 0) {
records = dnsAnswers.map(answer => {
// 处理不同格式的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 || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 如果value是JSON字符串,尝试解析
else if (value.startsWith('{') && value.endsWith('}')) {
try {
const parsedValue = JSON.parse(value);
value = parsedValue.data || parsedValue.value || value;
// 解析后的值也需要trim
if (typeof value === 'string') {
value = value.trim();
}
} catch (e) {
// 解析失败,保持原值但trim
value = value.trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
// 2. 尝试解析字符串类型的Answers - 增强的容错处理
else if (typeof dnsAnswers === 'string') {
try {
const parsedAnswers = JSON.parse(dnsAnswers);
if (Array.isArray(parsedAnswers)) {
records = parsedAnswers.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 || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
} catch (e) {
// 解析失败,继续尝试其他字段
}
}
// 3. 尝试从log.Answer字段获取(单数形式)
if (!records && log.Answer) {
if (Array.isArray(log.Answer)) {
records = log.Answer.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 || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
} else if (typeof log.Answer === 'object') {
// 单个answer对象
const type = log.Answer.type || log.Answer.Type || '未知';
let value = log.Answer.value || log.Answer.Value || log.Answer.data || log.Answer.Data || '未知';
const ttl = log.Answer.TTL || log.Answer.ttl || log.Answer.expires || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
records = `${type}: ${value} (ttl=${ttl})`;
} else if (typeof log.Answer === 'string') {
// 字符串类型的Answer - 处理每行缩进
records = log.Answer.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
}
// 4. 尝试从log.answer字段获取(小写单数形式)
if (!records && log.answer) {
if (Array.isArray(log.answer)) {
records = log.answer.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 || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
} else if (typeof log.answer === 'object') {
// 单个answer对象
const type = log.answer.type || log.answer.Type || '未知';
let value = log.answer.value || log.answer.Value || log.answer.data || log.answer.Data || '未知';
const ttl = log.answer.TTL || log.answer.ttl || log.answer.expires || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
records = `${type}: ${value} (ttl=${ttl})`;
} else if (typeof log.answer === 'string') {
// 字符串类型的answer - 处理每行缩进
records = log.answer.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
}
// 5. 尝试从log.Records字段获取
if (!records && log.Records) {
if (Array.isArray(log.Records)) {
records = log.Records.map(record => {
const type = record.type || record.Type || '未知';
let value = record.value || record.Value || record.data || record.Data || '未知';
const ttl = record.TTL || record.ttl || record.expires || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
} else if (typeof log.Records === 'string') {
// 字符串类型的Records - 处理每行缩进
records = log.Records.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
}
// 6. 尝试从log.records字段获取(小写形式)
if (!records && log.records) {
if (Array.isArray(log.records)) {
records = log.records.map(record => {
const type = record.type || record.Type || '未知';
let value = record.value || record.Value || record.data || record.Data || '未知';
const ttl = record.TTL || record.ttl || record.expires || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
} else if (typeof log.records === 'string') {
// 字符串类型的records - 处理每行缩进
records = log.records.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
}
// 7. 尝试从Response字段获取(兼容旧格式)
if (!records && dnsResponse && dnsResponse !== '无') {
// 如果Response是JSON字符串,尝试解析
if (dnsResponse.startsWith('[') && dnsResponse.endsWith(']')) {
try {
const parsedResponse = JSON.parse(dnsResponse);
if (Array.isArray(parsedResponse)) {
records = parsedResponse.map(item => {
const type = item.type || item.Type || '未知';
let value = item.value || item.Value || item.data || item.Data || '未知';
const ttl = item.TTL || item.ttl || item.expires || '未知';
// 增强的记录值提取逻辑:处理多种DNS记录格式
if (typeof value === 'string') {
// 如果value是完整的DNS记录字符串,提取出实际值
if (value.includes('\t') || value.includes('\\t')) {
// 处理实际制表符或转义的制表符
const parts = value.replace(/\\t/g, '\t').split('\t');
// 兼容不同长度的记录格式
if (parts.length >= 4) {
// 对于标准DNS响应格式:domain\tttl\tIN\ttype\tvalue
value = parts[parts.length - 1].trim();
} else if (parts.length >= 2) {
// 对于其他格式,使用最后一个字段
value = parts[parts.length - 1].trim();
}
}
// 如果value是JSON字符串,尝试解析
else if (value.startsWith('{') && value.endsWith('}')) {
try {
const parsedValue = JSON.parse(value);
value = parsedValue.data || parsedValue.value || value;
// 解析后的值也需要trim
if (typeof value === 'string') {
value = value.trim();
}
} catch (e) {
// 解析失败,保持原值但trim
value = value.trim();
}
}
// 对于其他所有字符串类型的值,直接trim
else {
value = value.trim();
}
}
return `${type}: ${value} (ttl=${ttl})`;
}).join('\n').trim();
}
} catch (e) {
// 解析失败,直接显示Response内容 - 处理每行缩进
records = dnsResponse.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
} else {
// Response不是JSON数组 - 处理每行缩进
records = dnsResponse.split('\n').map(line => line.trim()).filter(line => line !== '').join('\n');
}
}
// 8. 如果还是没有解析记录,显示友好提示
if (!records) {
records = '无解析记录';
}
return records;
})()}
DNSSEC专用服务器:
${dnssecServer}
响应细节
缓存状态:
${fromCache ? '缓存' : '非缓存'}
客户端详情
IP地址:
${clientIP} (${location})
`;
// 组装模态框
modalContent.appendChild(titleBar);
modalContent.appendChild(details);
modalContainer.appendChild(modalContent);
// 添加到页面
document.body.appendChild(modalContainer);
// 点击外部关闭
modalContainer.addEventListener('click', function(e) {
if (e.target === modalContainer) {
document.body.removeChild(modalContainer);
}
});
// ESC键关闭
document.addEventListener('keydown', function handleEsc(e) {
if (e.key === 'Escape') {
document.body.removeChild(modalContainer);
document.removeEventListener('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';
errorModal.style.zIndex = '9999';
const errorContent = document.createElement('div');
errorContent.className = 'bg-white rounded-lg shadow-xl p-6 w-full max-w-md';
errorContent.innerHTML = `
错误
加载日志详情失败: ${error.message}
`;
errorModal.appendChild(errorContent);
document.body.appendChild(errorModal);
}
}
// 关闭日志详情弹窗
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秒更新一次