1. 优化视觉效果、后端规范化
This commit is contained in:
@@ -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 重型版式:露出约 20–30% 再串行,避免页面加载即全量 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 可能已为 true,page 变更后需重新串行
|
||||
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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user