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>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<section ref="rootRef" class="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<!-- 粒子背景 -->
|
||||
<canvas ref="canvasRef" class="absolute inset-0 w-full h-full"></canvas>
|
||||
|
||||
<!-- 渐变背景 -->
|
||||
<div
|
||||
ref="gradientRef"
|
||||
class="absolute inset-0 opacity-80"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);"
|
||||
></div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="relative z-10 text-center px-6 md:px-10 max-w-5xl mx-auto">
|
||||
<!-- 主标题 - 分裂后合并动画 -->
|
||||
<h2 ref="titleRef" class="mb-8 md:mb-12 overflow-hidden">
|
||||
<div class="title-line flex justify-center gap-3 md:gap-4 mb-4 md:mb-6">
|
||||
<span
|
||||
v-for="(char, i) in line1Chars"
|
||||
:key="`l1-${i}`"
|
||||
:ref="el => setCharRef(el, 'line1', i)"
|
||||
class="inline-block text-5xl md:text-7xl lg:text-8xl font-bold text-white"
|
||||
style="opacity: 0; transform: translateY(100px) rotate(10deg);"
|
||||
v-html="char === ' ' ? ' ' : char"
|
||||
></span>
|
||||
</div>
|
||||
<div class="title-line flex justify-center gap-3 md:gap-4">
|
||||
<span
|
||||
v-for="(char, i) in line2Chars"
|
||||
:key="`l2-${i}`"
|
||||
:ref="el => setCharRef(el, 'line2', i)"
|
||||
class="inline-block text-5xl md:text-7xl lg:text-8xl font-bold text-white"
|
||||
style="opacity: 0; transform: translateY(100px) rotate(-10deg);"
|
||||
v-html="char === ' ' ? ' ' : char"
|
||||
></span>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<p ref="subtitleRef" class="text-xl md:text-3xl text-white/90 mb-12 md:mb-16 leading-relaxed max-w-3xl mx-auto" style="opacity: 0;">
|
||||
{{ t('home.cta.subtitle') }}
|
||||
</p>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<div ref="buttonsRef" class="flex flex-col sm:flex-row gap-6 justify-center items-center" style="opacity: 0;">
|
||||
<NuxtLink
|
||||
to="/contact"
|
||||
class="cta-button group relative px-10 py-5 rounded-full bg-white text-black text-lg font-bold overflow-hidden shadow-2xl hover:shadow-white/50 transition-all duration-300"
|
||||
>
|
||||
<span class="relative z-10 flex items-center gap-3">
|
||||
{{ t('home.cta.primaryButton') }}
|
||||
<Icon name="ph:arrow-right" class="w-6 h-6 transition-transform duration-300 group-hover:translate-x-2" />
|
||||
</span>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
to="/work"
|
||||
class="cta-button-secondary group px-10 py-5 rounded-full border-2 border-white text-white text-lg font-bold hover:bg-white hover:text-black transition-all duration-300"
|
||||
>
|
||||
<span class="flex items-center gap-3">
|
||||
{{ t('home.cta.secondaryButton') }}
|
||||
<Icon name="ph:eye" class="w-6 h-6" />
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- 滚动提示 -->
|
||||
<div ref="scrollHintRef" class="absolute bottom-12 left-1/2 -translate-x-1/2" style="opacity: 0;">
|
||||
<div class="flex flex-col items-center gap-2 animate-bounce">
|
||||
<span class="text-white/70 text-sm">{{ t('home.cta.scrollDown') }}</span>
|
||||
<Icon name="ph:caret-down" class="w-6 h-6 text-white/70" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import gsap from 'gsap'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const gradientRef = ref<HTMLElement | null>(null)
|
||||
const titleRef = ref<HTMLElement | null>(null)
|
||||
const subtitleRef = ref<HTMLElement | null>(null)
|
||||
const buttonsRef = ref<HTMLElement | null>(null)
|
||||
const scrollHintRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const line1Chars = computed(() => t('home.cta.titleLine1').split(''))
|
||||
const line2Chars = computed(() => t('home.cta.titleLine2').split(''))
|
||||
|
||||
const line1CharRefs = ref<(HTMLElement | null)[]>([])
|
||||
const line2CharRefs = ref<(HTMLElement | null)[]>([])
|
||||
|
||||
let animationId: number | null = null
|
||||
let gradientHue = 0
|
||||
|
||||
const setCharRef = (el: any, line: 'line1' | 'line2', index: number) => {
|
||||
if (line === 'line1') {
|
||||
line1CharRefs.value[index] = el
|
||||
} else {
|
||||
line2CharRefs.value[index] = el
|
||||
}
|
||||
}
|
||||
|
||||
// 星空粒子动画
|
||||
const initParticles = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
}
|
||||
resize()
|
||||
window.addEventListener('resize', resize)
|
||||
|
||||
const particles: Array<{
|
||||
x: number
|
||||
y: number
|
||||
vx: number
|
||||
vy: number
|
||||
size: number
|
||||
opacity: number
|
||||
twinkle: number
|
||||
}> = []
|
||||
|
||||
// 创建星星
|
||||
for (let i = 0; i < 150; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
vx: (Math.random() - 0.5) * 0.3,
|
||||
vy: (Math.random() - 0.5) * 0.3,
|
||||
size: Math.random() * 2.5 + 0.5,
|
||||
opacity: Math.random(),
|
||||
twinkle: Math.random() * Math.PI * 2
|
||||
})
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
particles.forEach(p => {
|
||||
// 更新位置
|
||||
p.x += p.vx
|
||||
p.y += p.vy
|
||||
|
||||
// 边界检测
|
||||
if (p.x < 0) p.x = canvas.width
|
||||
if (p.x > canvas.width) p.x = 0
|
||||
if (p.y < 0) p.y = canvas.height
|
||||
if (p.y > canvas.height) p.y = 0
|
||||
|
||||
// 闪烁效果
|
||||
p.twinkle += 0.02
|
||||
const twinkleOpacity = Math.sin(p.twinkle) * 0.5 + 0.5
|
||||
|
||||
// 绘制星星
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity * twinkleOpacity})`
|
||||
ctx.fill()
|
||||
|
||||
// 绘制光晕
|
||||
const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 3)
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 * twinkleOpacity})`)
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.size * 3, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
})
|
||||
|
||||
animationId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animate()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize)
|
||||
if (animationId) cancelAnimationFrame(animationId)
|
||||
}
|
||||
}
|
||||
|
||||
// 渐变色循环变化
|
||||
const animateGradient = () => {
|
||||
const animate = () => {
|
||||
gradientHue = (gradientHue + 0.2) % 360
|
||||
|
||||
if (gradientRef.value) {
|
||||
const h1 = gradientHue
|
||||
const h2 = (gradientHue + 60) % 360
|
||||
const h3 = (gradientHue + 120) % 360
|
||||
|
||||
gradientRef.value.style.background = `linear-gradient(135deg,
|
||||
hsl(${h1}, 70%, 60%) 0%,
|
||||
hsl(${h2}, 70%, 60%) 50%,
|
||||
hsl(${h3}, 70%, 75%) 100%
|
||||
)`
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animate()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
// 初始化粒子和渐变
|
||||
const cleanupParticles = initParticles()
|
||||
animateGradient()
|
||||
|
||||
// 文字分裂后合并动画
|
||||
const timeline = gsap.timeline()
|
||||
|
||||
// Line 1 字符动画
|
||||
line1CharRefs.value.forEach((char, i) => {
|
||||
if (!char) return
|
||||
timeline.to(char, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
duration: 0.8,
|
||||
ease: 'back.out(1.5)'
|
||||
}, i * 0.03)
|
||||
})
|
||||
|
||||
// Line 2 字符动画
|
||||
line2CharRefs.value.forEach((char, i) => {
|
||||
if (!char) return
|
||||
timeline.to(char, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
duration: 0.8,
|
||||
ease: 'back.out(1.5)'
|
||||
}, 0.5 + i * 0.03)
|
||||
})
|
||||
|
||||
// 副标题
|
||||
timeline.to(subtitleRef.value, {
|
||||
opacity: 1,
|
||||
duration: 1,
|
||||
ease: 'power2.out'
|
||||
}, '-=0.3')
|
||||
|
||||
// 按钮组
|
||||
timeline.to(buttonsRef.value, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.8,
|
||||
ease: 'power3.out'
|
||||
}, '-=0.5')
|
||||
|
||||
// 滚动提示
|
||||
timeline.to(scrollHintRef.value, {
|
||||
opacity: 1,
|
||||
duration: 0.6,
|
||||
ease: 'power2.out'
|
||||
}, '-=0.3')
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cleanupParticles) cleanupParticles()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cta-button {
|
||||
will-change: transform, box-shadow;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cta-button-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user