feat: 扩展首页内容,新增7个高级动画板块

 新增首页板块:
- HomeServices: 服务能力展示(3D卡片翻转 + 视差移动)
- HomeAchievements: 数字成就展示(数字滚动 + 粒子背景)
- HomeTestimonials: 客户评价轮播(3D轮播 + 拖拽交互)
- HomeTechStack: 技术栈展示(3D球体 + 鼠标控制旋转)
- HomeProcess: 创意流程时间线(横向滚动 + SVG线条动画)
- HomeUpdates: 最新动态网格(Bento布局 + 悬浮卡片)
- HomeCTA: 全屏CTA区域(文字分裂动画 + 星空粒子)

🎨 设计亮点:
- 每个板块独特的动画效果(翻转、滚动、拖拽、旋转、绘制)
- 避免传统布局,采用创意设计
- 丰富的交互效果(hover、拖拽、视差)
- Canvas粒子动画和SVG路径动画
- 响应式设计,完美适配移动端

🌍 国际化:
- 完整的中英文内容
- 涵盖所有新板块的文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
陈春
2026-06-03 19:14:53 +08:00
parent f4684f694e
commit cd87644d41
10 changed files with 2408 additions and 45 deletions
+287
View File
@@ -0,0 +1,287 @@
<template>
<section ref="rootRef" class="relative bg-gradient-to-b from-gray-50 to-white py-32 md:py-40 overflow-hidden">
<div class="w-page-container max-w-[1400px] mx-auto px-6 md:px-10">
<!-- 标题 -->
<div ref="headerRef" class="text-center mb-20" style="opacity: 0;">
<h2 class="text-4xl md:text-6xl 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>