1. 播放器歌词效果
This commit is contained in:
@@ -38,23 +38,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 九宫格:轻量入场(可关);避免每格 IntersectionObserver + 双解码拖垮 iOS -->
|
||||
<!-- 九宫格:串行挂载,每 300ms 仅 1 格 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"
|
||||
class="aspect-square relative nine-grid-slot"
|
||||
:class="{ 'nine-grid-placeholder': imgIdx >= nineShownCount }"
|
||||
>
|
||||
<div
|
||||
v-show="imgIdx < nineShownCount"
|
||||
class="absolute inset-0 nine-grid-cell"
|
||||
:class="[frameClass, { 'nine-grid-cell--anim': nineGridAnimate, 'nine-grid-cell--in': nineGridEntered }]"
|
||||
:style="nineGridAnimate ? { transitionDelay: `${Math.min(imgIdx, 8) * 45}ms` } : undefined"
|
||||
v-if="imgIdx < nineShownCount && nineGridAnimate"
|
||||
v-reveal="getRevealConfig(imgIdx)"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img
|
||||
:src="img"
|
||||
class="nine-grid-img preview-img"
|
||||
:loading="nineGridAnimate ? 'lazy' : 'eager'"
|
||||
decoding="async"
|
||||
@click.stop="previewImage(img)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="imgIdx < nineShownCount"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img
|
||||
:src="img"
|
||||
class="nine-grid-img preview-img"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
@click.stop="previewImage(img)"
|
||||
/>
|
||||
@@ -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 {
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="九宫格入场动画">
|
||||
<a-switch v-model:checked="cfg.nineGridAnimate" />
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">关闭:提前预加载、进入窗口即展示且不暂停滚动(推荐 iOS);开启:轻量淡入并短暂停滚。</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">开启:进入视口后暂停滚动,每 300ms 入场一格(方向动效);全部完成后再继续。关闭:直接展示,更稳(推荐 iOS)。</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user