fix: replace scroll-then-snap with direction-based one-screen-at-a-time navigation

This commit is contained in:
Omega 2026-04-20 23:26:26 +08:00
parent 7a5430d40a
commit c705bc96e9
1 changed files with 95 additions and 86 deletions

View File

@ -974,106 +974,115 @@
// 移动端:入场动画 + 视差滚动 // 移动端:入场动画 + 视差滚动
if (window.innerWidth <= 900) { if (window.innerWidth <= 900) {
// 移除 CSS scroll-snap不可靠用 JS 实现 // 3. 逐屏滚动 - 方向感知,禁止自由滚动
document.body.style.scrollSnapType = 'none';
document.querySelectorAll('[style*="scroll-snap"]').forEach(el => {
el.style.scrollSnapType = 'none';
el.style.scrollSnapAlign = 'none';
el.style.scrollSnapStop = 'normal';
});
// 1. 入场动画(一次性)
const mobileItems = document.querySelectorAll('#pain-points .pain-item, #approach .approach-item');
const mobileObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) entry.target.classList.add('in-view');
});
}, { threshold: 0.05, rootMargin: "0px 0px -5% 0px" });
mobileItems.forEach(item => mobileObserver.observe(item));
// 2. 数字编号滚动视差(基于卡片相对视口位置)
let ticking = false;
const painNums = document.querySelectorAll('#pain-points .pain-num');
function updateParallax() {
painNums.forEach(num => {
const card = num.parentElement;
if (!card) return;
const rect = card.getBoundingClientRect();
const speed = 0.15;
const yOffset = -(rect.top * speed);
num.style.transform = `translateY(${yOffset}px)`;
});
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updateParallax);
ticking = true;
}
}, { passive: true });
updateParallax();
// 3. JS 逐屏吸附
// 收集所有吸附点
const snapTargets = []; const snapTargets = [];
// Hero
const hero = document.querySelector('.hero'); const hero = document.querySelector('.hero');
if (hero) snapTargets.push(hero); if (hero) snapTargets.push(hero);
// 痛点每个卡片
document.querySelectorAll('#pain-points .pain-item').forEach(item => snapTargets.push(item)); document.querySelectorAll('#pain-points .pain-item').forEach(item => snapTargets.push(item));
// 其他 section
['solutions', 'approach', 'geo', 'contact'].forEach(id => { ['solutions', 'approach', 'geo', 'contact'].forEach(id => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) snapTargets.push(el); if (el) snapTargets.push(el);
}); });
let scrollEndTimer = null; let currentIndex = 0;
let isSnapping = false; let isAnimating = false;
let touchStartY = 0; let touchStartY = 0;
let lastScrollTop = 0; let touchStartX = 0;
let animFrame = null;
document.addEventListener('touchstart', (e) => { // 找到离视口顶部最近的 section 索引
touchStartY = e.touches[0].clientY; function findCurrentIndex() {
}, { passive: true }); let best = 0;
let bestDist = Infinity;
window.addEventListener('scroll', () => { snapTargets.forEach((target, i) => {
// 清除之前的定时器 const top = target.getBoundingClientRect().top + window.scrollY;
clearTimeout(scrollEndTimer); const dist = Math.abs(top - window.scrollY);
// 300ms 无滚动视为停止 if (dist < bestDist) {
scrollEndTimer = setTimeout(() => { bestDist = dist;
if (isSnapping) return; // 正在吸附中不触发 best = i;
snapToNearest();
}, 200);
lastScrollTop = window.scrollY;
}, { passive: true });
function snapToNearest() {
const vh = window.innerHeight;
const center = window.scrollY + vh * 0.4; // 吸附点偏上 40% 位置
let nearest = null;
let minDist = Infinity;
snapTargets.forEach(target => {
const rect = target.getBoundingClientRect();
const top = rect.top + window.scrollY;
const dist = Math.abs(top - center);
if (dist < minDist) {
minDist = dist;
nearest = target;
} }
}); });
return best;
}
if (nearest) { // 切换到指定 section
isSnapping = true; function goTo(index) {
const targetY = nearest.getBoundingClientRect().top + window.scrollY; if (index < 0 || index >= snapTargets.length || isAnimating) return;
window.scrollTo({ top: targetY, behavior: 'smooth' }); isAnimating = true;
// 吸附完成后解锁 const targetY = snapTargets[index].getBoundingClientRect().top + window.scrollY;
setTimeout(() => { isSnapping = false; }, 800); const startY = window.scrollY;
const diff = targetY - startY;
const duration = 600;
const startTime = performance.now();
// 自定义缓动:先快后慢,有惯性感觉
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function step(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(progress);
window.scrollTo(0, startY + diff * eased);
if (progress < 1) {
animFrame = requestAnimationFrame(step);
} else {
isAnimating = false;
currentIndex = index;
} }
} }
animFrame = requestAnimationFrame(step);
}
// 初始化
currentIndex = findCurrentIndex();
// 完全接管触摸滚动 - 禁止原生滚动
let canScroll = true;
document.addEventListener('touchmove', (e) => {
e.preventDefault();
}, { passive: false });
document.addEventListener('touchstart', (e) => {
if (!canScroll || isAnimating) return;
touchStartY = e.touches[0].clientY;
touchStartX = e.touches[0].clientX;
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!canScroll || isAnimating) return;
const dy = touchStartY - e.changedTouches[0].clientY;
const dx = touchStartX - e.changedTouches[0].clientX;
// 水平滑动不处理(可能是浏览器前进后退手势)
if (Math.abs(dx) > Math.abs(dy)) return;
// 最小滑动阈值,防止误触
if (Math.abs(dy) < 40) return;
if (dy > 0) {
// 上滑 -> 下一屏
goTo(currentIndex + 1);
} else {
// 下滑 -> 上一屏
goTo(currentIndex - 1);
}
}, { passive: true });
// 导航链接点击:定位到对应 section 并更新索引
document.querySelectorAll('.nav-links a, .btn').forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
if (!href || !href.startsWith('#')) return;
const targetId = href.substring(1);
const idx = snapTargets.findIndex(t => t.id === targetId);
if (idx >= 0) {
e.preventDefault();
currentIndex = idx;
goTo(idx);
}
});
});
} }
</script> </script>
</body> </body>