Files
arcticnewone/app/components/home/HomeTechStack.vue
T
陈春 cd87644d41 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>
2026-06-03 19:14:53 +08:00

254 lines
8.3 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 py-32 md:py-40 overflow-hidden">
<div class="w-page-container 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" style="opacity: 0;">
<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)'
}
)
})
}, 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>