bb0020b406
✨ 新增 pin 效果(板块固定,内容随滚动切换): 1. HomeTestimonials - 客户评价 - 固定板块,滚动自动切换卡片 - 每屏切换一张评价卡片 2. HomeTechStack - 技术栈 - 固定板块,3D 球体自动旋转一圈 - 滚动控制旋转进度 3. HomeProcess - 创意流程 - 固定板块,横向滚动浏览所有步骤 - 6 个步骤依次展示 4. HomeServices - 服务能力 - 固定板块,逐张卡片聚焦展示 - 聚焦卡片放大,其他缩小变暗 - 所有卡片飞入归位后逐个聚焦 5. HomeAchievements - 数字成就 - 固定板块,逐个成就项展示 - 每个数字滚动计数动画触发 6. HomeUpdates - 最新动态 - 固定板块,逐批展示卡片 - 分批出现,每批 3 个卡片 🎯 效果: - 滚动到板块时自动固定(pin) - 内部元素随滚动依次展示 - 所有内部元素展示完毕后才继续下一个板块 - 每个板块独特的 pin 动画效果 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
274 lines
8.8 KiB
Vue
274 lines
8.8 KiB
Vue
<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.fromTo(contentRef.value,
|
||
{ autoAlpha: 0, x: 100 },
|
||
{
|
||
scrollTrigger: {
|
||
trigger: contentRef.value,
|
||
start: 'top 80%',
|
||
toggleActions: 'play none none reverse'
|
||
},
|
||
autoAlpha: 1,
|
||
x: 0,
|
||
duration: 1,
|
||
ease: 'power3.out'
|
||
}
|
||
)
|
||
|
||
// 技术标签从中心爆炸散开
|
||
techElements.value.forEach((el, index) => {
|
||
if (!el) return
|
||
|
||
gsap.fromTo(el,
|
||
{ autoAlpha: 0, scale: 0 },
|
||
{
|
||
scrollTrigger: {
|
||
trigger: sphereRef.value,
|
||
start: 'top 75%',
|
||
toggleActions: 'play none none reverse'
|
||
},
|
||
autoAlpha: 1,
|
||
scale: 1,
|
||
duration: 1,
|
||
delay: index * 0.05,
|
||
ease: 'back.out(2)'
|
||
}
|
||
)
|
||
})
|
||
|
||
// Pin 滚动效果:固定板块,球体旋转一圈展示所有技术
|
||
const rotationTimeline = gsap.timeline()
|
||
|
||
rotationTimeline.to(autoRotation.value, {
|
||
y: 360,
|
||
duration: 3,
|
||
ease: 'none',
|
||
onUpdate: updatePositions
|
||
})
|
||
|
||
ScrollTrigger.create({
|
||
trigger: rootRef.value,
|
||
start: 'top top',
|
||
end: () => `+=${window.innerHeight * 2}`,
|
||
pin: true,
|
||
scrub: 1,
|
||
anticipatePin: 1,
|
||
animation: rotationTimeline,
|
||
})
|
||
}, 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>
|