From 1bc881994f4421f2ac43e995c86b07fb776de116 Mon Sep 17 00:00:00 2001 From: chenchun Date: Sun, 26 Apr 2026 23:07:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(home):=20=E5=B0=86=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=E4=B8=BA=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将首页的加载动画、介绍动画和英雄区域拆分为独立的组件文件 移除index.vue中冗余的样式和逻辑,通过组件引用方式重构 优化代码组织结构,提高可维护性 --- app/components/home/HomeHero.vue | 228 ++++++++++++++++++ app/components/home/HomeIntro.vue | 88 +++++++ app/components/home/HomeLoader.vue | 65 +++++ app/pages/index.vue | 369 ++--------------------------- 4 files changed, 400 insertions(+), 350 deletions(-) create mode 100644 app/components/home/HomeHero.vue create mode 100644 app/components/home/HomeIntro.vue create mode 100644 app/components/home/HomeLoader.vue diff --git a/app/components/home/HomeHero.vue b/app/components/home/HomeHero.vue new file mode 100644 index 0000000..e51bc7a --- /dev/null +++ b/app/components/home/HomeHero.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/app/components/home/HomeIntro.vue b/app/components/home/HomeIntro.vue new file mode 100644 index 0000000..5b95e95 --- /dev/null +++ b/app/components/home/HomeIntro.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/app/components/home/HomeLoader.vue b/app/components/home/HomeLoader.vue new file mode 100644 index 0000000..c553fba --- /dev/null +++ b/app/components/home/HomeLoader.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index a553354..b271480 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -5,13 +5,13 @@ import type Lenis from 'lenis' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useUserStore } from '~/stores/user' +import HomeLoader from '~/components/home/HomeLoader.vue' +import HomeIntro from '~/components/home/HomeIntro.vue' +import HomeHero from '~/components/home/HomeHero.vue' gsap.registerPlugin(ScrollTrigger) const { t } = useI18n() -const introTitle = 'ArcticNewOne' -const introLetters = introTitle.split('') -const heroPinDistance = 1200 const isIntroActive = ref(true) const userStore = useUserStore() const demoUserName = computed(() => userStore.displayName) @@ -38,39 +38,14 @@ useSeoMeta({ twitterDescription: () => t('home.welcome'), }) -// 背景图是装饰层,左右两列共用同一套模板,只通过 side 区分位置和速度。 -const heroGalleryColumns = [ - { - side: 'left', - images: [ - 'https://picsum.photos/id/1018/900/1200', - 'https://picsum.photos/id/1015/900/1200', - 'https://picsum.photos/id/1024/900/1200', - 'https://picsum.photos/id/1039/900/1200', - ], - }, - { - side: 'right', - images: [ - 'https://picsum.photos/id/1043/900/1200', - 'https://picsum.photos/id/1050/900/1200', - 'https://picsum.photos/id/1067/900/1200', - 'https://picsum.photos/id/1074/900/1200', - ], - }, -] as const const pageRef = ref(null) -const loaderRef = ref(null) -const loaderBarRef = ref(null) -const introRef = ref(null) -const introTitleRef = ref(null) -const heroStageRef = ref(null) -const heroRef = ref(null) -const heroPanelRef = ref(null) +const homeLoaderRef = ref | null>(null) +const homeIntroRef = ref | null>(null) +const homeHeroRef = ref | null>(null) + let heroAnimationContext: gsap.Context | null = null let removeLoadListener: (() => void) | null = null let restorePageScroll: (() => void) | null = null -let cleanupHeroScroll: (() => void) | null = null let hasIntroStarted = false let loaderTween: gsap.core.Tween | null = null let introTimeline: gsap.core.Timeline | null = null @@ -83,13 +58,11 @@ const setHomeIntroActive = (active: boolean) => { onMounted(() => { const pageElement = pageRef.value - const loaderElement = loaderRef.value - const loaderBarElement = loaderBarRef.value - const introElement = introRef.value - const introTitleElement = introTitleRef.value - const heroStageElement = heroStageRef.value - const heroElement = heroRef.value - const heroPanelElement = heroPanelRef.value + const loaderElement = homeLoaderRef.value?.loaderRef + const loaderBarElement = homeLoaderRef.value?.loaderBarRef + const introElement = homeIntroRef.value?.introRef + const introTitleElement = homeIntroRef.value?.introTitleRef + const heroElement = homeHeroRef.value?.heroRef if ( !pageElement @@ -97,78 +70,21 @@ onMounted(() => { || !loaderBarElement || !introElement || !introTitleElement - || !heroStageElement || !heroElement - || !heroPanelElement ) { return } const introCharElements = gsap.utils.toArray('.hero-intro-char', introElement) - const tmElement = introElement.querySelector('.hero-intro-tm') + const tmElement = introElement.querySelector('.hero-intro-tm') - if (introCharElements.length === 0 || !tmElement) { - return - } + if (introCharElements.length === 0 || !tmElement) { + return + } const { $lenis } = useNuxtApp() as ReturnType & { $lenis?: Lenis } - const topHeader = document.querySelector('.site-header-top') heroAnimationContext = gsap.context(() => { - const pinEndScale = 0.65 - const finalScale = 0.18 - const getExitDistance = () => window.innerHeight - - const setupHeroScroll = () => { - cleanupHeroScroll?.() - - gsap.set(heroPanelElement, { - scale: 1, - force3D: true, - transformOrigin: 'center center', - }) - - let renderedScale = -1 - - const setHeroPanelScale = (scale: number) => { - if (Math.abs(renderedScale - scale) < 0.001) { - return - } - - renderedScale = scale - gsap.set(heroPanelElement, { scale }) - } - - const updateHeroPanelScale = () => { - const exitDistance = getExitDistance() - const totalDistance = heroPinDistance + exitDistance - // Lenis 存在时只读 Lenis 的平滑滚动值,避免向上滚时 window.scrollY 和 Lenis 值互相打架。 - const currentScroll = $lenis ? $lenis.scroll : window.scrollY - const currentDistance = gsap.utils.clamp(0, totalDistance, currentScroll - heroStageElement.offsetTop) - - if (currentDistance <= heroPinDistance) { - setHeroPanelScale(gsap.utils.interpolate(1, pinEndScale, currentDistance / heroPinDistance)) - return - } - - setHeroPanelScale( - gsap.utils.interpolate( - pinEndScale, - finalScale, - (currentDistance - heroPinDistance) / exitDistance, - ), - ) - } - - // CSS sticky 负责固定;ticker 每帧读取当前滚动距离,避免依赖 scroll/ScrollTrigger 事件链。 - updateHeroPanelScale() - gsap.ticker.add(updateHeroPanelScale) - - cleanupHeroScroll = () => { - gsap.ticker.remove(updateHeroPanelScale) - } - } - const playIntro = () => { if (hasIntroStarted) { return @@ -265,22 +181,6 @@ onMounted(() => { this._headerTriggered = false } }, 'introOut') - // 取消原本通过 index.vue 直接控制导航栏 yPercent 的行为 - // 因为我们要让 Header 自己执行从屏幕底部(y: 100vh)飞入的高级动画 - // if (topHeader) { - // introTimeline.to(topHeader, { - // yPercent: 0, - // autoAlpha: 1, - // duration: 0.55, - // ease: 'power3.out', - // }, 'introOut+=0.45') - // } - - // 在动画即将完成或者 Hero 入场时触发自定义事件,通知导航栏执行自身的从下往上飞入动画 - // 将触发时机再延后,等白色背景开始渲染的瞬间就触发 - // introTimeline.add(() => { - // window.dispatchEvent(new Event('hero-intro-done')) - // }, 'introOut') } const releasePageScroll = () => { @@ -303,7 +203,6 @@ onMounted(() => { gsap.set(introCharElements, { y: 0, yPercent: 110 }) gsap.set(tmElement, { autoAlpha: 0 }) gsap.set(heroElement, { yPercent: 100 }) - setupHeroScroll() loaderTween = gsap.to(loaderBarElement, { scaleX: 0.82, @@ -344,8 +243,6 @@ onBeforeUnmount(() => { loaderTween = null introTimeline?.kill() introTimeline = null - cleanupHeroScroll?.() - cleanupHeroScroll = null removeLoadListener?.() removeLoadListener = null restorePageScroll?.() @@ -353,7 +250,6 @@ onBeforeUnmount(() => { heroAnimationContext?.revert() heroAnimationContext = null }) -