Files
arcticnewone/app/components/home/HomeTestimonials.vue
T
陈春 05f2e56ff3 fix: 加快客户评价滚动速度 + 修复阴影被裁剪
- 滚动速度从 0.5px/帧 提升到 1.5px/帧
- 使用 mask-image 渐变边缘替代 overflow-hidden
  卡片阴影不再被裁剪,边缘自然淡出
- 添加 py-4 上下留白,阴影完整显示

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:26:06 +08:00

164 lines
5.3 KiB
Vue

<template>
<section ref="rootRef" class="relative bg-gradient-to-b from-gray-50 to-white min-h-screen flex flex-col items-center justify-center overflow-hidden py-20">
<div class="w-full max-w-[1400px] mx-auto px-6 md:px-10">
<!-- 标题 -->
<div ref="headerRef" class="text-center mb-16 md:mb-20">
<h2 class="text-5xl md:text-7xl font-bold tracking-tight text-black mb-6">
{{ t('home.testimonials.title') }}
</h2>
<p class="text-lg md:text-xl text-gray-600 max-w-2xl mx-auto">
{{ t('home.testimonials.subtitle') }}
</p>
</div>
<!-- 自动滚动轮播 -->
<div class="relative -mx-6 md:-mx-10 px-6 md:px-10 overflow-hidden" style="mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);">
<div
ref="trackRef"
class="flex gap-6 md:gap-8 py-4"
:style="{ transform: `translateX(${offset}px)` }"
@mouseenter="pauseAutoplay"
@mouseleave="resumeAutoplay"
>
<!-- 复制两份实现无缝循环 -->
<div
v-for="(item, index) in allTestimonials"
:key="`card-${index}`"
class="testimonial-card flex-shrink-0 w-[320px] md:w-[400px] bg-white rounded-3xl p-8 md:p-10 shadow-lg hover:shadow-xl transition-shadow duration-300"
>
<!-- 评分 -->
<div class="flex gap-1 mb-6">
<Icon v-for="i in 5" :key="i" name="ph:star-fill" class="w-5 h-5 text-yellow-400" />
</div>
<!-- 评价内容 -->
<p class="text-gray-700 text-base md:text-lg leading-relaxed mb-8 italic">
"{{ t(`home.testimonials.items.${item.id}.content`) }}"
</p>
<!-- 客户信息 -->
<div class="flex items-center gap-4">
<div class="w-14 h-14 md:w-16 md:h-16 rounded-full overflow-hidden bg-gradient-to-br" :class="item.avatarGradient">
<div class="w-full h-full flex items-center justify-center text-white text-xl md:text-2xl font-bold">
{{ item.initial }}
</div>
</div>
<div>
<h4 class="text-lg md:text-xl font-bold text-black">
{{ t(`home.testimonials.items.${item.id}.name`) }}
</h4>
<p class="text-sm md:text-base text-gray-500">
{{ t(`home.testimonials.items.${item.id}.role`) }}
</p>
</div>
</div>
<!-- 装饰引号 -->
<div class="absolute top-8 right-8 w-10 h-10 opacity-10">
<Icon name="ph:quotes-duotone" class="w-full h-full text-black" />
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const { t } = useI18n()
const rootRef = ref<HTMLElement | null>(null)
const headerRef = ref<HTMLElement | null>(null)
const trackRef = ref<HTMLElement | null>(null)
let ctx: gsap.Context | null = null
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' },
]
// 复制一份用于无缝循环
const allTestimonials = computed(() => [...testimonials, ...testimonials])
// 自动滚动状态
const offset = ref(0)
const speed = 1.5 // 每帧移动像素
let isPaused = false
const cardWidth = 400 + 32 // 卡片宽度 + gap
const startAutoplay = () => {
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 pauseAutoplay = () => {
isPaused = true
}
const resumeAutoplay = () => {
isPaused = false
}
onMounted(async () => {
await nextTick()
if (!rootRef.value || !headerRef.value) return
ctx = gsap.context(() => {
// 标题入场动画
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()
})
onUnmounted(() => {
ctx?.revert()
if (animFrameId) cancelAnimationFrame(animFrameId)
})
</script>
<style scoped>
.testimonial-card {
position: relative;
will-change: transform;
}
</style>