1. 优化视觉效果、后端规范化

This commit is contained in:
李琦
2026-08-03 08:57:18 +08:00
parent 9efc2b21d6
commit 38558b0b43
5 changed files with 661 additions and 158 deletions

View File

@@ -11,7 +11,7 @@
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
<div :class="singleFrameClass">
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
<img :src="displayImg1" class="preview-img" @click.stop="previewImage(originalImg1)" />
</div>
</div>
@@ -32,10 +32,10 @@
:class="frameClass"
>
<img
:src="page.img1"
:src="displayImg1"
class="preview-img overlap-img"
decoding="async"
@click.stop="previewImage(page.img1)"
@click.stop="previewImage(originalImg1)"
/>
</div>
</div>
@@ -50,10 +50,10 @@
:class="frameClass"
>
<img
:src="page.img2"
:src="displayImg2"
class="preview-img overlap-img"
decoding="async"
@click.stop="previewImage(page.img2)"
@click.stop="previewImage(originalImg2)"
/>
</div>
</div>
@@ -77,10 +77,10 @@
>
<img
ref="olfsLargeImgRef"
:src="page.images[0]"
:src="displayAt(0)"
class="preview-img olfs-img"
decoding="async"
@click.stop="previewImage(page.images[0])"
@click.stop="previewImage(originalAt(0))"
/>
</div>
</div>
@@ -97,19 +97,19 @@
:class="frameClass"
>
<img
:src="page.images[i]"
:src="displayAt(i)"
class="preview-img olfs-img"
decoding="async"
@click.stop="previewImage(page.images[i])"
@click.stop="previewImage(originalAt(i))"
/>
</div>
</div>
</div>
<!-- 九宫格串行挂载 + immediate安卓 activateiOS 近视口 IO -->
<!-- 九宫格近视口 IO 串行挂载双端不停滚 -->
<div v-else-if="page.type === 'nine-grid'" ref="multiRootRef" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
<div
v-for="(img, imgIdx) in (page.images || [])"
v-for="(_, imgIdx) in (page.images || [])"
:key="imgIdx"
class="aspect-square relative nine-grid-slot"
:class="{ 'nine-grid-placeholder': imgIdx >= shownCount }"
@@ -121,10 +121,10 @@
:class="frameClass"
>
<img
:src="img"
:src="displayAt(imgIdx)"
class="nine-grid-img preview-img"
decoding="async"
@click.stop="previewImage(img)"
@click.stop="previewImage(originalAt(imgIdx))"
/>
</div>
<div
@@ -133,11 +133,11 @@
:class="frameClass"
>
<img
:src="img"
:src="displayAt(imgIdx)"
class="nine-grid-img preview-img"
loading="eager"
decoding="async"
@click.stop="previewImage(img)"
@click.stop="previewImage(originalAt(imgIdx))"
/>
</div>
</div>
@@ -146,7 +146,7 @@
<!-- 瀑布流固定槽位 + 串行挂载 -->
<div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
<div
v-for="(img, imgIdx) in (page.images || [])"
v-for="(_, imgIdx) in (page.images || [])"
:key="imgIdx"
class="g-masonry-slot"
:class="{ 'g-masonry-placeholder': imgIdx >= shownCount }"
@@ -157,7 +157,7 @@
class="g-masonry-item"
:class="frameClass"
>
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
<img :src="displayAt(imgIdx)" class="preview-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
</div>
</div>
</div>
@@ -165,14 +165,14 @@
<!-- 轮播滑动 -->
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
<div v-for="(_, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
<img :src="displayAt(imgIdx)" class="preview-img" @click.stop="previewImage(originalAt(imgIdx))" />
</div>
</div>
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张"></button>
<button class="g-arrow g-next" @click="goTo(index + 1)" aria-label="下一张"></button>
<div class="g-dots">
<button v-for="(img, imgIdx) in page.images" :key="imgIdx"
<button v-for="(_, imgIdx) in page.images" :key="imgIdx"
class="g-dot" :class="{ active: imgIdx === index }" @click="goTo(imgIdx)"></button>
</div>
</div>
@@ -180,7 +180,7 @@
<!-- 拍立得固定槽位 + 串行挂载 -->
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
<div
v-for="(img, imgIdx) in (page.images || [])"
v-for="(_, imgIdx) in (page.images || [])"
:key="imgIdx"
class="g-polaroid-slot"
:class="{ 'g-polaroid-placeholder': imgIdx >= shownCount }"
@@ -192,7 +192,7 @@
class="g-polaroid-item"
:style="{ transform: polaroidTransform(imgIdx) }"
>
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" />
<img :src="displayAt(imgIdx)" class="preview-img" decoding="async" @click.stop="previewImage(originalAt(imgIdx))" />
</div>
</div>
</div>
@@ -220,7 +220,7 @@ const OLFS_SETTLE_MS = 320
const OLFS_DECODE_TIMEOUT_ANDROID_MS = 1200
const OLFS_DECODE_TIMEOUT_IOS_MS = 400
/** 安卓九宫格/一大五小:父级 activate+停滚;其余自观测不停滚 */
/** 仅用于 OLFS 是否跳过强制 decode */
const isIOSClient = (() => {
if (typeof navigator === 'undefined') return false
const ua = navigator.userAgent || ''
@@ -230,18 +230,16 @@ const isIOSClient = (() => {
return false
})()
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
/** 自观测串行:瀑布/拍立得双端;九宫格/一大五小仅 iOS安卓走 activate */
const SEQUENTIAL_SELF_TYPES = new Set(
isIOSClient
? ['masonry', 'polaroid', 'nine-grid', 'one-large-five-small']
: ['masonry', 'polaroid']
)
/** 双端均不停滚:近视口 IO 串行挂载 */
const SEQUENTIAL_SELF_TYPES = new Set([
'masonry',
'polaroid',
'nine-grid',
'one-large-five-small',
])
const props = defineProps({
page: { type: Object, required: true },
/** 重型版式:父级停滚后设为 true 才串行挂载(安卓) */
activate: { type: Boolean, default: false },
/** 九宫格:开启串行 v-reveal关闭则一次展示 */
nineGridAnimate: { type: Boolean, default: false },
})
@@ -262,6 +260,16 @@ let overlapSecondTimer = null
const seq = createSequentialMount()
const frameClass = computed(() => borderClass(props.page.borderStyle))
/** 展示:缩放图优先;预览:原图优先 */
const pickDisplay = (original, resized) => resized || original || ''
const pickOriginal = (original, resized) => original || resized || ''
const displayImg1 = computed(() => pickDisplay(props.page?.img1, props.page?.img1Resized))
const displayImg2 = computed(() => pickDisplay(props.page?.img2, props.page?.img2Resized))
const originalImg1 = computed(() => pickOriginal(props.page?.img1, props.page?.img1Resized))
const originalImg2 = computed(() => pickOriginal(props.page?.img2, props.page?.img2Resized))
const displayAt = (i) => pickDisplay(props.page?.images?.[i], props.page?.imagesResized?.[i])
const originalAt = (i) => pickOriginal(props.page?.images?.[i], props.page?.imagesResized?.[i])
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const clearOverlapSecondTimer = () => {
@@ -294,10 +302,12 @@ const emitReady = () => {
emit('ready')
}
/** 关闭九宫格动画时提前暖缓存 */
/** 关闭九宫格动画时提前暖缓存(优先展示用缩放图) */
const preloadNineGridImages = () => {
if (preloadStarted || props.nineGridAnimate || props.page?.type !== 'nine-grid') return
const urls = (props.page.images || []).filter(Boolean)
const urls = (props.page.images || [])
.map((_, i) => displayAt(i))
.filter(Boolean)
if (!urls.length) return
preloadStarted = true
for (const url of urls) {
@@ -481,22 +491,19 @@ const bindByType = async () => {
}
if (SEQUENTIAL_SELF_TYPES.has(type)) {
await bindMultiObserver()
} else if (props.activate && HOLD_ACTIVATE_TYPES.has(type)) {
await startSequentialMount()
}
}
watch(
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
async (on) => {
if (!on) return
await startSequentialMount()
},
{ flush: 'post' }
)
watch(
() => [props.page?.type, props.page?.images, props.page?.img1, props.page?.img2],
() => [
props.page?.type,
props.page?.images,
props.page?.imagesResized,
props.page?.img1,
props.page?.img2,
props.page?.img1Resized,
props.page?.img2Resized,
],
async () => {
seq.cancel()
olfsToken += 1
@@ -589,7 +596,8 @@ function onScroll() {
}
function previewImage(src) {
if (src) emit('preview-image', src)
const url = typeof src === 'string' ? src.trim() : String(src?.value || src || '').trim()
if (url && url !== '[object Object]') emit('preview-image', url)
}
const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]