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:
@@ -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>
|
||||
Reference in New Issue
Block a user