Files
arcticnewone/app/components/home/HomeTestimonials.vue
T
陈春 ae20560a35 fix: 修复首页加载和动画时序问题
🐛 问题修复:
- 移除所有新板块的初始 opacity: 0 样式
- 防止新板块初始隐藏导致黑屏问题
- 确保页面加载后内容立即可见
- 修复菜单延迟出现的问题

🎨 优化调整:
- 改用 CSS class 控制动画初始状态
- ScrollTrigger 动画在滚动到对应位置时才触发
- 不影响首屏 Hero 和 Intro 的加载动画
- 保持所有板块的入场动画效果

 修复的组件:
- HomeServices
- HomeAchievements
- HomeTestimonials
- HomeTechStack
- HomeProcess
- HomeUpdates
- HomeCTA

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

288 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 h-[500px] md:h-[600px]" @mousedown="startDrag" @mousemove="onDrag" @mouseup="endDrag" @mouseleave="endDrag">
<div ref="carouselRef" class="flex items-center h-full cursor-grab active:cursor-grabbing">
<div
v-for="(testimonial, index) in testimonials"
:key="testimonial.id"
:ref="el => setCardRef(el, index)"
class="testimonial-card absolute flex-shrink-0 w-[320px] md:w-[400px] bg-white rounded-3xl p-8 md:p-10 shadow-xl"
:style="getCardStyle(index)"
>
<!-- 评分 -->
<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.${testimonial.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="testimonial.avatarGradient">
<div class="w-full h-full flex items-center justify-center text-white text-xl md:text-2xl font-bold">
{{ testimonial.initial }}
</div>
</div>
<div>
<h4 class="text-lg md:text-xl font-bold text-black">
{{ t(`home.testimonials.items.${testimonial.id}.name`) }}
</h4>
<p class="text-sm md:text-base text-gray-500">
{{ t(`home.testimonials.items.${testimonial.id}.role`) }}
</p>
</div>
</div>
<!-- 公司 logo装饰 -->
<div class="absolute top-8 right-8 w-10 h-10 opacity-20">
<Icon name="ph:quotes-duotone" class="w-full h-full text-black" />
</div>
</div>
</div>
<!-- 导航点 -->
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 flex gap-3">
<button
v-for="(_, index) in testimonials"
:key="index"
@click="goToSlide(index)"
class="w-3 h-3 rounded-full transition-all duration-300"
:class="currentIndex === index ? 'bg-black w-8' : 'bg-gray-300 hover:bg-gray-400'"
></button>
</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 carouselRef = ref<HTMLElement | null>(null)
const cardElements = ref<(HTMLElement | null)[]>([])
const currentIndex = ref(2) // 从中间开始
const isDragging = ref(false)
const startX = ref(0)
const currentX = ref(0)
let ctx: gsap.Context | null = null
let autoplayInterval: ReturnType<typeof setInterval> | null = null
const setCardRef = (el: any, index: number) => {
if (el) cardElements.value[index] = el
}
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'
}
]
// 计算卡片样式(3D 轮播效果)
const getCardStyle = (index: number) => {
const diff = index - currentIndex.value
const distance = 450 // 卡片间距
const offset = diff * distance
// 中心卡片
if (diff === 0) {
return {
transform: `translateX(calc(50vw - 50%)) translateZ(0) scale(1)`,
opacity: 1,
zIndex: 10,
filter: 'blur(0px)'
}
}
// 左右卡片
const scale = 1 - Math.abs(diff) * 0.1
const opacity = Math.max(0.3, 1 - Math.abs(diff) * 0.3)
const blur = Math.abs(diff) * 3
return {
transform: `translateX(calc(50vw - 50% + ${offset}px)) translateZ(-${Math.abs(diff) * 100}px) scale(${scale})`,
opacity: opacity,
zIndex: 10 - Math.abs(diff),
filter: `blur(${blur}px)`
}
}
// 拖拽功能
const startDrag = (e: MouseEvent) => {
isDragging.value = true
startX.value = e.clientX
if (carouselRef.value) {
carouselRef.value.style.cursor = 'grabbing'
}
}
const onDrag = (e: MouseEvent) => {
if (!isDragging.value) return
currentX.value = e.clientX - startX.value
}
const endDrag = () => {
if (!isDragging.value) return
isDragging.value = false
if (carouselRef.value) {
carouselRef.value.style.cursor = 'grab'
}
// 根据拖拽距离决定是否切换
if (Math.abs(currentX.value) > 100) {
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 stopAutoplay = () => {
if (autoplayInterval) {
clearInterval(autoplayInterval)
autoplayInterval = null
}
}
onMounted(async () => {
await nextTick()
if (!rootRef.value || !headerRef.value) return
ctx = gsap.context(() => {
// 标题动画
gsap.fromTo(headerRef.value,
{ autoAlpha: 0, y: 50 },
{
scrollTrigger: {
trigger: headerRef.value,
start: 'top 80%',
toggleActions: 'play none none reverse'
},
autoAlpha: 1,
y: 0,
duration: 1,
ease: 'power3.out'
}
)
// 卡片从中心扩散入场
cardElements.value.forEach((card, index) => {
if (!card) return
gsap.fromTo(card,
{ autoAlpha: 0, scale: 0.5 },
{
scrollTrigger: {
trigger: rootRef.value,
start: 'top 70%',
toggleActions: 'play none none reverse'
},
autoAlpha: 1,
scale: 1,
duration: 0.8,
delay: index * 0.1,
ease: 'back.out(1.5)',
onComplete: () => {
if (index === cardElements.value.length - 1) {
updateCards()
}
}
}
)
})
}, rootRef.value)
// 启动自动播放
startAutoplay()
})
onUnmounted(() => {
ctx?.revert()
stopAutoplay()
})
</script>
<style scoped>
.testimonial-card {
transition: transform 0.8s ease, opacity 0.8s ease, filter 0.8s ease;
will-change: transform, opacity, filter;
}
</style>