Files
arcticnewone/app/components/home/HomeTechStack.vue
T
陈春 ae20560a35 fix: 修复首页加载和动画时序问题
🐛 问题修复:
- 移除所有新板块的初始 opacity: 0 样式
- 防止新板块初始隐藏导致黑屏问题
- 确保页面加载后内容立即可见
- 修复菜单延迟出现的问题

🎨 优化调整:
- 改用 CSS class 控制动画初始状态
- ScrollTrigger 动画在滚动到对应位置时才触发
- 不影响首屏 Hero 和 Intro 的加载动画
- 保持所有板块的入场动画效果

 修复的组件:
- HomeServices
- HomeAchievements
- HomeTestimonials
- HomeTechStack
- HomeProcess
- HomeUpdates
- HomeCTA

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:26:57 +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 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)'
}
)
})
}, 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>