182 lines
3.9 KiB
Vue
182 lines
3.9 KiB
Vue
<template>
|
|
<div class="fullpage-container" @wheel.prevent="handleWheel" @touchstart="handleTouchStart"
|
|
@touchend="handleTouchEnd">
|
|
<div class="fullpage-wrapper" :style="wrapperStyle">
|
|
<slot />
|
|
</div>
|
|
<div class="fullpage-dots">
|
|
<span v-for="i in pageCount" :key="i" class="dot" :class="{ active: currentIndex === i - 1 }"
|
|
@click="goTo(i - 1)">
|
|
<span class="dot-inner" />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, useSlots, watch, nextTick } from 'vue'
|
|
|
|
const slots = useSlots()
|
|
const pageCount = computed(() => {
|
|
const defaultSlot = slots.default?.()
|
|
return defaultSlot ? defaultSlot.length : 0
|
|
})
|
|
|
|
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.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
|
|
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)
|
|
}
|
|
|
|
function handleTouchStart(e) {
|
|
touchStartY.value = e.touches[0].clientY
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
defineExpose({ currentIndex, goTo })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fullpage-container {
|
|
width: 100%;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.fullpage-wrapper {
|
|
width: 100%;
|
|
will-change: transform;
|
|
}
|
|
|
|
/* 圆点导航升级 */
|
|
.fullpage-dots {
|
|
position: fixed;
|
|
right: 28px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
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;
|
|
top: unset;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
flex-direction: row;
|
|
}
|
|
.dot.active::before {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
}
|
|
</style>
|