恢复滚动方案

This commit is contained in:
李琦
2026-08-01 15:00:15 +08:00
parent f65000336a
commit 8fea25a5fc
2 changed files with 139 additions and 14 deletions

View File

@@ -38,13 +38,19 @@
</div>
</div>
<!-- 九宫格保留方向性 v-reveal 新动画 -->
<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"
v-reveal="getRevealConfig(imgIdx)"
class="aspect-square relative" :class="frameClass">
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" />
</div>
<!-- 九宫格全入视口 activate 后再挂载/加载方向性 v-reveal -->
<div v-else-if="page.type === 'nine-grid'" ref="nineGridRef" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
<template v-if="activate">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
v-reveal="getRevealConfig(imgIdx)"
class="aspect-square relative" :class="frameClass">
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" />
</div>
</template>
<template v-else>
<div v-for="(_, imgIdx) in (page.images || [])" :key="`ph-${imgIdx}`"
class="aspect-square relative nine-grid-placeholder" :class="frameClass" aria-hidden="true" />
</template>
</div>
<!-- 瀑布流 -->
@@ -83,16 +89,57 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import { borderClass } from '@/composables/borderStyles'
const DECODE_TIMEOUT_MS = 4000
const props = defineProps({
page: { type: Object, required: true },
/** 九宫格专用:父级在 section 全入视口后设为 true 才加载 */
activate: { type: Boolean, default: false },
})
const emit = defineEmits(['preview-image'])
const emit = defineEmits(['preview-image', 'ready'])
const nineGridRef = ref(null)
let readyEmitted = false
const frameClass = computed(() => borderClass(props.page.borderStyle))
const waitImagesReady = async () => {
await nextTick()
const root = nineGridRef.value
if (!root) return
const imgs = [...root.querySelectorAll('img[src]')]
if (!imgs.length) return
const decodeAll = Promise.allSettled(
imgs.map((img) => {
if (typeof img.decode === 'function') return img.decode().catch(() => {})
if (img.complete) return Promise.resolve()
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true })
img.addEventListener('error', resolve, { once: true })
})
})
)
await Promise.race([
decodeAll,
new Promise((resolve) => setTimeout(resolve, DECODE_TIMEOUT_MS)),
])
}
watch(
() => props.activate && props.page?.type === 'nine-grid',
async (on) => {
if (!on || readyEmitted) return
await waitImagesReady()
if (readyEmitted) return
readyEmitted = true
emit('ready')
},
{ flush: 'post' }
)
const singleWrapClass = computed(() =>
props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
)
@@ -174,6 +221,9 @@ function getRevealConfig(imgIdx) {
backface-visibility: hidden;
}
.nine-grid-img:hover { transform: translateZ(0) scale(1.03); }
.nine-grid-placeholder {
background: rgba(247, 246, 242, 0.85);
}
.overlap-wrap { height: clamp(320px, 54vh, 460px); }

View File

@@ -124,12 +124,21 @@
</div>
<!-- 3-N. 画廊循环 -->
<div v-for="(page, pIdx) in data.photoPages" :key="pIdx" class="page-section p-6 relative" :class="data.scrollMode === 'snap' ? 'snap-start' : ''" :style="pageHeightStyle(page)">
<div v-for="(page, pIdx) in data.photoPages" :key="pIdx"
class="page-section p-6 relative"
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
:style="pageHeightStyle(page)"
:data-nine-grid="page.type === 'nine-grid' ? String(pIdx) : undefined">
<div class="absolute inset-0 bg-white/40 -z-10"></div>
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
<PhotoGallery :page="page" @preview-image="openImagePreview" />
<PhotoGallery
:page="page"
:activate="page.type === 'nine-grid' ? !!nineGridActivate[pIdx] : false"
@preview-image="openImagePreview"
@ready="onNineGridReady(pIdx)"
/>
</div>
</div>
@@ -332,13 +341,20 @@ const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMapOptions = ref(false)
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
const albumPreviewImages = ref([])
const nineGridActivate = reactive({})
const nineGridHold = ref(false)
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
const rsvpOptions = [ { value: '1', label: '1 人出席' }, { value: '2', label: '2 人出席' }, { value: '3', label: '3 人及以上' }, { value: '0', label: '遗憾缺席' } ]
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
let freeScrollLastTs = 0
let nineGridObserver = null
let nineGridRevealTimer = null
const nineGridDone = new Set()
const BOTTOM_RETURN_DELAY = 30000
const FREE_SCROLL_BASE_STEP = 1.2
const NINE_GRID_REVEAL_HOLD_MS = 900
const NINE_GRID_FULL_RATIO = 0.9
const freeInterval = computed(() => {
const v = Number(data.value.freeScrollInterval)
@@ -538,6 +554,52 @@ const goNextSection = () => {
scheduleBottomReturn()
}
const canAutoAdvance = () =>
isAutoScroll.value
&& !isInteracting.value
&& !isDrawerOpen.value
&& !previewVisible.value
&& !showMapOptions.value
&& !nineGridHold.value
const beginNineGridGate = (pIdx) => {
if (nineGridDone.has(pIdx)) return
nineGridDone.add(pIdx)
nineGridHold.value = true
nineGridActivate[pIdx] = true
}
const onNineGridReady = (pIdx) => {
if (!nineGridDone.has(pIdx)) return
clearTimeout(nineGridRevealTimer)
nineGridRevealTimer = setTimeout(() => {
nineGridHold.value = false
nineGridRevealTimer = null
}, NINE_GRID_REVEAL_HOLD_MS)
}
const bindNineGridObserver = () => {
nineGridObserver?.disconnect()
nineGridObserver = null
if (typeof IntersectionObserver === 'undefined') return
const nodes = scrollContainerRef.value?.querySelectorAll('[data-nine-grid]')
if (!nodes?.length) return
nineGridObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.intersectionRatio < NINE_GRID_FULL_RATIO) continue
const raw = entry.target.getAttribute('data-nine-grid')
if (raw == null) continue
const pIdx = Number(raw)
if (!Number.isFinite(pIdx) || nineGridDone.has(pIdx)) continue
beginNineGridGate(pIdx)
}
},
{ threshold: [0, 0.85, 1] }
)
nodes.forEach((el) => nineGridObserver.observe(el))
}
const startAutoScroll = () => {
clearTimeout(scrollStartTimer)
clearInterval(snapTimer)
@@ -545,16 +607,16 @@ const startAutoScroll = () => {
cancelAnimationFrame(rafId)
rafId = null
if (data.value.scrollMode === 'snap') {
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value) goNextSection()
if (canAutoAdvance()) goNextSection()
snapTimer = setInterval(() => {
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value) goNextSection()
if (canAutoAdvance()) goNextSection()
}, 4500)
} else {
freeScrollLastTs = 0
const tickFreeScroll = (now) => {
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
freeScrollLastTs = now
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value && !previewVisible.value && !showMapOptions.value) {
if (canAutoAdvance()) {
if (isAtBottom()) {
scheduleBottomReturn()
} else {
@@ -689,6 +751,7 @@ onMounted(async () => {
}
await nextTick()
bindNineGridObserver()
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
})
@@ -696,6 +759,10 @@ onMounted(async () => {
onUnmounted(() => {
document.documentElement.classList.remove('page-scroll-snap')
window.removeEventListener('scroll', handleScroll)
nineGridObserver?.disconnect()
nineGridObserver = null
clearTimeout(nineGridRevealTimer)
nineGridRevealTimer = null
cancelAnimationFrame(rafId)
cancelAnimationFrame(animId)
clearInterval(snapTimer)
@@ -707,6 +774,14 @@ onUnmounted(() => {
})
watch(() => data.value.scrollMode, syncPageScrollMode)
watch(
() => data.value.photoPages,
async () => {
await nextTick()
bindNineGridObserver()
},
{ deep: true }
)
watch(freeScrollSignature, restartAutoScroll)
watch(
() => [isDrawerOpen.value, previewVisible.value, showMapOptions.value],