web异常待修复
This commit is contained in:
@@ -35,6 +35,72 @@
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 统计数据更新动画 */
|
||||
.stat-value.update {
|
||||
position: relative;
|
||||
animation: stat-pulse 1s ease;
|
||||
}
|
||||
|
||||
@keyframes stat-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(76, 175, 80, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格行淡入淡出动画 */
|
||||
table tr.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
table tr.fade-out {
|
||||
animation: fadeOut 0.3s ease-in;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 通知样式 */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// 初始化仪表盘面板
|
||||
function initDashboardPanel() {
|
||||
// 初始化小型图表
|
||||
if (typeof initMiniCharts === 'function') {
|
||||
initMiniCharts();
|
||||
}
|
||||
// 加载统计数据
|
||||
loadDashboardData();
|
||||
// 启动实时更新
|
||||
@@ -29,19 +33,19 @@ function updateStatCards() {
|
||||
if (data && data.dns) {
|
||||
// 屏蔽请求
|
||||
const blockedCount = data.dns.Blocked || data.dns.blocked || 0;
|
||||
updateStatCard('blocked-count', blockedCount);
|
||||
smoothUpdateStatCard('blocked-count', blockedCount);
|
||||
|
||||
// 允许请求
|
||||
const allowedCount = data.dns.Allowed || data.dns.allowed || 0;
|
||||
updateStatCard('allowed-count', allowedCount);
|
||||
smoothUpdateStatCard('allowed-count', allowedCount);
|
||||
|
||||
// 错误请求
|
||||
const errorCount = data.dns.Errors || data.dns.errors || 0;
|
||||
updateStatCard('error-count', errorCount);
|
||||
smoothUpdateStatCard('error-count', errorCount);
|
||||
|
||||
// 总请求数
|
||||
const totalCount = blockedCount + allowedCount + errorCount;
|
||||
updateStatCard('total-queries', totalCount);
|
||||
smoothUpdateStatCard('total-queries', totalCount);
|
||||
|
||||
// 更新数据历史记录和小型图表
|
||||
if (typeof updateDataHistory === 'function') {
|
||||
@@ -56,15 +60,14 @@ function updateStatCards() {
|
||||
}
|
||||
} else {
|
||||
// 处理其他可能的数据格式
|
||||
// 修复语法错误,使用传统的对象属性访问方式
|
||||
const blockedValue = data && (data.Blocked !== undefined ? data.Blocked : (data.blocked !== undefined ? data.blocked : 0));
|
||||
const allowedValue = data && (data.Allowed !== undefined ? data.Allowed : (data.allowed !== undefined ? data.allowed : 0));
|
||||
const errorValue = data && (data.Errors !== undefined ? data.Errors : (data.errors !== undefined ? data.errors : 0));
|
||||
updateStatCard('blocked-count', blockedValue);
|
||||
updateStatCard('allowed-count', allowedValue);
|
||||
updateStatCard('error-count', errorValue);
|
||||
smoothUpdateStatCard('blocked-count', blockedValue);
|
||||
smoothUpdateStatCard('allowed-count', allowedValue);
|
||||
smoothUpdateStatCard('error-count', errorValue);
|
||||
const totalCount = blockedValue + allowedValue + errorValue;
|
||||
updateStatCard('total-queries', totalCount);
|
||||
smoothUpdateStatCard('total-queries', totalCount);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -75,13 +78,32 @@ function updateStatCards() {
|
||||
apiRequest('/shield')
|
||||
.then(data => {
|
||||
let rulesCount = 0;
|
||||
|
||||
// 增强的数据格式处理,确保能正确处理各种返回格式
|
||||
if (Array.isArray(data)) {
|
||||
rulesCount = data.length;
|
||||
} else if (data && data.rules && Array.isArray(data.rules)) {
|
||||
rulesCount = data.rules.length;
|
||||
} else if (data && data.domainRules) {
|
||||
// 处理可能的规则分类格式
|
||||
let domainRulesCount = 0;
|
||||
let regexRulesCount = 0;
|
||||
|
||||
if (Array.isArray(data.domainRules)) {
|
||||
domainRulesCount = data.domainRules.length;
|
||||
} else if (typeof data.domainRules === 'object') {
|
||||
domainRulesCount = Object.keys(data.domainRules).length;
|
||||
}
|
||||
|
||||
if (data.regexRules && Array.isArray(data.regexRules)) {
|
||||
regexRulesCount = data.regexRules.length;
|
||||
}
|
||||
|
||||
rulesCount = domainRulesCount + regexRulesCount;
|
||||
}
|
||||
|
||||
updateStatCard('rules-count', rulesCount);
|
||||
// 确保至少显示0而不是--
|
||||
smoothUpdateStatCard('rules-count', rulesCount);
|
||||
|
||||
// 更新数据历史记录和小型图表
|
||||
if (typeof updateDataHistory === 'function') {
|
||||
@@ -94,6 +116,41 @@ function updateStatCards() {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取规则数失败:', error);
|
||||
// 即使出错也要设置为0,避免显示--
|
||||
smoothUpdateStatCard('rules-count', 0);
|
||||
});
|
||||
|
||||
// 获取Hosts条目数量
|
||||
apiRequest('/shield/hosts')
|
||||
.then(data => {
|
||||
let hostsCount = 0;
|
||||
|
||||
// 处理各种可能的数据格式
|
||||
if (Array.isArray(data)) {
|
||||
hostsCount = data.length;
|
||||
} else if (data && data.hosts && Array.isArray(data.hosts)) {
|
||||
hostsCount = data.hosts.length;
|
||||
} else if (data && typeof data === 'object' && data !== null) {
|
||||
// 如果是对象格式,计算键的数量
|
||||
hostsCount = Object.keys(data).length;
|
||||
}
|
||||
|
||||
// 确保至少显示0而不是--
|
||||
smoothUpdateStatCard('hosts-count', hostsCount);
|
||||
|
||||
// 更新数据历史记录和小型图表
|
||||
if (typeof updateDataHistory === 'function') {
|
||||
updateDataHistory('hosts', hostsCount);
|
||||
}
|
||||
|
||||
if (typeof updateMiniChart === 'function' && typeof dataHistory !== 'undefined') {
|
||||
updateMiniChart('hosts-chart', dataHistory.hosts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取Hosts数量失败:', error);
|
||||
// 即使出错也要设置为0,避免显示--
|
||||
smoothUpdateStatCard('hosts-count', 0);
|
||||
});
|
||||
|
||||
// 获取Hosts条目数
|
||||
@@ -106,7 +163,7 @@ function updateStatCards() {
|
||||
hostsCount = data.hosts.length;
|
||||
}
|
||||
|
||||
updateStatCard('hosts-count', hostsCount);
|
||||
smoothUpdateStatCard('hosts-count', hostsCount);
|
||||
|
||||
// 更新数据历史记录和小型图表
|
||||
if (typeof updateDataHistory === 'function') {
|
||||
@@ -122,6 +179,7 @@ function updateStatCards() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 更新单个统计卡片
|
||||
function updateStatCard(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
@@ -139,6 +197,61 @@ function updateStatCard(elementId, value) {
|
||||
}
|
||||
}
|
||||
|
||||
// 平滑更新统计卡片(数字递增动画)
|
||||
function smoothUpdateStatCard(elementId, newValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
// 获取旧值
|
||||
const oldValue = previousStats[elementId] || 0;
|
||||
|
||||
// 如果值相同,不更新
|
||||
if (newValue === oldValue) return;
|
||||
|
||||
// 如果是初始值,直接更新
|
||||
if (oldValue === 0 || oldValue === '--') {
|
||||
updateStatCard(elementId, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置动画持续时间
|
||||
const duration = 500; // 500ms
|
||||
const startTime = performance.now();
|
||||
|
||||
// 动画函数
|
||||
function animate(currentTime) {
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const progress = Math.min(elapsedTime / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeOutQuad = 1 - (1 - progress) * (1 - progress);
|
||||
|
||||
// 计算当前值
|
||||
const currentValue = Math.floor(oldValue + (newValue - oldValue) * easeOutQuad);
|
||||
|
||||
// 更新显示
|
||||
element.textContent = formatNumber(currentValue);
|
||||
|
||||
// 继续动画
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
// 动画完成,设置最终值
|
||||
element.textContent = formatNumber(newValue);
|
||||
// 添加光晕效果
|
||||
element.classList.add('update');
|
||||
setTimeout(() => {
|
||||
element.classList.remove('update');
|
||||
}, 1000);
|
||||
// 更新记录
|
||||
previousStats[elementId] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始动画
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
// 加载24小时统计数据
|
||||
function loadHourlyStats() {
|
||||
apiRequest('/hourly-stats')
|
||||
@@ -524,7 +637,6 @@ function renderRequestsPieChart(labels, data) {
|
||||
// 加载最常屏蔽的域名
|
||||
function loadTopBlockedDomains() {
|
||||
// 首先获取表格元素并显示加载状态
|
||||
// 修复语法错误,使用传统的DOM访问方式
|
||||
const topBlockedTable = document.getElementById('top-blocked-table');
|
||||
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
|
||||
if (tbody) {
|
||||
@@ -537,12 +649,9 @@ function loadTopBlockedDomains() {
|
||||
// 处理多种可能的数据格式,特别优化对用户提供格式的支持
|
||||
let processedData = [];
|
||||
|
||||
console.log('最常屏蔽域名API返回数据:', data);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
// 数组格式:直接使用,并过滤出有效的域名数据
|
||||
processedData = data.filter(item => item && (item.domain || item.name || item.Domain || item.Name) && (item.count !== undefined || item.Count !== undefined || item.hits !== undefined || item.Hits !== undefined));
|
||||
console.log('处理后的域名数据:', processedData);
|
||||
} else if (data && data.domains && Array.isArray(data.domains)) {
|
||||
// 嵌套在domains属性中
|
||||
processedData = data.domains;
|
||||
@@ -554,7 +663,7 @@ function loadTopBlockedDomains() {
|
||||
}));
|
||||
}
|
||||
|
||||
renderTopBlockedDomains(processedData);
|
||||
smoothRenderTable('#top-blocked-table', processedData, renderDomainRow);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取最常屏蔽域名失败:', error);
|
||||
@@ -570,48 +679,6 @@ function loadTopBlockedDomains() {
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染最常屏蔽的域名表格
|
||||
function renderTopBlockedDomains(domains) {
|
||||
// 修复语法错误,使用传统的DOM访问方式
|
||||
const topBlockedTable = document.getElementById('top-blocked-table');
|
||||
const tbody = topBlockedTable ? topBlockedTable.querySelector('tbody') : null;
|
||||
if (!tbody) return;
|
||||
|
||||
console.log('准备渲染的域名数据:', domains);
|
||||
|
||||
if (!domains || domains.length === 0) {
|
||||
showEmpty(tbody, '暂无屏蔽记录');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
domains.forEach((domain, index) => {
|
||||
if (!domain) return;
|
||||
|
||||
// 支持不同的字段名和格式,特别针对用户提供的数据格式优化
|
||||
const domainName = domain.domain || domain.name || domain.Domain || domain.Name || '未知域名';
|
||||
const count = domain.count !== undefined ? domain.count :
|
||||
(domain.Count !== undefined ? domain.Count :
|
||||
(domain.hits !== undefined ? domain.hits :
|
||||
(domain.Hits !== undefined ? domain.Hits : 0)));
|
||||
|
||||
console.log(`渲染域名 ${index + 1}:`, {domainName, count});
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${domainName}</td>
|
||||
<td>${formatNumber(count)}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// 初始化表格排序
|
||||
if (typeof initTableSort === 'function') {
|
||||
initTableSort('top-blocked-table');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最常解析的域名
|
||||
function loadTopResolvedDomains() {
|
||||
apiRequest('/top-resolved')
|
||||
@@ -633,7 +700,7 @@ function loadTopResolvedDomains() {
|
||||
}));
|
||||
}
|
||||
|
||||
renderTopResolvedDomains(processedData);
|
||||
smoothRenderTable('#top-resolved-table', processedData, renderDomainRow);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取最常解析域名失败:', error);
|
||||
@@ -650,31 +717,177 @@ function loadTopResolvedDomains() {
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染最常解析的域名表格
|
||||
function renderTopResolvedDomains(domains) {
|
||||
const tbody = document.getElementById('top-resolved-table').querySelector('tbody');
|
||||
// 渲染域名行
|
||||
function renderDomainRow(item, index) {
|
||||
if (!item) return null;
|
||||
|
||||
// 支持不同的字段名和格式
|
||||
const domainName = item.domain || item.name || item.Domain || item.Name || '未知域名';
|
||||
const count = item.count !== undefined ? item.count :
|
||||
(item.Count !== undefined ? item.Count :
|
||||
(item.hits !== undefined ? item.hits :
|
||||
(item.Hits !== undefined ? item.Hits : 0)));
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'fade-in'; // 添加淡入动画类
|
||||
row.dataset.domain = domainName;
|
||||
row.dataset.count = count;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${domainName}</td>
|
||||
<td class="count-cell">${formatNumber(count)}</td>
|
||||
`;
|
||||
|
||||
// 设置动画延迟,创建级联效果
|
||||
row.style.animationDelay = `${index * 50}ms`;
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// 平滑渲染表格数据
|
||||
function smoothRenderTable(tableId, newData, rowRenderer) {
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table ? table.querySelector('tbody') : null;
|
||||
if (!tbody) return;
|
||||
|
||||
if (!domains || domains.length === 0) {
|
||||
showEmpty(tbody, '暂无解析记录');
|
||||
if (!newData || newData.length === 0) {
|
||||
showEmpty(tbody, '暂无数据记录');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
// 创建映射以提高查找效率
|
||||
const oldRows = Array.from(tbody.querySelectorAll('tr'));
|
||||
const rowMap = new Map();
|
||||
|
||||
domains.forEach((domain, index) => {
|
||||
// 支持不同的字段名和格式
|
||||
const domainName = domain.domain || domain.name || domain.Domain || domain.Name || '未知域名';
|
||||
const count = domain.count || domain.Count || domain.hits || domain.Hits || 0;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${domainName}</td>
|
||||
<td>${formatNumber(count)}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
oldRows.forEach(row => {
|
||||
if (!row.querySelector('td:first-child')) return;
|
||||
const key = row.dataset.domain || row.querySelector('td:first-child').textContent;
|
||||
rowMap.set(key, row);
|
||||
});
|
||||
|
||||
// 初始化表格排序
|
||||
initTableSort('top-resolved-table');
|
||||
// 准备新的数据行
|
||||
const newRows = [];
|
||||
const updatedRows = new Set();
|
||||
|
||||
// 处理每一条新数据
|
||||
newData.forEach((item, index) => {
|
||||
const key = item.domain || item.name || item.Domain || item.Name || '未知域名';
|
||||
|
||||
if (rowMap.has(key)) {
|
||||
// 数据项已存在,更新它
|
||||
const existingRow = rowMap.get(key);
|
||||
const oldCount = parseInt(existingRow.dataset.count) || 0;
|
||||
const count = item.count !== undefined ? item.count :
|
||||
(item.Count !== undefined ? item.Count :
|
||||
(item.hits !== undefined ? item.hits :
|
||||
(item.Hits !== undefined ? item.Hits : 0)));
|
||||
|
||||
// 更新数据属性
|
||||
existingRow.dataset.count = count;
|
||||
|
||||
// 如果计数变化,应用平滑更新
|
||||
if (oldCount !== count) {
|
||||
const countCell = existingRow.querySelector('.count-cell');
|
||||
if (countCell) {
|
||||
smoothUpdateNumber(countCell, oldCount, count);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新位置
|
||||
existingRow.style.animationDelay = `${index * 50}ms`;
|
||||
newRows.push(existingRow);
|
||||
updatedRows.add(key);
|
||||
} else {
|
||||
// 新数据项,创建新行
|
||||
const newRow = rowRenderer(item, index);
|
||||
if (newRow) {
|
||||
// 先设置透明度为0,避免在错误位置闪烁
|
||||
newRow.style.opacity = '0';
|
||||
newRows.push(newRow);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 移除不再存在的数据行
|
||||
oldRows.forEach(row => {
|
||||
if (!row.querySelector('td:first-child')) return;
|
||||
const key = row.dataset.domain || row.querySelector('td:first-child').textContent;
|
||||
if (!updatedRows.has(key)) {
|
||||
// 添加淡出动画
|
||||
row.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
if (row.parentNode === tbody) {
|
||||
tbody.removeChild(row);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// 批量更新表格内容,减少重排
|
||||
requestAnimationFrame(() => {
|
||||
// 保留未移除的行并按新顺序插入
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
newRows.forEach(row => {
|
||||
// 如果是新行,添加到文档片段
|
||||
if (!row.parentNode || row.parentNode !== tbody) {
|
||||
fragment.appendChild(row);
|
||||
}
|
||||
// 如果是已有行,移除它以便按新顺序重新插入
|
||||
else if (tbody.contains(row)) {
|
||||
tbody.removeChild(row);
|
||||
fragment.appendChild(row);
|
||||
}
|
||||
});
|
||||
|
||||
// 将文档片段添加到表格
|
||||
tbody.appendChild(fragment);
|
||||
|
||||
// 触发动画
|
||||
setTimeout(() => {
|
||||
newRows.forEach(row => {
|
||||
row.style.opacity = '1';
|
||||
});
|
||||
}, 10);
|
||||
|
||||
// 初始化表格排序
|
||||
if (typeof initTableSort === 'function') {
|
||||
initTableSort(tableId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 平滑更新数字
|
||||
function smoothUpdateNumber(element, oldValue, newValue) {
|
||||
// 如果值相同,不更新
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
// 设置动画持续时间
|
||||
const duration = 500;
|
||||
const startTime = performance.now();
|
||||
|
||||
function animate(currentTime) {
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const progress = Math.min(elapsedTime / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeOutQuad = 1 - (1 - progress) * (1 - progress);
|
||||
|
||||
// 计算当前值
|
||||
const currentValue = Math.floor(oldValue + (newValue - oldValue) * easeOutQuad);
|
||||
|
||||
// 更新显示
|
||||
element.textContent = formatNumber(currentValue);
|
||||
|
||||
// 继续动画
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
// 动画完成
|
||||
element.textContent = formatNumber(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始动画
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
Reference in New Issue
Block a user