document.getElementById('copyright-year').textContent = new Date().getFullYear(); const panels = [...document.querySelectorAll('.page-panel')]; const track = document.querySelector('.page-track'); const navLinks = [...document.querySelectorAll('.nav-link')]; const dots = [...document.querySelectorAll('.page-dots a')]; const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); let current = 0; let lockedUntil = 0; let transitionTimer; const names = ['首页', '关于星年', '技术与服务', '联系我们', '公司信息与备案']; const viewport = document.querySelector('.page-viewport'); function updatePosition() { const height = viewport.clientHeight; const last = panels.length - 1; const offset = current === last ? (last - 1) * height + panels[last].offsetHeight : current * height; track.style.transform = `translate3d(0, -${offset}px, 0)`; } window.addEventListener('resize', updatePosition); if ('ResizeObserver' in window) { const sizeObserver = new ResizeObserver(updatePosition); sizeObserver.observe(viewport); sizeObserver.observe(panels[panels.length - 1]); } function goTo(index, updateHash = true) { index = Math.max(0, Math.min(panels.length - 1, index)); const previous = panels[current]; const restoreFocus = previous.contains(document.activeElement); current = index; // Main panels occupy one viewport; the final step reveals the shorter footer. updatePosition(); document.body.dataset.page = String(index); panels.forEach((panel, i) => { panel.inert = i !== index; panel.setAttribute('aria-hidden', String(i !== index)); }); navLinks.forEach(link => { const active = link.hash === `#${panels[index].id}` || (panels[index].id === 'legal' && link.hash === '#contact'); link.classList.toggle('active', active); if (active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); }); dots.forEach((dot, i) => { if (i === index) dot.setAttribute('aria-current', 'page'); else dot.removeAttribute('aria-current'); }); document.getElementById('page-status').textContent = `第 ${index + 1} 页,共 ${panels.length} 页:${names[index]}`; if (updateHash && location.hash !== `#${panels[index].id}`) history.replaceState(null, '', `#${panels[index].id}`); lockedUntil = performance.now() + (reducedMotion.matches ? 180 : 850); clearTimeout(transitionTimer); transitionTimer = setTimeout(() => { if (restoreFocus) panels[current].focus({ preventScroll: true }); }, reducedMotion.matches ? 0 : 780); } panels.forEach(panel => { panel.tabIndex = -1; }); function pageFromHash() { const index = panels.findIndex(panel => `#${panel.id}` === location.hash); goTo(index < 0 ? 0 : index, false); } pageFromHash(); window.addEventListener('hashchange', pageFromHash); document.addEventListener('click', event => { const link = event.target.closest('a[href^="#"]'); if (!link || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const index = panels.findIndex(panel => `#${panel.id}` === link.hash); if (index >= 0) { event.preventDefault(); goTo(index); } else if (link.hash === '#main') { event.preventDefault(); panels[current].focus({ preventScroll: true }); } }); function scrollArea() { return panels[current].querySelector('.panel-scroll'); } function canScroll(direction, area = scrollArea()) { if (!area) return false; return direction > 0 ? area.scrollTop + area.clientHeight < area.scrollHeight - 2 : area.scrollTop > 2; } let lastWheel = -Infinity; let wheelTotal = 0; let gestureUsed = false; document.addEventListener('wheel', event => { if (event.ctrlKey || Math.abs(event.deltaX) > Math.abs(event.deltaY) || !event.deltaY) return; const now = performance.now(); if (now - lastWheel > 220) { wheelTotal = 0; gestureUsed = false; } lastWheel = now; const direction = Math.sign(event.deltaY); if (canScroll(direction) && now >= lockedUntil) { // Long content remains readable; a new gesture at its boundary turns the page. gestureUsed = true; return; } event.preventDefault(); if (gestureUsed || now < lockedUntil) return; const units = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? window.innerHeight : 1; wheelTotal += event.deltaY * units; if (Math.abs(wheelTotal) >= 32) { gestureUsed = true; goTo(current + Math.sign(wheelTotal)); } }, { passive: false }); let touch = null; document.addEventListener('touchstart', event => { if (event.touches.length !== 1) { touch = null; return; } const area = scrollArea(); touch = { x: event.touches[0].clientX, y: event.touches[0].clientY, top: area?.scrollTop ?? 0, max: area ? area.scrollHeight - area.clientHeight : 0 }; }, { passive: true }); function touchCanScroll(direction) { return direction > 0 ? touch.top < touch.max - 2 : touch.top > 2; } document.addEventListener('touchmove', event => { if (!touch || event.touches.length !== 1) return; const dy = touch.y - event.touches[0].clientY; const dx = touch.x - event.touches[0].clientX; if (Math.abs(dy) > Math.abs(dx) && !touchCanScroll(Math.sign(dy))) event.preventDefault(); }, { passive: false }); document.addEventListener('touchend', event => { if (!touch || !event.changedTouches.length) return; const dy = touch.y - event.changedTouches[0].clientY; const dx = touch.x - event.changedTouches[0].clientX; if (Math.abs(dy) > 55 && Math.abs(dy) > Math.abs(dx) && !touchCanScroll(Math.sign(dy)) && performance.now() >= lockedUntil) { goTo(current + Math.sign(dy)); } touch = null; }, { passive: true }); document.addEventListener('touchcancel', () => { touch = null; }, { passive: true }); document.addEventListener('keydown', event => { if (event.altKey || event.ctrlKey || event.metaKey || event.target.closest('input,textarea,select,button,[contenteditable="true"]')) return; const forward = ['ArrowDown', 'PageDown', ' '].includes(event.key); const backward = ['ArrowUp', 'PageUp'].includes(event.key); if (event.key === 'Home' || event.key === 'End') { event.preventDefault(); goTo(event.key === 'Home' ? 0 : panels.length - 1); } else if (forward || backward) { const direction = event.key === ' ' && event.shiftKey ? -1 : forward ? 1 : -1; event.preventDefault(); if (performance.now() < lockedUntil) return; const area = scrollArea(); if (canScroll(direction, area)) { area.scrollBy({ top: direction * area.clientHeight * 0.8, behavior: reducedMotion.matches ? 'instant' : 'smooth' }); } else goTo(current + direction); } });