1. 播放器歌词效果
This commit is contained in:
@@ -27,27 +27,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一大五小 -->
|
||||
<div v-else-if="page.type === 'one-large-five-small'"
|
||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
||||
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
|
||||
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图 decode 后再挂小图) -->
|
||||
<div
|
||||
v-else-if="page.type === 'one-large-five-small'"
|
||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
|
||||
>
|
||||
<div
|
||||
class="col-span-2 row-span-2 relative olfs-slot"
|
||||
:class="{ 'olfs-placeholder': shownCount < 1 }"
|
||||
>
|
||||
<div
|
||||
v-if="shownCount >= 1"
|
||||
v-reveal="{ variant: 'up', immediate: true }"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img
|
||||
ref="olfsLargeImgRef"
|
||||
:src="page.images[0]"
|
||||
class="preview-img olfs-img"
|
||||
decoding="async"
|
||||
@click.stop="previewImage(page.images[0])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
||||
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
|
||||
<div
|
||||
v-for="i in [1, 2, 3, 4, 5]"
|
||||
:key="i"
|
||||
class="relative olfs-slot"
|
||||
:class="{ 'olfs-placeholder': shownCount <= i }"
|
||||
>
|
||||
<div
|
||||
v-if="shownCount > i"
|
||||
v-reveal="{ variant: 'up', immediate: true }"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img
|
||||
:src="page.images[i]"
|
||||
class="preview-img olfs-img"
|
||||
decoding="async"
|
||||
@click.stop="previewImage(page.images[i])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 九宫格:串行挂载,每 300ms 仅 1 格 v-reveal(不预挂九格观察器) -->
|
||||
<!-- 九宫格:串行挂载 + immediate(由父级 activate 触发) -->
|
||||
<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 }"
|
||||
:class="{ 'nine-grid-placeholder': imgIdx >= shownCount }"
|
||||
>
|
||||
<div
|
||||
v-if="imgIdx < nineShownCount && nineGridAnimate"
|
||||
v-if="imgIdx < shownCount && nineGridAnimate"
|
||||
v-reveal="getRevealConfig(imgIdx)"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
@@ -60,7 +95,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="imgIdx < nineShownCount"
|
||||
v-else-if="imgIdx < shownCount"
|
||||
class="absolute inset-0"
|
||||
:class="frameClass"
|
||||
>
|
||||
@@ -75,11 +110,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 瀑布流 -->
|
||||
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
||||
class="g-masonry-item" :class="frameClass">
|
||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
||||
<!-- 瀑布流:串行挂载 + immediate -->
|
||||
<div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
|
||||
<div
|
||||
v-for="(img, imgIdx) in (page.images || [])"
|
||||
:key="imgIdx"
|
||||
>
|
||||
<div
|
||||
v-if="imgIdx < shownCount"
|
||||
v-reveal="{ variant: 'up', immediate: true }"
|
||||
class="g-masonry-item"
|
||||
:class="frameClass"
|
||||
>
|
||||
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,11 +142,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍立得拼贴 -->
|
||||
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
|
||||
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
|
||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
||||
<!-- 拍立得:串行挂载 + immediate -->
|
||||
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
|
||||
<div
|
||||
v-for="(img, imgIdx) in (page.images || [])"
|
||||
:key="imgIdx"
|
||||
>
|
||||
<div
|
||||
v-if="imgIdx < shownCount"
|
||||
v-reveal="{ variant: 'scale', immediate: true }"
|
||||
class="g-polaroid-item"
|
||||
:style="{ transform: polaroidTransform(imgIdx) }"
|
||||
>
|
||||
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,29 +166,66 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
import { createSequentialMount } from '@/composables/useSequentialMount'
|
||||
|
||||
const STEP_MS = 300
|
||||
const SETTLE_MS = 400
|
||||
const NINE_STEP_MS = 300
|
||||
const NINE_SETTLE_MS = 400
|
||||
const MULTI_STEP_MS = 220
|
||||
const MULTI_SETTLE_MS = 300
|
||||
const OLFS_STEP_MS = 300
|
||||
const OLFS_SETTLE_MS = 320
|
||||
const OLFS_DECODE_TIMEOUT_MS = 1200
|
||||
|
||||
/** 瀑布流/拍立得仍自观测;九宫格/一大五小由父级 activate+停滚驱动 */
|
||||
const SEQUENTIAL_SELF_TYPES = new Set(['masonry', 'polaroid'])
|
||||
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
/** 九宫格专用:父级在 section 入视口后设为 true 才挂载展示 */
|
||||
/** 九宫格/一大五小:父级停滚后设为 true 才串行挂载 */
|
||||
activate: { type: Boolean, default: false },
|
||||
/** 开启:串行 v-reveal;关闭:预加载后直接展示 */
|
||||
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||
nineGridAnimate: { type: Boolean, default: false },
|
||||
})
|
||||
const emit = defineEmits(['preview-image', 'ready'])
|
||||
|
||||
const nineShownCount = ref(0)
|
||||
const shownCount = ref(0)
|
||||
const multiRootRef = ref(null)
|
||||
const olfsLargeImgRef = ref(null)
|
||||
let readyEmitted = false
|
||||
let preloadStarted = false
|
||||
let seqToken = 0
|
||||
let multiObserver = null
|
||||
let multiStarted = false
|
||||
let olfsToken = 0
|
||||
|
||||
const seq = createSequentialMount()
|
||||
const frameClass = computed(() => borderClass(props.page.borderStyle))
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
/** 关闭动画时提前暖缓存 */
|
||||
const waitImgReady = (img, timeoutMs) => {
|
||||
if (!img) return Promise.resolve()
|
||||
const decode = () => {
|
||||
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 })
|
||||
})
|
||||
}
|
||||
return Promise.race([
|
||||
decode(),
|
||||
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
||||
])
|
||||
}
|
||||
|
||||
const emitReady = () => {
|
||||
if (readyEmitted) return
|
||||
readyEmitted = true
|
||||
emit('ready')
|
||||
}
|
||||
|
||||
/** 关闭九宫格动画时提前暖缓存 */
|
||||
const preloadNineGridImages = () => {
|
||||
if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
|
||||
const urls = (props.page.images || []).filter(Boolean)
|
||||
@@ -148,69 +238,153 @@ const preloadNineGridImages = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const emitReady = () => {
|
||||
if (readyEmitted) return
|
||||
readyEmitted = true
|
||||
emit('ready')
|
||||
const runNineSequential = async () => {
|
||||
const total = (props.page.images || []).length
|
||||
const { cancelled } = await seq.run({
|
||||
total,
|
||||
stepMs: NINE_STEP_MS,
|
||||
settleMs: NINE_SETTLE_MS,
|
||||
setCount: (n) => { shownCount.value = n },
|
||||
})
|
||||
if (!cancelled) emitReady()
|
||||
}
|
||||
|
||||
/** 每 300ms 只挂载 1 格并触发单格 v-reveal */
|
||||
const runSequentialReveal = async () => {
|
||||
const token = ++seqToken
|
||||
const total = (props.page.images || []).length
|
||||
const showNineInstant = async () => {
|
||||
shownCount.value = (props.page.images || []).length
|
||||
await nextTick()
|
||||
emitReady()
|
||||
}
|
||||
|
||||
/** 一大五小:先大图并等 decode,再慢挂小图 */
|
||||
const runOlfsSequential = async () => {
|
||||
const token = ++olfsToken
|
||||
const total = Math.min(6, (props.page.images || []).length)
|
||||
if (!total) {
|
||||
emitReady()
|
||||
return
|
||||
}
|
||||
|
||||
nineShownCount.value = 0
|
||||
shownCount.value = 0
|
||||
await nextTick()
|
||||
if (token !== olfsToken) return
|
||||
|
||||
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 sleep(OLFS_STEP_MS)
|
||||
if (token !== olfsToken) return
|
||||
shownCount.value = 1
|
||||
await nextTick()
|
||||
await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS)
|
||||
if (token !== olfsToken) return
|
||||
|
||||
for (let n = 2; n <= total; n += 1) {
|
||||
await sleep(OLFS_STEP_MS)
|
||||
if (token !== olfsToken) return
|
||||
shownCount.value = n
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
if (token !== seqToken) return
|
||||
await sleep(SETTLE_MS)
|
||||
if (token !== seqToken) return
|
||||
if (token !== olfsToken) return
|
||||
await sleep(OLFS_SETTLE_MS)
|
||||
emitReady()
|
||||
}
|
||||
|
||||
const showNineGridInstant = async () => {
|
||||
const runMultiSequential = async () => {
|
||||
const type = props.page?.type
|
||||
if (!SEQUENTIAL_SELF_TYPES.has(type) || multiStarted) return
|
||||
multiStarted = true
|
||||
const total = (props.page.images || []).length
|
||||
nineShownCount.value = total
|
||||
await seq.run({
|
||||
total,
|
||||
stepMs: MULTI_STEP_MS,
|
||||
settleMs: MULTI_SETTLE_MS,
|
||||
setCount: (n) => { shownCount.value = n },
|
||||
})
|
||||
}
|
||||
|
||||
const unbindMultiObserver = () => {
|
||||
multiObserver?.disconnect()
|
||||
multiObserver = null
|
||||
}
|
||||
|
||||
const bindMultiObserver = async () => {
|
||||
unbindMultiObserver()
|
||||
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return
|
||||
await nextTick()
|
||||
emitReady()
|
||||
const el = multiRootRef.value
|
||||
if (!el || typeof IntersectionObserver === 'undefined') {
|
||||
// 无观察器时直接串行,避免永不出现
|
||||
if (!multiStarted) runMultiSequential()
|
||||
return
|
||||
}
|
||||
// 露出约 30% 再串行,避免进屏前播完
|
||||
multiObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio < 0.3) continue
|
||||
multiObserver?.disconnect()
|
||||
multiObserver = null
|
||||
runMultiSequential()
|
||||
break
|
||||
}
|
||||
},
|
||||
{ threshold: [0.3, 0.5, 0.7], rootMargin: '0px' }
|
||||
)
|
||||
multiObserver.observe(el)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.activate && props.page?.type === 'nine-grid',
|
||||
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
|
||||
async (on) => {
|
||||
if (!on || readyEmitted) return
|
||||
if (props.nineGridAnimate) await runSequentialReveal()
|
||||
else await showNineGridInstant()
|
||||
const type = props.page?.type
|
||||
if (type === 'nine-grid') {
|
||||
if (props.nineGridAnimate) await runNineSequential()
|
||||
else await showNineInstant()
|
||||
return
|
||||
}
|
||||
if (type === 'one-large-five-small') {
|
||||
await runOlfsSequential()
|
||||
}
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.page?.type, props.nineGridAnimate, props.page?.images],
|
||||
() => [props.page?.type, props.page?.images],
|
||||
async () => {
|
||||
seq.cancel()
|
||||
olfsToken += 1
|
||||
unbindMultiObserver()
|
||||
multiStarted = false
|
||||
shownCount.value = 0
|
||||
readyEmitted = false
|
||||
if (props.page?.type === 'nine-grid') {
|
||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||
return
|
||||
}
|
||||
if (props.page?.type === 'one-large-five-small') return
|
||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
||||
await bindMultiObserver()
|
||||
}
|
||||
},
|
||||
{ deep: true, flush: 'post' }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.nineGridAnimate,
|
||||
() => {
|
||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||
},
|
||||
{ deep: true }
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
seqToken += 1
|
||||
seq.cancel()
|
||||
olfsToken += 1
|
||||
unbindMultiObserver()
|
||||
})
|
||||
|
||||
function getRevealConfig(imgIdx) {
|
||||
@@ -218,17 +392,19 @@ function getRevealConfig(imgIdx) {
|
||||
const col = imgIdx % 3
|
||||
|
||||
if (imgIdx === 4) {
|
||||
return { variant: 'scale', delay: 0 }
|
||||
return { variant: 'scale', delay: 0, immediate: true }
|
||||
}
|
||||
if (col === 1) {
|
||||
return {
|
||||
variant: row === 0 ? 'down' : 'up',
|
||||
delay: 0,
|
||||
immediate: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
variant: col === 0 ? 'left' : 'right',
|
||||
delay: 0,
|
||||
immediate: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +479,22 @@ function polaroidTransform(i) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.olfs-slot {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
contain: layout paint;
|
||||
}
|
||||
.olfs-placeholder {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
background: transparent !important;
|
||||
}
|
||||
.olfs-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlap-wrap {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user