恢复滚动方案
This commit is contained in:
@@ -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); }
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user