优化修复
This commit is contained in:
@@ -68,8 +68,14 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
const parsedData = JSON.parse(responseText);
|
||||
|
||||
// 检查解析后的数据是否有效
|
||||
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||
console.warn('解析后的数据为空');
|
||||
if (parsedData === null) {
|
||||
console.warn('解析后的数据为null');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 允许返回空数组,但不允许返回空对象
|
||||
if (typeof parsedData === 'object' && !Array.isArray(parsedData) && Object.keys(parsedData).length === 0) {
|
||||
console.warn('解析后的数据为空对象');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,15 +165,15 @@ function processRealTimeData(stats) {
|
||||
let trendIcon = '---';
|
||||
|
||||
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||
|
||||
// 首次加载时初始化历史数据,不计算趋势
|
||||
if (prevResponseTime === null) {
|
||||
if (window.dashboardHistoryData.prevResponseTime === undefined || window.dashboardHistoryData.prevResponseTime === null) {
|
||||
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||
responsePercent = '0.0%';
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
} else {
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||
|
||||
// 计算变化百分比
|
||||
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
@@ -209,46 +209,15 @@ function processRealTimeData(stats) {
|
||||
|
||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||
if (queryPercentElem) {
|
||||
// 计算查询类型趋势
|
||||
let queryPercent = '---';
|
||||
let trendClass = 'text-gray-400';
|
||||
let trendIcon = '---';
|
||||
|
||||
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
|
||||
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount;
|
||||
|
||||
// 首次加载时初始化历史数据,不计算趋势
|
||||
if (prevTopQueryTypeCount === null) {
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
|
||||
queryPercent = '0.0%';
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
} else {
|
||||
// 计算变化百分比
|
||||
if (prevTopQueryTypeCount > 0) {
|
||||
const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
|
||||
queryPercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
|
||||
// 设置趋势图标和颜色
|
||||
if (changePercent > 0) {
|
||||
trendIcon = '↑';
|
||||
trendClass = 'text-primary';
|
||||
} else if (changePercent < 0) {
|
||||
trendIcon = '↓';
|
||||
trendClass = 'text-secondary';
|
||||
} else {
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新历史数据
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
|
||||
}
|
||||
// 计算最常用查询类型的百分比
|
||||
let queryTypePercentage = 0;
|
||||
if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
|
||||
const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
|
||||
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
|
||||
}
|
||||
|
||||
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
|
||||
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
|
||||
queryPercentElem.textContent = `${Math.round(queryTypePercentage)}%`;
|
||||
queryPercentElem.className = 'text-sm flex items-center text-primary';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +279,17 @@ function processRealTimeData(stats) {
|
||||
// 实时更新TOP客户端和TOP域名数据
|
||||
async function updateTopData() {
|
||||
try {
|
||||
// 隐藏所有加载中状态
|
||||
const clientsLoadingElement = document.getElementById('top-clients-loading');
|
||||
if (clientsLoadingElement) {
|
||||
clientsLoadingElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
const domainsLoadingElement = document.getElementById('top-domains-loading');
|
||||
if (domainsLoadingElement) {
|
||||
domainsLoadingElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 获取最新的TOP客户端数据
|
||||
let clientsData = [];
|
||||
try {
|
||||
@@ -618,10 +598,10 @@ async function loadDashboardData() {
|
||||
updateCharts(stats, queryTypeStats);
|
||||
|
||||
// 更新表格数据
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
await updateTopBlockedTable(topBlockedDomains);
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
updateTopClientsTable(topClients);
|
||||
updateTopDomainsTable(topDomains);
|
||||
await updateTopClientsTable(topClients);
|
||||
await updateTopDomainsTable(topDomains);
|
||||
|
||||
// 尝试从stats中获取总查询数等信息
|
||||
if (stats.dns) {
|
||||
@@ -650,26 +630,35 @@ async function loadDashboardData() {
|
||||
let trendIcon = '---';
|
||||
|
||||
if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) {
|
||||
// 存储当前值用于下次计算趋势
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime;
|
||||
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||
|
||||
// 计算变化百分比
|
||||
if (prevResponseTime > 0) {
|
||||
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
// 首次加载时初始化历史数据,不计算趋势
|
||||
if (window.dashboardHistoryData.prevResponseTime === undefined || window.dashboardHistoryData.prevResponseTime === null) {
|
||||
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||
responsePercent = '0.0%';
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
} else {
|
||||
const prevResponseTime = window.dashboardHistoryData.prevResponseTime;
|
||||
|
||||
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
|
||||
if (changePercent > 0) {
|
||||
trendIcon = '↓';
|
||||
trendClass = 'text-danger';
|
||||
} else if (changePercent < 0) {
|
||||
trendIcon = '↑';
|
||||
trendClass = 'text-success';
|
||||
} else {
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
// 计算变化百分比
|
||||
if (prevResponseTime > 0) {
|
||||
const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100;
|
||||
responsePercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
|
||||
// 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的)
|
||||
if (changePercent > 0) {
|
||||
trendIcon = '↓';
|
||||
trendClass = 'text-danger';
|
||||
} else if (changePercent < 0) {
|
||||
trendIcon = '↑';
|
||||
trendClass = 'text-success';
|
||||
} else {
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新历史数据
|
||||
window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,11 +674,17 @@ async function loadDashboardData() {
|
||||
// 直接使用API返回的查询类型
|
||||
const queryType = stats.topQueryType || '---';
|
||||
|
||||
// 设置默认趋势显示
|
||||
// 计算最常用查询类型的百分比
|
||||
let queryTypePercentage = 0;
|
||||
if (stats.dns && stats.dns.QueryTypes && stats.dns.Queries > 0) {
|
||||
const topTypeCount = stats.dns.QueryTypes[queryType] || 0;
|
||||
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
|
||||
}
|
||||
|
||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||
if (queryPercentElem) {
|
||||
queryPercentElem.textContent = '• ---';
|
||||
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
|
||||
queryPercentElem.textContent = `• ${Math.round(queryTypePercentage)}%`;
|
||||
queryPercentElem.className = 'text-sm flex items-center text-primary';
|
||||
}
|
||||
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
@@ -1124,7 +1119,7 @@ function updateStatsCards(stats) {
|
||||
}
|
||||
|
||||
// 更新Top屏蔽域名表格
|
||||
function updateTopBlockedTable(domains) {
|
||||
async function updateTopBlockedTable(domains) {
|
||||
console.log('更新Top屏蔽域名表格,收到数据:', domains);
|
||||
const tableBody = document.getElementById('top-blocked-table');
|
||||
|
||||
@@ -1155,14 +1150,37 @@ function updateTopBlockedTable(domains) {
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const domain = tableData[i];
|
||||
// 检查域名是否是跟踪器
|
||||
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
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>
|
||||
` : '';
|
||||
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
||||
<span class="font-medium truncate">${domain.name}</span>
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium truncate">${domain.name}</span>
|
||||
${isTracker ? `
|
||||
<div class="tracker-icon-container relative ml-2">
|
||||
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-danger">${formatNumber(domain.count)}</span>
|
||||
@@ -1171,6 +1189,23 @@ function updateTopBlockedTable(domains) {
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
|
||||
trackerIconContainers.forEach(container => {
|
||||
const tooltip = container.querySelector('.tracker-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.display = 'none';
|
||||
|
||||
container.addEventListener('mouseenter', () => {
|
||||
tooltip.style.display = 'block';
|
||||
});
|
||||
|
||||
container.addEventListener('mouseleave', () => {
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新最近屏蔽域名表格
|
||||
@@ -1209,7 +1244,7 @@ function updateRecentBlockedTable(domains) {
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const domain = tableData[i];
|
||||
const time = formatTime(domain.timestamp);
|
||||
html += `
|
||||
@@ -1226,8 +1261,187 @@ function updateRecentBlockedTable(domains) {
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// IP地理位置缓存(检查是否已经存在,避免重复声明)
|
||||
if (typeof ipGeolocationCache === 'undefined') {
|
||||
var ipGeolocationCache = {};
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||
}
|
||||
|
||||
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
|
||||
if (typeof trackersDatabase === 'undefined') {
|
||||
var trackersDatabase = null;
|
||||
var trackersLoaded = false;
|
||||
var trackersLoading = false;
|
||||
}
|
||||
|
||||
// 加载跟踪器数据库
|
||||
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 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;
|
||||
}
|
||||
|
||||
// 获取IP地理位置信息
|
||||
async function getIpGeolocation(ip) {
|
||||
// 检查是否为内网IP
|
||||
if (isPrivateIP(ip)) {
|
||||
return "内网 内网";
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const now = Date.now();
|
||||
if (ipGeolocationCache[ip] && (now - ipGeolocationCache[ip].timestamp) < GEOLOCATION_CACHE_EXPIRY) {
|
||||
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
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 更新TOP客户端表格
|
||||
function updateTopClientsTable(clients) {
|
||||
async function updateTopClientsTable(clients) {
|
||||
console.log('更新TOP客户端表格,收到数据:', clients);
|
||||
const tableBody = document.getElementById('top-clients-table');
|
||||
|
||||
@@ -1237,6 +1451,15 @@ function updateTopClientsTable(clients) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏加载中状态
|
||||
const loadingElement = document.getElementById('top-clients-loading');
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 显示数据区域
|
||||
tableBody.classList.remove('hidden');
|
||||
|
||||
let tableData = [];
|
||||
|
||||
// 适配不同的数据结构
|
||||
@@ -1265,18 +1488,22 @@ function updateTopClientsTable(clients) {
|
||||
console.log('使用示例数据填充TOP客户端表格');
|
||||
}
|
||||
|
||||
// 只显示前5个客户端
|
||||
tableData = tableData.slice(0, 5);
|
||||
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const client = tableData[i];
|
||||
// 获取IP地理信息
|
||||
const location = await getIpGeolocation(client.ip);
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||
<span class="font-medium truncate">${client.ip}</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium truncate">${client.ip}</span>
|
||||
<span class="text-xs text-gray-500">${location}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-primary">${formatNumber(client.count)}</span>
|
||||
@@ -1288,7 +1515,7 @@ function updateTopClientsTable(clients) {
|
||||
}
|
||||
|
||||
// 更新请求域名排行表格
|
||||
function updateTopDomainsTable(domains) {
|
||||
async function updateTopDomainsTable(domains) {
|
||||
console.log('更新请求域名排行表格,收到数据:', domains);
|
||||
const tableBody = document.getElementById('top-domains-table');
|
||||
|
||||
@@ -1326,18 +1553,40 @@ function updateTopDomainsTable(domains) {
|
||||
console.log('使用示例数据填充请求域名排行表格');
|
||||
}
|
||||
|
||||
// 只显示前5个域名
|
||||
tableData = tableData.slice(0, 5);
|
||||
|
||||
|
||||
let html = '';
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const domain = tableData[i];
|
||||
// 检查域名是否是跟踪器
|
||||
const trackerInfo = await isDomainInTrackerDatabase(domain.name);
|
||||
const isTracker = trackerInfo !== null;
|
||||
|
||||
// 构建跟踪器浮窗内容
|
||||
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>
|
||||
` : '';
|
||||
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
||||
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium truncate">${domain.name}${domain.dnssec ? ' <i class="fa fa-lock text-green-500"></i>' : ''}</span>
|
||||
${isTracker ? `
|
||||
<div class="tracker-icon-container relative ml-2">
|
||||
<i class="fa fa-eye text-red-500" title="已知跟踪器"></i>
|
||||
${trackerTooltip}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 flex-shrink-0 font-semibold text-success">${formatNumber(domain.count)}</span>
|
||||
@@ -1346,15 +1595,32 @@ function updateTopDomainsTable(domains) {
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
|
||||
// 添加跟踪器图标悬停事件
|
||||
const trackerIconContainers = tableBody.querySelectorAll('.tracker-icon-container');
|
||||
trackerIconContainers.forEach(container => {
|
||||
const tooltip = container.querySelector('.tracker-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.display = 'none';
|
||||
|
||||
container.addEventListener('mouseenter', () => {
|
||||
tooltip.style.display = 'block';
|
||||
});
|
||||
|
||||
container.addEventListener('mouseleave', () => {
|
||||
tooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前选中的时间范围
|
||||
let currentTimeRange = '24h'; // 默认为24小时
|
||||
let currentTimeRange = '30d'; // 默认为30天
|
||||
let isMixedView = true; // 是否为混合视图 - 默认显示混合视图
|
||||
let lastSelectedIndex = 0; // 最后选中的按钮索引
|
||||
let lastSelectedIndex = 2; // 最后选中的按钮索引,30天是第三个按钮
|
||||
|
||||
// 详细图表专用变量
|
||||
let detailedCurrentTimeRange = '24h'; // 详细图表当前时间范围
|
||||
let detailedCurrentTimeRange = '30d'; // 详细图表当前时间范围
|
||||
let detailedIsMixedView = false; // 详细图表是否为混合视图
|
||||
|
||||
// 初始化时间范围切换
|
||||
@@ -1491,10 +1757,12 @@ function initTimeRangeToggle() {
|
||||
// 移除自定义鼠标悬停提示效果
|
||||
});
|
||||
|
||||
// 确保默认选中第一个按钮并显示混合内容
|
||||
// 确保默认选中30天按钮并显示混合内容
|
||||
if (timeRangeButtons.length > 0) {
|
||||
const firstButton = timeRangeButtons[0];
|
||||
const firstStyle = buttonStyles[0];
|
||||
// 选择30天按钮(索引为2),如果不存在则使用第一个按钮
|
||||
const defaultButtonIndex = 2;
|
||||
const defaultButton = timeRangeButtons[defaultButtonIndex] || timeRangeButtons[0];
|
||||
const defaultStyle = buttonStyles[defaultButtonIndex % buttonStyles.length] || buttonStyles[0];
|
||||
|
||||
// 先重置所有按钮
|
||||
timeRangeButtons.forEach((btn, index) => {
|
||||
@@ -1506,13 +1774,13 @@ function initTimeRangeToggle() {
|
||||
btn.classList.add(...btnStyle.hover);
|
||||
});
|
||||
|
||||
// 然后设置第一个按钮为激活状态,并标记为混合视图
|
||||
firstButton.classList.remove(...firstStyle.normal);
|
||||
firstButton.classList.remove(...firstStyle.hover);
|
||||
firstButton.classList.add('active', 'mixed-view-active');
|
||||
firstButton.classList.add(...firstStyle.active);
|
||||
firstButton.classList.add(...firstStyle.activeHover);
|
||||
console.log('默认选中第一个按钮并显示混合内容:', firstButton.textContent.trim());
|
||||
// 然后设置30天按钮为激活状态,并标记为混合视图
|
||||
defaultButton.classList.remove(...defaultStyle.normal);
|
||||
defaultButton.classList.remove(...defaultStyle.hover);
|
||||
defaultButton.classList.add('active', 'mixed-view-active');
|
||||
defaultButton.classList.add(...defaultStyle.active);
|
||||
defaultButton.classList.add(...defaultStyle.activeHover);
|
||||
console.log('默认选中30天按钮并显示混合内容:', defaultButton.textContent.trim());
|
||||
|
||||
// 设置默认显示混合内容
|
||||
isMixedView = true;
|
||||
|
||||
@@ -10,9 +10,11 @@ let logsChart = null;
|
||||
let currentSortField = '';
|
||||
let currentSortDirection = 'desc'; // 默认降序
|
||||
|
||||
// IP地理位置缓存
|
||||
let ipGeolocationCache = {};
|
||||
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||
// IP地理位置缓存(检查是否已经存在,避免重复声明)
|
||||
if (typeof ipGeolocationCache === 'undefined') {
|
||||
var ipGeolocationCache = {};
|
||||
var GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||
}
|
||||
|
||||
// 获取IP地理位置信息
|
||||
async function getIpGeolocation(ip) {
|
||||
@@ -112,10 +114,12 @@ function isPrivateIP(ip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 跟踪器数据库缓存
|
||||
let trackersDatabase = null;
|
||||
let trackersLoaded = false;
|
||||
let trackersLoading = false;
|
||||
// 跟踪器数据库缓存(检查是否已经存在,避免重复声明)
|
||||
if (typeof trackersDatabase === 'undefined') {
|
||||
var trackersDatabase = null;
|
||||
var trackersLoaded = false;
|
||||
var trackersLoading = false;
|
||||
}
|
||||
|
||||
// 域名信息数据库缓存
|
||||
let domainInfoDatabase = null;
|
||||
@@ -477,6 +481,270 @@ function extractPrimaryDomain(domain) {
|
||||
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;
|
||||
|
||||
// 遍历所有数据行,找到该列的最大宽度
|
||||
rows.forEach(row => {
|
||||
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;
|
||||
|
||||
// 找到对应的数据列并设置宽度
|
||||
rows.forEach(row => {
|
||||
const cell = row.children[index];
|
||||
if (cell) {
|
||||
cell.style.width = width;
|
||||
cell.style.minWidth = width;
|
||||
cell.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 initLogsPage() {
|
||||
console.log('初始化查询日志页面');
|
||||
@@ -499,6 +767,9 @@ function initLogsPage() {
|
||||
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||||
connectLogsWebSocket();
|
||||
|
||||
// 初始化列宽调节功能
|
||||
initResizableColumns();
|
||||
|
||||
// 窗口大小改变时重新加载日志表格
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
@@ -723,7 +994,7 @@ function loadLogsStats() {
|
||||
}
|
||||
|
||||
// 加载日志详情
|
||||
function loadLogs() {
|
||||
async function loadLogs() {
|
||||
// 显示加载状态
|
||||
const loadingEl = document.getElementById('logs-loading');
|
||||
if (loadingEl) {
|
||||
@@ -748,61 +1019,70 @@ function loadLogs() {
|
||||
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();
|
||||
|
||||
try {
|
||||
// 使用封装的apiRequest函数进行API调用
|
||||
const logsData = await apiRequest(endpoint);
|
||||
|
||||
if (logsData && logsData.error) {
|
||||
console.error('加载日志详情失败:', logsData.error);
|
||||
// 隐藏加载状态
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载日志详情失败:', 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新日志表格
|
||||
|
||||
Reference in New Issue
Block a user