feat: 实时访客监控 - 轮询检测新访客 + 艺术化动画提示
This commit is contained in:
+124
-6
@@ -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>© 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);
|
||||
|
||||
/**
|
||||
* 帮助弹窗的测试账号数据,中英双语展示。
|
||||
|
||||
Reference in New Issue
Block a user