进出动画效果

This commit is contained in:
2026-02-27 16:03:45 +08:00
parent 8478c5bd7f
commit d17e8a48eb
5 changed files with 82 additions and 39 deletions

View File

@@ -14,7 +14,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, useSlots, watch, nextTick } from 'vue' import { ref, computed, useSlots, nextTick } from 'vue'
const slots = useSlots() const slots = useSlots()
const pageCount = computed(() => { const pageCount = computed(() => {
@@ -35,21 +35,34 @@ const wrapperStyle = computed(() => ({
function goTo(index) { function goTo(index) {
if (isAnimating.value || index === currentIndex.value) return if (isAnimating.value || index === currentIndex.value) return
if (index < 0 || index >= pageCount.value) return if (index < 0 || index >= pageCount.value) return
direction.value = index > currentIndex.value ? 'down' : 'up' const prevIndex = currentIndex.value
direction.value = index > prevIndex ? 'down' : 'up'
isAnimating.value = true isAnimating.value = true
triggerLeaveAnimation(prevIndex)
currentIndex.value = index currentIndex.value = index
triggerPageAnimation(index) triggerEnterAnimation(index)
setTimeout(() => { isAnimating.value = false }, 900) setTimeout(() => { isAnimating.value = false }, 900)
} }
function triggerPageAnimation(index) { function triggerLeaveAnimation(index) {
nextTick(() => { nextTick(() => {
const pages = document.querySelectorAll('.fullpage-wrapper > *') const pages = document.querySelectorAll('.fullpage-wrapper > *')
const page = pages[index] const page = pages[index]
if (!page) return if (!page) return
page.classList.remove('page-enter') page.classList.remove('page-leave-up', 'page-leave-down', 'page-enter-up', 'page-enter-down')
void page.offsetWidth void page.offsetWidth
page.classList.add('page-enter') page.classList.add(direction.value === 'down' ? 'page-leave-up' : 'page-leave-down')
})
}
function triggerEnterAnimation(index) {
nextTick(() => {
const pages = document.querySelectorAll('.fullpage-wrapper > *')
const page = pages[index]
if (!page) return
page.classList.remove('page-leave-up', 'page-leave-down', 'page-enter-up', 'page-enter-down')
void page.offsetWidth
page.classList.add(direction.value === 'down' ? 'page-enter-down' : 'page-enter-up')
}) })
} }
@@ -142,26 +155,56 @@ defineExpose({ currentIndex, goTo })
box-shadow: 0 0 8px rgba(0, 191, 165, 0.6); box-shadow: 0 0 8px rgba(0, 191, 165, 0.6);
} }
/* 页面入场动画 - 仅针对非绝对定位元素 */ /* 页面内元素进场/离场动画,通过 .anim-item 标记需要动画的元素 */
:global(.page-enter .text-side),
:global(.page-enter .image-side), :global(.page-enter-down .anim-item) {
:global(.page-enter .content-wrapper) { animation: enterFromBottom 0.95s cubic-bezier(0.22, 1, 0.36, 1) both;
animation: fadeSlideUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; animation-delay: 0.5s;
} }
:global(.page-enter .image-side) { :global(.page-enter-up .anim-item) {
animation-delay: 0.1s; animation: enterFromTop 0.95s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: 0.5s;
} }
@keyframes fadeSlideUp { :global(.page-leave-up .anim-item) {
from { animation: leaveToTop 0.32s cubic-bezier(0.76, 0, 0.24, 1) both;
opacity: 0; }
transform: translateY(32px);
} :global(.page-leave-down .anim-item) {
to { animation: leaveToBottom 0.32s cubic-bezier(0.76, 0, 0.24, 1) both;
opacity: 1; }
transform: translateY(0);
} /* 子元素依次错开出现(仅进场) */
:global(.page-enter-down .anim-item:nth-child(1)),
:global(.page-enter-up .anim-item:nth-child(1)) { animation-delay: 0.5s; }
:global(.page-enter-down .anim-item:nth-child(2)),
:global(.page-enter-up .anim-item:nth-child(2)) { animation-delay: 0.62s; }
:global(.page-enter-down .anim-item:nth-child(3)),
:global(.page-enter-up .anim-item:nth-child(3)) { animation-delay: 0.74s; }
:global(.page-enter-down .anim-item:nth-child(4)),
:global(.page-enter-up .anim-item:nth-child(4)) { animation-delay: 0.86s; }
:global(.page-enter-down .anim-item:nth-child(5)),
:global(.page-enter-up .anim-item:nth-child(5)) { animation-delay: 0.98s; }
@keyframes enterFromBottom {
from { opacity: 0; transform: translateY(36px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes enterFromTop {
from { opacity: 0; transform: translateY(-36px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes leaveToTop {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-24px); }
}
@keyframes leaveToBottom {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(24px); }
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -2,16 +2,16 @@
<section class="page page-advantage"> <section class="page page-advantage">
<div class="content-wrapper"> <div class="content-wrapper">
<div class="image-side"> <div class="image-side">
<img src="/src/assets/images/three-phone.png" class="phone-img" /> <img src="/src/assets/images/three-phone.png" class="phone-img anim-item" />
</div> </div>
<div class="text-side"> <div class="text-side">
<h2 class="section-title"> <h2 class="section-title anim-item">
Every opening is an<br /> Every opening is an<br />
unknown encounter. unknown encounter.
</h2> </h2>
<div class="section-card"> <div class="section-card anim-item">
<img src="/src/assets/images/three-douhao.png" class="douhao-img"> <img src="/src/assets/images/three-douhao.png" class="douhao-img">
<p class="section-desc"> <p class="section-desc anim-item">
Every moment you open it, you embark on a poetic encounter with the unknown, and meet the unexpected beauty. Every moment you open it, you embark on a poetic encounter with the unknown, and meet the unexpected beauty.
</p> </p>
<div class="hero-buttons"> <div class="hero-buttons">

View File

@@ -2,16 +2,16 @@
<section class="page page-contact"> <section class="page page-contact">
<div class="content-wrapper"> <div class="content-wrapper">
<div class="text-side"> <div class="text-side">
<h2 class="section-title"> <h2 class="section-title anim-item">
Customize Keyboard<br /> Customize Keyboard<br />
AI Reply . AI Reply .
</h2> </h2>
<p class="section-desc"> <p class="section-desc anim-item">
Personalize your chat keyboard and get tailored, high-EQ responses for every conversation Personalize your chat keyboard and get tailored, high-EQ responses for every conversation
</p> </p>
</div> </div>
<div class="image-side"> <div class="image-side">
<img src="/src/assets/images/four-phone.png" class="phone-img" /> <img src="/src/assets/images/four-phone.png" class="phone-img anim-item" />
</div> </div>
</div> </div>

View File

@@ -6,16 +6,16 @@
<div class="hero"> <div class="hero">
<div class="hero-phones"> <div class="hero-phones">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title"> <h1 class="hero-title anim-item">
Not just a tool but<br /> Not just a tool but<br />
giving meaning to every click giving meaning to every click
</h1> </h1>
<div class="hero-buttons"> <div class="hero-buttons anim-item">
<a href="#"><img src="/src/assets/images/android.png" class="btn-img" /></a> <a href="#"><img src="/src/assets/images/android.png" class="btn-img" /></a>
<a href="#"><img src="/src/assets/images/appStore.png" class="btn-img" /></a> <a href="#"><img src="/src/assets/images/appStore.png" class="btn-img" /></a>
</div> </div>
</div> </div>
<img src="/src/assets/images/bg-top-phone.png" class="phone-main" /> <img src="/src/assets/images/bg-top-phone.png" class="phone-main anim-item" />
</div> </div>
<div class="deco deco-coin1">🎁</div> <div class="deco deco-coin1">🎁</div>
<div class="deco deco-coin2">🏆</div> <div class="deco deco-coin2">🏆</div>
@@ -131,14 +131,14 @@
line-height: 1.3; line-height: 1.3;
color: #1a1a1a; color: #1a1a1a;
margin-bottom: clamp(16px, 2vh, 40px); margin-bottom: clamp(16px, 2vh, 40px);
animation: fadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.1s both; animation: none;
} }
.hero-buttons { .hero-buttons {
display: flex; display: flex;
gap: clamp(10px, 3vw, 35px); gap: clamp(10px, 3vw, 35px);
justify-content: center; justify-content: center;
animation: fadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.25s both; animation: none;
} }
.btn-img { .btn-img {
@@ -196,7 +196,7 @@
height: clamp(330px, 58vh, 780px); height: clamp(330px, 58vh, 780px);
width: auto; width: auto;
display: block; display: block;
animation: fadeIn 1s cubic-bezier(0.22, 1, 0.36, 1) 0.35s both; animation: none;
} }
@keyframes fadeIn { @keyframes fadeIn {

View File

@@ -4,7 +4,7 @@
<div class="content-wrapper"> <div class="content-wrapper">
<div class="text-side"> <div class="text-side">
<div class="title-wrap"> <div class="title-wrap">
<h2 class="section-title"> <h2 class="section-title anim-item">
Customize Keyboard<br /> Customize Keyboard<br />
Define Chat Style Define Chat Style
</h2> </h2>
@@ -12,13 +12,13 @@
<img src="/src/assets/images/two-2.png" class="deco-2" /> <img src="/src/assets/images/two-2.png" class="deco-2" />
</div> </div>
<div class="desc-wrap"> <div class="desc-wrap">
<p class="section-desc"> <p class="section-desc anim-item">
Spice up your keyboard with one-of-a-kind skins and customize its look to the fullestyour chat style is one-of-a-kind, and your keyboard should be too. Spice up your keyboard with one-of-a-kind skins and customize its look to the fullestyour chat style is one-of-a-kind, and your keyboard should be too.
</p> </p>
</div> </div>
<div class="btns-wrap"> <div class="btns-wrap">
<div class="download-btns"> <div class="download-btns anim-item">
<a href="#"><img src="/src/assets/images/android-two.png" class="btn-img" /></a> <a href="#"><img src="/src/assets/images/android-two.png" class="btn-img" /></a>
<a href="#"><img src="/src/assets/images/appStore-two.png" class="btn-img" /></a> <a href="#"><img src="/src/assets/images/appStore-two.png" class="btn-img" /></a>
@@ -30,7 +30,7 @@
</div> </div>
</div> </div>
<img src="/src/assets/images/two-phone.png" class="two-phone" /> <img src="/src/assets/images/two-phone.png" class="two-phone anim-item" />
</section> </section>
</template> </template>