feat(i18n): 添加首页英雄区域的多语言支持
为首页英雄区域添加中英文翻译内容,包括服务列表、描述文本和按钮文字 同时优化AppLogo组件的动画安全检查和HomeHero组件的布局与动画效果
This commit is contained in:
@@ -36,7 +36,13 @@ const logoHover = () => {
|
|||||||
|
|
||||||
logoObserver?.kill()
|
logoObserver?.kill()
|
||||||
|
|
||||||
gsap.set(logoRef.value.children[1] as HTMLElement, {
|
// Ensure children exist before attempting to animate them
|
||||||
|
const firstChild = logoRef.value.children[0] as HTMLElement
|
||||||
|
const secondChild = logoRef.value.children[1] as HTMLElement
|
||||||
|
|
||||||
|
if (!firstChild || !secondChild) return
|
||||||
|
|
||||||
|
gsap.set(secondChild, {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -45,12 +51,12 @@ const logoHover = () => {
|
|||||||
logoObserver = ScrollTrigger.observe({
|
logoObserver = ScrollTrigger.observe({
|
||||||
target: logoRef.value,
|
target: logoRef.value,
|
||||||
onHover: (self) => {
|
onHover: (self) => {
|
||||||
gsap.to(self.target.children[0] as HTMLElement, {
|
gsap.to(firstChild, {
|
||||||
y: -10,
|
y: -10,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: 'power2.out',
|
ease: 'power2.out',
|
||||||
})
|
})
|
||||||
gsap.to(self.target.children[1] as HTMLElement, {
|
gsap.to(secondChild, {
|
||||||
y: 10,
|
y: 10,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: 'power2.out',
|
ease: 'power2.out',
|
||||||
@@ -59,12 +65,12 @@ const logoHover = () => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onHoverEnd: (self) => {
|
onHoverEnd: (self) => {
|
||||||
gsap.to(self.target.children[0] as HTMLElement, {
|
gsap.to(firstChild, {
|
||||||
y: 0,
|
y: 0,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: 'power2.out',
|
ease: 'power2.out',
|
||||||
})
|
})
|
||||||
gsap.to(self.target.children[1] as HTMLElement, {
|
gsap.to(secondChild, {
|
||||||
y: 0,
|
y: 0,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: 'power2.out',
|
ease: 'power2.out',
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||||||
import { useNuxtApp } from '#app'
|
import { useNuxtApp } from '#app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 定义背景画廊与主视图绑定的总滚动距离(像素),数值越大,用户向下滚动时 Hero 面板缩放的过程就越长
|
// 定义背景画廊与主视图绑定的总滚动距离(像素),数值越大,用户向下滚动时 Hero 面板缩放的过程就越长
|
||||||
const heroPinDistance = 1200
|
const heroPinDistance = 1200
|
||||||
const heroGalleryColumns = [
|
const heroGalleryColumns = [
|
||||||
@@ -13,6 +16,8 @@ const heroGalleryColumns = [
|
|||||||
const heroStageRef = ref<HTMLElement | null>(null)
|
const heroStageRef = ref<HTMLElement | null>(null)
|
||||||
const heroRef = ref<HTMLElement | null>(null)
|
const heroRef = ref<HTMLElement | null>(null)
|
||||||
const heroPanelRef = ref<HTMLElement | null>(null)
|
const heroPanelRef = ref<HTMLElement | null>(null)
|
||||||
|
const brandNameRef = ref<HTMLElement | null>(null)
|
||||||
|
const studioNameRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
let cleanupHeroScroll: (() => void) | null = null
|
let cleanupHeroScroll: (() => void) | null = null
|
||||||
|
|
||||||
@@ -22,6 +27,15 @@ const play = () => new Promise<void>((resolve) => {
|
|||||||
|
|
||||||
let eventDispatched = false
|
let eventDispatched = false
|
||||||
|
|
||||||
|
// 获取所有需要执行动画的字符和 TM 标志 DOM 节点
|
||||||
|
const brandChars = gsap.utils.toArray('.hero-brand-char', brandNameRef.value)
|
||||||
|
const studioChars = gsap.utils.toArray('.hero-studio-char', studioNameRef.value)
|
||||||
|
const tm = brandNameRef.value?.querySelector('.hero-brand-tm')
|
||||||
|
|
||||||
|
// 初始隐藏底部文字:通过 autoAlpha (opacity + visibility) 控制,不仅下移而且完全不可见
|
||||||
|
gsap.set([...brandChars, ...studioChars], { yPercent: 110, autoAlpha: 0 })
|
||||||
|
if (tm) gsap.set(tm, { autoAlpha: 0 })
|
||||||
|
|
||||||
// 执行入场动画:将 Hero 整体容器从屏幕下方(yPercent: 100)滑入屏幕(yPercent: 0)
|
// 执行入场动画:将 Hero 整体容器从屏幕下方(yPercent: 100)滑入屏幕(yPercent: 0)
|
||||||
gsap.to(heroRef.value, {
|
gsap.to(heroRef.value, {
|
||||||
yPercent: 0,
|
yPercent: 0,
|
||||||
@@ -32,6 +46,21 @@ const play = () => new Promise<void>((resolve) => {
|
|||||||
if (!eventDispatched && this.progress() > 0.6) {
|
if (!eventDispatched && this.progress() > 0.6) {
|
||||||
eventDispatched = true
|
eventDispatched = true
|
||||||
window.dispatchEvent(new CustomEvent('hero-intro-done'))
|
window.dispatchEvent(new CustomEvent('hero-intro-done'))
|
||||||
|
|
||||||
|
// 导航栏动画大约 1.4s,我们在导航栏入场即将结束时开始播放底部文字动画
|
||||||
|
gsap.timeline({ delay: 1.0 })
|
||||||
|
.to([...brandChars, ...studioChars], {
|
||||||
|
yPercent: 0,
|
||||||
|
autoAlpha: 1, // 恢复透明度和可见性
|
||||||
|
duration: 1.2,
|
||||||
|
stagger: 0.08,
|
||||||
|
ease: 'power3.out'
|
||||||
|
})
|
||||||
|
.to(tm, {
|
||||||
|
autoAlpha: 1,
|
||||||
|
duration: 0.6,
|
||||||
|
ease: 'power2.out'
|
||||||
|
}, '-=0.4')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onComplete: resolve,
|
onComplete: resolve,
|
||||||
@@ -130,8 +159,73 @@ defineExpose({ play })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="heroPanelRef" class="absolute inset-0 grid place-items-center bg-white text-5xl will-change-transform">
|
<div ref="heroPanelRef" class="absolute inset-0 flex flex-col justify-between items-center bg-white text-black will-change-transform pt-28 pb-10 md:pt-36 md:pb-12 overflow-hidden">
|
||||||
<p>Hero Section</p>
|
<div class="flex flex-col justify-between w-page-container max-w-[calc(100%-2rem)] mx-auto px-4 h-full">
|
||||||
|
<!-- Top Section -->
|
||||||
|
<div class="flex flex-col md:flex-row justify-between w-full">
|
||||||
|
<!-- Top Left: Services List -->
|
||||||
|
<div class="flex flex-col gap-2 font-bold text-base md:text-[1rem] tracking-tight leading-[1.5]">
|
||||||
|
<p>{{ t('hero.services.webDesign') }}</p>
|
||||||
|
<p>{{ t('hero.services.socialMedia') }}</p>
|
||||||
|
<p>{{ t('hero.services.marketing') }}</p>
|
||||||
|
<p>{{ t('hero.services.development') }}</p>
|
||||||
|
<p>{{ t('hero.services.seo') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Right: Description and Button -->
|
||||||
|
<div class="flex flex-col gap-8 max-w-[22rem] mt-10 md:mt-0 text-left">
|
||||||
|
<p class="font-bold text-[1rem] leading-[1.5] tracking-tight">
|
||||||
|
{{ t('hero.description') }}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button class="bg-black text-white px-7 py-3.5 rounded-[2rem] font-semibold text-base transition-transform hover:scale-105 shadow-[0_15px_30px_-5px_rgba(0,0,0,0.3)]">
|
||||||
|
{{ t('hero.letsTalk') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle Section: 4 small dots -->
|
||||||
|
<div class="flex justify-between w-full items-start flex-1 mt-16 md:mt-24 mb-6 md:mb-0">
|
||||||
|
<div class="w-2.5 h-2.5 bg-black rounded-full"></div>
|
||||||
|
<div class="w-2.5 h-2.5 bg-black rounded-full"></div>
|
||||||
|
<div class="w-2.5 h-2.5 bg-black rounded-full"></div>
|
||||||
|
<div class="w-2.5 h-2.5 bg-black rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Section -->
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-end w-full">
|
||||||
|
<!-- Bottom Left: Brand Name -->
|
||||||
|
<h1 ref="brandNameRef" class="text-[11vw] md:text-[8vw] font-bold leading-[0.8] tracking-tighter m-0 -ml-1 whitespace-nowrap pt-4 pb-2">
|
||||||
|
<span class="relative inline-flex overflow-visible">
|
||||||
|
<span
|
||||||
|
v-for="(letter, index) in t('app.brand').split('')"
|
||||||
|
:key="`brand-${index}`"
|
||||||
|
class="relative inline-block overflow-hidden"
|
||||||
|
>
|
||||||
|
<span class="hero-brand-char inline-block will-change-transform opacity-0 invisible pb-4">{{ letter }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="hero-brand-tm text-[0.45em] font-black align-super ml-[0.1em] opacity-0 invisible relative -top-[0.2em]">™</span>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Bottom Right: Studio -->
|
||||||
|
<div class="flex flex-col items-start md:items-end mt-4 md:mt-0">
|
||||||
|
<p class="text-xs md:text-sm font-semibold text-gray-500 mb-2 md:mb-6 mr-1">© {{ new Date().getFullYear() }} {{ t('app.brand') }}™ {{ t('nav.studio') }}</p>
|
||||||
|
<h2 ref="studioNameRef" class="text-[11vw] md:text-[8vw] font-bold leading-[0.8] tracking-tighter m-0 -mr-1 whitespace-nowrap pt-4 pb-2">
|
||||||
|
<span class="relative inline-flex overflow-hidden">
|
||||||
|
<span
|
||||||
|
v-for="(letter, index) in t('nav.studio').split('')"
|
||||||
|
:key="`studio-${index}`"
|
||||||
|
class="relative inline-block overflow-hidden"
|
||||||
|
>
|
||||||
|
<span class="hero-studio-char inline-block will-change-transform opacity-0 invisible pb-4">{{ letter }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -14,5 +14,16 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"title": "Home",
|
"title": "Home",
|
||||||
"welcome": "Welcome to Arctic New One"
|
"welcome": "Welcome to Arctic New One"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"services": {
|
||||||
|
"webDesign": "Web Design",
|
||||||
|
"socialMedia": "Social Media",
|
||||||
|
"marketing": "Marketing",
|
||||||
|
"development": "Development",
|
||||||
|
"seo": "SEO Optimization"
|
||||||
|
},
|
||||||
|
"description": "No cookie cutter sites. No empty claims. Only practical tools and smart strategies that drive growth and build brands.",
|
||||||
|
"letsTalk": "Let's talk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,16 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"title": "首页",
|
"title": "首页",
|
||||||
"welcome": "欢迎来到 Arctic New One"
|
"welcome": "欢迎来到 Arctic New One"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"services": {
|
||||||
|
"webDesign": "网页设计",
|
||||||
|
"socialMedia": "社交媒体",
|
||||||
|
"marketing": "市场运营",
|
||||||
|
"development": "程序开发",
|
||||||
|
"seo": "SEO 优化"
|
||||||
|
},
|
||||||
|
"description": "拒绝千篇一律的网站。拒绝空洞的承诺。只提供实用的工具和明智的策略,以推动增长并建立品牌。",
|
||||||
|
"letsTalk": "联系我们"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user