Files
dns-server/static/js/logs.js

1639 lines
76 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
<div>暂无查询日志</div>
</td>
`;
tableBody.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 ? `
<div class="tracker-tooltip absolute z-50 bg-white shadow-lg rounded-md border p-3 min-w-64 text-sm">
<div class="font-semibold mb-1">已知跟踪器</div>
<div class="mb-1">名称: ${trackerInfo.name}</div>
<div class="mb-1">类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}</div>
${trackerInfo.url ? `<div class="mb-1">URL: <a href="${trackerInfo.url}" target="_blank" class="text-blue-500 hover:underline">${trackerInfo.url}</a></div>` : ''}
${trackerInfo.source ? `<div class="mb-1">源: ${trackerInfo.source}</div>` : ''}
</div>
` : '';
if (isMobile) {
// 移动设备只显示时间和请求信息
row.innerHTML = `
<td class="py-3 px-4">
<div class="text-sm font-medium">${formattedTime}</div>
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
</td>
<td class="py-3 px-4 text-sm" colspan="5">
<div class="font-medium flex items-center relative">
${log.DNSSEC ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
<div class="tracker-icon-container relative">
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
${trackerTooltip}
</div>
${log.Domain}
</div>
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span></div>
<div class="text-xs text-gray-500 mt-1">客户端: ${log.ClientIP}</div>
</td>
`;
} else {
// 桌面设备显示完整信息
row.innerHTML = `
<td class="py-3 px-4">
<div class="text-sm font-medium">${formattedTime}</div>
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="font-medium">${log.ClientIP}</div>
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="font-medium flex items-center relative">
${log.DNSSEC ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
<div class="tracker-icon-container relative">
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
${trackerTooltip}
</div>
${log.Domain}
</div>
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span>${log.DNSSEC ? ', <span class="text-green-500"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.EDNS ? ', <span class="text-blue-500"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
<div class="text-xs text-gray-500 mt-1">DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}</div>
</td>
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
<td class="py-3 px-4 text-sm text-center">
${isBlocked ?
`<button class="unblock-btn px-3 py-1 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-xs" data-domain="${log.Domain}">放行</button>` :
`<button class="block-btn px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-xs" data-domain="${log.Domain}">拦截</button>`
}
</td>
`;
}
// 添加跟踪器图标悬停事件
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 = '<i class="fa fa-times text-xl"></i>';
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 = `
<!-- 第1组基本信息 -->
<div class="text-xs">
<div class="font-medium text-gray-700 mb-2">基本信息</div>
<div class="grid grid-cols-2 gap-y-2 gap-x-4">
<div>
<div class="text-gray-500">日期:</div>
<div class="text-gray-800">${dateStr}</div>
</div>
<div>
<div class="text-gray-500">时间:</div>
<div class="text-gray-800">${timeStr}</div>
</div>
<div>
<div class="text-gray-500">状态:</div>
<div class="${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
</div>
</div>
<div>
<div class="text-gray-500">域名:</div>
<div class="text-gray-800">${domain}</div>
</div>
<div>
<div class="text-gray-500">类型:</div>
<div class="text-gray-800">${queryType}</div>
</div>
<div class="col-span-2">
<div class="text-gray-500">DNS特性:</div>
<div class="text-gray-800">
${dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>DNSSEC ' : ''}
${edns ? '<i class="fa fa-exchange text-blue-500 mr-1" title="EDNS已启用"></i>EDNS' : ''}
${!dnssec && !edns ? '无' : ''}
</div>
</div>
<div class="col-span-2">
<div class="text-gray-500">跟踪器信息:</div>
<div class="text-gray-800">
${isTracker ? `
<div class="flex items-center">
<i class="fa fa-eye text-red-500 mr-1"></i>
<span>${trackerInfo.name} (${trackersDatabase.categories[trackerInfo.categoryId] || '未知'})</span>
</div>
` : '无'}
</div>
</div>
<div class="col-span-2">
<div class="text-gray-500">解析记录:</div>
<div class="text-gray-800 whitespace-pre-wrap break-all text-left">
${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;
})()}
</div>
</div>
<div class="col-span-2">
<div class="text-gray-500">DNS服务器:</div>
<div class="text-gray-800">${dnsServer}</div>
</div>
<div class="col-span-2">
<div class="text-gray-500">DNSSEC专用服务器:</div>
<div class="text-gray-800">${dnssecServer}</div>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="border-t border-gray-200 my-3"></div>
<!-- 第2组响应细节 -->
<div class="text-xs">
<div class="font-medium text-gray-700 mb-2">响应细节</div>
<div class="grid grid-cols-2 gap-y-2 gap-x-4">
<div>
<div class="text-gray-500">响应时间:</div>
<div class="text-gray-800">${responseTime}毫秒</div>
</div>
<div>
<div class="text-gray-500">规则:</div>
<div class="text-gray-800">${blockRule}</div>
</div>
<div>
<div class="text-gray-500">响应代码:</div>
<div class="text-gray-800">无</div>
</div>
<div>
<div class="text-gray-500">缓存状态:</div>
<div class="${fromCache ? 'text-primary' : 'text-gray-500'}">
${fromCache ? '缓存' : '非缓存'}
</div>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="border-t border-gray-200 my-3"></div>
<!-- 第3组客户端详情 -->
<div class="text-xs">
<div class="font-medium text-gray-700 mb-2">客户端详情</div>
<div>
<div class="text-gray-500">IP地址:</div>
<div class="text-gray-800">${clientIP} (${location})</div>
</div>
</div>
`;
// 组装模态框
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 = `
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">错误</h3>
<button onclick="document.body.removeChild(errorModal)" class="text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div class="text-red-600">
加载日志详情失败: ${error.message}
</div>
`;
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秒更新一次