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

This commit is contained in:
李琦
2026-08-03 08:31:29 +08:00
parent 709893b0b0
commit 9efc2b21d6
2 changed files with 97 additions and 47 deletions

View File

@@ -15,7 +15,7 @@
</div>
</div>
<!-- 错落双图固定 2 占位 + 串行挂载 -->
<!-- 错落双图固定 2 视口 30% / 50% -->
<div
v-else-if="page.type === 'overlap'"
ref="multiRootRef"
@@ -208,16 +208,19 @@ import { createSequentialMount } from '@/composables/useSequentialMount'
const NINE_STEP_MS = 300
const NINE_SETTLE_MS = 400
const MULTI_STEP_MS = 220
const MULTI_SETTLE_MS = 300
const OVERLAP_STEP_MS = 260
const OVERLAP_SETTLE_MS = 300
/** 瀑布/拍立得不停滚,步进略拉长减轻同时 decode */
const MULTI_STEP_MS = 360
const MULTI_SETTLE_MS = 320
/** 错落:视口露出比例分步挂载 + 两图最小间隔 */
const OVERLAP_RATIO_FIRST = 0.3
const OVERLAP_RATIO_SECOND = 0.5
const OVERLAP_MIN_GAP_MS = 520
const OLFS_STEP_MS = 300
const OLFS_SETTLE_MS = 320
const OLFS_DECODE_TIMEOUT_ANDROID_MS = 1200
const OLFS_DECODE_TIMEOUT_IOS_MS = 400
/** 安卓:父级 activate+停滚;iOS近视口 IO 串行(不立刻挂载) */
/** 安卓九宫格/一大五小:父级 activate+停滚;其余自观测不停滚 */
const isIOSClient = (() => {
if (typeof navigator === 'undefined') return false
const ua = navigator.userAgent || ''
@@ -227,15 +230,13 @@ const isIOSClient = (() => {
return false
})()
const HOLD_ACTIVATE_TYPES = new Set([
'nine-grid',
'one-large-five-small',
'overlap',
'masonry',
'polaroid',
])
/** iOS 自观测;安卓重型版式全部由父级 activate 驱动 */
const SEQUENTIAL_SELF_TYPES = new Set(isIOSClient ? [...HOLD_ACTIVATE_TYPES] : [])
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']
)
const props = defineProps({
page: { type: Object, required: true },
@@ -254,12 +255,20 @@ let preloadStarted = false
let multiObserver = null
let multiStarted = false
let olfsToken = 0
let overlapToken = 0
let overlapFirstAt = 0
let overlapSecondTimer = null
const seq = createSequentialMount()
const frameClass = computed(() => borderClass(props.page.borderStyle))
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const clearOverlapSecondTimer = () => {
clearTimeout(overlapSecondTimer)
overlapSecondTimer = null
}
/** iOS 跳过 img.decode手动滚动时强制 decode 易触发内存压力);仅等 load/complete */
const waitImgReady = (img, timeoutMs) => {
if (!img) return Promise.resolve()
@@ -352,17 +361,6 @@ const runOlfsSequential = async () => {
emitReady()
}
/** 错落双图:两槽串行 */
const runOverlapSequential = async () => {
const { cancelled } = await seq.run({
total: 2,
stepMs: OVERLAP_STEP_MS,
settleMs: OVERLAP_SETTLE_MS,
setCount: (n) => { shownCount.value = n },
})
if (!cancelled) emitReady()
}
const runMultiSequential = async () => {
const total = (props.page.images || []).length
const { cancelled } = await seq.run({
@@ -379,6 +377,54 @@ const unbindMultiObserver = () => {
multiObserver = null
}
const showOverlapSecond = (token) => {
if (token !== overlapToken) return
if (shownCount.value >= 2) return
shownCount.value = 2
clearOverlapSecondTimer()
unbindMultiObserver()
emitReady()
}
const scheduleOverlapSecond = (token) => {
if (token !== overlapToken || shownCount.value >= 2 || overlapSecondTimer) return
const elapsed = overlapFirstAt ? performance.now() - overlapFirstAt : 0
const wait = Math.max(0, OVERLAP_MIN_GAP_MS - elapsed)
overlapSecondTimer = setTimeout(() => showOverlapSecond(token), wait)
}
/** 错落双图:不停滚;露出 30% 挂第一张50% 挂第二张(并保证最小间隔) */
const bindOverlapObserver = async () => {
unbindMultiObserver()
clearOverlapSecondTimer()
const token = ++overlapToken
overlapFirstAt = 0
await nextTick()
const el = multiRootRef.value
if (!el || typeof IntersectionObserver === 'undefined') {
shownCount.value = 2
emitReady()
return
}
multiObserver = new IntersectionObserver(
(entries) => {
if (token !== overlapToken) return
for (const entry of entries) {
const ratio = entry.intersectionRatio
if (ratio >= OVERLAP_RATIO_FIRST && shownCount.value < 1) {
shownCount.value = 1
overlapFirstAt = performance.now()
}
if (ratio >= OVERLAP_RATIO_SECOND && shownCount.value >= 1) {
scheduleOverlapSecond(token)
}
}
},
{ threshold: [0, OVERLAP_RATIO_FIRST, OVERLAP_RATIO_SECOND, 0.75, 1] }
)
multiObserver.observe(el)
}
/** 按 page.type 调度对应的串行挂载函数 */
const runSequentialByType = async () => {
const type = props.page?.type
@@ -391,10 +437,6 @@ const runSequentialByType = async () => {
await runOlfsSequential()
return
}
if (type === 'overlap') {
await runOverlapSequential()
return
}
if (type === 'masonry' || type === 'polaroid') {
await runMultiSequential()
}
@@ -415,7 +457,7 @@ const bindMultiObserver = async () => {
await startSequentialMount()
return
}
// iOS 重型版式:露出约 2030% 再串行,避免页面加载即全量 decode
// 近视口再串行,避免页面加载即全量 decode(不停滚)
multiObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
@@ -431,6 +473,19 @@ const bindMultiObserver = async () => {
multiObserver.observe(el)
}
const bindByType = async () => {
const type = props.page?.type
if (type === 'overlap') {
await bindOverlapObserver()
return
}
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) => {
@@ -445,20 +500,18 @@ watch(
async () => {
seq.cancel()
olfsToken += 1
overlapToken += 1
clearOverlapSecondTimer()
unbindMultiObserver()
multiStarted = false
shownCount.value = 0
readyEmitted = false
preloadStarted = false
overlapFirstAt = 0
if (props.page?.type === 'nine-grid' && !props.nineGridAnimate) {
preloadNineGridImages()
}
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
await bindMultiObserver()
} else if (props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type)) {
// 安卓activate 可能已为 truepage 变更后需重新串行
await startSequentialMount()
}
await bindByType()
},
{ deep: true, flush: 'post' }
)
@@ -472,12 +525,14 @@ watch(
onMounted(() => {
if (!props.nineGridAnimate) preloadNineGridImages()
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
bindByType()
})
onUnmounted(() => {
seq.cancel()
olfsToken += 1
overlapToken += 1
clearOverlapSecondTimer()
unbindMultiObserver()
})

View File

@@ -748,15 +748,10 @@ const likeBusy = ref(false)
let likePulseTimer = null
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
const albumPreviewImages = ref([])
/** 重型画廊:安卓进视口中线后停滚再串行加载
* iOS 跳过停滚,由 PhotoGallery 近视口 IO 串行挂载(保留滚动连续性) */
const HOLD_GALLERY_TYPES = new Set([
'nine-grid',
'one-large-five-small',
'overlap',
'masonry',
'polaroid',
])
/** 九宫格 / 一大五小:安卓进视口中线后停滚再串行加载
* 错落/瀑布/拍立得不停滚,由组件内 IO 分步挂载
* iOS 全部跳过停滚,近视口 IO 串行(保留滚动连续性) */
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
const isHoldGalleryType = (type) => !isIOS && HOLD_GALLERY_TYPES.has(type)
const galleryActivate = reactive({})
const galleryHold = ref(false)