/* ================= 星空背景 ================= */ (function () { const canvas = document.getElementById('stars'); const ctx = canvas.getContext('2d'); let w, h, stars = [], dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { w = canvas.width = window.innerWidth * dpr; h = canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + 'px'; canvas.style.height = window.innerHeight + 'px'; const count = Math.min(160, Math.floor(window.innerWidth / 9)); stars = Array.from({ length: count }, () => ({ x: Math.random() * w, y: Math.random() * h, r: (Math.random() * 1.4 + .3) * dpr, a: Math.random() * .6 + .15, s: Math.random() * .0009 + .0003, dy: Math.random() * .05 + .015 })); } let t = 0; function draw() { ctx.clearRect(0, 0, w, h); t += 1; for (const s of stars) { const alpha = s.a + Math.sin(t * s.s * 12) * .25; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,244,214,' + Math.max(0.03, Math.min(1, alpha)) + ')'; ctx.fill(); s.y += s.dy * dpr; if (s.y > h + 5) { s.y = -5; s.x = Math.random() * w; } } requestAnimationFrame(draw); } resize(); window.addEventListener('resize', resize); draw(); })(); /* ================= 打字机 ================= */ (function () { const el = document.getElementById('typed'); const lines = [ '我们的故事,从那年秋天开始', '三年同窗,一生朋友', '愿此去前程似锦,再相逢依然少年' ]; let li = 0, ci = 0, deleting = false; function tick() { const cur = lines[li]; if (!deleting) { ci++; el.textContent = cur.slice(0, ci); if (ci === cur.length) { deleting = true; return setTimeout(tick, 1800); } return setTimeout(tick, 95); } else { ci--; el.textContent = cur.slice(0, ci); if (ci === 0) { deleting = false; li = (li + 1) % lines.length; return setTimeout(tick, 350); } return setTimeout(tick, 40); } } setTimeout(tick, 900); })(); /* ================= 滚动出现 ================= */ (function () { const els = document.querySelectorAll('.reveal'); if (!('IntersectionObserver' in window)) { els.forEach(e => e.classList.add('visible')); return; } const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const el = entry.target; // 延时随文档顺序单调不减(不能取模:那样第 7 个延时回到 0, // 会先于它上面的元素出现),到第 6 级后不再累加。 const idx = Array.from(el.parentElement.children).indexOf(el); const delay = Math.min(idx, 6) * 70; setTimeout(() => el.classList.add('visible'), delay); io.unobserve(el); } }); }, { threshold: .12, rootMargin: '0px 0px -60px 0px' }); els.forEach(e => io.observe(e)); })(); /* ================= 数字滚动 ================= */ (function () { const nums = document.querySelectorAll('[data-count]'); const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (!entry.isIntersecting) return; const el = entry.target; const target = parseInt(el.dataset.count, 10); const suffix = target === 999 ? '+' : ''; let start = null; const dur = 1600; function step(ts) { if (!start) start = ts; const p = Math.min((ts - start) / dur, 1); const eased = 1 - Math.pow(1 - p, 3); el.textContent = Math.floor(target * eased) + suffix; if (p < 1) requestAnimationFrame(step); else el.textContent = target + suffix; } requestAnimationFrame(step); io.unobserve(el); }); }, { threshold: .5 }); nums.forEach(n => io.observe(n)); })(); /* ================= 进度条 + 回顶部 ================= */ (function () { const bar = document.getElementById('progress'); const top = document.getElementById('top'); function onScroll() { const st = window.scrollY || document.documentElement.scrollTop; const max = document.documentElement.scrollHeight - window.innerHeight; bar.style.width = (max > 0 ? (st / max) * 100 : 0) + '%'; top.classList.toggle('show', st > 500); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); top.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); })(); /* ================= 留言墙 ================= */ (function () { const API = '/api/messages'; const list = document.getElementById('msgList'); const nameI = document.getElementById('msgName'); const textI = document.getElementById('msgText'); const btn = document.getElementById('msgSend'); let sending = false; const BTN_TEXT = '留 言'; // 跟随页面渲染出的占位符(班级名由 src/site.config.ts 注入),避免在此硬编码 const PLACEHOLDER = textI.placeholder || '想说点什么……'; function setTip(tip) { list.innerHTML = ''; const p = document.createElement('p'); p.style.cssText = 'color:#66708a;font-size:14px;'; p.textContent = tip; list.appendChild(p); } function render(data) { list.innerHTML = ''; if (!data || !data.length) { setTip('还没有留言,来做第一个吧~'); return; } data.forEach((m, i) => { const d = document.createElement('div'); d.className = 'msg'; d.style.animationDelay = Math.min(i * 0.05, 0.4) + 's'; const who = document.createElement('div'); who.className = 'who'; const span = document.createElement('span'); span.textContent = m.name; const time = document.createElement('time'); time.textContent = m.time; who.append(span, time); const body = document.createElement('div'); body.className = 'body'; body.textContent = m.text; d.append(who, body); list.appendChild(d); }); } async function load() { setTip('加载中…'); try { const res = await fetch(API, { headers: { Accept: 'application/json' } }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) throw new Error(data.error || '加载失败'); render(data.data); } catch (err) { setTip('留言加载失败,请刷新重试~'); } } async function send() { if (sending) return; const name = nameI.value.trim(); const text = textI.value.trim(); if (!text) { textI.focus(); textI.placeholder = '至少写一句话呀~'; setTimeout(() => { textI.placeholder = PLACEHOLDER; }, 2000); return; } sending = true; btn.disabled = true; btn.textContent = '发送中…'; try { const res = await fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, text }) }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) throw new Error(data.error || '发送失败'); textI.value = ''; btn.textContent = BTN_TEXT; btn.disabled = false; sending = false; // 直接把新留言插到最前面,不用重新拉全量 if (data.data) { const first = list.querySelector('.msg'); const d = document.createElement('div'); d.className = 'msg'; d.style.animationDelay = '0s'; const who = document.createElement('div'); who.className = 'who'; const span = document.createElement('span'); span.textContent = data.data.name; const time = document.createElement('time'); time.textContent = data.data.time; who.append(span, time); const body = document.createElement('div'); body.className = 'body'; body.textContent = data.data.text; d.append(who, body); if (first && first.parentNode === list) list.insertBefore(d, first); else { if (list.querySelector('p')) list.innerHTML = ''; list.appendChild(d); } } return; } catch (err) { btn.textContent = err.message && err.message.length <= 10 ? err.message : '发送失败'; setTimeout(() => { btn.textContent = BTN_TEXT; btn.disabled = false; }, 1600); sending = false; return; } } btn.addEventListener('click', send); textI.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') btn.click(); }); load(); })();