From 2ed3c5092df102c29c99d7347b2abd4d31852a85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E6=98=A5?=
Date: Wed, 3 Jun 2026 20:24:08 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=A2=E6=88=B7=E8=AF=84?=
=?UTF-8?q?=E4=BB=B7=E6=94=B9=E4=B8=BA=E6=97=A0=E7=BC=9D=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E6=BB=9A=E5=8A=A8=E8=BD=AE=E6=92=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
✨ 重写为自动滚动:
- 无缝循环:复制一组卡片,滚到末尾自动重置
- requestAnimationFrame 驱动,流畅 60fps
- 鼠标悬停暂停,移开继续
- 移除导航点
- 移除模糊效果
- 移除拖拽交互
- 移除 3D 轮播效果
🎯 效果:
- 卡片从右向左匀速滚动
- 悬停时暂停,方便阅读
- 简洁干净,无多余装饰
Co-Authored-By: Claude Opus 4.8 (1M context)
---
app/components/home/HomeTestimonials.vue | 262 +++++++----------------
1 file changed, 75 insertions(+), 187 deletions(-)
diff --git a/app/components/home/HomeTestimonials.vue b/app/components/home/HomeTestimonials.vue
index 1215a3c..2ccad63 100644
--- a/app/components/home/HomeTestimonials.vue
+++ b/app/components/home/HomeTestimonials.vue
@@ -11,15 +11,20 @@
-
-
-
+
+
+
+
@@ -28,43 +33,32 @@
- "{{ t(`home.testimonials.items.${testimonial.id}.content`) }}"
+ "{{ t(`home.testimonials.items.${item.id}.content`) }}"
-
+
- {{ testimonial.initial }}
+ {{ item.initial }}
- {{ t(`home.testimonials.items.${testimonial.id}.name`) }}
+ {{ t(`home.testimonials.items.${item.id}.name`) }}
- {{ t(`home.testimonials.items.${testimonial.id}.role`) }}
+ {{ t(`home.testimonials.items.${item.id}.role`) }}
-
-
-
-
-
-
-
@@ -82,142 +76,51 @@ const { t } = useI18n()
const rootRef = ref
(null)
const headerRef = ref(null)
-const carouselRef = ref(null)
-const cardElements = ref<(HTMLElement | null)[]>([])
-const currentIndex = ref(2) // 从中间开始
-const isDragging = ref(false)
-const startX = ref(0)
-const currentX = ref(0)
+const trackRef = ref(null)
let ctx: gsap.Context | null = null
-let autoplayInterval: ReturnType | null = null
-
-const setCardRef = (el: any, index: number) => {
- if (el) cardElements.value[index] = el
-}
+let animFrameId: number | null = null
const testimonials = [
- {
- id: 'client1',
- initial: 'A',
- avatarGradient: 'from-blue-500 to-cyan-500'
- },
- {
- id: 'client2',
- initial: 'B',
- avatarGradient: 'from-purple-500 to-pink-500'
- },
- {
- id: 'client3',
- initial: 'C',
- avatarGradient: 'from-orange-500 to-red-500'
- },
- {
- id: 'client4',
- initial: 'D',
- avatarGradient: 'from-green-500 to-emerald-500'
- },
- {
- id: 'client5',
- initial: 'E',
- avatarGradient: 'from-indigo-500 to-purple-500'
- }
+ { id: 'client1', initial: 'A', avatarGradient: 'from-blue-500 to-cyan-500' },
+ { id: 'client2', initial: 'B', avatarGradient: 'from-purple-500 to-pink-500' },
+ { id: 'client3', initial: 'C', avatarGradient: 'from-orange-500 to-red-500' },
+ { id: 'client4', initial: 'D', avatarGradient: 'from-green-500 to-emerald-500' },
+ { id: 'client5', initial: 'E', avatarGradient: 'from-indigo-500 to-purple-500' },
]
-// 计算卡片样式(3D 轮播效果)
-const getCardStyle = (index: number) => {
- const diff = index - currentIndex.value
- const distance = 450 // 卡片间距
- const offset = diff * distance
+// 复制一份用于无缝循环
+const allTestimonials = computed(() => [...testimonials, ...testimonials])
- // 中心卡片
- if (diff === 0) {
- return {
- transform: `translateX(calc(50vw - 50%)) translateZ(0) scale(1)`,
- opacity: 1,
- zIndex: 10,
- filter: 'blur(0px)'
- }
- }
+// 自动滚动状态
+const offset = ref(0)
+const speed = 0.5 // 每帧移动像素
+let isPaused = false
+const cardWidth = 400 + 32 // 卡片宽度 + gap
- // 左右卡片
- const scale = 1 - Math.abs(diff) * 0.12
- const opacity = Math.max(0.5, 1 - Math.abs(diff) * 0.25)
-
- return {
- transform: `translateX(calc(50vw - 50% + ${offset}px)) scale(${scale})`,
- opacity: opacity,
- zIndex: 10 - Math.abs(diff),
- }
-}
-
-// 拖拽功能(鼠标 + 触摸)
-const startDrag = (e: MouseEvent | TouchEvent) => {
- isDragging.value = true
- startX.value = 'touches' in e ? e.touches[0].clientX : e.clientX
- if (carouselRef.value) {
- carouselRef.value.style.cursor = 'grabbing'
- }
-}
-
-const onDrag = (e: MouseEvent | TouchEvent) => {
- if (!isDragging.value) return
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
- currentX.value = clientX - startX.value
-}
-
-const endDrag = () => {
- if (!isDragging.value) return
- isDragging.value = false
-
- if (carouselRef.value) {
- carouselRef.value.style.cursor = 'grab'
- }
-
- // 降低阈值到 50px,更灵敏
- if (Math.abs(currentX.value) > 50) {
- if (currentX.value > 0) {
- goToSlide(Math.max(0, currentIndex.value - 1))
- } else {
- goToSlide(Math.min(testimonials.length - 1, currentIndex.value + 1))
- }
- }
-
- currentX.value = 0
- startX.value = 0
-}
-
-// 切换到指定幻灯片
-const goToSlide = (index: number) => {
- currentIndex.value = index
- updateCards()
-}
-
-// 更新卡片位置
-const updateCards = () => {
- cardElements.value.forEach((card, index) => {
- if (!card) return
- const style = getCardStyle(index)
- gsap.to(card, {
- ...style,
- duration: 0.8,
- ease: 'power2.out'
- })
- })
-}
-
-// 自动播放
const startAutoplay = () => {
- autoplayInterval = setInterval(() => {
- const nextIndex = (currentIndex.value + 1) % testimonials.length
- goToSlide(nextIndex)
- }, 5000)
+ const totalWidth = testimonials.length * cardWidth
+
+ const animate = () => {
+ if (!isPaused) {
+ offset.value -= speed
+
+ // 当第一组卡片完全滚出时,重置位置实现无缝
+ if (Math.abs(offset.value) >= totalWidth) {
+ offset.value += totalWidth
+ }
+ }
+ animFrameId = requestAnimationFrame(animate)
+ }
+
+ animFrameId = requestAnimationFrame(animate)
}
-const stopAutoplay = () => {
- if (autoplayInterval) {
- clearInterval(autoplayInterval)
- autoplayInterval = null
- }
+const pauseAutoplay = () => {
+ isPaused = true
+}
+
+const resumeAutoplay = () => {
+ isPaused = false
}
onMounted(async () => {
@@ -225,51 +128,36 @@ onMounted(async () => {
if (!rootRef.value || !headerRef.value) return
ctx = gsap.context(() => {
- // 初始状态:标题隐藏,卡片居中缩小
- gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
- cardElements.value.forEach((card, index) => {
- if (!card) return
- gsap.set(card, { autoAlpha: 0, scale: 0.5 })
- })
-
- // 入场动画(无 pin):标题 + 卡片从中心扩散
- const totalCards = testimonials.length
- const tl = gsap.timeline()
-
- tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
- cardElements.value.forEach((card, index) => {
- if (!card) return
- tl.to(card, { autoAlpha: 1, scale: 1, duration: 0.6, ease: 'back.out(1.5)' }, 0.3 + index * 0.08)
- })
-
- // 初始化到第一张卡片
- tl.call(() => { currentIndex.value = 0 }, [], 0.3)
-
- ScrollTrigger.create({
- trigger: rootRef.value,
- start: 'top 75%',
- toggleActions: 'play none none none',
- animation: tl,
- })
+ // 标题入场动画
+ gsap.fromTo(headerRef.value,
+ { autoAlpha: 0, y: 50 },
+ {
+ scrollTrigger: {
+ trigger: rootRef.value,
+ start: 'top 75%',
+ toggleActions: 'play none none none',
+ },
+ autoAlpha: 1,
+ y: 0,
+ duration: 0.8,
+ ease: 'power3.out',
+ }
+ )
}, rootRef.value)
- // 不启动自动播放,使用滚动控制
- // startAutoplay()
+ // 启动自动滚动
+ startAutoplay()
})
onUnmounted(() => {
ctx?.revert()
- stopAutoplay()
+ if (animFrameId) cancelAnimationFrame(animFrameId)
})