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) })