Files
arcticnewone/app/components/home/HomeServices.vue
T
陈春 77567b2cc2 refactor: 移除新增板块的 ScrollTrigger pin 效果
从 HomeServices 开始往后的 6 个板块,
去掉 pin(固定)和 scrub(滚动驱动)效果,
改为正常的入场动画:滚动到时触发动画,播放完毕后正常滚动

修改的板块:
- HomeServices: 卡片飞入归位(无 pin)
- HomeAchievements: 逐个数字展示(无 pin)
- HomeTestimonials: 卡片扩散入场(无 pin)
- HomeTechStack: 标签散开入场(无 pin)
- HomeProcess: 标题 + 线条 + 步骤入场(无 pin)
- HomeUpdates: 卡片错开入场(无 pin)

保留 HomePartnersWork 的 pin 效果(原有板块)

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

269 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-white min-h-screen flex 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.services.title') }}
</h2>
<p class="text-xl md:text-2xl text-gray-600 max-w-2xl mx-auto">
{{ t('home.services.subtitle') }}
</p>
</div>
<!-- 服务卡片网格 -->
<div
ref="cardsRef"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div
v-for="(service, index) in services"
:key="service.id"
:ref="el => setCardRef(el, index)"
class="service-card group relative cursor-pointer"
:style="{ transformStyle: 'preserve-3d' }"
@mouseenter="() => handleCardHover(index, true)"
@mouseleave="() => handleCardHover(index, false)"
>
<!-- 前面 -->
<div
class="card-face card-front absolute inset-0 rounded-3xl p-8 flex flex-col justify-between overflow-hidden"
:class="service.gradient"
>
<div class="relative z-10">
<div class="w-16 h-16 md:w-20 md:h-20 rounded-2xl bg-white/90 backdrop-blur-sm flex items-center justify-center mb-6 shadow-lg">
<Icon :name="service.icon" class="w-8 h-8 md:w-10 md:h-10 text-black" />
</div>
<h3 class="text-2xl md:text-3xl font-bold text-white mb-3">
{{ t(`home.services.items.${service.id}.title`) }}
</h3>
<p class="text-white/80 text-sm md:text-base">
{{ t(`home.services.items.${service.id}.brief`) }}
</p>
</div>
<!-- 装饰图案 -->
<div class="absolute bottom-0 right-0 w-48 h-48 opacity-10">
<Icon :name="service.icon" class="w-full h-full text-white" />
</div>
</div>
<!-- 背面 -->
<div
class="card-face card-back absolute inset-0 rounded-3xl p-8 bg-white border-2 flex flex-col justify-between shadow-2xl"
:class="service.borderColor"
>
<div>
<Icon :name="service.icon" class="w-12 h-12 md:w-14 md:h-14 text-black mb-4" />
<h3 class="text-xl md:text-2xl font-bold text-black mb-4">
{{ t(`home.services.items.${service.id}.title`) }}
</h3>
<p class="text-gray-700 text-sm md:text-base leading-relaxed mb-6">
{{ t(`home.services.items.${service.id}.description`) }}
</p>
</div>
<ul class="space-y-2">
<li
v-for="(feature, i) in service.features"
:key="i"
class="flex items-center text-sm md:text-base text-gray-600"
>
<Icon name="ph:check-circle-fill" :class="service.iconColor" class="w-5 h-5 mr-2 flex-shrink-0" />
<span>{{ t(`home.services.items.${service.id}.features.${i}`) }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } 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 cardsRef = ref<HTMLElement | null>(null)
const cardElements = ref<(HTMLElement | null)[]>([])
let ctx: gsap.Context | null = null
const setCardRef = (el: any, index: number) => {
if (el) cardElements.value[index] = el
}
const services = [
{
id: 'webDesign',
icon: 'ph:palette-duotone',
gradient: 'bg-gradient-to-br from-purple-500 via-pink-500 to-red-500',
borderColor: 'border-purple-500',
iconColor: 'text-purple-500',
features: [0, 1, 2]
},
{
id: 'development',
icon: 'ph:code-duotone',
gradient: 'bg-gradient-to-br from-blue-500 via-cyan-500 to-teal-500',
borderColor: 'border-blue-500',
iconColor: 'text-blue-500',
features: [0, 1, 2]
},
{
id: 'branding',
icon: 'ph:sparkle-duotone',
gradient: 'bg-gradient-to-br from-amber-500 via-orange-500 to-red-500',
borderColor: 'border-amber-500',
iconColor: 'text-amber-500',
features: [0, 1, 2]
},
{
id: 'marketing',
icon: 'ph:megaphone-duotone',
gradient: 'bg-gradient-to-br from-green-500 via-emerald-500 to-teal-500',
borderColor: 'border-green-500',
iconColor: 'text-green-500',
features: [0, 1, 2]
},
{
id: 'seo',
icon: 'ph:magnifying-glass-duotone',
gradient: 'bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500',
borderColor: 'border-indigo-500',
iconColor: 'text-indigo-500',
features: [0, 1, 2]
},
{
id: 'consulting',
icon: 'ph:lightbulb-duotone',
gradient: 'bg-gradient-to-br from-rose-500 via-pink-500 to-fuchsia-500',
borderColor: 'border-rose-500',
iconColor: 'text-rose-500',
features: [0, 1, 2]
}
]
// 视差效果
const handleMouseMove = (e: MouseEvent) => {
if (!cardsRef.value) return
const rect = cardsRef.value.getBoundingClientRect()
const x = (e.clientX - rect.left) / rect.width - 0.5
const y = (e.clientY - rect.top) / rect.height - 0.5
cardElements.value.forEach((card, index) => {
if (!card) return
const offset = (index % 3) + 1
gsap.to(card, {
x: x * 20 * offset,
y: y * 20 * offset,
duration: 0.6,
ease: 'power2.out'
})
})
}
const handleMouseLeave = () => {
cardElements.value.forEach(card => {
if (!card) return
gsap.to(card, {
x: 0,
y: 0,
duration: 0.6,
ease: 'power2.out'
})
})
}
// 卡片翻转
const handleCardHover = (index: number, isHover: boolean) => {
const card = cardElements.value[index]
if (!card) return
gsap.to(card, {
rotateY: isHover ? 180 : 0,
duration: 0.6,
ease: 'power2.inOut'
})
}
onMounted(async () => {
await nextTick()
if (!rootRef.value || !headerRef.value) return
ctx = gsap.context(() => {
// 初始状态
gsap.set(headerRef.value, { autoAlpha: 0, y: 50 })
const positions = [
{ x: -200, y: -200, rotate: -15 },
{ x: 0, y: -200, rotate: 10 },
{ x: 200, y: -200, rotate: -15 },
{ x: -200, y: 200, rotate: 15 },
{ x: 0, y: 200, rotate: -10 },
{ x: 200, y: 200, rotate: 15 }
]
cardElements.value.forEach((card, index) => {
if (!card) return
const p = positions[index]
gsap.set(card, { autoAlpha: 0, x: p.x, y: p.y, scale: 0.8, rotateZ: p.rotate })
})
// 入场动画(无 pin
const tl = gsap.timeline()
tl.to(headerRef.value, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0)
cardElements.value.forEach((card, index) => {
if (!card) return
tl.to(card, {
autoAlpha: 1, x: 0, y: 0, scale: 1, rotateZ: 0,
duration: 0.8, ease: 'back.out(1.2)',
}, 0.2 + index * 0.1)
})
ScrollTrigger.create({
trigger: rootRef.value,
start: 'top 75%',
toggleActions: 'play none none none',
animation: tl,
})
}, rootRef.value)
})
onUnmounted(() => {
ctx?.revert()
})
</script>
<style scoped>
.service-card {
perspective: 1000px;
height: clamp(260px, 38vh, 400px);
}
.card-face {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg);
}
/* 移动端单列时卡片更高 */
@media (max-width: 767px) {
.service-card {
height: clamp(300px, 50vh, 420px);
}
}
</style>