This commit is contained in:
Alex Yang
2025-11-29 23:40:01 +08:00
parent 26889f5b38
commit 70cf1a7306
30 changed files with 1644023 additions and 817 deletions

View File

@@ -758,14 +758,29 @@ 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;
}
const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0;
const formattedNewValue = formatNumber(newValue);
@@ -776,124 +791,152 @@ function updateStatsCards(stats) {
// 先移除可能存在的光晕效果类
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') || '';
const originalContent = element.innerHTML;
// 配置翻页容器样式
const containerStyle = `
position: relative;
display: inline-block;
overflow: hidden;
height: ${element.offsetHeight}px;
width: ${element.offsetWidth}px;
`;
// 创建翻页容器
const flipContainer = document.createElement('div');
flipContainer.style.cssText = containerStyle;
flipContainer.className = 'number-flip-container';
// 创建旧值元素
const oldValueElement = document.createElement('div');
oldValueElement.textContent = originalContent;
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%);
`;
// 复制原始元素的样式到新元素
const computedStyle = getComputedStyle(element);
[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;
});
// 替换原始元素的内容
element.textContent = '';
flipContainer.appendChild(oldValueElement);
flipContainer.appendChild(newValueElement);
element.appendChild(flipContainer);
// 启动翻页动画
setTimeout(() => {
oldValueElement.style.transform = 'translateY(-100%)';
newValueElement.style.transform = 'translateY(0)';
}, 50);
// 动画结束后,恢复原始元素
setTimeout(() => {
// 清理并设置最终值
try {
// 配置翻页容器样式,确保与原始元素大小完全一致
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%);';
// 复制原始元素的样式到新元素,确保大小完全一致
const computedStyle = getComputedStyle(element);
[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');
}
// 添加当前卡片颜色的深色光晕效果
// 根据父级卡片类型确定光晕颜色
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秒后移除光晕效果
setTimeout(() => {
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
}, 2000);
}
}, 450);
// 清除动画状态标记
delete animationInProgress[elementId];
}
}
// 更新百分比元素的函数
@@ -901,13 +944,37 @@ function updateStatsCards(stats) {
const element = document.getElementById(elementId);
if (!element) return;
element.style.opacity = '0';
element.style.transition = 'opacity 200ms ease-out';
// 检查是否有正在进行的动画
if (animationInProgress[elementId + '_percent']) {
clearTimeout(animationInProgress[elementId + '_percent']);
}
setTimeout(() => {
element.textContent = value;
element.style.opacity = '1';
}, 200);
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);
}
}
}
// 平滑更新数量显示
@@ -917,10 +984,14 @@ function updateStatsCards(stats) {
animateValue('error-queries', errorQueries);
animateValue('active-ips', activeIPs);
// 平滑更新文本和百分比
updatePercentage('top-query-type', topQueryType);
updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`);
updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`);
// 直接更新文本和百分比,移除动画效果
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)}%`;
// 计算并平滑更新百分比
if (totalQueries > 0) {
@@ -962,11 +1033,9 @@ function updateTopBlockedTable(domains) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ 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 }
{ name: '---.---.---', count: '---' },
{ name: '---.---.---', count: '---' },
{ name: '---.---.---', count: '---' }
];
console.log('使用示例数据填充Top屏蔽域名表格');
}
@@ -1016,11 +1085,11 @@ function updateRecentBlockedTable(domains) {
if (tableData.length === 0) {
const now = Date.now();
tableData = [
{ 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: '恶意' }
{ 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: '恶意' }
];
console.log('使用示例数据填充最近屏蔽域名表格');
}
@@ -1073,11 +1142,11 @@ function updateTopClientsTable(clients) {
// 如果没有有效数据,提供示例数据
if (tableData.length === 0) {
tableData = [
{ 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 }
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' },
{ ip: '---.---.---', count: '---' }
];
console.log('使用示例数据填充TOP客户端表格');
}

19
static/js/vendor/tailwind.js vendored Normal file
View File

@@ -0,0 +1,19 @@
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFFB',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
info: '#86909C',
dark: '#1D2129',
light: '#F2F3F5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
}
}