优化修复

This commit is contained in:
Alex Yang
2026-01-03 01:11:42 +08:00
parent 1dd1f15788
commit f247eaeaa8
16 changed files with 1288 additions and 315 deletions

View File

@@ -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;