42c96f7363
🎨 设计调整: - 所有板块改为 min-h-screen 全屏展示 - 每个板块独立占据一屏,视觉冲击力更强 - 统一使用 flex 居中布局 - 调整标题和内容间距,更适合全屏显示 - 保持响应式设计和所有动画效果 📐 修改的组件: - HomeServices: 服务能力展示 - HomeAchievements: 数字成就展示 - HomeTestimonials: 客户评价轮播 - HomeTechStack: 技术栈展示 - HomeProcess: 创意流程时间线 - HomeUpdates: 最新动态网格 ✨ 视觉效果: - 更加现代化的全屏滚动体验 - 每个板块都有足够的呼吸空间 - 符合整体设计风格 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
288 lines
7.9 KiB
Vue
288 lines
7.9 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" style="opacity: 0;">
|
||
<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>
|