2598 lines
100 KiB
JavaScript
2598 lines
100 KiB
JavaScript
// logs.js - 查询日志页面功能
|
||
|
||
// 全局变量
|
||
let currentPage = 1;
|
||
let totalPages = 1;
|
||
let logsPerPage = 30; // 默认显示30条记录
|
||
let currentFilter = '';
|
||
let currentSearch = '';
|
||
let logsChart = null;
|
||
let currentSortField = 'timestamp'; // 默认按时间排序,显示最新记录
|
||
let currentSortDirection = 'desc'; // 默认降序
|
||
|
||
// 内存使用监控
|
||
let memoryMonitor = {
|
||
enabled: true,
|
||
interval: null,
|
||
history: [],
|
||
maxHistory: 50,
|
||
|
||
// 开始监控
|
||
start() {
|
||
if (this.enabled && !this.interval) {
|
||
this.interval = setInterval(() => {
|
||
this.checkMemoryUsage();
|
||
}, 30000); // 每30秒检查一次
|
||
}
|
||
},
|
||
|
||
// 停止监控
|
||
stop() {
|
||
if (this.interval) {
|
||
clearInterval(this.interval);
|
||
this.interval = null;
|
||
}
|
||
},
|
||
|
||
// 检查内存使用情况
|
||
checkMemoryUsage() {
|
||
if (performance && performance.memory) {
|
||
const memory = performance.memory;
|
||
const usage = {
|
||
timestamp: Date.now(),
|
||
used: Math.round(memory.usedJSHeapSize / 1024 / 1024 * 100) / 100, // MB
|
||
total: Math.round(memory.totalJSHeapSize / 1024 / 1024 * 100) / 100, // MB
|
||
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024 * 100) / 100, // MB
|
||
usagePercent: Math.round((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100 * 100) / 100 // %
|
||
};
|
||
|
||
this.history.push(usage);
|
||
|
||
// 限制历史记录大小
|
||
if (this.history.length > this.maxHistory) {
|
||
this.history.shift();
|
||
}
|
||
|
||
// 内存使用过高时的处理
|
||
if (usage.usagePercent > 80) {
|
||
console.warn('内存使用过高:', usage);
|
||
// 可以在这里添加自动清理机制
|
||
this.triggerMemoryCleanup();
|
||
}
|
||
|
||
console.log('内存使用情况:', usage);
|
||
}
|
||
},
|
||
|
||
// 触发内存清理
|
||
triggerMemoryCleanup() {
|
||
console.log('触发内存清理...');
|
||
|
||
// 清理IP地理位置缓存
|
||
if (ipGeolocationCache && typeof ipGeolocationCache === 'object') {
|
||
const cacheSize = Object.keys(ipGeolocationCache).length;
|
||
console.log('清理前IP地理位置缓存大小:', cacheSize);
|
||
|
||
// 清理超出大小限制的缓存
|
||
if (GEOLOCATION_CACHE_ORDER && GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
|
||
while (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
|
||
const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
|
||
if (oldestIp) {
|
||
delete ipGeolocationCache[oldestIp];
|
||
}
|
||
}
|
||
console.log('清理后IP地理位置缓存大小:', Object.keys(ipGeolocationCache).length);
|
||
}
|
||
}
|
||
|
||
// 清理域名信息缓存
|
||
if (domainInfoCache && domainInfoCache.size > 0) {
|
||
const cacheSize = domainInfoCache.size;
|
||
console.log('清理前域名信息缓存大小:', cacheSize);
|
||
|
||
// 清理超出大小限制的缓存
|
||
if (domainInfoCache.size > DOMAIN_INFO_CACHE_MAX_SIZE) {
|
||
while (domainInfoCache.size > DOMAIN_INFO_CACHE_MAX_SIZE) {
|
||
const firstKey = domainInfoCache.keys().next().value;
|
||
domainInfoCache.delete(firstKey);
|
||
}
|
||
console.log('清理后域名信息缓存大小:', domainInfoCache.size);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 获取内存使用统计
|
||
getStats() {
|
||
if (this.history.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const recent = this.history[this.history.length - 1];
|
||
const avg = this.history.reduce((sum, item) => sum + item.used, 0) / this.history.length;
|
||
const max = Math.max(...this.history.map(item => item.used));
|
||
const min = Math.min(...this.history.map(item => item.used));
|
||
|
||
return {
|
||
recent,
|
||
avg: Math.round(avg * 100) / 100,
|
||
max: Math.round(max * 100) / 100,
|
||
min: Math.round(min * 100) / 100,
|
||
history: this.history
|
||
};
|
||
}
|
||
};
|
||
|
||
// IP地理位置缓存(检查是否已经存在,避免重复声明)
|
||
if (typeof ipGeolocationCache === 'undefined') {
|
||
var ipGeolocationCache = {};
|
||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||
var GEOLOCATION_CACHE_MAX_SIZE = 1000; // 缓存最大大小
|
||
var GEOLOCATION_CACHE_ORDER = []; // 用于LRU策略的访问顺序
|
||
}
|
||
|
||
// 清理IP地理位置缓存,保持在最大大小以内
|
||
function cleanupGeolocationCache() {
|
||
if (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
|
||
// 移除最旧的缓存项
|
||
const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
|
||
if (oldestIp) {
|
||
delete ipGeolocationCache[oldestIp];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新缓存访问顺序(用于LRU策略)
|
||
function updateCacheAccessOrder(ip) {
|
||
// 移除现有的位置
|
||
const index = GEOLOCATION_CACHE_ORDER.indexOf(ip);
|
||
if (index > -1) {
|
||
GEOLOCATION_CACHE_ORDER.splice(index, 1);
|
||
}
|
||
// 添加到末尾(表示最近访问)
|
||
GEOLOCATION_CACHE_ORDER.push(ip);
|
||
// 清理超出大小限制的缓存
|
||
cleanupGeolocationCache();
|
||
}
|
||
|
||
// 获取IP地理位置信息
|
||
async function getIpGeolocation(ip) {
|
||
// 检查是否为内网IP
|
||
if (isPrivateIP(ip)) {
|
||
return "内网 内网";
|
||
}
|
||
|
||
// 检查缓存
|
||
const now = Date.now();
|
||
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
|
||
// 更新缓存访问顺序
|
||
updateCacheAccessOrder(ip);
|
||
return ipGeolocationCache[ip].location;
|
||
}
|
||
|
||
try {
|
||
// 使用whois.pconline.com.cn API获取IP地理位置
|
||
const url = `https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`;
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
// 解析响应数据
|
||
const data = await response.json();
|
||
let location = "未知 未知";
|
||
|
||
if (data && data.addr) {
|
||
// 直接使用addr字段作为完整的地理位置信息
|
||
location = data.addr;
|
||
}
|
||
|
||
// 保存到缓存
|
||
ipGeolocationCache[ip] = {
|
||
location: location,
|
||
timestamp: now
|
||
};
|
||
|
||
// 更新缓存访问顺序
|
||
updateCacheAccessOrder(ip);
|
||
|
||
return location;
|
||
} catch (error) {
|
||
console.error('获取IP地理位置失败:', error);
|
||
return "未知 未知";
|
||
}
|
||
}
|
||
|
||
// 检查是否为内网IP
|
||
function isPrivateIP(ip) {
|
||
const parts = ip.split('.');
|
||
|
||
// 检查IPv4内网地址
|
||
if (parts.length === 4) {
|
||
const first = parseInt(parts[0]);
|
||
const second = parseInt(parts[1]);
|
||
|
||
// 10.0.0.0/8
|
||
if (first === 10) {
|
||
return true;
|
||
}
|
||
// 172.16.0.0/12
|
||
if (first === 172 && second >= 16 && second <= 31) {
|
||
return true;
|
||
}
|
||
// 192.168.0.0/16
|
||
if (first === 192 && second === 168) {
|
||
return true;
|
||
}
|
||
// 127.0.0.0/8 (localhost)
|
||
if (first === 127) {
|
||
return true;
|
||
}
|
||
// 169.254.0.0/16 (link-local)
|
||
if (first === 169 && second === 254) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 检查IPv6内网地址
|
||
if (ip.includes(':')) {
|
||
// ::1/128 (localhost)
|
||
if (ip === '::1' || ip.startsWith('0:0:0:0:0:0:0:1')) {
|
||
return true;
|
||
}
|
||
// fc00::/7 (unique local address)
|
||
if (ip.startsWith('fc') || ip.startsWith('fd')) {
|
||
return true;
|
||
}
|
||
// fe80::/10 (link-local)
|
||
if (ip.startsWith('fe80:')) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
|
||
if (typeof trackersDatabase === 'undefined') {
|
||
var trackersDatabase = null;
|
||
var trackersLoaded = false;
|
||
var trackersLoading = false;
|
||
}
|
||
|
||
// 域名信息数据库缓存
|
||
let domainInfoDatabase = null;
|
||
let domainInfoLoaded = false;
|
||
let domainInfoLoading = false;
|
||
|
||
// 域名信息查询缓存
|
||
let domainInfoCache = new Map();
|
||
let DOMAIN_INFO_CACHE_MAX_SIZE = 500; // 缓存最大大小
|
||
|
||
// 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() {
|
||
|
||
if (domainInfoLoaded) {
|
||
return domainInfoDatabase;
|
||
}
|
||
|
||
if (domainInfoLoading) {
|
||
// 等待正在进行的加载完成
|
||
while (domainInfoLoading) {
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
}
|
||
return domainInfoDatabase;
|
||
}
|
||
|
||
domainInfoLoading = true;
|
||
|
||
try {
|
||
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;
|
||
}
|
||
|
||
domainInfoDatabase = await response.json();
|
||
domainInfoLoaded = true;
|
||
return domainInfoDatabase;
|
||
} catch (error) {
|
||
console.error('加载域名信息数据库失败,错误信息:', error.message);
|
||
console.error('错误堆栈:', error.stack);
|
||
domainInfoDatabase = { domains: {}, categories: {} };
|
||
return domainInfoDatabase;
|
||
} finally {
|
||
domainInfoLoading = false;
|
||
}
|
||
}
|
||
|
||
// 检查域名是否在跟踪器数据库中,并返回跟踪器信息
|
||
async function isDomainInTrackerDatabase(domain) {
|
||
if (!trackersDatabase || !trackersLoaded) {
|
||
await loadTrackersDatabase();
|
||
}
|
||
|
||
if (!trackersDatabase || !trackersDatabase.trackers) {
|
||
return null;
|
||
}
|
||
|
||
// 检查域名是否直接作为跟踪器键存在
|
||
if (trackersDatabase.trackers.hasOwnProperty(domain)) {
|
||
return trackersDatabase.trackers[domain];
|
||
}
|
||
|
||
// 检查域名是否在跟踪器URL中
|
||
for (const trackerKey in trackersDatabase.trackers) {
|
||
if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) {
|
||
const tracker = trackersDatabase.trackers[trackerKey];
|
||
if (tracker && tracker.url) {
|
||
try {
|
||
const trackerUrl = new URL(tracker.url);
|
||
if (trackerUrl.hostname === domain) {
|
||
return tracker;
|
||
}
|
||
} catch (e) {
|
||
// 忽略无效URL
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 根据域名查找对应的网站信息
|
||
async function getDomainInfo(domain) {
|
||
// 规范化域名,移除可能的端口号
|
||
const normalizedDomain = domain.replace(/:\d+$/, '').toLowerCase();
|
||
|
||
// 检查缓存
|
||
if (domainInfoCache.has(normalizedDomain)) {
|
||
return domainInfoCache.get(normalizedDomain);
|
||
}
|
||
|
||
if (!domainInfoDatabase || !domainInfoLoaded) {
|
||
await loadDomainInfoDatabase();
|
||
}
|
||
|
||
if (!domainInfoDatabase || !domainInfoDatabase.domains) {
|
||
console.error('域名信息数据库无效或为空');
|
||
return null;
|
||
}
|
||
|
||
// 遍历所有公司
|
||
for (const companyKey in domainInfoDatabase.domains) {
|
||
if (domainInfoDatabase.domains.hasOwnProperty(companyKey)) {
|
||
const companyData = domainInfoDatabase.domains[companyKey];
|
||
const companyName = companyData.company || companyKey;
|
||
|
||
// 遍历公司下的所有网站和类别
|
||
for (const websiteKey in companyData) {
|
||
if (companyData.hasOwnProperty(websiteKey) && websiteKey !== 'company') {
|
||
const website = companyData[websiteKey];
|
||
|
||
// 如果有URL属性,直接检查域名
|
||
if (website.url) {
|
||
// 处理字符串类型的URL
|
||
if (typeof website.url === 'string') {
|
||
if (isDomainMatch(website.url, normalizedDomain, website.categoryId)) {
|
||
const result = {
|
||
name: website.name,
|
||
icon: website.icon,
|
||
categoryId: website.categoryId,
|
||
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
|
||
company: website.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
// 处理对象类型的URL
|
||
else if (typeof website.url === 'object') {
|
||
for (const urlKey in website.url) {
|
||
if (website.url.hasOwnProperty(urlKey)) {
|
||
const urlValue = website.url[urlKey];
|
||
if (isDomainMatch(urlValue, normalizedDomain, website.categoryId)) {
|
||
const result = {
|
||
name: website.name,
|
||
icon: website.icon,
|
||
categoryId: website.categoryId,
|
||
categoryName: domainInfoDatabase.categories[website.categoryId] || '未知',
|
||
company: website.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (typeof website === 'object' && website !== null) {
|
||
// 没有URL属性,可能是嵌套的类别
|
||
for (const nestedWebsiteKey in website) {
|
||
if (website.hasOwnProperty(nestedWebsiteKey) && nestedWebsiteKey !== 'company') {
|
||
const nestedWebsite = website[nestedWebsiteKey];
|
||
|
||
if (nestedWebsite.url) {
|
||
// 处理字符串类型的URL
|
||
if (typeof nestedWebsite.url === 'string') {
|
||
if (isDomainMatch(nestedWebsite.url, normalizedDomain, nestedWebsite.categoryId)) {
|
||
const result = {
|
||
name: nestedWebsite.name,
|
||
icon: nestedWebsite.icon,
|
||
categoryId: nestedWebsite.categoryId,
|
||
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
|
||
company: nestedWebsite.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
// 处理对象类型的URL
|
||
else if (typeof nestedWebsite.url === 'object') {
|
||
for (const urlKey in nestedWebsite.url) {
|
||
if (nestedWebsite.url.hasOwnProperty(urlKey)) {
|
||
const urlValue = nestedWebsite.url[urlKey];
|
||
if (isDomainMatch(urlValue, normalizedDomain, nestedWebsite.categoryId)) {
|
||
const result = {
|
||
name: nestedWebsite.name,
|
||
icon: nestedWebsite.icon,
|
||
categoryId: nestedWebsite.categoryId,
|
||
categoryName: domainInfoDatabase.categories[nestedWebsite.categoryId] || '未知',
|
||
company: nestedWebsite.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (typeof nestedWebsite === 'object' && nestedWebsite !== null) {
|
||
// 嵌套类别中的嵌套类别,递归检查
|
||
for (const secondNestedWebsiteKey in nestedWebsite) {
|
||
if (nestedWebsite.hasOwnProperty(secondNestedWebsiteKey) && secondNestedWebsiteKey !== 'company') {
|
||
const secondNestedWebsite = nestedWebsite[secondNestedWebsiteKey];
|
||
|
||
if (secondNestedWebsite.url) {
|
||
// 处理字符串类型的URL
|
||
if (typeof secondNestedWebsite.url === 'string') {
|
||
if (isDomainMatch(secondNestedWebsite.url, normalizedDomain, secondNestedWebsite.categoryId)) {
|
||
const result = {
|
||
name: secondNestedWebsite.name,
|
||
icon: secondNestedWebsite.icon,
|
||
categoryId: secondNestedWebsite.categoryId,
|
||
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
|
||
company: secondNestedWebsite.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
// 处理对象类型的URL
|
||
else if (typeof secondNestedWebsite.url === 'object') {
|
||
for (const urlKey in secondNestedWebsite.url) {
|
||
if (secondNestedWebsite.url.hasOwnProperty(urlKey)) {
|
||
const urlValue = secondNestedWebsite.url[urlKey];
|
||
if (isDomainMatch(urlValue, normalizedDomain, secondNestedWebsite.categoryId)) {
|
||
const result = {
|
||
name: secondNestedWebsite.name,
|
||
icon: secondNestedWebsite.icon,
|
||
categoryId: secondNestedWebsite.categoryId,
|
||
categoryName: domainInfoDatabase.categories[secondNestedWebsite.categoryId] || '未知',
|
||
company: secondNestedWebsite.company || companyName
|
||
};
|
||
// 存入缓存
|
||
addToDomainInfoCache(normalizedDomain, result);
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 添加到域名信息缓存
|
||
function addToDomainInfoCache(domain, info) {
|
||
// 检查缓存大小
|
||
if (domainInfoCache.size >= DOMAIN_INFO_CACHE_MAX_SIZE) {
|
||
// 移除最早的缓存项
|
||
const firstKey = domainInfoCache.keys().next().value;
|
||
domainInfoCache.delete(firstKey);
|
||
}
|
||
// 添加新的缓存项
|
||
domainInfoCache.set(domain, info);
|
||
}
|
||
|
||
// 检查域名是否匹配
|
||
function isDomainMatch(urlValue, targetDomain, categoryId) {
|
||
|
||
// 规范化目标域名,去除末尾的点
|
||
const normalizedTargetDomain = targetDomain.replace(/\.$/, '').toLowerCase();
|
||
|
||
try {
|
||
// 尝试将URL值解析为完整URL
|
||
const url = new URL(urlValue);
|
||
let hostname = url.hostname.toLowerCase();
|
||
// 规范化主机名,去除末尾的点
|
||
hostname = hostname.replace(/\.$/, '');
|
||
|
||
// 根据类别ID选择匹配方式
|
||
if (categoryId === 2) {
|
||
// CDN类别,使用域名后缀匹配
|
||
if (normalizedTargetDomain.endsWith('.' + hostname) || normalizedTargetDomain === hostname) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
} else {
|
||
// 其他类别,使用完整域名匹配
|
||
if (hostname === normalizedTargetDomain) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 如果是纯域名而不是完整URL
|
||
let urlDomain = urlValue.toLowerCase();
|
||
// 规范化纯域名,去除末尾的点
|
||
urlDomain = urlDomain.replace(/\.$/, '');
|
||
|
||
// 根据类别ID选择匹配方式
|
||
if (categoryId === 2) {
|
||
// CDN类别,使用域名后缀匹配
|
||
if (normalizedTargetDomain.endsWith('.' + urlDomain) || normalizedTargetDomain === urlDomain) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
} else {
|
||
// 其他类别,使用完整域名匹配
|
||
if (urlDomain === normalizedTargetDomain) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取主域名
|
||
function extractPrimaryDomain(domain) {
|
||
|
||
const parts = domain.split('.');
|
||
|
||
if (parts.length <= 2) {
|
||
return domain;
|
||
}
|
||
|
||
// 处理常见的三级域名
|
||
const commonSubdomains = ['www', 'mail', 'news', 'map', 'image', 'video', 'cdn', 'api', 'blog', 'shop', 'cloud', 'docs', 'help', 'support', 'dev', 'test', 'staging'];
|
||
|
||
if (commonSubdomains.includes(parts[0])) {
|
||
const result = parts.slice(1).join('.');
|
||
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'];
|
||
|
||
for (const tld of countryTLDs) {
|
||
if (domain.endsWith('.' + tld)) {
|
||
const mainParts = domain.split('.');
|
||
const result = mainParts.slice(-tld.split('.').length - 1).join('.');
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// 默认情况:返回最后两个部分
|
||
const result = parts.slice(-2).join('.');
|
||
return result;
|
||
}
|
||
|
||
// 初始化列宽调节功能
|
||
function initResizableColumns() {
|
||
const table = document.querySelector('.resizable-table');
|
||
if (!table) return;
|
||
|
||
// 为每个表头添加调整手柄元素
|
||
function addResizeHandles() {
|
||
const headers = table.querySelectorAll('th');
|
||
headers.forEach(header => {
|
||
// 移除已存在的手柄
|
||
const existingHandle = header.querySelector('.resize-handle');
|
||
if (existingHandle) {
|
||
existingHandle.remove();
|
||
}
|
||
|
||
// 创建新的调整手柄
|
||
const resizeHandle = document.createElement('div');
|
||
resizeHandle.className = 'resize-handle';
|
||
resizeHandle.style.cssText = `
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 10px;
|
||
height: 100%;
|
||
cursor: col-resize;
|
||
background: rgba(59, 130, 246, 0.1);
|
||
z-index: 10;
|
||
transition: background-color 0.2s ease;
|
||
`;
|
||
|
||
// 添加悬停效果
|
||
resizeHandle.addEventListener('mouseenter', () => {
|
||
resizeHandle.style.background = 'rgba(59, 130, 246, 0.3)';
|
||
});
|
||
|
||
resizeHandle.addEventListener('mouseleave', () => {
|
||
if (!resizeHandle.classList.contains('dragging')) {
|
||
resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)';
|
||
}
|
||
});
|
||
|
||
header.style.position = 'relative';
|
||
header.appendChild(resizeHandle);
|
||
});
|
||
}
|
||
|
||
// 计算列宽并设置固定宽度
|
||
function calculateAndSetColumnWidths() {
|
||
// 确保表格可见
|
||
table.style.visibility = 'visible';
|
||
|
||
// 保存当前表格布局
|
||
const originalLayout = table.style.tableLayout;
|
||
table.style.tableLayout = 'auto';
|
||
|
||
// 获取所有表头和数据行
|
||
const headers = table.querySelectorAll('th');
|
||
const rows = table.querySelectorAll('tbody tr');
|
||
|
||
// 计算每列的最大宽度
|
||
const columnWidths = [];
|
||
headers.forEach((header, index) => {
|
||
// 获取表头宽度
|
||
let maxWidth = header.offsetWidth;
|
||
|
||
// 遍历部分数据行(最多前20行),找到该列的最大宽度
|
||
// 这样可以在保证准确性的同时提高性能
|
||
const maxRowsToCheck = Math.min(20, rows.length);
|
||
for (let i = 0; i < maxRowsToCheck; i++) {
|
||
const row = rows[i];
|
||
const cell = row.children[index];
|
||
if (cell) {
|
||
maxWidth = Math.max(maxWidth, cell.offsetWidth);
|
||
}
|
||
}
|
||
|
||
// 添加一些 padding
|
||
maxWidth += 20;
|
||
|
||
// 保存最大宽度
|
||
columnWidths[index] = maxWidth;
|
||
});
|
||
|
||
// 设置每列的固定宽度
|
||
headers.forEach((header, index) => {
|
||
const width = `${columnWidths[index]}px`;
|
||
header.style.width = width;
|
||
header.style.minWidth = width;
|
||
header.style.maxWidth = width;
|
||
});
|
||
|
||
// 恢复表格布局
|
||
table.style.tableLayout = 'fixed';
|
||
}
|
||
|
||
// 保存列宽设置的函数
|
||
function saveColumnWidths() {
|
||
const headers = table.querySelectorAll('th');
|
||
const columnWidths = {};
|
||
headers.forEach((header, index) => {
|
||
columnWidths[index] = header.style.width;
|
||
});
|
||
localStorage.setItem('logsTableColumnWidths', JSON.stringify(columnWidths));
|
||
}
|
||
|
||
// 恢复列宽设置的函数
|
||
function restoreColumnWidths() {
|
||
const headers = table.querySelectorAll('th');
|
||
const savedWidths = localStorage.getItem('logsTableColumnWidths');
|
||
|
||
if (savedWidths) {
|
||
const columnWidths = JSON.parse(savedWidths);
|
||
|
||
// 设置表格布局为fixed
|
||
table.style.tableLayout = 'fixed';
|
||
|
||
headers.forEach((header, index) => {
|
||
if (columnWidths[index]) {
|
||
const width = columnWidths[index];
|
||
header.style.width = width;
|
||
header.style.minWidth = width;
|
||
header.style.maxWidth = width;
|
||
|
||
// 找到对应的数据列并设置宽度
|
||
const rows = table.querySelectorAll('tbody tr');
|
||
rows.forEach(row => {
|
||
const cell = row.children[index];
|
||
if (cell) {
|
||
cell.style.width = width;
|
||
cell.style.minWidth = width;
|
||
cell.style.maxWidth = width;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
// 没有保存的宽度,计算并设置列宽
|
||
calculateAndSetColumnWidths();
|
||
}
|
||
}
|
||
|
||
// 恢复保存的列宽设置或计算初始列宽
|
||
restoreColumnWidths();
|
||
|
||
// 添加调整手柄
|
||
addResizeHandles();
|
||
|
||
// 拖拽状态变量
|
||
let currentHeader = null;
|
||
let startX = 0;
|
||
let startWidth = 0;
|
||
let isDragging = false;
|
||
|
||
// 鼠标按下事件
|
||
table.addEventListener('mousedown', (e) => {
|
||
const resizeHandle = e.target.closest('.resize-handle');
|
||
if (resizeHandle) {
|
||
currentHeader = resizeHandle.parentElement;
|
||
startX = e.clientX;
|
||
startWidth = currentHeader.offsetWidth;
|
||
isDragging = true;
|
||
|
||
// 添加拖拽状态类
|
||
currentHeader.classList.add('dragging');
|
||
resizeHandle.classList.add('dragging');
|
||
|
||
// 改变拖拽手柄样式
|
||
resizeHandle.style.background = 'rgba(59, 130, 246, 0.6)';
|
||
|
||
// 阻止默认事件和冒泡
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// 阻止文本选择
|
||
document.addEventListener('selectstart', preventSelect, { capture: true });
|
||
document.addEventListener('copy', preventCopy, { capture: true });
|
||
|
||
// 添加全局事件监听器
|
||
document.addEventListener('mousemove', onMouseMove, { capture: true });
|
||
document.addEventListener('mouseup', onMouseUp, { capture: true });
|
||
}
|
||
});
|
||
|
||
// 鼠标移动事件处理函数
|
||
function onMouseMove(e) {
|
||
if (!currentHeader) return;
|
||
|
||
// 阻止默认事件
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// 计算新宽度
|
||
const deltaX = e.clientX - startX;
|
||
const newWidth = Math.max(50, startWidth + deltaX);
|
||
|
||
// 设置新宽度
|
||
const width = `${newWidth}px`;
|
||
currentHeader.style.width = width;
|
||
currentHeader.style.minWidth = width;
|
||
currentHeader.style.maxWidth = width;
|
||
|
||
// 找到对应的数据列并设置宽度
|
||
const headers = table.querySelectorAll('th');
|
||
const index = Array.from(headers).indexOf(currentHeader);
|
||
const rows = table.querySelectorAll('tbody tr');
|
||
rows.forEach(row => {
|
||
const cell = row.children[index];
|
||
if (cell) {
|
||
cell.style.width = width;
|
||
cell.style.minWidth = width;
|
||
cell.style.maxWidth = width;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 鼠标释放事件处理函数
|
||
function onMouseUp(e) {
|
||
if (!currentHeader) return;
|
||
|
||
// 阻止默认事件
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// 获取调整手柄
|
||
const resizeHandle = currentHeader.querySelector('.resize-handle');
|
||
|
||
// 移除拖拽状态类
|
||
currentHeader.classList.remove('dragging');
|
||
resizeHandle.classList.remove('dragging');
|
||
|
||
// 恢复拖拽手柄样式
|
||
resizeHandle.style.background = 'rgba(59, 130, 246, 0.1)';
|
||
|
||
// 保存列宽设置
|
||
saveColumnWidths();
|
||
|
||
// 重置状态
|
||
currentHeader = null;
|
||
isDragging = false;
|
||
|
||
// 移除事件监听器
|
||
document.removeEventListener('selectstart', preventSelect, { capture: true });
|
||
document.removeEventListener('copy', preventCopy, { capture: true });
|
||
document.removeEventListener('mousemove', onMouseMove, { capture: true });
|
||
document.removeEventListener('mouseup', onMouseUp, { capture: true });
|
||
}
|
||
|
||
// 阻止文本选择和复制
|
||
function preventSelect(e) {
|
||
e.preventDefault();
|
||
}
|
||
|
||
function preventCopy(e) {
|
||
e.preventDefault();
|
||
}
|
||
}
|
||
|
||
// 执行搜索操作
|
||
function performSearch(searchTerm) {
|
||
// 获取搜索文本框
|
||
const searchInput = document.getElementById('logs-search');
|
||
if (searchInput) {
|
||
// 设置搜索内容
|
||
searchInput.value = searchTerm;
|
||
// 更新currentSearch变量
|
||
currentSearch = searchTerm;
|
||
// 重置页码到第一页
|
||
currentPage = 1;
|
||
// 重新加载日志
|
||
loadLogs();
|
||
}
|
||
}
|
||
|
||
// 初始化查询日志页面
|
||
function initLogsPage() {
|
||
|
||
// 启动内存监控
|
||
memoryMonitor.start();
|
||
|
||
// 加载日志统计数据
|
||
loadLogsStats();
|
||
|
||
// 加载日志详情
|
||
loadLogs();
|
||
|
||
// 初始化图表
|
||
initLogsChart();
|
||
|
||
// 绑定事件
|
||
bindLogsEvents();
|
||
|
||
// 初始化日志详情弹窗
|
||
initLogDetailModal();
|
||
|
||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||
connectLogsWebSocket();
|
||
|
||
// 初始化列宽调节功能
|
||
initResizableColumns();
|
||
|
||
// 窗口大小改变时重新加载日志表格
|
||
window.addEventListener('resize', handleWindowResize);
|
||
|
||
// 在页面卸载时清理资源
|
||
window.addEventListener('beforeunload', cleanupLogsResources);
|
||
}
|
||
|
||
// 处理窗口大小改变
|
||
function handleWindowResize() {
|
||
// 重新加载日志表格,以适应新的屏幕尺寸
|
||
loadLogs();
|
||
}
|
||
|
||
// 清理资源
|
||
function cleanupLogsResources() {
|
||
// 清除WebSocket连接
|
||
if (logsWsConnection) {
|
||
logsWsConnection.close();
|
||
logsWsConnection = null;
|
||
}
|
||
|
||
// 清除重连计时器
|
||
if (logsWsReconnectTimer) {
|
||
clearTimeout(logsWsReconnectTimer);
|
||
logsWsReconnectTimer = null;
|
||
}
|
||
|
||
// 清除窗口大小改变事件监听器
|
||
window.removeEventListener('resize', handleWindowResize);
|
||
|
||
// 清除图表实例
|
||
if (logsChart) {
|
||
logsChart.destroy();
|
||
logsChart = null;
|
||
}
|
||
|
||
// 清除表格事件委托
|
||
const tableBody = document.getElementById('logs-table-body');
|
||
if (tableBody) {
|
||
tableBody.removeEventListener('click', handleTableClick);
|
||
}
|
||
|
||
// 清理缓存数据(保留但限制大小)
|
||
if (ipGeolocationCache && typeof ipGeolocationCache === 'object') {
|
||
// 清理超出大小限制的缓存
|
||
if (GEOLOCATION_CACHE_ORDER && GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
|
||
while (GEOLOCATION_CACHE_ORDER.length > GEOLOCATION_CACHE_MAX_SIZE) {
|
||
const oldestIp = GEOLOCATION_CACHE_ORDER.shift();
|
||
if (oldestIp) {
|
||
delete ipGeolocationCache[oldestIp];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 清理跟踪器和域名信息数据库(可选,根据内存使用情况决定)
|
||
// 注意:如果这些数据在其他地方也使用,不要在这里清理
|
||
// if (trackersDatabase) {
|
||
// trackersDatabase = null;
|
||
// trackersLoaded = false;
|
||
// }
|
||
// if (domainInfoDatabase) {
|
||
// domainInfoDatabase = null;
|
||
// domainInfoLoaded = false;
|
||
// }
|
||
|
||
// 清除模态框和工具提示
|
||
const modals = document.querySelectorAll('.fixed.inset-0.bg-black.bg-opacity-50');
|
||
modals.forEach(modal => {
|
||
modal.remove();
|
||
});
|
||
|
||
// 清除事件监听器(如果有其他全局事件监听器)
|
||
window.removeEventListener('beforeunload', cleanupLogsResources);
|
||
|
||
// 停止内存监控
|
||
memoryMonitor.stop();
|
||
|
||
console.log('Resources cleaned up successfully');
|
||
|
||
// 输出最终内存使用统计
|
||
if (memoryMonitor && memoryMonitor.getStats) {
|
||
const stats = memoryMonitor.getStats();
|
||
if (stats) {
|
||
console.log('最终内存使用统计:', {
|
||
avg: stats.avg,
|
||
max: stats.max,
|
||
min: stats.min,
|
||
recent: stats.recent
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 绑定事件
|
||
function bindLogsEvents() {
|
||
// 搜索按钮
|
||
const searchBtn = document.getElementById('logs-search-btn');
|
||
const searchInput = document.getElementById('logs-search');
|
||
const clearSearchBtn = document.getElementById('logs-clear-search');
|
||
|
||
// 显示或隐藏清除按钮
|
||
function toggleClearSearchBtn() {
|
||
if (searchInput && clearSearchBtn) {
|
||
if (searchInput.value.trim() !== '') {
|
||
clearSearchBtn.classList.remove('hidden');
|
||
} else {
|
||
clearSearchBtn.classList.add('hidden');
|
||
}
|
||
}
|
||
}
|
||
|
||
if (searchBtn) {
|
||
searchBtn.addEventListener('click', () => {
|
||
currentSearch = document.getElementById('logs-search').value.trim();
|
||
toggleClearSearchBtn();
|
||
currentPage = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
// 搜索框事件
|
||
if (searchInput) {
|
||
// 搜索框回车事件
|
||
searchInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
currentSearch = searchInput.value.trim();
|
||
toggleClearSearchBtn();
|
||
currentPage = 1;
|
||
loadLogs();
|
||
}
|
||
});
|
||
|
||
// 搜索框输入事件,用于显示/隐藏清除按钮
|
||
searchInput.addEventListener('input', toggleClearSearchBtn);
|
||
}
|
||
|
||
// 清除搜索按钮事件
|
||
if (clearSearchBtn) {
|
||
clearSearchBtn.addEventListener('click', () => {
|
||
if (searchInput) {
|
||
searchInput.value = '';
|
||
currentSearch = '';
|
||
toggleClearSearchBtn();
|
||
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');
|
||
const perPageSelectBottom = document.getElementById('logs-per-page-bottom');
|
||
|
||
// 同步两个下拉选择框的值
|
||
function syncPerPageSelects(value) {
|
||
if (perPageSelect) {
|
||
perPageSelect.value = value;
|
||
}
|
||
if (perPageSelectBottom) {
|
||
perPageSelectBottom.value = value;
|
||
}
|
||
}
|
||
|
||
// 为顶部下拉选择框添加事件处理
|
||
if (perPageSelect) {
|
||
perPageSelect.addEventListener('change', () => {
|
||
logsPerPage = parseInt(perPageSelect.value);
|
||
syncPerPageSelects(logsPerPage);
|
||
currentPage = 1;
|
||
loadLogs();
|
||
});
|
||
}
|
||
|
||
// 为底部下拉选择框添加事件处理
|
||
if (perPageSelectBottom) {
|
||
perPageSelectBottom.addEventListener('change', () => {
|
||
logsPerPage = parseInt(perPageSelectBottom.value);
|
||
syncPerPageSelects(logsPerPage);
|
||
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();
|
||
});
|
||
});
|
||
|
||
// 初始化排序图标
|
||
updateSortIcons();
|
||
}
|
||
|
||
// 更新排序图标
|
||
function updateSortIcons() {
|
||
const sortHeaders = document.querySelectorAll('th[data-sort]');
|
||
sortHeaders.forEach(header => {
|
||
const sortField = header.getAttribute('data-sort');
|
||
const icon = header.querySelector('i');
|
||
|
||
// 重置所有图标
|
||
icon.className = 'fa fa-sort ml-1 text-xs';
|
||
|
||
// 设置当前排序字段的图标
|
||
if (sortField === currentSortField) {
|
||
if (currentSortDirection === 'asc') {
|
||
icon.className = 'fa fa-sort-asc ml-1 text-xs';
|
||
} else {
|
||
icon.className = 'fa fa-sort-desc ml-1 text-xs';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 加载日志统计数据
|
||
function loadLogsStats() {
|
||
// 使用封装的apiRequest函数进行API调用
|
||
apiRequest('/logs/stats')
|
||
.then(data => {
|
||
if (data && data.error) {
|
||
console.error('加载日志统计数据失败:', data.error);
|
||
return;
|
||
}
|
||
|
||
// 更新统计卡片
|
||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||
|
||
// 计算屏蔽率
|
||
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||
})
|
||
.catch(error => {
|
||
console.error('加载日志统计数据失败:', error);
|
||
});
|
||
}
|
||
|
||
// 加载日志详情
|
||
async function loadLogs() {
|
||
// 显示加载状态
|
||
const loadingEl = document.getElementById('logs-loading');
|
||
if (loadingEl) {
|
||
loadingEl.classList.remove('hidden');
|
||
}
|
||
|
||
// 构建请求URL
|
||
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||
|
||
// 添加过滤条件
|
||
if (currentFilter) {
|
||
endpoint += `&result=${currentFilter}`;
|
||
}
|
||
|
||
// 添加搜索条件
|
||
if (currentSearch) {
|
||
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
|
||
}
|
||
|
||
// 添加排序条件
|
||
if (currentSortField) {
|
||
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
||
}
|
||
|
||
try {
|
||
// 使用封装的apiRequest函数进行API调用
|
||
const logsData = await apiRequest(endpoint);
|
||
|
||
if (logsData && logsData.error) {
|
||
console.error('加载日志详情失败:', logsData.error);
|
||
// 隐藏加载状态
|
||
if (loadingEl) {
|
||
loadingEl.classList.add('hidden');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 加载日志总数
|
||
const [logs, countData] = await Promise.all([
|
||
Promise.resolve(logsData || []), // 确保logsData是数组
|
||
apiRequest('/logs/count')
|
||
]);
|
||
|
||
// 确保logs是数组
|
||
const logsArray = Array.isArray(logs) ? logs : [];
|
||
// 确保countData是有效的
|
||
const totalLogs = countData && countData.count ? countData.count : logsArray.length;
|
||
|
||
// 计算总页数
|
||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||
|
||
// 更新日志表格
|
||
await updateLogsTable(logsArray);
|
||
|
||
// 绑定操作按钮事件
|
||
bindActionButtonsEvents();
|
||
|
||
// 更新分页信息
|
||
updateLogsPagination();
|
||
|
||
// 重新初始化列宽调节功能,确保新添加的行也能继承列宽设置
|
||
initResizableColumns();
|
||
|
||
// 隐藏加载状态
|
||
if (loadingEl) {
|
||
loadingEl.classList.add('hidden');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载日志详情失败:', error);
|
||
|
||
// 隐藏加载状态
|
||
if (loadingEl) {
|
||
loadingEl.classList.add('hidden');
|
||
}
|
||
|
||
// 显示空状态
|
||
const tableBody = document.getElementById('logs-table-body');
|
||
if (tableBody) {
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<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>
|
||
</tr>
|
||
`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新日志表格
|
||
async function updateLogsTable(logs) {
|
||
const tableBody = document.getElementById('logs-table-body');
|
||
if (!tableBody) return;
|
||
|
||
// 创建DocumentFragment来批量处理DOM操作
|
||
const fragment = document.createDocumentFragment();
|
||
|
||
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>
|
||
`;
|
||
fragment.appendChild(emptyRow);
|
||
} else {
|
||
// 检测是否为移动设备
|
||
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';
|
||
row.dataset.logId = log.id || Math.random().toString(36).substr(2, 9); // 添加唯一标识
|
||
row.dataset.log = JSON.stringify(log); // 存储日志数据
|
||
|
||
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
||
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';
|
||
|
||
// 构建跟踪器浮窗内容
|
||
// HTML转义函数,防止注入攻击
|
||
function escapeHtml(text) {
|
||
return text.replace(/[&<>'"]/g, function (match) {
|
||
return {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
"'": ''',
|
||
'"': '"'
|
||
}[match];
|
||
});
|
||
}
|
||
|
||
const escapedName = escapeHtml(trackerInfo && trackerInfo.name ? trackerInfo.name : '未知');
|
||
const escapedCategory = escapeHtml(trackerInfo && trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? trackersDatabase.categories[trackerInfo.categoryId] : '未知');
|
||
const escapedUrl = trackerInfo && trackerInfo.url ? escapeHtml(trackerInfo.url) : '';
|
||
const escapedSource = trackerInfo && trackerInfo.source ? escapeHtml(trackerInfo.source) : '';
|
||
|
||
const trackerTooltip = isTracker ? `
|
||
<div class="tracker-tooltip absolute z-50 bg-white dark:bg-gray-800 shadow-lg rounded-lg border border-gray-200 dark:border-gray-700 p-4 min-w-72 text-sm text-gray-900 dark:text-gray-100 transition-all duration-200 invisible opacity-0 -translate-y-2 translate-x-4 w-72">
|
||
<div class="flex items-start justify-between mb-3">
|
||
<div class="flex items-center">
|
||
<i class="fa fa-eye text-red-500 dark:text-red-400 mr-2 mt-0.5"></i>
|
||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">已知跟踪器</h4>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<div class="flex items-start">
|
||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">名称:</span>
|
||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedName}</span>
|
||
</div>
|
||
<div class="flex items-start">
|
||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">类别:</span>
|
||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedCategory}</span>
|
||
</div>
|
||
${trackerInfo.url ? `
|
||
<div class="flex items-start">
|
||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">URL:</span>
|
||
<a href="${escapedUrl}" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline flex-grow break-all">${escapedUrl}</a>
|
||
</div>
|
||
` : ''}
|
||
${trackerInfo.source ? `
|
||
<div class="flex items-start">
|
||
<span class="text-gray-500 dark:text-gray-400 font-medium w-24">源:</span>
|
||
<span class="text-gray-900 dark:text-gray-100 flex-grow">${escapedSource}</span>
|
||
</div>
|
||
` : ''}
|
||
</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 dark:text-gray-400 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 dark:text-green-400 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||
<div class="tracker-icon-container relative">
|
||
${isTracker ? '<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>' : '<i class="fa fa-eye-slash text-gray-300 dark:text-gray-600 mr-1" title="非跟踪器"></i>'}
|
||
${trackerTooltip}
|
||
</div>
|
||
<span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.domain}')">${log.domain}</span>
|
||
</div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span></div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">客户端: <span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.clientIP}')">${log.clientIP}</span></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 dark:text-gray-400 mt-1">${formattedDate}</div>
|
||
</td>
|
||
<td class="py-3 px-4 text-sm">
|
||
<div class="font-medium cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.clientIP}')">${log.clientIP}</div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 location-${log.clientIP.replace(/[.:]/g, '-')}">${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 dark:text-green-400 mr-1" title="DNSSEC已启用"></i>' : ''}
|
||
<div class="tracker-icon-container relative">
|
||
${isTracker ? '<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>' : '<i class="fa fa-eye-slash text-gray-300 dark:text-gray-600 mr-1" title="非跟踪器"></i>'}
|
||
${trackerTooltip}
|
||
</div>
|
||
<span class="cursor-pointer text-primary dark:text-blue-400 hover:underline" onclick="event.stopPropagation(); performSearch('${log.domain}')">${log.domain}</span>
|
||
</div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">类型: ${log.queryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass} dark:text-blue-400">${log.fromCache ? '缓存' : '非缓存'}</span>${log.dnssec ? ', <span class="text-green-500 dark:text-green-400"><i class="fa fa-lock"></i> DNSSEC</span>' : ''}${log.edns ? ', <span class="text-blue-500 dark:text-blue-400"><i class="fa fa-exchange"></i> EDNS</span>' : ''}</div>
|
||
<div class="text-xs text-gray-500 dark:text-gray-400 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>
|
||
`;
|
||
|
||
// 更新IP地理位置信息
|
||
const locationElement = row.querySelector(`.location-${log.clientIP.replace(/[.:]/g, '-')}`);
|
||
if (locationElement) {
|
||
// 调用getIpGeolocation函数获取地理位置
|
||
getIpGeolocation(log.clientIP).then(location => {
|
||
locationElement.textContent = location;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加跟踪器图标悬停事件
|
||
if (isTracker) {
|
||
const iconContainer = row.querySelector('.tracker-icon-container');
|
||
const tooltip = iconContainer.querySelector('.tracker-tooltip');
|
||
if (iconContainer && tooltip) {
|
||
// 确保容器是相对定位,作为浮窗的定位基准
|
||
iconContainer.style.position = 'relative';
|
||
|
||
// 移除内联样式,使用CSS类控制显示
|
||
tooltip.removeAttribute('style');
|
||
|
||
// 鼠标进入事件
|
||
iconContainer.addEventListener('mouseenter', () => {
|
||
tooltip.classList.remove('invisible', 'opacity-0', '-translate-y-2');
|
||
tooltip.classList.add('opacity-100', 'translate-y-0');
|
||
});
|
||
|
||
// 鼠标离开事件
|
||
iconContainer.addEventListener('mouseleave', () => {
|
||
tooltip.classList.add('opacity-0', '-translate-y-2');
|
||
tooltip.classList.remove('opacity-100', 'translate-y-0');
|
||
// 延迟移除invisible类,等待过渡动画完成
|
||
setTimeout(() => {
|
||
tooltip.classList.add('invisible');
|
||
}, 200);
|
||
});
|
||
}
|
||
}
|
||
|
||
fragment.appendChild(row);
|
||
}
|
||
}
|
||
|
||
// 一次性清空表格并添加所有行
|
||
tableBody.innerHTML = '';
|
||
tableBody.appendChild(fragment);
|
||
|
||
// 添加事件委托
|
||
setupTableEventDelegation();
|
||
}
|
||
|
||
// 设置表格事件委托
|
||
function setupTableEventDelegation() {
|
||
const tableBody = document.getElementById('logs-table-body');
|
||
if (!tableBody) return;
|
||
|
||
// 移除现有的事件监听器
|
||
tableBody.removeEventListener('click', handleTableClick);
|
||
|
||
// 添加新的事件监听器
|
||
tableBody.addEventListener('click', handleTableClick);
|
||
}
|
||
|
||
// 处理表格点击事件
|
||
function handleTableClick(e) {
|
||
// 处理拦截按钮点击
|
||
const blockBtn = e.target.closest('.block-btn');
|
||
if (blockBtn) {
|
||
e.preventDefault();
|
||
const domain = blockBtn.dataset.domain;
|
||
blockDomain(domain);
|
||
return;
|
||
}
|
||
|
||
// 处理放行按钮点击
|
||
const unblockBtn = e.target.closest('.unblock-btn');
|
||
if (unblockBtn) {
|
||
e.preventDefault();
|
||
const domain = unblockBtn.dataset.domain;
|
||
unblockDomain(domain);
|
||
return;
|
||
}
|
||
|
||
// 处理行点击,显示详情
|
||
const row = e.target.closest('tr');
|
||
if (row && !e.target.closest('button')) {
|
||
const logData = row.dataset.log;
|
||
if (logData) {
|
||
try {
|
||
const log = JSON.parse(logData);
|
||
showLogDetailModal(log);
|
||
} catch (error) {
|
||
console.error('解析日志数据失败:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新分页信息
|
||
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;
|
||
}
|
||
|
||
// 销毁现有图表
|
||
if (logsChart) {
|
||
logsChart.destroy();
|
||
logsChart = null;
|
||
}
|
||
|
||
// 创建图表
|
||
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`;
|
||
|
||
|
||
// 创建WebSocket连接
|
||
logsWsConnection = new WebSocket(wsUrl);
|
||
|
||
// 连接打开事件
|
||
logsWsConnection.onopen = function() {
|
||
};
|
||
|
||
// 接收消息事件
|
||
logsWsConnection.onmessage = function(event) {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.type === 'initial_data' || data.type === 'stats_update') {
|
||
// 只更新统计数据,不更新日志详情
|
||
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秒后重连
|
||
|
||
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 {
|
||
|
||
// 创建拦截规则,使用AdBlock Plus格式
|
||
const blockRule = `||${domain}^`;
|
||
|
||
// 调用API添加拦截规则
|
||
const response = await apiRequest('/shield', 'POST', { rule: blockRule });
|
||
|
||
|
||
// 处理不同的响应格式
|
||
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() {
|
||
// 此函数已被 setupTableEventDelegation 中的事件委托替代
|
||
// 保留此函数以保持与现有代码的兼容性
|
||
console.log('Action buttons events are now handled by event delegation');
|
||
}
|
||
|
||
// 刷新规则列表
|
||
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 {
|
||
|
||
// 创建放行规则,使用AdBlock Plus格式
|
||
const allowRule = `@@||${domain}^`;
|
||
|
||
// 调用API添加放行规则
|
||
const response = await apiRequest('/shield', 'POST', { rule: allowRule });
|
||
|
||
|
||
// 处理不同的响应格式
|
||
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) {
|
||
|
||
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';
|
||
modalContainer.style.zIndex = '9999';
|
||
modalContainer.style.opacity = '0';
|
||
modalContainer.style.transition = 'opacity 0.3s ease-in-out';
|
||
|
||
// 创建模态框内容
|
||
const modalContent = document.createElement('div');
|
||
modalContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto';
|
||
modalContent.style.transform = 'scale(0.9) translateY(20px)';
|
||
modalContent.style.transition = 'transform 0.3s ease-in-out';
|
||
modalContent.style.opacity = '0';
|
||
|
||
// 创建标题栏
|
||
const header = document.createElement('div');
|
||
header.className = 'sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-between items-center';
|
||
|
||
const title = document.createElement('h3');
|
||
title.className = 'text-xl font-semibold text-gray-900 dark:text-gray-100';
|
||
title.textContent = '日志详情';
|
||
|
||
const closeButton = document.createElement('button');
|
||
closeButton.innerHTML = '<i class="fa fa-times text-xl"></i>';
|
||
closeButton.className = 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 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 dark:text-gray-300 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 dark:text-gray-400">日期</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${dateStr}</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">时间</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${timeStr}</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">状态</div>
|
||
<div class="text-sm font-medium ${result === 'blocked' ? 'text-red-600 dark:text-red-400' : result === 'allowed' ? 'text-green-600 dark:text-green-400' : 'text-gray-500 dark:text-gray-400'}">
|
||
${result === 'blocked' ? '已拦截' : result === 'allowed' ? '允许' : result}
|
||
</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">域名</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 break-all">${domain}</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">类型</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${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 dark:text-gray-400">DNS特性</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||
${dnssec ? '<i class="fa fa-lock text-green-500 dark:text-green-400 mr-1" title="DNSSEC已启用"></i>DNSSEC ' : ''}
|
||
${edns ? '<i class="fa fa-exchange text-blue-500 dark:text-blue-400 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 dark:text-gray-400">域名信息</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 p-3 bg-gray-50 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600 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 dark:text-gray-400 mr-2">类别:</span>
|
||
<span class="flex-grow">${domainInfo.categoryName || '未知'}</span>
|
||
</div>
|
||
<div class="flex items-center flex-wrap">
|
||
<span class="text-gray-500 dark:text-gray-400 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 dark:text-gray-400">跟踪器信息</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||
${isTracker ? `
|
||
<div class="flex items-center">
|
||
<i class="fa fa-eye text-red-500 dark:text-red-400 mr-1" title="已知跟踪器"></i>
|
||
<span>${trackerInfo.name} (${trackerInfo.categoryId && trackersDatabase && trackersDatabase.categories ? 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 dark:text-gray-400">解析记录</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-all bg-gray-50 dark:bg-gray-700 p-3 rounded-md border border-gray-200 dark:border-gray-600">${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 dark:text-gray-400">DNS服务器</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${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 dark:text-gray-400">DNSSEC专用服务器</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${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 dark:border-gray-700';
|
||
|
||
const responseDetailsTitle = document.createElement('h4');
|
||
responseDetailsTitle.className = 'text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider';
|
||
responseDetailsTitle.textContent = '响应细节';
|
||
|
||
// 准备响应细节内容,根据条件添加规则信息
|
||
let responseDetailsHTML = `
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">响应时间</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${responseTime}毫秒</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">响应代码</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${getResponseCodeText(log.responseCode)}</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">缓存状态</div>
|
||
<div class="text-sm font-medium ${fromCache ? 'text-primary dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'}">
|
||
${fromCache ? '缓存' : '非缓存'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 只有被屏蔽时才显示规则信息
|
||
if (result === 'blocked') {
|
||
responseDetailsHTML += `
|
||
<div class="space-y-1">
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">规则</div>
|
||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">${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 dark:border-gray-700';
|
||
|
||
const clientDetailsTitle = document.createElement('h4');
|
||
clientDetailsTitle.className = 'text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider';
|
||
clientDetailsTitle.textContent = '客户端详情';
|
||
|
||
// 创建客户端IP容器,为后续更新地理位置做准备
|
||
const clientIPContainer = document.createElement('div');
|
||
clientIPContainer.className = 'text-sm font-medium text-gray-900 dark:text-gray-100';
|
||
clientIPContainer.innerHTML = `${clientIP} <span id="modal-location-${clientIP.replace(/[.:]/g, '-')}">(${location})</span>`;
|
||
|
||
const clientIPDiv = document.createElement('div');
|
||
clientIPDiv.className = 'space-y-1';
|
||
clientIPDiv.innerHTML = `
|
||
<div class="text-xs text-gray-500 dark:text-gray-400">IP地址</div>
|
||
`;
|
||
clientIPDiv.appendChild(clientIPContainer);
|
||
|
||
clientDetails.appendChild(clientDetailsTitle);
|
||
clientDetails.appendChild(clientIPDiv);
|
||
|
||
// 动态更新地理位置信息
|
||
const locationElement = clientIPDiv.querySelector(`#modal-location-${clientIP.replace(/[.:]/g, '-')}`);
|
||
if (locationElement) {
|
||
getIpGeolocation(clientIP).then(location => {
|
||
locationElement.textContent = `(${location})`;
|
||
});
|
||
}
|
||
|
||
// 操作按钮区域
|
||
const actionButtons = document.createElement('div');
|
||
actionButtons.className = 'pt-4 border-t border-gray-200 dark:border-gray-700 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);
|
||
|
||
// 触发动画效果,使其平滑显示
|
||
setTimeout(() => {
|
||
modalContainer.style.opacity = '1';
|
||
modalContent.style.transform = 'scale(1) translateY(0)';
|
||
modalContent.style.opacity = '1';
|
||
}, 10);
|
||
|
||
// 关闭模态框函数
|
||
function closeModal() {
|
||
modalContainer.style.opacity = '0';
|
||
modalContent.style.transform = 'scale(0.9) translateY(20px)';
|
||
modalContent.style.opacity = '0';
|
||
|
||
// 等待动画结束后移除元素
|
||
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';
|
||
errorModal.style.zIndex = '9999';
|
||
errorModal.style.opacity = '0';
|
||
errorModal.style.transition = 'opacity 0.3s ease-in-out';
|
||
|
||
const errorContent = document.createElement('div');
|
||
errorContent.className = 'bg-white dark:bg-gray-800 dark:text-gray-100 rounded-xl shadow-2xl p-6 w-full max-w-md';
|
||
errorContent.style.transform = 'scale(0.9) translateY(20px)';
|
||
errorContent.style.transition = 'transform 0.3s ease-in-out';
|
||
errorContent.style.opacity = '0';
|
||
|
||
errorContent.innerHTML = `
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">错误</h3>
|
||
<button onclick="closeErrorModal()" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition-colors">
|
||
<i class="fa fa-times text-xl"></i>
|
||
</button>
|
||
</div>
|
||
<div class="text-red-600 dark:text-red-400 text-sm">
|
||
加载日志详情失败: ${error.message}
|
||
</div>
|
||
`;
|
||
|
||
errorModal.appendChild(errorContent);
|
||
document.body.appendChild(errorModal);
|
||
|
||
// 触发动画效果,使其平滑显示
|
||
setTimeout(() => {
|
||
errorModal.style.opacity = '1';
|
||
errorContent.style.transform = 'scale(1) translateY(0)';
|
||
errorContent.style.opacity = '1';
|
||
}, 10);
|
||
|
||
// 关闭错误模态框函数
|
||
function closeErrorModal() {
|
||
errorModal.style.opacity = '0';
|
||
errorContent.style.transform = 'scale(0.9) translateY(20px)';
|
||
errorContent.style.opacity = '0';
|
||
|
||
// 等待动画结束后移除元素
|
||
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秒更新一次
|