feat: 初始化Arctic New One项目基础架构

添加项目基础文件结构,包括:
- 国际化多语言支持
- Tailwind CSS配置
- 自定义布局组件
- 平滑滚动和动画效果
- 自定义光标交互
- 错误页面处理
This commit is contained in:
2026-04-26 19:03:48 +08:00
parent 03ce24e689
commit fc52afda99
16 changed files with 2467 additions and 20 deletions
+212
View File
@@ -0,0 +1,212 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const { t } = useI18n()
// 背景图是装饰层,左右两列共用同一套模板,只通过 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 heroRef = ref<HTMLElement | null>(null)
const heroPanelRef = ref<HTMLElement | null>(null)
const nextSectionRef = ref<HTMLElement | null>(null)
let heroAnimationContext: gsap.Context | null = null
onMounted(() => {
if (!heroRef.value || !nextSectionRef.value) {
return
}
heroAnimationContext = gsap.context(() => {
// pinDistance 控制 Hero 固定阶段的滚动长度;scrollScrub 控制滚动追随的柔和程度。
const pinDistance = 1200
const scrollScrub = 0.65
gsap.set(heroPanelRef.value, {
scale: 1,
transformOrigin: 'center center',
})
// 第一段:Hero 停在屏幕中,前景白色面板先缩小,露出背后的图片列。
gsap.timeline({
scrollTrigger: {
trigger: heroRef.value,
start: 'top top',
end: `+=${pinDistance}`,
scrub: scrollScrub,
pin: true,
anticipatePin: 1,
},
})
.to(heroPanelRef.value, {
scale: 0.65,
ease: 'none',
})
// 第二段:取消 pin 后,下一个板块开始进入,白色面板继续缩小。
gsap.fromTo(heroPanelRef.value, {
scale: 0.65,
}, {
scale: 0.18,
ease: 'none',
immediateRender: false,
scrollTrigger: {
trigger: nextSectionRef.value,
start: 'top bottom',
end: 'top top',
scrub: scrollScrub,
},
})
}, heroRef.value)
ScrollTrigger.refresh()
})
onBeforeUnmount(() => {
heroAnimationContext?.revert()
heroAnimationContext = null
})
</script>
<template>
<main id="top">
<section ref="heroRef" class="hero-section">
<div class="hero-backdrop" aria-hidden="true">
<div
v-for="column in heroGalleryColumns"
:key="column.side"
class="hero-gallery"
:class="`hero-gallery-${column.side}`"
>
<!-- 复制两份图片组配合 translateY(-50%) 做无缝循环 -->
<div
class="hero-gallery-track"
:class="{ 'hero-gallery-track-slow': column.side === 'right' }"
>
<div v-for="copy in 2" :key="`${column.side}-copy-${copy}`" class="hero-gallery-group">
<img
v-for="image in column.images"
:key="`${column.side}-${copy}-${image}`"
class="hero-gallery-image"
:src="image"
alt=""
decoding="async"
loading="lazy"
/>
</div>
</div>
</div>
</div>
<div ref="heroPanelRef" class="hero-panel">
<p>Hero Section</p>
</div>
</section>
<section ref="nextSectionRef" class="next-section">
<h2>Next Section</h2>
</section>
<section id="work" class="work-section" data-cursor-label="Read more">
<p>{{ t('home.welcome') }}</p>
</section>
</main>
</template>
<style scoped>
.hero-section {
position: relative;
height: 100svh;
overflow: hidden;
background: #111;
}
.hero-backdrop {
position: absolute;
inset: 0;
overflow: hidden;
background: #000;
}
.hero-gallery {
position: absolute;
top: -40px;
bottom: -40px;
width: 49vw;
overflow: hidden;
}
.hero-gallery-left {
left: 0;
}
.hero-gallery-right {
right: 0;
}
.hero-gallery-track {
will-change: transform;
animation: hero-gallery-scroll 28s linear infinite;
}
.hero-gallery-track-slow {
animation-duration: 42s;
}
.hero-gallery-group {
display: grid;
gap: 36px;
padding-bottom: 36px;
}
.hero-gallery-image {
display: block;
width: 100%;
height: clamp(260px, 42vh, 430px);
border-radius: 0;
object-fit: cover;
}
@keyframes hero-gallery-scroll {
to {
transform: translateY(-50%);
}
}
.hero-panel {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: #fff;
font-size: 48px;
will-change: transform;
}
.next-section,
.work-section {
min-height: 100svh;
display: grid;
place-items: center;
}
</style>