From e94f7d92e955e4cfa0527bb051fd8fa3d75e71aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=98=A5?= Date: Wed, 3 Jun 2026 20:03:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=9D=BF=E5=9D=97=20pin=20=E6=BB=9A=E5=8A=A8=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=E9=97=B4=E9=9A=99=20+=20=E6=9C=8D=E5=8A=A1=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=B1=8F=E5=B9=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 问题1: 滚动到下一个内容时有空白区 根因: 时间线动画之间有间隙(动画 duration < 步骤间距) 修复: 所有板块改用 cursor 模式,每个步骤精确衔接,无空白 🐛 问题2: 服务卡片太大,一屏显示不全 根因: 卡片高度固定 h-[380px],不适配小屏幕 修复: 改用 clamp(260px, 38vh, 400px) 自适应屏幕高度 🔧 修复的 6 个板块: 1. HomeTestimonials - 淡入淡出切换无间隙 2. HomeServices - 卡片聚焦无间隙 + 自适应高度 3. HomeAchievements - 数字展示无间隙 4. HomeTechStack - 球体旋转无间隙 5. HomeProcess - 步骤展示 + 横向滚动无间隙 6. HomeUpdates - 卡片分批出现无间隙 📐 cursor 模式: 每一步动画 duration 正好等于步骤间距, 动画结束 = 下一步开始,完美衔接 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/components/home/HomeAchievements.vue | 14 ++++---- app/components/home/HomeProcess.vue | 39 ++++++++++----------- app/components/home/HomeServices.vue | 44 ++++++++++++++---------- app/components/home/HomeTechStack.vue | 15 ++++---- app/components/home/HomeTestimonials.vue | 22 ++++++------ app/components/home/HomeUpdates.vue | 12 ++++--- 6 files changed, 80 insertions(+), 66 deletions(-) diff --git a/app/components/home/HomeAchievements.vue b/app/components/home/HomeAchievements.vue index 90425f9..30a7ef4 100644 --- a/app/components/home/HomeAchievements.vue +++ b/app/components/home/HomeAchievements.vue @@ -213,16 +213,17 @@ onMounted(async () => { gsap.set(item, { autoAlpha: 0, x: index % 2 === 0 ? -100 : 100 }) }) - // 统一时间线:逐个展示 + // 统一时间线:逐个展示,无空白 const tl = gsap.timeline() + let cursor = 0 for (let i = 0; i < totalItems; i++) { - // 每个成就项入场,前一个淡出 if (i > 0) { + // 前一个淡出,和后一个淡入在同一时间开始 tl.to(achievementElements.value[i - 1], { autoAlpha: 0, x: i % 2 === 0 ? 100 : -100, - duration: 0.5, ease: 'power2.in', - }, i) + duration: 0.6, ease: 'power2.in', + }, cursor) } tl.to(achievementElements.value[i], { autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out', @@ -230,13 +231,14 @@ onMounted(async () => { const numberEl = numberElements.value[i] if (numberEl) animateNumber(numberEl, achievements[i].number, achievements[i].suffix) } - }, i + 0.2) + }, cursor + 0.1) + cursor += 1 } ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${(totalItems + 1) * window.innerHeight}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 0.5, anticipatePin: 1, diff --git a/app/components/home/HomeProcess.vue b/app/components/home/HomeProcess.vue index 4ac7426..e374ab1 100644 --- a/app/components/home/HomeProcess.vue +++ b/app/components/home/HomeProcess.vue @@ -223,45 +223,44 @@ onMounted(async () => { gsap.set(step, { autoAlpha: 0, y: 60, scale: 0.85 }) }) - // 统一时间线 + // 统一时间线:无空白 const tl = gsap.timeline() + let cursor = 0 // 第一屏:标题入场 - tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0) + tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, cursor) + cursor += 1 - // 连接线绘制 + // 第二屏:连接线绘制 + 所有步骤卡片出现 if (lineRef.value && linePathRef.value) { const pathLength = linePathRef.value.getTotalLength() gsap.set(linePathRef.value, { strokeDasharray: pathLength, strokeDashoffset: pathLength }) - tl.to(lineRef.value, { autoAlpha: 1, duration: 0.3 }, 0.5) - tl.to(linePathRef.value, { strokeDashoffset: 0, duration: 2, ease: 'power2.inOut' }, 0.5) + tl.to(lineRef.value, { autoAlpha: 1, duration: 0.3 }, cursor) + tl.to(linePathRef.value, { strokeDashoffset: 0, duration: 1.5, ease: 'power2.inOut' }, cursor) } + stepElements.value.forEach((step, index) => { + if (!step) return + tl.to(step, { + autoAlpha: 1, y: 0, scale: 1, + duration: 0.6, ease: 'back.out(1.5)', + }, cursor + index * 0.1) + }) + cursor += 1 - // 横向滚动:步骤卡片逐个出现 + // 第三屏起:横向滚动 if (scrollContainerRef.value) { const container = scrollContainerRef.value.querySelector('.flex') if (container) { const scrollWidth = container.scrollWidth - window.innerWidth - - // 逐个展示步骤 - stepElements.value.forEach((step, index) => { - if (!step) return - tl.to(step, { - autoAlpha: 1, y: 0, scale: 1, - duration: 0.6, ease: 'back.out(1.5)', - }, 1 + index * 0.3) - }) - - // 横向滚动 - tl.to(container, { x: -scrollWidth, duration: 3, ease: 'none' }, 1) + tl.to(container, { x: -scrollWidth, duration: 2, ease: 'none' }, cursor) + cursor += 2 } } - const totalSteps = stepElements.value.length || 6 ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${(totalSteps + 2) * window.innerHeight}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 0.5, anticipatePin: 1, diff --git a/app/components/home/HomeServices.vue b/app/components/home/HomeServices.vue index a9eaca8..accf37a 100644 --- a/app/components/home/HomeServices.vue +++ b/app/components/home/HomeServices.vue @@ -22,7 +22,7 @@ v-for="(service, index) in services" :key="service.id" :ref="el => setCardRef(el, index)" - class="service-card group relative h-[380px] cursor-pointer" + class="service-card group relative cursor-pointer" :style="{ transformStyle: 'preserve-3d' }" @mouseenter="() => handleCardHover(index, true)" @mouseleave="() => handleCardHover(index, false)" @@ -218,50 +218,48 @@ onMounted(async () => { gsap.set(card, { autoAlpha: 0, x: p.x, y: p.y, scale: 0.8, rotateZ: p.rotate }) }) - // 统一时间线 + // 统一时间线:所有动画填满时间轴,无空白 const totalCards = services.length const tl = gsap.timeline() + let cursor = 0 // 时间游标 // 第一屏:标题入场 + 所有卡片飞入归位 - tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0) + tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, cursor) cardElements.value.forEach((card, index) => { if (!card) return tl.to(card, { autoAlpha: 1, x: 0, y: 0, scale: 1, rotateZ: 0, - duration: 1, ease: 'back.out(1.2)', - }, 0.2 + index * 0.1) + duration: 0.8, ease: 'back.out(1.2)', + }, cursor + 0.1 + index * 0.08) }) + cursor += 1 - // 后续屏:逐张聚焦 + // 后续屏:逐张聚焦,每步 duration=1 填满整步 for (let focus = 0; focus < totalCards; focus++) { - const pos = 1.5 + focus cardElements.value.forEach((card, index) => { if (!card) return tl.to(card, { scale: index === focus ? 1.05 : 0.85, autoAlpha: index === focus ? 1 : 0.4, - duration: 0.6, ease: 'power2.out', - }, pos) - if (index === focus) { - tl.call(() => handleCardHover(focus, true), [], pos) - } else { - tl.call(() => handleCardHover(index, false), [], pos) - } + duration: 0.8, ease: 'power2.out', + }, cursor) + tl.call(() => handleCardHover(index, index === focus), [], cursor + 0.2) }) + cursor += 1 } - // 最后:恢复正常 - const resetPos = 1.5 + totalCards + // 最后一屏:恢复正常 cardElements.value.forEach((card, index) => { if (!card) return - tl.to(card, { scale: 1, autoAlpha: 1, duration: 0.5, ease: 'power2.out' }, resetPos) - tl.call(() => handleCardHover(index, false), [], resetPos) + tl.to(card, { scale: 1, autoAlpha: 1, duration: 0.6, ease: 'power2.out' }, cursor) + tl.call(() => handleCardHover(index, false), [], cursor) }) + cursor += 1 ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${(totalCards + 3) * window.innerHeight}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 0.5, anticipatePin: 1, @@ -278,6 +276,7 @@ onUnmounted(() => { diff --git a/app/components/home/HomeTechStack.vue b/app/components/home/HomeTechStack.vue index 8204664..55b8c00 100644 --- a/app/components/home/HomeTechStack.vue +++ b/app/components/home/HomeTechStack.vue @@ -200,27 +200,30 @@ onMounted(async () => { gsap.set(el, { autoAlpha: 0, scale: 0 }) }) - // 统一时间线:入场 + pin 旋转 + // 统一时间线:入场 + pin 旋转,无空白 const tl = gsap.timeline() + let cursor = 0 // 第一屏:内容入场 + 技术标签散开 - tl.to(contentRef.value, { autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out' }, 0) + tl.to(contentRef.value, { autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out' }, cursor) techElements.value.forEach((el, index) => { if (!el) return tl.to(el, { autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(2)', - }, 0.3 + index * 0.05) + }, cursor + 0.1 + index * 0.05) }) + cursor += 1 // 后续屏:球体旋转一圈 tl.to(autoRotation.value, { - y: 360, duration: 3, ease: 'none', onUpdate: updatePositions, - }, 1.5) + y: 360, duration: 2, ease: 'none', onUpdate: updatePositions, + }, cursor) + cursor += 2 ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${window.innerHeight * 3}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 1, anticipatePin: 1, diff --git a/app/components/home/HomeTestimonials.vue b/app/components/home/HomeTestimonials.vue index 04a0cc7..fdafdc8 100644 --- a/app/components/home/HomeTestimonials.vue +++ b/app/components/home/HomeTestimonials.vue @@ -233,33 +233,35 @@ onMounted(async () => { gsap.set(card, { autoAlpha: 0, scale: 0.5 }) }) - // 统一时间线:入场 + pin 滚动切换 + // 统一时间线:入场 + pin 滚动切换,无空白 const totalCards = testimonials.length const tl = gsap.timeline() + let cursor = 0 // 第一屏:标题入场 + 第一张卡片出现 - tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0) - tl.to(cardElements.value[0], { autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(1.5)' }, 0.3) - tl.call(() => { currentIndex.value = 0 }, [], 0.3) + tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, cursor) + tl.to(cardElements.value[0], { autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(1.5)' }, cursor + 0.2) + tl.call(() => { currentIndex.value = 0 }, [], cursor + 0.2) + cursor += 1 // 后续屏:滚动切换卡片,每屏切一张 for (let i = 1; i < totalCards; i++) { - const pos = i // 当前卡片缩小淡出 tl.to(cardElements.value[i - 1], { autoAlpha: 0, scale: 0.5, duration: 0.5, ease: 'power2.in' - }, pos) - // 新卡片放大淡入 + }, cursor) + // 新卡片放大淡入(紧接淡出开始,略有重叠) tl.to(cardElements.value[i], { autoAlpha: 1, scale: 1, duration: 0.6, ease: 'back.out(1.5)' - }, pos + 0.3) - tl.call(() => { currentIndex.value = i }, [], pos + 0.3) + }, cursor + 0.2) + tl.call(() => { currentIndex.value = i }, [], cursor + 0.2) + cursor += 1 } ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${totalCards * window.innerHeight}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 0.5, anticipatePin: 1, diff --git a/app/components/home/HomeUpdates.vue b/app/components/home/HomeUpdates.vue index 490b8a9..b027295 100644 --- a/app/components/home/HomeUpdates.vue +++ b/app/components/home/HomeUpdates.vue @@ -171,11 +171,13 @@ onMounted(async () => { gsap.set(card, { autoAlpha: 0, y: 60, scale: 0.9 }) }) - // 统一时间线 + // 统一时间线:无空白 const tl = gsap.timeline() + let cursor = 0 // 第一屏:标题入场 - tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0) + tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, cursor) + cursor += 1 // 分批展示卡片:每批 2 个,逐批出现 const totalCards = updates.length @@ -183,7 +185,6 @@ onMounted(async () => { const batches = Math.ceil(totalCards / batchSize) for (let batch = 0; batch < batches; batch++) { - const pos = 0.8 + batch const start = batch * batchSize const end = Math.min(start + batchSize, totalCards) @@ -191,14 +192,15 @@ onMounted(async () => { tl.to(cardElements.value[i], { autoAlpha: 1, y: 0, scale: 1, duration: 0.7, ease: 'power3.out', - }, pos + (i - start) * 0.12) + }, cursor + (i - start) * 0.12) } + cursor += 1 } ScrollTrigger.create({ trigger: rootRef.value, start: 'top top', - end: () => `+=${(batches + 1) * window.innerHeight}`, + end: () => `+=${cursor * window.innerHeight}`, pin: true, scrub: 0.5, anticipatePin: 1,