更新页面效果
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 九宫格 -->
|
||||
<div v-else-if="page.type === 'nine-grid'" v-reveal="{ variant: 'up', delay: 80 }" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||
<!-- <div v-else-if="page.type === 'nine-grid'" v-reveal="{ variant: 'up', delay: 80 }" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">-->
|
||||
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
|
||||
class="aspect-square relative" :class="frameClass">
|
||||
<img :src="img" loading="lazy" decoding="async" class="nine-grid-img" />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
/**
|
||||
* v-reveal 滚动入场指令
|
||||
*
|
||||
@@ -28,6 +26,19 @@ function parseBinding(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function findScrollRoot(el) {
|
||||
let parent = el.parentElement
|
||||
while (parent && parent !== document.body) {
|
||||
const style = window.getComputedStyle(parent)
|
||||
const overflowY = style.overflowY
|
||||
if ((overflowY === 'auto' || overflowY === 'scroll') && parent.scrollHeight > parent.clientHeight) {
|
||||
return parent
|
||||
}
|
||||
parent = parent.parentElement
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const reveal = {
|
||||
mounted(el, binding) {
|
||||
const { variant, delay } = parseBinding(binding.value)
|
||||
@@ -43,7 +54,7 @@ const reveal = {
|
||||
observer.unobserve(el)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
|
||||
{ threshold: 0.12, root: findScrollRoot(el), rootMargin: '0px 0px -8% 0px' }
|
||||
)
|
||||
observer.observe(el)
|
||||
el.__revealObserver = observer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="wrapper relative w-full min-h-[100vh] bg-[#Fdfbf7] overflow-x-hidden text-[#2c2c2c]">
|
||||
<div class="wrapper relative w-full h-[100vh] bg-[#Fdfbf7] overflow-hidden text-[#2c2c2c]">
|
||||
|
||||
<!-- 入场揭幕帘幕(请柬封面) -->
|
||||
<div v-show="showIntro" class="curtain-root" :class="[{ 'is-opening': introOpening }, introExitClass]" @click="openInvitation">
|
||||
@@ -33,10 +33,10 @@
|
||||
<CanvasEffects :petal-enabled="data.petalEnabled" :bubble-enabled="data.bubbleEnabled" />
|
||||
|
||||
<!-- 顶部滚动进度条 -->
|
||||
<div class="fixed top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
|
||||
<div class="absolute top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
|
||||
|
||||
<!-- 右上角控制台 -->
|
||||
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
|
||||
<div class="absolute top-6 right-5 z-50 flex flex-col items-end gap-4">
|
||||
<!-- 音乐控制 -->
|
||||
<div class="music-wrap">
|
||||
<div class="flex flex-col gap-3 items-end">
|
||||
@@ -70,7 +70,9 @@
|
||||
|
||||
<!-- 主滚动区域(注意:不要加 scroll-smooth,它会劫持 JS 滚动赋值导致卡顿) -->
|
||||
<div ref="scrollContainerRef"
|
||||
class="w-full min-h-[100vh] no-scrollbar relative"
|
||||
class="w-full h-full overflow-y-auto no-scrollbar relative"
|
||||
:class="data.scrollMode === 'snap' ? 'snap-y snap-mandatory' : ''"
|
||||
@scroll="handleScroll"
|
||||
@touchstart="handleUserInteraction" @touchmove="handleUserInteraction"
|
||||
@wheel="handleUserInteraction" @mousedown="handleUserInteraction">
|
||||
|
||||
@@ -250,8 +252,8 @@
|
||||
</div>
|
||||
|
||||
<!-- RSVP 抽屉 -->
|
||||
<div v-if="isDrawerOpen" class="fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm transition-opacity duration-500" @click="closeDrawer"></div>
|
||||
<div class="fixed bottom-0 left-0 w-full bg-white rounded-t-[2.5rem] shadow-2xl transition-transform duration-500 z-[70] p-10 flex flex-col" :style="{ transform: isDrawerOpen ? 'translateY(0)' : 'translateY(100%)' }">
|
||||
<div v-if="isDrawerOpen" class="absolute inset-0 z-[60] bg-black/40 backdrop-blur-sm transition-opacity duration-500" @click="closeDrawer"></div>
|
||||
<div class="absolute bottom-0 left-0 w-full bg-white rounded-t-[2.5rem] shadow-2xl transition-transform duration-500 z-[70] p-10 flex flex-col" :style="{ transform: isDrawerOpen ? 'translateY(0)' : 'translateY(100%)' }">
|
||||
<div @click="closeDrawer" class="absolute top-6 right-6 p-2 text-[#a88c6b] cursor-pointer hover:text-[#2c2c2c] transition-colors text-xl">✕</div>
|
||||
<span class="text-xl text-center text-[#a88c6b] mb-1 tracking-[0.3em] font-light mt-2">出席回执</span>
|
||||
<span class="text-center text-[10px] text-[#8A8680] mb-10 tracking-[0.4em]">敬 请 回 复</span>
|
||||
@@ -282,7 +284,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMapOptions" class="fixed inset-0 z-[80] bg-black/35 backdrop-blur-sm flex items-end justify-center px-4 pb-5" @click.self="showMapOptions = false">
|
||||
<div v-if="showMapOptions" class="absolute inset-0 z-[80] bg-black/35 backdrop-blur-sm flex items-end justify-center px-4 pb-5" @click.self="showMapOptions = false">
|
||||
<div class="map-sheet">
|
||||
<div class="map-sheet-title">选择导航方式</div>
|
||||
<button type="button" @click="openMap('amap')"><i class="fa-solid fa-location-arrow"></i>高德地图</button>
|
||||
@@ -393,11 +395,11 @@ const formalPageHeightStyle = computed(() => sectionHeightStyle(data.value.forma
|
||||
const pageHeightStyle = (page) => sectionHeightStyle(page.height)
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
const pageScrollMax = () => Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||
const handleScroll = () => {
|
||||
const max = pageScrollMax()
|
||||
progress.value = max > 0 ? pageScrollTop() / max : 0
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
const max = el.scrollHeight - el.clientHeight
|
||||
progress.value = max > 0 ? el.scrollTop / max : 0
|
||||
if (isAtBottom()) scheduleBottomReturn()
|
||||
else clearBottomReturn()
|
||||
}
|
||||
@@ -413,7 +415,10 @@ const handleUserInteraction = () => {
|
||||
|
||||
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
|
||||
|
||||
const isAtBottom = () => pageScrollMax() - pageScrollTop() <= 8
|
||||
const isAtBottom = () => {
|
||||
const el = scrollContainerRef.value
|
||||
return !!el && el.scrollHeight - el.clientHeight - el.scrollTop <= 8
|
||||
}
|
||||
|
||||
const clearBottomReturn = () => {
|
||||
clearTimeout(bottomReturnTimer)
|
||||
@@ -421,7 +426,8 @@ const clearBottomReturn = () => {
|
||||
}
|
||||
|
||||
const scheduleBottomReturn = (reset = false) => {
|
||||
if (!isAtBottom() || !isAutoScroll.value) return
|
||||
const el = scrollContainerRef.value
|
||||
if (!el || !isAtBottom() || !isAutoScroll.value) return
|
||||
if (bottomReturnTimer && !reset) return
|
||||
clearBottomReturn()
|
||||
bottomReturnTimer = setTimeout(() => {
|
||||
@@ -437,22 +443,26 @@ const scheduleBottomReturn = (reset = false) => {
|
||||
|
||||
// 带时长的平滑滚动(easeInOutQuad),duration 由后台 pageSwitchDuration 控制
|
||||
const smoothScrollTo = (target, duration) => {
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
cancelAnimationFrame(animId)
|
||||
const start = pageScrollTop(), dist = target - start
|
||||
const start = el.scrollTop, dist = target - start
|
||||
if (!dist) return
|
||||
if (duration <= 0) { window.scrollTo(0, target); return }
|
||||
if (duration <= 0) { el.scrollTop = target; return }
|
||||
const t0 = performance.now()
|
||||
const tick = (now) => {
|
||||
const p = Math.min(1, (now - t0) / duration)
|
||||
const e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2
|
||||
window.scrollTo(0, start + dist * e)
|
||||
el.scrollTop = start + dist * e
|
||||
if (p < 1) animId = requestAnimationFrame(tick)
|
||||
}
|
||||
animId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
const goNextSection = () => {
|
||||
const top = pageScrollTop()
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
const top = el.scrollTop
|
||||
const next = sectionEls().find((e) => e.offsetTop > top + 12)
|
||||
if (next) {
|
||||
smoothScrollTo(next.offsetTop, switchDuration.value)
|
||||
@@ -481,7 +491,7 @@ const startAutoScroll = () => {
|
||||
if (isAtBottom()) {
|
||||
scheduleBottomReturn()
|
||||
} else {
|
||||
window.scrollTo(0, pageScrollTop() + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
|
||||
scrollContainerRef.value.scrollTop += FREE_SCROLL_BASE_STEP * (dt / freeInterval.value)
|
||||
if (isAtBottom()) scheduleBottomReturn()
|
||||
}
|
||||
}
|
||||
@@ -491,10 +501,6 @@ const startAutoScroll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const syncPageScrollMode = () => {
|
||||
document.documentElement.classList.toggle('page-scroll-snap', data.value.scrollMode === 'snap')
|
||||
}
|
||||
|
||||
const scheduleAutoScrollStart = () => {
|
||||
clearTimeout(scrollStartTimer)
|
||||
scrollStartTimer = setTimeout(() => startAutoScroll(), firstScreenDuration.value)
|
||||
@@ -574,9 +580,6 @@ const handleSubmitRsvp = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if ('scrollRestoration' in history) history.scrollRestoration = 'manual'
|
||||
window.scrollTo(0, 0)
|
||||
|
||||
try {
|
||||
const res = await getConfig()
|
||||
if (res.data && res.data !== '{}') {
|
||||
@@ -597,8 +600,6 @@ onMounted(async () => {
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
|
||||
syncPageScrollMode()
|
||||
|
||||
if (data.value.introEnabled) showIntro.value = true
|
||||
|
||||
audio.setTracks(data.value.musicList, data.value.activeMusicUrl)
|
||||
@@ -613,13 +614,10 @@ onMounted(async () => {
|
||||
scheduleAutoScrollStart()
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove('page-scroll-snap')
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(animId)
|
||||
clearInterval(snapTimer)
|
||||
@@ -630,7 +628,6 @@ onUnmounted(() => {
|
||||
clearBottomReturn()
|
||||
})
|
||||
|
||||
watch(() => data.value.scrollMode, syncPageScrollMode)
|
||||
watch(freeScrollSignature, restartAutoScroll)
|
||||
</script>
|
||||
|
||||
@@ -640,12 +637,6 @@ watch(freeScrollSignature, restartAutoScroll)
|
||||
font-variant-numeric: lining-nums;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
:global(html.page-scroll-snap) {
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
:global(html.page-scroll-snap body) {
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
.wrapper .font-serif { font-family: var(--font-serif); }
|
||||
.wrapper .font-sans { font-family: var(--font-sans); }
|
||||
.control-btn {
|
||||
@@ -658,7 +649,7 @@ watch(freeScrollSignature, restartAutoScroll)
|
||||
}
|
||||
.formal-main {
|
||||
font-family: var(--font-calligraphy);
|
||||
font-size: 25px;
|
||||
font-size: 24px;
|
||||
line-height: 1.9;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.32em;
|
||||
|
||||
Reference in New Issue
Block a user