修复规则问题
This commit is contained in:
@@ -38,13 +38,6 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
// 优化错误响应处理
|
||||
console.warn(`API请求失败: ${response.status}`);
|
||||
|
||||
// 处理401未授权错误,重定向到登录页面
|
||||
if (response.status === 401) {
|
||||
console.warn('未授权访问,重定向到登录页面');
|
||||
window.location.href = '/login';
|
||||
return { error: '未授权访问' };
|
||||
}
|
||||
|
||||
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||
try {
|
||||
const errorData = JSON.parse(responseText);
|
||||
|
||||
@@ -6,18 +6,12 @@ let dnsRequestsChart = null;
|
||||
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
||||
let queryTypeChart = null; // 解析类型统计饼图
|
||||
let intervalId = null;
|
||||
let dashboardWsConnection = null;
|
||||
let dashboardWsReconnectTimer = null;
|
||||
let wsConnection = null;
|
||||
let wsReconnectTimer = null;
|
||||
// 存储统计卡片图表实例
|
||||
let statCardCharts = {};
|
||||
// 存储统计卡片历史数据
|
||||
let statCardHistoryData = {};
|
||||
// 存储仪表盘历史数据,用于计算趋势
|
||||
window.dashboardHistoryData = window.dashboardHistoryData || {
|
||||
prevResponseTime: null,
|
||||
prevActiveIPs: null,
|
||||
prevTopQueryTypeCount: null
|
||||
};
|
||||
|
||||
// 引入颜色配置文件
|
||||
const COLOR_CONFIG = window.COLOR_CONFIG || {};
|
||||
@@ -33,6 +27,8 @@ async function initDashboard() {
|
||||
// 初始化图表
|
||||
initCharts();
|
||||
|
||||
// 初始化统计卡片图表
|
||||
initStatCardCharts();
|
||||
|
||||
|
||||
// 初始化时间范围切换
|
||||
@@ -59,22 +55,22 @@ function connectWebSocket() {
|
||||
console.log('正在连接WebSocket:', wsUrl);
|
||||
|
||||
// 创建WebSocket连接
|
||||
dashboardWsConnection = new WebSocket(wsUrl);
|
||||
wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
// 连接打开事件
|
||||
dashboardWsConnection.onopen = function() {
|
||||
wsConnection.onopen = function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
showNotification('数据更新成功', 'success');
|
||||
|
||||
// 清除重连计时器
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
if (wsReconnectTimer) {
|
||||
clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 接收消息事件
|
||||
dashboardWsConnection.onmessage = function(event) {
|
||||
wsConnection.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
@@ -88,16 +84,16 @@ function connectWebSocket() {
|
||||
};
|
||||
|
||||
// 连接关闭事件
|
||||
dashboardWsConnection.onclose = function(event) {
|
||||
wsConnection.onclose = function(event) {
|
||||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||
dashboardWsConnection = null;
|
||||
wsConnection = null;
|
||||
|
||||
// 设置重连
|
||||
setupReconnect();
|
||||
};
|
||||
|
||||
// 连接错误事件
|
||||
dashboardWsConnection.onerror = function(error) {
|
||||
wsConnection.onerror = function(error) {
|
||||
console.error('WebSocket连接错误:', error);
|
||||
};
|
||||
|
||||
@@ -110,14 +106,14 @@ function connectWebSocket() {
|
||||
|
||||
// 设置重连逻辑
|
||||
function setupReconnect() {
|
||||
if (dashboardWsReconnectTimer) {
|
||||
if (wsReconnectTimer) {
|
||||
return; // 已经有重连计时器在运行
|
||||
}
|
||||
|
||||
const reconnectDelay = 5000; // 5秒后重连
|
||||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||
|
||||
dashboardWsReconnectTimer = setTimeout(() => {
|
||||
wsReconnectTimer = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
@@ -128,6 +124,9 @@ function processRealTimeData(stats) {
|
||||
// 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片
|
||||
updateStatsCards(stats);
|
||||
|
||||
// 更新统计卡片图表
|
||||
updateStatCardCharts(stats);
|
||||
|
||||
// 获取查询类型统计数据
|
||||
let queryTypeStats = null;
|
||||
if (stats.dns && stats.dns.QueryTypes) {
|
||||
@@ -157,8 +156,6 @@ function processRealTimeData(stats) {
|
||||
|
||||
// 更新新卡片数据
|
||||
if (document.getElementById('avg-response-time')) {
|
||||
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
|
||||
|
||||
// 计算响应时间趋势
|
||||
let responsePercent = '---';
|
||||
let trendClass = 'text-gray-400';
|
||||
@@ -188,9 +185,16 @@ function processRealTimeData(stats) {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('avg-response-time').textContent = responseTime;
|
||||
// 使用滚轮效果更新响应时间
|
||||
if (stats.avgResponseTime) {
|
||||
animateValue('avg-response-time', stats.avgResponseTime + 'ms');
|
||||
} else {
|
||||
document.getElementById('avg-response-time').textContent = '---';
|
||||
}
|
||||
|
||||
const responseTimePercentElem = document.getElementById('response-time-percent');
|
||||
if (responseTimePercentElem) {
|
||||
// 直接更新文本,移除动画效果
|
||||
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
|
||||
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
|
||||
}
|
||||
@@ -198,42 +202,15 @@ function processRealTimeData(stats) {
|
||||
|
||||
if (document.getElementById('top-query-type')) {
|
||||
const queryType = stats.topQueryType || '---';
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
|
||||
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 || stats.topQueryTypeCount;
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
|
||||
|
||||
// 计算变化百分比
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
|
||||
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
|
||||
queryPercentElem.textContent = '• ---';
|
||||
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
|
||||
}
|
||||
|
||||
// 使用滚轮效果更新查询类型
|
||||
animateValue('top-query-type', queryType);
|
||||
}
|
||||
|
||||
if (document.getElementById('active-ips')) {
|
||||
@@ -265,7 +242,8 @@ function processRealTimeData(stats) {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('active-ips').textContent = activeIPs;
|
||||
// 使用滚轮效果更新活跃IP数量
|
||||
animateValue('active-ips', activeIPs);
|
||||
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
|
||||
if (activeIpsPercentElem) {
|
||||
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
|
||||
@@ -274,7 +252,7 @@ function processRealTimeData(stats) {
|
||||
}
|
||||
|
||||
// 实时更新TOP客户端和TOP域名数据
|
||||
updateTopData();
|
||||
updateTopData(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理实时数据失败:', error);
|
||||
@@ -282,25 +260,43 @@ function processRealTimeData(stats) {
|
||||
}
|
||||
|
||||
// 实时更新TOP客户端和TOP域名数据
|
||||
async function updateTopData() {
|
||||
async function updateTopData(stats = null) {
|
||||
try {
|
||||
// 获取最新的TOP客户端数据
|
||||
let clientsData = [];
|
||||
try {
|
||||
clientsData = await api.getTopClients();
|
||||
} catch (error) {
|
||||
console.error('获取TOP客户端数据失败:', error);
|
||||
}
|
||||
|
||||
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
|
||||
if (clientsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopClientsTable(clientsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-clients-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
// 如果提供了WebSocket数据,直接使用
|
||||
if (stats && stats.topClients) {
|
||||
updateTopClientsTable(stats.topClients);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-clients-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 否则从API获取最新的TOP客户端数据
|
||||
let clientsData = [];
|
||||
try {
|
||||
clientsData = await api.getTopClients();
|
||||
} catch (error) {
|
||||
console.error('获取TOP客户端数据失败:', error);
|
||||
}
|
||||
|
||||
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
|
||||
if (clientsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopClientsTable(clientsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-clients-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
const mockClients = [
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' }
|
||||
];
|
||||
updateTopClientsTable(mockClients);
|
||||
}
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
// API调用失败或返回错误,使用模拟数据
|
||||
const mockClients = [
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
@@ -310,35 +306,43 @@ async function updateTopData() {
|
||||
];
|
||||
updateTopClientsTable(mockClients);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了WebSocket数据,直接使用
|
||||
if (stats && stats.topDomains) {
|
||||
updateTopDomainsTable(stats.topDomains);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-domains-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// API调用失败或返回错误,使用模拟数据
|
||||
const mockClients = [
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' },
|
||||
{ ip: '---.---.---.---', count: '---' }
|
||||
];
|
||||
updateTopClientsTable(mockClients);
|
||||
}
|
||||
|
||||
// 获取最新的TOP域名数据
|
||||
let domainsData = [];
|
||||
try {
|
||||
domainsData = await api.getTopDomains();
|
||||
} catch (error) {
|
||||
console.error('获取TOP域名数据失败:', error);
|
||||
}
|
||||
|
||||
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
|
||||
if (domainsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopDomainsTable(domainsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-domains-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
// 否则从API获取最新的TOP域名数据
|
||||
let domainsData = [];
|
||||
try {
|
||||
domainsData = await api.getTopDomains();
|
||||
} catch (error) {
|
||||
console.error('获取TOP域名数据失败:', error);
|
||||
}
|
||||
|
||||
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
|
||||
if (domainsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopDomainsTable(domainsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-domains-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
const mockDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
updateTopDomainsTable(mockDomains);
|
||||
}
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
// API调用失败或返回错误,使用模拟数据
|
||||
const mockDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
@@ -348,16 +352,6 @@ async function updateTopData() {
|
||||
];
|
||||
updateTopDomainsTable(mockDomains);
|
||||
}
|
||||
} else {
|
||||
// API调用失败或返回错误,使用模拟数据
|
||||
const mockDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
updateTopDomainsTable(mockDomains);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新TOP数据失败:', error);
|
||||
@@ -396,15 +390,15 @@ function fallbackToIntervalRefresh() {
|
||||
// 清理资源
|
||||
function cleanupResources() {
|
||||
// 清除WebSocket连接
|
||||
if (dashboardWsConnection) {
|
||||
dashboardWsConnection.close();
|
||||
dashboardWsConnection = null;
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
|
||||
// 清除重连计时器
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
if (wsReconnectTimer) {
|
||||
clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
}
|
||||
|
||||
// 清除定时刷新
|
||||
@@ -743,6 +737,20 @@ async function loadDashboardData() {
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
// 格式化数字,添加千位分隔符
|
||||
function formatNumber(num, element) {
|
||||
// 如果是数字类型,转换为字符串
|
||||
if (typeof num === 'number') {
|
||||
// 处理浮点数(例如响应时间)
|
||||
if (num % 1 !== 0 && element && element.id.includes('response-time')) {
|
||||
return num.toFixed(2);
|
||||
}
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
// 如果已经是字符串,直接返回
|
||||
return num;
|
||||
}
|
||||
|
||||
function updateStatsCards(stats) {
|
||||
console.log('更新统计卡片,收到数据:', stats);
|
||||
|
||||
@@ -792,184 +800,22 @@ function updateStatsCards(stats) {
|
||||
queryTypePercentage = stats[0].queryTypePercentage || 0;
|
||||
activeIPs = stats[0].activeIPs || 0;
|
||||
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
|
||||
|
||||
}
|
||||
|
||||
// 存储正在进行的动画状态,避免动画重叠
|
||||
const animationInProgress = {};
|
||||
|
||||
// 为数字元素添加翻页滚动特效
|
||||
// 为数字元素添加滚轮式滚动特效
|
||||
// 直接更新数字元素,移除滚动动画
|
||||
function animateValue(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
// 如果该元素正在进行动画,取消当前动画并立即更新值
|
||||
if (animationInProgress[elementId]) {
|
||||
// 清除之前可能设置的定时器
|
||||
clearTimeout(animationInProgress[elementId].timeout1);
|
||||
clearTimeout(animationInProgress[elementId].timeout2);
|
||||
clearTimeout(animationInProgress[elementId].timeout3);
|
||||
|
||||
// 立即设置新值,避免显示错乱
|
||||
const formattedNewValue = formatNumber(newValue);
|
||||
element.innerHTML = formattedNewValue;
|
||||
return;
|
||||
}
|
||||
// 先调用formatNumber获取格式化后的值
|
||||
const formattedNewValue = formatNumber(newValue, element);
|
||||
const currentValue = element.textContent;
|
||||
|
||||
const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0;
|
||||
const formattedNewValue = formatNumber(newValue);
|
||||
|
||||
// 如果值没有变化,不执行动画
|
||||
if (oldValue === newValue && element.textContent === formattedNewValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 先移除可能存在的光晕效果类
|
||||
element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow');
|
||||
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
|
||||
|
||||
// 保存原始样式
|
||||
const originalStyle = element.getAttribute('style') || '';
|
||||
|
||||
try {
|
||||
// 复制原始元素的样式到新元素,确保大小完全一致
|
||||
const computedStyle = getComputedStyle(element);
|
||||
|
||||
// 配置翻页容器样式,确保与原始元素大小完全一致
|
||||
const containerStyle =
|
||||
'position: relative; ' +
|
||||
'display: ' + computedStyle.display + '; ' +
|
||||
'overflow: hidden; ' +
|
||||
'height: ' + element.offsetHeight + 'px; ' +
|
||||
'width: ' + element.offsetWidth + 'px; ' +
|
||||
'margin: ' + computedStyle.margin + '; ' +
|
||||
'padding: ' + computedStyle.padding + '; ' +
|
||||
'box-sizing: ' + computedStyle.boxSizing + '; ' +
|
||||
'line-height: ' + computedStyle.lineHeight + ';';
|
||||
|
||||
// 创建翻页容器
|
||||
const flipContainer = document.createElement('div');
|
||||
flipContainer.style.cssText = containerStyle;
|
||||
flipContainer.className = 'number-flip-container';
|
||||
|
||||
// 创建旧值元素
|
||||
const oldValueElement = document.createElement('div');
|
||||
oldValueElement.textContent = element.textContent;
|
||||
oldValueElement.style.cssText =
|
||||
'position: absolute; ' +
|
||||
'top: 0; ' +
|
||||
'left: 0; ' +
|
||||
'width: 100%; ' +
|
||||
'height: 100%; ' +
|
||||
'display: flex; ' +
|
||||
'align-items: center; ' +
|
||||
'justify-content: center; ' +
|
||||
'transition: transform 400ms ease-in-out; ' +
|
||||
'transform-origin: center;';
|
||||
|
||||
// 创建新值元素
|
||||
const newValueElement = document.createElement('div');
|
||||
newValueElement.textContent = formattedNewValue;
|
||||
newValueElement.style.cssText =
|
||||
'position: absolute; ' +
|
||||
'top: 0; ' +
|
||||
'left: 0; ' +
|
||||
'width: 100%; ' +
|
||||
'height: 100%; ' +
|
||||
'display: flex; ' +
|
||||
'align-items: center; ' +
|
||||
'justify-content: center; ' +
|
||||
'transition: transform 400ms ease-in-out; ' +
|
||||
'transform-origin: center; ' +
|
||||
'transform: translateY(100%);';
|
||||
[oldValueElement, newValueElement].forEach(el => {
|
||||
el.style.fontSize = computedStyle.fontSize;
|
||||
el.style.fontWeight = computedStyle.fontWeight;
|
||||
el.style.color = computedStyle.color;
|
||||
el.style.fontFamily = computedStyle.fontFamily;
|
||||
el.style.textAlign = computedStyle.textAlign;
|
||||
el.style.lineHeight = computedStyle.lineHeight;
|
||||
el.style.width = '100%';
|
||||
el.style.height = '100%';
|
||||
el.style.margin = '0';
|
||||
el.style.padding = '0';
|
||||
el.style.boxSizing = 'border-box';
|
||||
el.style.whiteSpace = computedStyle.whiteSpace;
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.textOverflow = 'ellipsis';
|
||||
// 确保垂直对齐正确
|
||||
el.style.verticalAlign = 'middle';
|
||||
});
|
||||
|
||||
// 替换原始元素的内容
|
||||
element.textContent = '';
|
||||
flipContainer.appendChild(oldValueElement);
|
||||
flipContainer.appendChild(newValueElement);
|
||||
element.appendChild(flipContainer);
|
||||
|
||||
// 标记该元素正在进行动画
|
||||
animationInProgress[elementId] = {};
|
||||
|
||||
// 启动翻页动画
|
||||
animationInProgress[elementId].timeout1 = setTimeout(() => {
|
||||
if (oldValueElement && newValueElement) {
|
||||
oldValueElement.style.transform = 'translateY(-100%)';
|
||||
newValueElement.style.transform = 'translateY(0)';
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// 动画结束后,恢复原始元素
|
||||
animationInProgress[elementId].timeout2 = setTimeout(() => {
|
||||
try {
|
||||
// 清理并设置最终值
|
||||
element.innerHTML = formattedNewValue;
|
||||
if (originalStyle) {
|
||||
element.setAttribute('style', originalStyle);
|
||||
} else {
|
||||
element.removeAttribute('style');
|
||||
}
|
||||
|
||||
// 添加当前卡片颜色的深色光晕效果
|
||||
const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50');
|
||||
let glowColorClass = '';
|
||||
|
||||
if (card) {
|
||||
if (card.classList.contains('bg-blue-50') || card.id.includes('total') || card.id.includes('response')) {
|
||||
glowColorClass = 'number-glow-dark-blue';
|
||||
} else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) {
|
||||
glowColorClass = 'number-glow-dark-red';
|
||||
} else if (card.classList.contains('bg-green-50') || card.id.includes('allowed') || card.id.includes('active')) {
|
||||
glowColorClass = 'number-glow-dark-green';
|
||||
} else if (card.classList.contains('bg-yellow-50') || card.id.includes('error') || card.id.includes('cpu')) {
|
||||
glowColorClass = 'number-glow-dark-yellow';
|
||||
}
|
||||
}
|
||||
|
||||
if (glowColorClass) {
|
||||
element.classList.add(glowColorClass);
|
||||
|
||||
// 2秒后移除光晕效果
|
||||
animationInProgress[elementId].timeout3 = setTimeout(() => {
|
||||
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新元素失败:', e);
|
||||
} finally {
|
||||
// 清除动画状态标记
|
||||
delete animationInProgress[elementId];
|
||||
}
|
||||
}, 450);
|
||||
} catch (e) {
|
||||
console.error('创建动画失败:', e);
|
||||
// 出错时直接设置值
|
||||
element.innerHTML = formattedNewValue;
|
||||
if (originalStyle) {
|
||||
element.setAttribute('style', originalStyle);
|
||||
} else {
|
||||
element.removeAttribute('style');
|
||||
}
|
||||
// 清除动画状态标记
|
||||
delete animationInProgress[elementId];
|
||||
// 如果值没有变化,不执行更新
|
||||
if (currentValue !== formattedNewValue) {
|
||||
element.textContent = formattedNewValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,37 +824,8 @@ function updateStatsCards(stats) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
// 检查是否有正在进行的动画
|
||||
if (animationInProgress[elementId + '_percent']) {
|
||||
clearTimeout(animationInProgress[elementId + '_percent']);
|
||||
}
|
||||
|
||||
try {
|
||||
element.style.opacity = '0';
|
||||
element.style.transition = 'opacity 200ms ease-out';
|
||||
|
||||
// 保存定时器ID,便于后续可能的取消
|
||||
animationInProgress[elementId + '_percent'] = setTimeout(() => {
|
||||
try {
|
||||
element.textContent = value;
|
||||
element.style.opacity = '1';
|
||||
} catch (e) {
|
||||
console.error('更新百分比元素失败:', e);
|
||||
} finally {
|
||||
// 清除动画状态标记
|
||||
delete animationInProgress[elementId + '_percent'];
|
||||
}
|
||||
}, 200);
|
||||
} catch (e) {
|
||||
console.error('设置百分比动画失败:', e);
|
||||
// 出错时直接设置值
|
||||
try {
|
||||
element.textContent = value;
|
||||
element.style.opacity = '1';
|
||||
} catch (e2) {
|
||||
console.error('直接更新百分比元素也失败:', e2);
|
||||
}
|
||||
}
|
||||
// 直接更新文本,移除所有动画效果
|
||||
element.textContent = value;
|
||||
}
|
||||
|
||||
// 平滑更新数量显示
|
||||
@@ -1018,14 +835,10 @@ function updateStatsCards(stats) {
|
||||
animateValue('error-queries', errorQueries);
|
||||
animateValue('active-ips', activeIPs);
|
||||
|
||||
// 直接更新文本和百分比,移除动画效果
|
||||
const topQueryTypeElement = document.getElementById('top-query-type');
|
||||
const queryTypePercentageElement = document.getElementById('query-type-percentage');
|
||||
const activeIpsPercentElement = document.getElementById('active-ips-percent');
|
||||
|
||||
if (topQueryTypeElement) topQueryTypeElement.textContent = topQueryType;
|
||||
if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`;
|
||||
if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`;
|
||||
// 平滑更新文本和百分比
|
||||
updatePercentage('top-query-type', topQueryType);
|
||||
updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`);
|
||||
updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`);
|
||||
|
||||
// 计算并平滑更新百分比
|
||||
if (totalQueries > 0) {
|
||||
@@ -1067,9 +880,11 @@ function updateTopBlockedTable(domains) {
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ name: '---.---.---', count: '---' },
|
||||
{ name: '---.---.---', count: '---' },
|
||||
{ name: '---.---.---', count: '---' }
|
||||
{ name: 'example1.com', count: 150 },
|
||||
{ name: 'example2.com', count: 130 },
|
||||
{ name: 'example3.com', count: 120 },
|
||||
{ name: 'example4.com', count: 110 },
|
||||
{ name: 'example5.com', count: 100 }
|
||||
];
|
||||
console.log('使用示例数据填充Top屏蔽域名表格');
|
||||
}
|
||||
@@ -1078,7 +893,7 @@ function updateTopBlockedTable(domains) {
|
||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||
const domain = tableData[i];
|
||||
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 items-center justify-between p-3 rounded-md hover:bg-gray-50 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>
|
||||
@@ -1119,11 +934,11 @@ function updateRecentBlockedTable(domains) {
|
||||
if (tableData.length === 0) {
|
||||
const now = Date.now();
|
||||
tableData = [
|
||||
{ name: '---.---.---', timestamp: now - 5 * 60 * 1000, type: '广告' },
|
||||
{ name: '---.---.---', timestamp: now - 15 * 60 * 1000, type: '恶意' },
|
||||
{ name: '---.---.---', timestamp: now - 30 * 60 * 1000, type: '广告' },
|
||||
{ name: '---.---.---', timestamp: now - 45 * 60 * 1000, type: '追踪' },
|
||||
{ name: '---.---.---', timestamp: now - 60 * 60 * 1000, type: '恶意' }
|
||||
{ name: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' },
|
||||
{ name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' },
|
||||
{ name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' },
|
||||
{ name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' },
|
||||
{ name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' }
|
||||
];
|
||||
console.log('使用示例数据填充最近屏蔽域名表格');
|
||||
}
|
||||
@@ -1133,7 +948,7 @@ function updateRecentBlockedTable(domains) {
|
||||
const domain = tableData[i];
|
||||
const time = formatTime(domain.timestamp);
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-warning">
|
||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-warning">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium truncate">${domain.name}</div>
|
||||
<div class="text-sm text-gray-500 mt-1">${time}</div>
|
||||
@@ -1176,11 +991,11 @@ function updateTopClientsTable(clients) {
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ ip: '---.---.---', count: '---' },
|
||||
{ ip: '---.---.---', count: '---' },
|
||||
{ ip: '---.---.---', count: '---' },
|
||||
{ ip: '---.---.---', count: '---' },
|
||||
{ ip: '---.---.---', count: '---' }
|
||||
{ ip: '192.168.1.100', count: 120 },
|
||||
{ ip: '192.168.1.101', count: 95 },
|
||||
{ ip: '192.168.1.102', count: 80 },
|
||||
{ ip: '192.168.1.103', count: 65 },
|
||||
{ ip: '192.168.1.104', count: 50 }
|
||||
];
|
||||
console.log('使用示例数据填充TOP客户端表格');
|
||||
}
|
||||
@@ -1192,7 +1007,7 @@ function updateTopClientsTable(clients) {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const client = tableData[i];
|
||||
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 items-center justify-between p-3 rounded-md hover:bg-gray-50 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>
|
||||
@@ -1253,7 +1068,7 @@ function updateTopDomainsTable(domains) {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
const domain = tableData[i];
|
||||
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 items-center justify-between p-3 rounded-md hover:bg-gray-50 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>
|
||||
@@ -1326,8 +1141,7 @@ function initTimeRangeToggle() {
|
||||
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
|
||||
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
|
||||
|
||||
// 设置非选中状态样式
|
||||
button.classList.add('transition-colors', 'duration-200');
|
||||
// 设置非选中状态样式,移除过渡动画
|
||||
button.classList.add(...styleConfig.normal);
|
||||
button.classList.add(...styleConfig.hover);
|
||||
|
||||
@@ -1467,11 +1281,8 @@ function initCharts() {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
|
||||
animation: {
|
||||
duration: 500, // 延长动画时间,使过渡更平滑
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
// 禁用图表动画
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
@@ -1539,11 +1350,8 @@ function initCharts() {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
|
||||
animation: {
|
||||
duration: 300,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
// 禁用图表动画
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
@@ -1704,7 +1512,7 @@ function initDetailedTimeRangeToggle() {
|
||||
'bg-green-500', 'bg-purple-500', 'bg-gray-100', 'mixed-view-active');
|
||||
|
||||
// 设置非选中状态样式
|
||||
button.classList.add('transition-colors', 'duration-200');
|
||||
// 移除过渡动画类
|
||||
button.classList.add(...styleConfig.normal);
|
||||
button.classList.add(...styleConfig.hover);
|
||||
|
||||
@@ -1842,11 +1650,8 @@ function drawDetailedDNSRequestsChart() {
|
||||
detailedDnsRequestsChart.data.labels = results[0].labels;
|
||||
detailedDnsRequestsChart.data.datasets = datasets;
|
||||
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
|
||||
// 使用平滑过渡动画更新图表
|
||||
detailedDnsRequestsChart.update({
|
||||
duration: 800,
|
||||
easing: 'easeInOutQuart'
|
||||
});
|
||||
// 更新图表,不使用动画
|
||||
detailedDnsRequestsChart.update();
|
||||
} else {
|
||||
detailedDnsRequestsChart = new Chart(chartContext, {
|
||||
type: 'line',
|
||||
@@ -1857,10 +1662,8 @@ function drawDetailedDNSRequestsChart() {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 800,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
// 禁用图表动画
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: showLegend,
|
||||
@@ -2413,15 +2216,8 @@ function updateChartData(chartId, newValue) {
|
||||
chart.data.datasets[0].data = historyData;
|
||||
chart.data.labels = generateTimeLabels(historyData.length);
|
||||
|
||||
// 使用自定义动画配置更新图表,确保平滑过渡,避免空白区域
|
||||
chart.update({
|
||||
duration: 300, // 增加动画持续时间
|
||||
easing: 'easeInOutQuart', // 使用平滑的缓动函数
|
||||
transition: {
|
||||
duration: 300,
|
||||
easing: 'easeInOutQuart'
|
||||
}
|
||||
});
|
||||
// 更新图表,不使用动画
|
||||
chart.update();
|
||||
}
|
||||
|
||||
// 从统计数据中获取规则数
|
||||
@@ -2527,11 +2323,8 @@ function initStatCardCharts() {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
// 添加动画配置,确保平滑过渡
|
||||
animation: {
|
||||
duration: 800,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
// 禁用图表动画
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
@@ -2633,32 +2426,85 @@ function generateTimeLabels(count) {
|
||||
return labels;
|
||||
}
|
||||
|
||||
// 检查元素内容是否溢出
|
||||
function isContentOverflow(element) {
|
||||
if (!element) return false;
|
||||
return element.scrollWidth > element.clientWidth;
|
||||
}
|
||||
|
||||
// 格式化数字显示(使用K/M后缀)
|
||||
function formatNumber(num) {
|
||||
function formatNumber(num, element = null) {
|
||||
// 如果不是数字,直接返回
|
||||
if (isNaN(num) || num === '---') {
|
||||
return num;
|
||||
}
|
||||
|
||||
// 显示完整数字的最大长度阈值
|
||||
const MAX_FULL_LENGTH = 5;
|
||||
// 转换为数字类型
|
||||
const numericValue = Number(num);
|
||||
// 获取数字的字符串表示形式
|
||||
const numStr = numericValue.toString();
|
||||
|
||||
// 先获取完整数字字符串
|
||||
const fullNumStr = num.toString();
|
||||
// 检查是否需要使用K/M格式
|
||||
let useCompactFormat = false;
|
||||
|
||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||
return fullNumStr;
|
||||
// 方法1: 基于元素内容是否溢出判断
|
||||
if (element) {
|
||||
// 临时设置元素内容为完整数字
|
||||
const originalContent = element.textContent;
|
||||
element.textContent = numStr;
|
||||
// 检查是否溢出
|
||||
useCompactFormat = isContentOverflow(element);
|
||||
// 恢复原始内容
|
||||
element.textContent = originalContent;
|
||||
}
|
||||
// 方法2: 基于窗口宽度和数字长度的自适应判断
|
||||
else {
|
||||
// 根据窗口宽度动态调整阈值
|
||||
let maxFullLength = 5;
|
||||
if (window.innerWidth < 768) {
|
||||
maxFullLength = 4; // 小屏幕更严格
|
||||
} else if (window.innerWidth < 1024) {
|
||||
maxFullLength = 5; // 中等屏幕
|
||||
}
|
||||
|
||||
// 如果数字长度超过阈值,则使用K/M格式
|
||||
useCompactFormat = numStr.length > maxFullLength;
|
||||
}
|
||||
|
||||
// 否则使用缩写格式
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
// 如果需要使用紧凑格式
|
||||
if (useCompactFormat) {
|
||||
if (numericValue >= 1000000) {
|
||||
return (numericValue / 1000000).toFixed(1) + 'M';
|
||||
} else if (numericValue >= 1000) {
|
||||
return (numericValue / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
}
|
||||
|
||||
return fullNumStr;
|
||||
return numStr;
|
||||
}
|
||||
|
||||
// 重新计算所有统计卡片的数字显示格式
|
||||
function updateStatsCardsFormat() {
|
||||
const statCardElements = document.querySelectorAll('.stat-card .stat-value');
|
||||
statCardElements.forEach(element => {
|
||||
// 获取原始数值(可能已经是K/M格式)
|
||||
const text = element.textContent;
|
||||
let originalNum;
|
||||
|
||||
// 解析K/M格式的数字
|
||||
if (text.includes('M')) {
|
||||
originalNum = parseFloat(text) * 1000000;
|
||||
} else if (text.includes('K')) {
|
||||
originalNum = parseFloat(text) * 1000;
|
||||
} else {
|
||||
originalNum = parseFloat(text);
|
||||
}
|
||||
|
||||
// 重新计算显示格式
|
||||
if (!isNaN(originalNum)) {
|
||||
element.textContent = formatNumber(originalNum, element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新运行状态
|
||||
@@ -2730,7 +2576,7 @@ function showNotification(message, type = 'info') {
|
||||
// 创建通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.id = 'notification';
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-0 opacity-0`;
|
||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform translate-y-0 opacity-100`;
|
||||
|
||||
// 设置样式和内容
|
||||
let bgColor, textColor, icon;
|
||||
@@ -2767,18 +2613,9 @@ function showNotification(message, type = 'info') {
|
||||
// 添加到页面
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
// 自动关闭,直接移除元素,无动画效果
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-y-0', 'opacity-0');
|
||||
notification.classList.add('-translate-y-2', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// 自动关闭
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-y-0', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -2957,6 +2794,9 @@ function handleResponsive() {
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新统计卡片数字格式,确保在窗口缩小时内容不溢出
|
||||
updateStatsCardsFormat();
|
||||
});
|
||||
|
||||
// 添加触摸事件支持,用于移动端
|
||||
@@ -3045,4 +2885,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后,初始化统计卡片的数字格式,确保内容不会溢出
|
||||
setTimeout(() => {
|
||||
updateStatsCardsFormat();
|
||||
}, 500);
|
||||
});
|
||||
@@ -9,7 +9,6 @@ function setupNavigation() {
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
@@ -22,6 +21,14 @@ function setupNavigation() {
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
|
||||
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||
const target = item.getAttribute('href').substring(1);
|
||||
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,52 +40,57 @@ function setupNavigation() {
|
||||
|
||||
// 打开侧边栏函数
|
||||
function openSidebar() {
|
||||
console.log('Opening sidebar...');
|
||||
console.log('打开侧边栏');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
sidebar.classList.add('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
sidebarOverlay.classList.add('block');
|
||||
sidebarOverlay.classList.add('flex');
|
||||
}
|
||||
// 防止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
console.log('Sidebar opened successfully');
|
||||
document.body.style.touchAction = 'none'; // 防止触摸滚动
|
||||
}
|
||||
|
||||
// 关闭侧边栏函数
|
||||
function closeSidebar() {
|
||||
console.log('Closing sidebar...');
|
||||
console.log('关闭侧边栏');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
sidebar.classList.remove('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.add('hidden');
|
||||
sidebarOverlay.classList.remove('block');
|
||||
sidebarOverlay.classList.remove('flex');
|
||||
}
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
console.log('Sidebar closed successfully');
|
||||
document.body.style.touchAction = '';
|
||||
}
|
||||
|
||||
// 切换侧边栏函数
|
||||
function toggleSidebarVisibility() {
|
||||
console.log('Toggling sidebar visibility...');
|
||||
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
||||
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||
console.log('Sidebar is hidden, opening...');
|
||||
openSidebar();
|
||||
} else {
|
||||
console.log('Sidebar is visible, closing...');
|
||||
closeSidebar();
|
||||
console.log('切换侧边栏');
|
||||
if (sidebar) {
|
||||
if (sidebar.classList.contains('-translate-x-full')) {
|
||||
openSidebar();
|
||||
} else {
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定切换按钮事件
|
||||
if (toggleSidebar) {
|
||||
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
||||
// 移除可能存在的旧事件监听器
|
||||
toggleSidebar.removeEventListener('click', toggleSidebarVisibility);
|
||||
// 重新添加事件监听器
|
||||
toggleSidebar.addEventListener('click', function(e) {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
toggleSidebarVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定关闭按钮事件
|
||||
@@ -109,84 +121,15 @@ function setupNavigation() {
|
||||
});
|
||||
}
|
||||
|
||||
// 页面初始化函数 - 根据当前hash值初始化对应页面
|
||||
function initPageByHash() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
|
||||
// 隐藏所有内容区域
|
||||
const contentSections = [
|
||||
document.getElementById('dashboard-content'),
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
|
||||
contentSections.forEach(section => {
|
||||
if (section) {
|
||||
section.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 显示当前页面内容
|
||||
const currentSection = document.getElementById(`${hash}-content`);
|
||||
if (currentSection) {
|
||||
currentSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 更新页面标题
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
if (pageTitle) {
|
||||
const titles = {
|
||||
'dashboard': '仪表盘',
|
||||
'shield': '屏蔽管理',
|
||||
'hosts': 'Hosts管理',
|
||||
'query': 'DNS屏蔽查询',
|
||||
'logs': '查询日志',
|
||||
'config': '系统设置'
|
||||
};
|
||||
pageTitle.textContent = titles[hash] || '仪表盘';
|
||||
}
|
||||
|
||||
// 页面特定初始化 - 使用setTimeout延迟调用,确保所有脚本文件都已加载完成
|
||||
if (hash === 'shield') {
|
||||
setTimeout(() => {
|
||||
if (typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'hosts') {
|
||||
setTimeout(() => {
|
||||
if (typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'logs') {
|
||||
setTimeout(() => {
|
||||
if (typeof initLogsPage === 'function') {
|
||||
initLogsPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'dashboard') {
|
||||
setTimeout(() => {
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
setupNavigation();
|
||||
|
||||
// 初始化页面
|
||||
initPageByHash();
|
||||
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
// 加载仪表盘数据
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
@@ -231,175 +174,5 @@ function formatUptime(milliseconds) {
|
||||
}
|
||||
}
|
||||
|
||||
// 账户功能 - 下拉菜单、注销和修改密码
|
||||
function setupAccountFeatures() {
|
||||
// 下拉菜单功能
|
||||
const accountDropdown = document.getElementById('account-dropdown');
|
||||
const accountMenu = document.getElementById('account-menu');
|
||||
const changePasswordBtn = document.getElementById('change-password-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const changePasswordModal = document.getElementById('change-password-modal');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
const cancelChangePasswordBtn = document.getElementById('cancel-change-password');
|
||||
const changePasswordForm = document.getElementById('change-password-form');
|
||||
const passwordMismatch = document.getElementById('password-mismatch');
|
||||
const newPassword = document.getElementById('new-password');
|
||||
const confirmPassword = document.getElementById('confirm-password');
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
if (accountDropdown && !accountDropdown.contains(e.target)) {
|
||||
accountMenu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 点击账户区域切换下拉菜单
|
||||
if (accountDropdown) {
|
||||
accountDropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
accountMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// 打开修改密码模态框
|
||||
if (changePasswordBtn) {
|
||||
changePasswordBtn.addEventListener('click', () => {
|
||||
accountMenu.classList.add('hidden');
|
||||
changePasswordModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭修改密码模态框
|
||||
function closeModal() {
|
||||
changePasswordModal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
changePasswordForm.reset();
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 绑定关闭模态框事件
|
||||
if (closeModalBtn) {
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (cancelChangePasswordBtn) {
|
||||
cancelChangePasswordBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// 点击模态框外部关闭模态框
|
||||
if (changePasswordModal) {
|
||||
changePasswordModal.addEventListener('click', (e) => {
|
||||
if (e.target === changePasswordModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 按ESC键关闭模态框
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !changePasswordModal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 密码匹配验证
|
||||
if (newPassword && confirmPassword) {
|
||||
confirmPassword.addEventListener('input', () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
} else {
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
newPassword.addEventListener('input', () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
} else {
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码表单提交
|
||||
if (changePasswordForm) {
|
||||
changePasswordForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 验证密码匹配
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(changePasswordForm);
|
||||
const data = {
|
||||
currentPassword: formData.get('currentPassword'),
|
||||
newPassword: formData.get('newPassword')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.status === 'success') {
|
||||
// 密码修改成功
|
||||
alert('密码修改成功');
|
||||
closeModal();
|
||||
} else {
|
||||
// 密码修改失败
|
||||
alert(result.error || '密码修改失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
alert('修改密码失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 注销功能
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
// 重定向到登录页面
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('注销失败:', error);
|
||||
alert('注销失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
setupNavigation();
|
||||
|
||||
// 设置账户功能
|
||||
setupAccountFeatures();
|
||||
|
||||
// 初始化页面
|
||||
initPageByHash();
|
||||
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
}
|
||||
|
||||
// 页面加载完成后执行初始化
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
@@ -52,8 +52,21 @@ function displayQueryResult(result, domain) {
|
||||
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||
|
||||
// 优先使用API返回的blacklistName字段,如果没有则使用blocksource
|
||||
let displaySource = '无';
|
||||
if (result.blocked) {
|
||||
if (result.blacklistName && result.blacklistName !== '') {
|
||||
displaySource = result.blacklistName;
|
||||
} else if (result.blocksource) {
|
||||
displaySource = result.blocksource;
|
||||
} else {
|
||||
displaySource = '未知';
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||
|
||||
// 更新结果显示
|
||||
document.getElementById('result-domain').textContent = domain;
|
||||
@@ -101,7 +114,7 @@ function displayQueryResult(result, domain) {
|
||||
|
||||
// 更新屏蔽来源显示
|
||||
if (blockSourceElement) {
|
||||
blockSourceElement.textContent = blockSource;
|
||||
blockSourceElement.textContent = displaySource;
|
||||
}
|
||||
|
||||
document.getElementById('result-time').textContent = timestamp;
|
||||
@@ -121,7 +134,8 @@ function saveQueryHistory(domain, result) {
|
||||
blocked: result.blocked,
|
||||
blockRuleType: result.blockRuleType,
|
||||
blockRule: result.blockRule,
|
||||
blocksource: result.blocksource
|
||||
blocksource: result.blocksource,
|
||||
blacklistName: result.blacklistName
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,7 +170,19 @@ function loadQueryHistory() {
|
||||
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
||||
|
||||
// 优先显示blacklistName,如果没有则显示blocksource
|
||||
let sourceDisplay = '无';
|
||||
if (item.result.blocked) {
|
||||
if (item.result.blacklistName && item.result.blacklistName !== '') {
|
||||
sourceDisplay = item.result.blacklistName;
|
||||
} else if (item.result.blocksource) {
|
||||
sourceDisplay = item.result.blocksource;
|
||||
} else {
|
||||
sourceDisplay = '未知';
|
||||
}
|
||||
}
|
||||
|
||||
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||
|
||||
return `
|
||||
@@ -168,7 +194,7 @@ function loadQueryHistory() {
|
||||
<span class="text-xs text-gray-500">${blockType}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">来源: ${sourceDisplay}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||
</div>
|
||||
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
||||
|
||||
Reference in New Issue
Block a user