Files
dns-server/static/js/logs.js
2026-01-02 02:43:10 +08:00

1814 lines
70 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;
// 域名信息数据库缓存
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 {
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 initLogsPage() {
console.log('初始化查询日志页面');
// 加载日志统计数据
loadLogsStats();
// 加载日志详情
loadLogs();
// 初始化图表
initLogsChart();
// 绑定事件
bindLogsEvents();
// 初始化日志详情弹窗
initLogDetailModal();
// 建立WebSocket连接用于实时更新统计数据和图表
connectLogsWebSocket();
// 窗口大小改变时重新加载日志表格
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);
});
}
// 加载日志详情
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="4">
<div class="font-medium flex items-center relative">
${log.dnssec ? '<i class="fa fa-lock text-green-500 mr-1" title="DNSSEC已启用"></i>' : ''}
<div class="tracker-icon-container relative">
${isTracker ? '<i class="fa fa-eye text-red-500 mr-1"></i>' : '<i class="fa fa-eye-slash text-gray-300 mr-1"></i>'}
${trackerTooltip}
</div>
${log.domain}
</div>
<div class="text-xs text-gray-500 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
<div class="text-xs text-gray-500 mt-1">客户端: ${log.clientIP}</div>
</td>
`;
} else {
// 桌面设备显示完整信息
row.innerHTML = `
<td class="py-3 px-4">
<div class="text-sm font-medium">${formattedTime}</div>
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="font-medium">${log.clientIP}</div>
<div class="text-xs text-gray-500 mt-1">${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-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');
}
}
}
// 独立的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 = '<i class="fa fa-times text-xl"></i>';
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 = `
<div class="space-y-1">
<div class="text-xs text-gray-500">日期</div>
<div class="text-sm font-medium text-gray-900">${dateStr}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">时间</div>
<div class="text-sm font-medium text-gray-900">${timeStr}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">状态</div>
<div class="text-sm font-medium ${result === 'blocked' ? 'text-red-600' : result === 'allowed' ? 'text-green-600' : 'text-gray-500'}">
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">域名</div>
<div class="text-sm font-medium text-gray-900 break-all">${domain}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">类型</div>
<div class="text-sm font-medium text-gray-900">${queryType}</div>
</div>
`;
// DNS特性
const dnsFeatures = document.createElement('div');
dnsFeatures.className = 'col-span-1 md:col-span-2 space-y-1';
dnsFeatures.innerHTML = `
<div class="text-xs text-gray-500">DNS特性</div>
<div class="text-sm font-medium text-gray-900">
${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>
`;
// 域名信息
const domainInfoDiv = document.createElement('div');
domainInfoDiv.className = 'col-span-1 md:col-span-2 space-y-1';
domainInfoDiv.innerHTML = `
<div class="text-xs text-gray-500">域名信息</div>
<div class="text-sm font-medium text-gray-900 p-3 bg-gray-50 rounded-md border border-gray-200 w-full">
${domainInfo ? `
<div class="flex items-center mb-2">
${domainInfo.icon ? `<img src="${domainInfo.icon}" alt="${domainInfo.name}" class="w-6 h-6 mr-2 rounded-sm" onerror="this.style.display='none'" />` : ''}
<span class="text-base font-semibold flex-grow">${domainInfo.name || '未知'}</span>
</div>
<div class="mt-1">
<div class="flex items-center mb-1 flex-wrap">
<span class="text-gray-500 mr-2">类别:</span>
<span class="flex-grow">${domainInfo.categoryName || '未知'}</span>
</div>
<div class="flex items-center flex-wrap">
<span class="text-gray-500 mr-2">所属单位:</span>
<span class="flex-grow">${domainInfo.company || '未知'}</span>
</div>
</div>
` : '无'}
</div>
`;
// 跟踪器信息
const trackerDiv = document.createElement('div');
trackerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
trackerDiv.innerHTML = `
<div class="text-xs text-gray-500">跟踪器信息</div>
<div class="text-sm font-medium text-gray-900">
${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>
`;
// 解析记录
const recordsDiv = document.createElement('div');
recordsDiv.className = 'col-span-1 md:col-span-2 space-y-1';
recordsDiv.innerHTML = `
<div class="text-xs text-gray-500">解析记录</div>
<div class="text-sm font-medium text-gray-900 whitespace-pre-wrap break-all bg-gray-50 p-3 rounded-md border border-gray-200">${dnsRecords}</div>
`;
// DNS服务器
const dnsServerDiv = document.createElement('div');
dnsServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnsServerDiv.innerHTML = `
<div class="text-xs text-gray-500">DNS服务器</div>
<div class="text-sm font-medium text-gray-900">${dnsServer}</div>
`;
// DNSSEC专用服务器
const dnssecServerDiv = document.createElement('div');
dnssecServerDiv.className = 'col-span-1 md:col-span-2 space-y-1';
dnssecServerDiv.innerHTML = `
<div class="text-xs text-gray-500">DNSSEC专用服务器</div>
<div class="text-sm font-medium text-gray-900">${dnssecServer}</div>
`;
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 = `
<div class="space-y-1">
<div class="text-xs text-gray-500">响应时间</div>
<div class="text-sm font-medium text-gray-900">${responseTime}毫秒</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">响应代码</div>
<div class="text-sm font-medium text-gray-900">${getResponseCodeText(log.responseCode)}</div>
</div>
<div class="space-y-1">
<div class="text-xs text-gray-500">缓存状态</div>
<div class="text-sm font-medium ${fromCache ? 'text-primary' : 'text-gray-500'}">
${fromCache ? '缓存' : '非缓存'}
</div>
</div>
`;
// 只有被屏蔽时才显示规则信息
if (result === 'blocked') {
responseDetailsHTML += `
<div class="space-y-1">
<div class="text-xs text-gray-500">规则</div>
<div class="text-sm font-medium text-gray-900">${blockRule || '-'}</div>
</div>
`;
}
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 = '客户端详情';
const clientIPDiv = document.createElement('div');
clientIPDiv.className = 'space-y-1';
clientIPDiv.innerHTML = `
<div class="text-xs text-gray-500">IP地址</div>
<div class="text-sm font-medium text-gray-900">${clientIP} (${location})</div>
`;
clientDetails.appendChild(clientDetailsTitle);
clientDetails.appendChild(clientIPDiv);
// 操作按钮区域
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 = `
<button class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors text-sm" id="unblock-domain-btn">
<i class="fa fa-unlock-alt mr-1"></i>放行
</button>
`;
} else {
// 未被拦截时显示拦截按钮
actionButtons.innerHTML = `
<button class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-sm" id="block-domain-btn">
<i class="fa fa-lock mr-1"></i>拦截
</button>
`;
}
// 组装内容
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 = `
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-900">错误</h3>
<button onclick="closeErrorModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none transition-colors">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<div class="text-red-600 text-sm">
加载日志详情失败: ${error.message}
</div>
`;
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秒更新一次