更新页面效果

This commit is contained in:
李琦
2026-08-01 09:48:16 +08:00
parent 832be572cf
commit 96616b9406
3 changed files with 45 additions and 42 deletions

View File

@@ -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" />

View File

@@ -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

View File

@@ -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) => {
// 带时长的平滑滚动easeInOutQuadduration 由后台 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;