From 439839727aa1a33a7112e5a5ea9b48b5177766b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sun, 2 Aug 2026 17:39:19 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=92=AD=E6=94=BE=E5=99=A8=E6=AD=8C?= =?UTF-8?q?=E8=AF=8D=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/PhotoGallery.vue | 141 ++++++++++-------- .../src/views/admin/AdminEditor.vue | 2 +- vite-tailwindcss/src/views/index/index.vue | 22 +-- 3 files changed, 89 insertions(+), 76 deletions(-) diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 2ddacbd..75eddb8 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -38,23 +38,36 @@ - +
+
+
+ @@ -101,36 +114,28 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue' import { borderClass } from '@/composables/borderStyles' -const STAGGER_STEP_MS = 45 +const STEP_MS = 300 +const SETTLE_MS = 400 const props = defineProps({ page: { type: Object, required: true }, /** 九宫格专用:父级在 section 入视口后设为 true 才挂载展示 */ activate: { type: Boolean, default: false }, - /** 是否开轻量入场动画;关闭时提前预加载,进入窗口即展示 */ + /** 开启:串行 v-reveal;关闭:预加载后直接展示 */ nineGridAnimate: { type: Boolean, default: false }, }) const emit = defineEmits(['preview-image', 'ready']) const nineShownCount = ref(0) -const nineGridEntered = ref(false) let readyEmitted = false -let staggerTimer = null -let enterRaf = 0 let preloadStarted = false +let seqToken = 0 const frameClass = computed(() => borderClass(props.page.borderStyle)) -const clearStagger = () => { - clearTimeout(staggerTimer) - staggerTimer = null - if (enterRaf) { - cancelAnimationFrame(enterRaf) - enterRaf = 0 - } -} +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) -/** 关闭动画时提前暖缓存,进入窗口即可直接出图 */ +/** 关闭动画时提前暖缓存 */ const preloadNineGridImages = () => { if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return const urls = (props.page.images || []).filter(Boolean) @@ -143,47 +148,51 @@ const preloadNineGridImages = () => { } } -const showNineGrid = async () => { - clearStagger() +const emitReady = () => { + if (readyEmitted) return + readyEmitted = true + emit('ready') +} + +/** 每 300ms 只挂载 1 格并触发单格 v-reveal */ +const runSequentialReveal = async () => { + const token = ++seqToken const total = (props.page.images || []).length if (!total) { - readyEmitted = true - emit('ready') + emitReady() return } - if (!props.nineGridAnimate) { - nineShownCount.value = total - nineGridEntered.value = true + nineShownCount.value = 0 + await nextTick() + + for (let i = 0; i < total; i += 1) { + if (token !== seqToken) return + await sleep(STEP_MS) + if (token !== seqToken) return + nineShownCount.value = i + 1 await nextTick() - readyEmitted = true - emit('ready') - return } - // 先挂载(opacity:0),再下一帧加 --in,保证 CSS 过渡生效;无双解码 / 无 v-reveal - nineGridEntered.value = false + if (token !== seqToken) return + await sleep(SETTLE_MS) + if (token !== seqToken) return + emitReady() +} + +const showNineGridInstant = async () => { + const total = (props.page.images || []).length nineShownCount.value = total await nextTick() - enterRaf = requestAnimationFrame(() => { - enterRaf = requestAnimationFrame(() => { - enterRaf = 0 - nineGridEntered.value = true - }) - }) - const wait = Math.min(total, 9) * STAGGER_STEP_MS + 420 - staggerTimer = setTimeout(() => { - if (readyEmitted) return - readyEmitted = true - emit('ready') - }, wait) + emitReady() } watch( () => props.activate && props.page?.type === 'nine-grid', async (on) => { if (!on || readyEmitted) return - await showNineGrid() + if (props.nineGridAnimate) await runSequentialReveal() + else await showNineGridInstant() }, { flush: 'post' } ) @@ -200,7 +209,28 @@ onMounted(() => { if (!props.nineGridAnimate) preloadNineGridImages() }) -onUnmounted(clearStagger) +onUnmounted(() => { + seqToken += 1 +}) + +function getRevealConfig(imgIdx) { + const row = Math.floor(imgIdx / 3) + const col = imgIdx % 3 + + if (imgIdx === 4) { + return { variant: 'scale', delay: 0 } + } + if (col === 1) { + return { + variant: row === 0 ? 'down' : 'up', + delay: 0, + } + } + return { + variant: col === 0 ? 'left' : 'right', + delay: 0, + } +} const singleWrapClass = computed(() => props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]' @@ -261,24 +291,17 @@ function polaroidTransform(i) { height: 100%; object-fit: cover; display: block; - content-visibility: auto; } .nine-grid-slot { background: transparent; contain: layout paint; } -.nine-grid-cell--anim { - opacity: 0; - transform: translate3d(0, 6px, 0); - transition: opacity 0.36s ease, transform 0.36s ease; -} -.nine-grid-cell--anim.nine-grid-cell--in { - opacity: 1; - transform: translate3d(0, 0, 0); -} -.nine-grid-cell:not(.nine-grid-cell--anim) { - opacity: 1; - transform: none; +.nine-grid-placeholder { + visibility: hidden; + pointer-events: none; + background: transparent !important; + border: none !important; + box-shadow: none !important; } .overlap-wrap { diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 4b1c5aa..eb6273b 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -162,7 +162,7 @@ -
关闭:提前预加载、进入窗口即展示且不暂停滚动(推荐 iOS);开启:轻量淡入并短暂停滚。
+
开启:进入视口后暂停滚动,每 300ms 入场一格(方向动效);全部完成后再继续。关闭:直接展示,更稳(推荐 iOS)。
diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 5574a5b..dbb5a37 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -778,9 +778,8 @@ let nineGridRevealTimer = null const nineGridDone = new Set() const BOTTOM_RETURN_DELAY = 30000 const FREE_SCROLL_BASE_STEP = 1.2 -/** 开启动画时,入场结束后短暂停一下再继续自动滚 */ -const NINE_GRID_REVEAL_HOLD_ANIM_MS = 520 -const NINE_GRID_FULL_RATIO = 0.9 +/** ready 后极短缓冲再恢复滚动(串行等待已在 PhotoGallery 内完成) */ +const NINE_GRID_RESUME_DELAY_MS = 200 const freeInterval = computed(() => { const v = Number(data.value.freeScrollInterval) @@ -993,22 +992,17 @@ const canAutoAdvance = () => const beginNineGridGate = (pIdx) => { if (nineGridDone.has(pIdx)) return nineGridDone.add(pIdx) - // 仅开启动画时暂停自动滚动;关闭动画不挡滚动 - if (data.value.nineGridAnimate) nineGridHold.value = true + nineGridHold.value = true nineGridActivate[pIdx] = true } const onNineGridReady = (pIdx) => { if (!nineGridDone.has(pIdx)) return - if (!data.value.nineGridAnimate) { - nineGridHold.value = false - return - } clearTimeout(nineGridRevealTimer) nineGridRevealTimer = setTimeout(() => { nineGridHold.value = false nineGridRevealTimer = null - }, NINE_GRID_REVEAL_HOLD_ANIM_MS) + }, NINE_GRID_RESUME_DELAY_MS) } const bindNineGridObserver = () => { @@ -1017,13 +1011,11 @@ const bindNineGridObserver = () => { if (typeof IntersectionObserver === 'undefined') return const nodes = scrollContainerRef.value?.querySelectorAll('[data-nine-grid]') if (!nodes?.length) return - const animate = !!data.value.nineGridAnimate + // 进入窗口即 activate;期间 nineGridHold 暂停自动滚 nineGridObserver = new IntersectionObserver( (entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue - // 开启动画:等几乎整页入屏;关闭:进入窗口即展示 - if (animate && entry.intersectionRatio < NINE_GRID_FULL_RATIO) continue const raw = entry.target.getAttribute('data-nine-grid') if (raw == null) continue const pIdx = Number(raw) @@ -1031,9 +1023,7 @@ const bindNineGridObserver = () => { beginNineGridGate(pIdx) } }, - animate - ? { threshold: [0, 0.85, 1] } - : { threshold: [0, 0.08, 0.2], rootMargin: '120px 0px' } + { threshold: [0, 0.08, 0.2], rootMargin: '120px 0px' } ) nodes.forEach((el) => nineGridObserver.observe(el)) }