feat: 实时访客监控 - 轮询检测新访客 + 艺术化动画提示

This commit is contained in:
freedakgmail
2026-07-06 22:42:08 +08:00
parent 9c86f17643
commit e0cf13255e
+124 -6
View File
@@ -617,6 +617,65 @@
:root[data-theme="light"] .theme-toggle .sun-icon { display: block; }
:root[data-theme="light"] .theme-toggle .moon-icon { display: none; }
/* ===== 访问计数实时动画 ===== */
.visitor-num {
display: inline-block;
transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.4s ease;
}
.visitor-num.bump {
transform: scale(1.6) rotate(-5deg);
color: var(--gradient-2);
text-shadow: 0 0 20px color-mix(in oklch, var(--gradient-2) 60%, transparent);
}
@keyframes pulseGlow {
0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--gradient-2) 40%, transparent); }
100% { box-shadow: 0 0 0 12px transparent; }
}
.visitor-pulse {
animation: pulseGlow 0.8s ease-out;
}
.visitor-toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(120%);
z-index: 90;
padding: 0.6rem 1.2rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
box-shadow: 0 12px 40px var(--glass-shadow), inset 0 1px 0 var(--glass-inset);
opacity: 0;
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.4s ease;
pointer-events: none;
white-space: nowrap;
}
.visitor-toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.visitor-toast .toast-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
color: white;
margin-right: 0.5rem;
vertical-align: middle;
}
.visitor-toast .toast-icon svg {
width: 0.75rem;
height: 0.75rem;
}
/* ===== 浅色模式文字适配 ===== */
:root[data-theme="light"] .text-slate-100 { color: oklch(0.2 0.01 260) !important; }
:root[data-theme="light"] .text-slate-400 { color: oklch(0.45 0.02 260) !important; }
@@ -701,11 +760,19 @@
<p>&copy; 2026 AI 应用门户 · AI Application Portal</p>
<p id="visitorCount" class="mt-1.5 inline-flex items-center gap-1.5 opacity-0 transition-opacity duration-500">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.5 12C4 7.5 7.7 5 12 5s8 2.5 9.5 7c-1.5 4.5-5.2 7-9.5 7s-8-2.5-9.5-7z"/></svg>
<span>本站已被访问 <strong id="visitorCountNum" class="font-bold text-slate-400">--</strong></span>
<span>本站已被访问 <strong id="visitorCountNum" class="visitor-num font-bold text-slate-400">--</strong></span>
</p>
</footer>
</main>
<!-- 实时访问通知气泡 -->
<div class="visitor-toast" id="visitorToast">
<span class="toast-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 19V5M5 12l7-7 7 7"/></svg>
</span>
<span id="visitorToastText">有新访客到来!</span>
</div>
<!-- Help Modal -->
<div class="help-overlay" id="helpOverlay" onclick="if(event.target===this)toggleHelpModal(false)">
<div class="help-modal p-5 md:p-6">
@@ -1173,27 +1240,78 @@
/**
* 调用第三方计数 API 获取并递增全局访问量,更新页脚显示。
* 之后定时轮询 get 接口(不递增),检测新访客并艺术化提示。
*/
let _lastCount = null;
let _toastTimer = null;
function updateVisitorCount() {
const countKey = 'all8ai-portal-visits-2026';
const url = `https://countapi.mileshilliard.com/api/v1/hit/${countKey}`;
fetch(url)
const hitUrl = `https://countapi.mileshilliard.com/api/v1/hit/${countKey}`;
fetch(hitUrl)
.then(res => res.json())
.then(data => {
if (data && data.value !== undefined) {
const count = Number(data.value);
_lastCount = count;
const numEl = document.getElementById('visitorCountNum');
const numEnEl = document.getElementById('visitorCountNumEn');
const wrapEl = document.getElementById('visitorCount');
if (numEl) numEl.textContent = count.toLocaleString();
if (numEnEl) numEnEl.textContent = count.toLocaleString();
const wrapEl = document.getElementById('visitorCount');
if (wrapEl) wrapEl.style.opacity = '1';
}
})
.catch(() => {});
}
function pollVisitorCount() {
const countKey = 'all8ai-portal-visits-2026';
const getUrl = `https://countapi.mileshilliard.com/api/v1/get/${countKey}`;
fetch(getUrl)
.then(res => res.json())
.then(data => {
if (data && data.value !== undefined) {
const count = Number(data.value);
if (_lastCount !== null && count > _lastCount) {
const diff = count - _lastCount;
_lastCount = count;
animateVisitorCount(count, diff);
} else if (_lastCount !== null && count < _lastCount) {
_lastCount = count;
}
}
})
.catch(() => {});
}
function animateVisitorCount(count, diff) {
const numEl = document.getElementById('visitorCountNum');
const wrapEl = document.getElementById('visitorCount');
if (!numEl) return;
numEl.classList.add('bump');
if (wrapEl) wrapEl.classList.add('visitor-pulse');
setTimeout(() => {
numEl.textContent = count.toLocaleString();
}, 200);
setTimeout(() => {
numEl.classList.remove('bump');
if (wrapEl) wrapEl.classList.remove('visitor-pulse');
}, 800);
const toast = document.getElementById('visitorToast');
const toastText = document.getElementById('visitorToastText');
if (toast && toastText) {
toastText.textContent = `✨ 新访客 +${diff} · 当前 ${count.toLocaleString()}`;
toast.classList.add('show');
if (_toastTimer) clearTimeout(_toastTimer);
_toastTimer = setTimeout(() => {
toast.classList.remove('show');
}, 4000);
}
}
updateVisitorCount();
setInterval(pollVisitorCount, 10000);
/**
* 帮助弹窗的测试账号数据,中英双语展示。