![]()
+
+
+
![]()
@@ -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))
}