Files
陈春 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

242 lines
8.2 KiB
Vue
Raw Permalink 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 class="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-20 items-center">
<!-- 左侧3D 球体 -->
<div class="relative h-[500px] md:h-[600px]">
<div
ref="sphereRef"
class="relative w-full h-full flex items-center justify-center"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<!-- 技术 logo 标签 -->
<div
v-for="(tech, index) in technologies"
:key="tech.id"
:ref="el => setTechRef(el, index)"
class="tech-tag absolute flex items-center gap-3 px-5 py-3 bg-white rounded-full shadow-lg border-2 cursor-pointer hover:scale-110 transition-transform duration-300"
:class="tech.borderColor"
:style="getTechStyle(index)"
>
<Icon :name="tech.icon" class="w-6 h-6 md:w-7 md:h-7" :class="tech.color" />
<span class="text-sm md:text-base font-semibold text-gray-800">{{ tech.name }}</span>
</div>
<!-- 中心装饰 -->
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="w-32 h-32 md:w-40 md:h-40 rounded-full bg-gradient-to-br from-blue-100 to-purple-100 blur-3xl opacity-50 animate-pulse"></div>
</div>
</div>
</div>
<!-- 右侧文本内容 -->
<div ref="contentRef" class="space-y-8">
<div>
<h2 class="text-4xl md:text-6xl font-bold tracking-tight text-black mb-6">
{{ t('home.techStack.title') }}
</h2>
<p class="text-lg md:text-xl text-gray-600 leading-relaxed mb-8">
{{ t('home.techStack.subtitle') }}
</p>
</div>
<!-- 技术分类 -->
<div class="space-y-6">
<div
v-for="category in categories"
:key="category.id"
class="tech-category p-6 rounded-2xl bg-gradient-to-br hover:shadow-lg transition-all duration-300"
:class="category.gradient"
>
<div class="flex items-center gap-3 mb-3">
<Icon :name="category.icon" class="w-6 h-6 text-white" />
<h3 class="text-xl font-bold text-white">
{{ t(`home.techStack.categories.${category.id}.title`) }}
</h3>
</div>
<p class="text-white/90 text-sm md:text-base">
{{ t(`home.techStack.categories.${category.id}.description`) }}
</p>
</div>
</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 sphereRef = ref<HTMLElement | null>(null)
const contentRef = ref<HTMLElement | null>(null)
const techElements = ref<(HTMLElement | null)[]>([])
const rotationX = ref(0)
const rotationY = ref(0)
const autoRotation = ref({ x: 0, y: 0 })
let ctx: gsap.Context | null = null
let autoRotateInterval: ReturnType<typeof setInterval> | null = null
const setTechRef = (el: any, index: number) => {
if (el) techElements.value[index] = el
}
const technologies = [
{ id: 'vue', name: 'Vue.js', icon: 'ph:file-vue', color: 'text-green-500', borderColor: 'border-green-400' },
{ id: 'react', name: 'React', icon: 'ph:atom', color: 'text-blue-500', borderColor: 'border-blue-400' },
{ id: 'nuxt', name: 'Nuxt', icon: 'ph:mountains', color: 'text-green-600', borderColor: 'border-green-500' },
{ id: 'tailwind', name: 'Tailwind', icon: 'ph:wind', color: 'text-cyan-500', borderColor: 'border-cyan-400' },
{ id: 'node', name: 'Node.js', icon: 'ph:hexagon', color: 'text-green-500', borderColor: 'border-green-400' },
{ id: 'typescript', name: 'TypeScript', icon: 'ph:code', color: 'text-blue-600', borderColor: 'border-blue-500' },
{ id: 'gsap', name: 'GSAP', icon: 'ph:magic-wand', color: 'text-purple-500', borderColor: 'border-purple-400' },
{ id: 'figma', name: 'Figma', icon: 'ph:figma-logo', color: 'text-pink-500', borderColor: 'border-pink-400' },
{ id: 'webgl', name: 'WebGL', icon: 'ph:cube', color: 'text-orange-500', borderColor: 'border-orange-400' }
]
const categories = [
{ id: 'frontend', icon: 'ph:monitor', gradient: 'from-blue-500 to-cyan-500' },
{ id: 'backend', icon: 'ph:database', gradient: 'from-purple-500 to-pink-500' },
{ id: 'design', icon: 'ph:palette', gradient: 'from-orange-500 to-red-500' }
]
// 计算 3D 球体位置
const getTechStyle = (index: number) => {
const total = technologies.length
const phi = Math.acos(-1 + (2 * index) / total)
const theta = Math.sqrt(total * Math.PI) * phi
// 球体半径
const radius = 200
// 3D 坐标转 2D
const x = radius * Math.cos(theta) * Math.sin(phi)
const y = radius * Math.sin(theta) * Math.sin(phi)
const z = radius * Math.cos(phi)
// 应用旋转
const rotX = (rotationX.value + autoRotation.value.x) * Math.PI / 180
const rotY = (rotationY.value + autoRotation.value.y) * Math.PI / 180
// 旋转 X 轴
const y1 = y * Math.cos(rotX) - z * Math.sin(rotX)
const z1 = y * Math.sin(rotX) + z * Math.cos(rotX)
// 旋转 Y 轴
const x2 = x * Math.cos(rotY) + z1 * Math.sin(rotY)
const z2 = -x * Math.sin(rotY) + z1 * Math.cos(rotY)
// 透视效果
const perspective = 600
const scale = perspective / (perspective + z2)
return {
left: '50%',
top: '50%',
transform: `translate(-50%, -50%) translate(${x2}px, ${y1}px) scale(${scale})`,
zIndex: Math.round(scale * 100),
opacity: scale < 0.6 ? 0.3 : 1
}
}
// 鼠标控制旋转
const handleMouseMove = (e: MouseEvent) => {
if (!sphereRef.value) return
const rect = sphereRef.value.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
const deltaX = (e.clientX - centerX) / rect.width
const deltaY = (e.clientY - centerY) / rect.height
gsap.to(rotationX, { value: -deltaY * 30, duration: 0.5 })
gsap.to(rotationY, { value: deltaX * 30, duration: 0.5 })
updatePositions()
}
const handleMouseLeave = () => {
gsap.to(rotationX, { value: 0, duration: 0.8 })
gsap.to(rotationY, { value: 0, duration: 0.8 })
updatePositions()
}
// 自动旋转
const startAutoRotation = () => {
autoRotateInterval = setInterval(() => {
autoRotation.value.y += 1
if (autoRotation.value.y >= 360) autoRotation.value.y = 0
updatePositions()
}, 50)
}
// 更新所有标签位置
const updatePositions = () => {
techElements.value.forEach((el, index) => {
if (!el) return
const style = getTechStyle(index)
Object.assign(el.style, style)
})
}
onMounted(async () => {
await nextTick()
if (!rootRef.value || !contentRef.value) return
ctx = gsap.context(() => {
// 初始状态
gsap.set(contentRef.value, { autoAlpha: 0, x: 100 })
techElements.value.forEach((el) => {
if (!el) return
gsap.set(el, { autoAlpha: 0, scale: 0 })
})
// 入场动画(无 pin):内容 + 标签散开
const tl = gsap.timeline()
tl.to(contentRef.value, { autoAlpha: 1, x: 0, duration: 0.8, ease: 'power3.out' }, 0)
techElements.value.forEach((el, index) => {
if (!el) return
tl.to(el, {
autoAlpha: 1, scale: 1, duration: 0.8, ease: 'back.out(2)',
}, 0.2 + index * 0.05)
})
ScrollTrigger.create({
trigger: rootRef.value,
start: 'top 75%',
toggleActions: 'play none none none',
animation: tl,
})
}, rootRef.value)
// 启动自动旋转
startAutoRotation()
})
onUnmounted(() => {
ctx?.revert()
if (autoRotateInterval) clearInterval(autoRotateInterval)
})
</script>
<style scoped>
.tech-tag {
will-change: transform, opacity;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.tech-category {
will-change: transform, box-shadow;
}
</style>