九宫格优化

This commit is contained in:
李琦
2026-08-01 15:16:13 +08:00
parent 58aabe517a
commit c3b4eeb4c4
2 changed files with 74 additions and 35 deletions

View File

@@ -38,19 +38,27 @@
</div>
</div>
<!-- 九宫格 activate 用透明占位撑布局全入后再挂载真图 + 方向性 v-reveal -->
<!-- 九宫格透明占位撑布局activate 后按行分批挂载真图减轻主线程尖峰 -->
<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
v-for="(img, imgIdx) in (page.images || [])"
:key="imgIdx"
class="aspect-square relative"
:class="[frameClass, imgIdx < nineShownCount ? '' : 'nine-grid-placeholder']"
>
<div
v-if="imgIdx < nineShownCount"
v-reveal="getRevealConfig(imgIdx)"
class="absolute inset-0"
>
<img
:src="img"
:data-nine-idx="imgIdx"
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>
</div>
<!-- 瀑布流 -->
@@ -92,7 +100,9 @@
import { ref, computed, watch, nextTick } from 'vue'
import { borderClass } from '@/composables/borderStyles'
const DECODE_TIMEOUT_MS = 4000
const BATCH_SIZE = 3
const BATCH_GAP_MS = 140
const BATCH_DECODE_TIMEOUT_MS = 2500
const props = defineProps({
page: { type: Object, required: true },
@@ -102,37 +112,65 @@ const props = defineProps({
const emit = defineEmits(['preview-image', 'ready'])
const nineGridRef = ref(null)
const nineShownCount = ref(0)
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)),
])
const decodeImg = (img) => {
if (!img) return Promise.resolve()
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 })
})
}
const yieldToMain = () => new Promise((resolve) => {
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => resolve())
} else {
setTimeout(resolve, 0)
}
})
const runBatchedReveal = async () => {
const images = props.page.images || []
const total = images.length
if (!total) return
for (let start = 0; start < total; start += BATCH_SIZE) {
nineShownCount.value = Math.min(total, start + BATCH_SIZE)
await nextTick()
await yieldToMain()
const root = nineGridRef.value
const end = nineShownCount.value
const imgs = root
? [...root.querySelectorAll('img[data-nine-idx]')].filter((el) => {
const i = Number(el.getAttribute('data-nine-idx'))
return i >= start && i < end
})
: []
await Promise.race([
Promise.allSettled(imgs.map(decodeImg)),
new Promise((resolve) => setTimeout(resolve, BATCH_DECODE_TIMEOUT_MS)),
])
if (end < total) {
await new Promise((resolve) => setTimeout(resolve, BATCH_GAP_MS))
await yieldToMain()
}
}
}
watch(
() => props.activate && props.page?.type === 'nine-grid',
async (on) => {
if (!on || readyEmitted) return
await waitImagesReady()
await runBatchedReveal()
if (readyEmitted) return
readyEmitted = true
emit('ready')

View File

@@ -117,7 +117,7 @@
<div class="formal-copy flex justify-center gap-9 h-[22rem] text-[#3A3A3A]">
<span class="vertical-text formal-main calligraphy-strong">{{ data.formalText1 }}</span>
<span class="vertical-text formal-main formal-main-offset calligraphy-strong">{{ data.formalText2 }}</span>
<span class="vertical-text formal-side calligraphy ml-4 border-l-[0.5px] border-[#a88c6b]/30 pl-6 h-64 mt-8 tracking-[0.28em]">见证我们的幸福</span>
<span class="vertical-text formal-side calligraphy ml-4 border-l-[0.5px] border-[#a88c6b]/30 pl-6 h-64 mt-28 tracking-[0.28em]">见证我们的幸福</span>
<span class="vertical-text formal-side calligraphy ml-4 h-64 mt-8 tracking-[0.28em]">诚挚邀请您</span>
</div>
</div>
@@ -353,7 +353,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_MS = 900
/** 分批入场已占时间,收尾再略停一下即可 */
const NINE_GRID_REVEAL_HOLD_MS = 400
const NINE_GRID_FULL_RATIO = 0.9
const freeInterval = computed(() => {