1764 lines
68 KiB
JavaScript
1764 lines
68 KiB
JavaScript
// 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);
|
||
|
||
try {
|
||
// 尝试将URL值解析为完整URL
|
||
console.log(' 尝试解析URL为完整URL');
|
||
const url = new URL(urlValue);
|
||
const hostname = url.hostname.toLowerCase();
|
||
console.log(' 解析成功,主机名:', hostname);
|
||
|
||
// 根据类别ID选择匹配方式
|
||
if (categoryId === 2) {
|
||
// CDN类别,使用域名后缀匹配
|
||
if (targetDomain.endsWith('.' + hostname) || targetDomain === hostname) {
|
||
console.log(' CDN域名后缀匹配成功');
|
||
return true;
|
||
} else {
|
||
console.log(' CDN域名后缀不匹配');
|
||
return false;
|
||
}
|
||
} else {
|
||
// 其他类别,使用完整域名匹配
|
||
if (hostname === targetDomain) {
|
||
console.log(' 完整域名匹配成功');
|
||
return true;
|
||
} else {
|
||
console.log(' 完整域名不匹配');
|
||
return false;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(' 解析URL失败,将其视为纯域名处理,错误信息:', e.message);
|
||
// 如果是纯域名而不是完整URL
|
||
const urlDomain = urlValue.toLowerCase();
|
||
console.log(' 处理为纯域名:', urlDomain);
|
||
|
||
// 根据类别ID选择匹配方式
|
||
if (categoryId === 2) {
|
||
// CDN类别,使用域名后缀匹配
|
||
if (targetDomain.endsWith('.' + urlDomain) || targetDomain === urlDomain) {
|
||
console.log(' CDN域名后缀匹配成功');
|
||
return true;
|
||
} else {
|
||
console.log(' CDN域名后缀不匹配');
|
||
return false;
|
||
}
|
||
} else {
|
||
// 其他类别,使用完整域名匹配
|
||
if (urlDomain === targetDomain) {
|
||
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">
|
||
${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">${domainInfo.name || '未知'}</span>
|
||
</div>
|
||
<div class="ml-8 mt-1">
|
||
<div class="flex items-center mb-1">
|
||
<span class="text-gray-500 w-16">类别:</span>
|
||
<span>${domainInfo.categoryName || '未知'}</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<span class="text-gray-500 w-16">所属公司:</span>
|
||
<span>${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);
|
||
|
||
// 组装内容
|
||
content.appendChild(basicInfo);
|
||
content.appendChild(responseDetails);
|
||
content.appendChild(clientDetails);
|
||
|
||
// 组装模态框
|
||
modalContent.appendChild(header);
|
||
modalContent.appendChild(content);
|
||
modalContainer.appendChild(modalContent);
|
||
|
||
// 添加到页面
|
||
document.body.appendChild(modalContainer);
|
||
|
||
// 关闭模态框函数
|
||
function closeModal() {
|
||
modalContainer.classList.add('animate-fade-out');
|
||
modalContent.classList.add('animate-slide-out');
|
||
|
||
// 等待动画结束后移除元素
|
||
setTimeout(() => {
|
||
document.body.removeChild(modalContainer);
|
||
}, 300);
|
||
}
|
||
|
||
// 点击外部关闭
|
||
modalContainer.addEventListener('click', (e) => {
|
||
if (e.target === modalContainer) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// 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秒更新一次
|