渐显动画

This commit is contained in:
2026-02-27 15:37:57 +08:00
parent 4d73a6dbf0
commit 8478c5bd7f
3 changed files with 117 additions and 37 deletions

View File

@@ -54,6 +54,11 @@ function handleNavigate(index) {
align-items: center;
padding: 0 @nav-px;
z-index: 1000;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
transition: background 0.3s ease;
}
.nav-logo {

View File

@@ -6,13 +6,15 @@
</div>
<div class="fullpage-dots">
<span v-for="i in pageCount" :key="i" class="dot" :class="{ active: currentIndex === i - 1 }"
@click="goTo(i - 1)" />
@click="goTo(i - 1)">
<span class="dot-inner" />
</span>
</div>
</div>
</template>
<script setup>
import { ref, computed, useSlots } from 'vue'
import { ref, computed, useSlots, watch, nextTick } from 'vue'
const slots = useSlots()
const pageCount = computed(() => {
@@ -23,29 +25,38 @@ const pageCount = computed(() => {
const currentIndex = ref(0)
const isAnimating = ref(false)
const touchStartY = ref(0)
const direction = ref('down')
const wrapperStyle = computed(() => ({
transform: `translateY(-${currentIndex.value * 100}vh)`,
transition: isAnimating.value ? 'transform 0.8s cubic-bezier(0.65, 0, 0.35, 1)' : 'none',
transition: isAnimating.value ? 'transform 0.9s cubic-bezier(0.76, 0, 0.24, 1)' : 'none',
}))
function goTo(index) {
if (isAnimating.value || index === currentIndex.value) return
if (index < 0 || index >= pageCount.value) return
direction.value = index > currentIndex.value ? 'down' : 'up'
isAnimating.value = true
currentIndex.value = index
setTimeout(() => {
isAnimating.value = false
}, 800)
triggerPageAnimation(index)
setTimeout(() => { isAnimating.value = false }, 900)
}
function triggerPageAnimation(index) {
nextTick(() => {
const pages = document.querySelectorAll('.fullpage-wrapper > *')
const page = pages[index]
if (!page) return
page.classList.remove('page-enter')
void page.offsetWidth
page.classList.add('page-enter')
})
}
function handleWheel(e) {
if (isAnimating.value) return
if (e.deltaY > 0) {
goTo(currentIndex.value + 1)
} else if (e.deltaY < 0) {
goTo(currentIndex.value - 1)
}
if (e.deltaY > 0) goTo(currentIndex.value + 1)
else if (e.deltaY < 0) goTo(currentIndex.value - 1)
}
function handleTouchStart(e) {
@@ -55,11 +66,8 @@ function handleTouchStart(e) {
function handleTouchEnd(e) {
const deltaY = touchStartY.value - e.changedTouches[0].clientY
if (Math.abs(deltaY) > 50) {
if (deltaY > 0) {
goTo(currentIndex.value + 1)
} else {
goTo(currentIndex.value - 1)
}
if (deltaY > 0) goTo(currentIndex.value + 1)
else goTo(currentIndex.value - 1)
}
}
@@ -79,17 +87,83 @@ defineExpose({ currentIndex, goTo })
will-change: transform;
}
/* 圆点导航升级 */
.fullpage-dots {
position: fixed;
right: 30px;
right: 28px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 12px;
gap: 16px;
z-index: 100;
}
.dot {
width: 12px;
height: 12px;
position: relative;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.dot::before {
content: '';
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
border: 1.5px solid rgba(0, 0, 0, 0.2);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dot-inner {
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
display: block;
}
.dot.active::before {
border-color: #00BFA5;
width: 20px;
height: 20px;
box-shadow: 0 0 0 3px rgba(0, 191, 165, 0.15);
}
.dot.active .dot-inner {
width: 6px;
height: 6px;
background: #00BFA5;
box-shadow: 0 0 8px rgba(0, 191, 165, 0.6);
}
/* 页面入场动画 - 仅针对非绝对定位元素 */
:global(.page-enter .text-side),
:global(.page-enter .image-side),
:global(.page-enter .content-wrapper) {
animation: fadeSlideUp 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
:global(.page-enter .image-side) {
animation-delay: 0.1s;
}
@keyframes fadeSlideUp {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.fullpage-dots {
right: unset;
@@ -99,22 +173,9 @@ defineExpose({ currentIndex, goTo })
transform: translateX(-50%);
flex-direction: row;
}
.dot.active::before {
width: 16px;
height: 16px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.dot.active {
background: #00BFA5;
border-color: #00BFA5;
transform: scale(1.3);
box-shadow: 0 0 8px rgba(0, 191, 165, 0.4);
}
</style>

View File

@@ -131,21 +131,24 @@
line-height: 1.3;
color: #1a1a1a;
margin-bottom: clamp(16px, 2vh, 40px);
animation: fadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.1s both;
}
.hero-buttons {
display: flex;
gap: clamp(10px, 3vw, 35px);
justify-content: center;
animation: fadeIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.25s both;
}
.btn-img {
height: clamp(36px, 6.6vh, 120px);
width: auto;
display: block;
transition: transform 0.3s;
transition: transform 0.3s, filter 0.3s;
&:hover {
transform: translateY(-2px);
transform: translateY(-3px) scale(1.04);
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.15));
}
}
@@ -193,6 +196,17 @@
height: clamp(330px, 58vh, 780px);
width: auto;
display: block;
animation: fadeIn 1s cubic-bezier(0.22, 1, 0.36, 1) 0.35s both;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
.phone-placeholder {