fix: 尝试安卓优化
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图 decode 后再挂小图) -->
|
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图 decode 后再挂小图) -->
|
||||||
<div
|
<div
|
||||||
v-else-if="page.type === 'one-large-five-small'"
|
v-else-if="page.type === 'one-large-five-small'"
|
||||||
|
ref="multiRootRef"
|
||||||
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
|
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -73,8 +74,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 九宫格:串行挂载 + immediate(由父级 activate 触发) -->
|
<!-- 九宫格:串行挂载 + immediate(由父级 activate 触发;iOS 自观测) -->
|
||||||
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
<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
|
<div
|
||||||
v-for="(img, imgIdx) in (page.images || [])"
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
:key="imgIdx"
|
:key="imgIdx"
|
||||||
@@ -176,8 +177,19 @@ const OLFS_STEP_MS = 300
|
|||||||
const OLFS_SETTLE_MS = 320
|
const OLFS_SETTLE_MS = 320
|
||||||
const OLFS_DECODE_TIMEOUT_MS = 1200
|
const OLFS_DECODE_TIMEOUT_MS = 1200
|
||||||
|
|
||||||
/** 瀑布流/拍立得仍自观测;九宫格/一大五小由父级 activate+停滚驱动 */
|
/** 瀑布流/拍立得仍自观测;九宫格/一大五小由父级 activate+停滚驱动
|
||||||
const SEQUENTIAL_SELF_TYPES = new Set(['masonry', 'polaroid'])
|
* iOS 性能足够,跳过停滚,这两种类型也走自观测(进视口自动挂载) */
|
||||||
|
const isIOSClient = (() => {
|
||||||
|
if (typeof navigator === 'undefined') return false
|
||||||
|
const ua = navigator.userAgent || ''
|
||||||
|
const platform = navigator.platform || ''
|
||||||
|
if (/iPhone|iPad|iPod/i.test(ua)) return true
|
||||||
|
if (platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1) return true
|
||||||
|
return false
|
||||||
|
})()
|
||||||
|
const SEQUENTIAL_SELF_TYPES = new Set(
|
||||||
|
isIOSClient ? ['masonry', 'polaroid', 'nine-grid', 'one-large-five-small'] : ['masonry', 'polaroid']
|
||||||
|
)
|
||||||
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -238,46 +250,25 @@ const preloadNineGridImages = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 安卓专项:把当前 page 的所有图都预热(不创建 DOM,仅触发浏览器缓存下载)
|
/** 注:曾尝试给安卓在 onMounted 阶段后台预热所有图,
|
||||||
* 让浏览器在空闲时下载+解码,避免滑到时才下载造成卡顿。
|
* 实测反而更卡顿(下载集中在画廊接近视口时)。已移除。
|
||||||
* iOS 不预热,保留原串行体验。 */
|
* 若需预热,应只针对单张且错开更长时间间隔,且仅在用户即将到达时触发。 */
|
||||||
const isAndroidClient = (() => {
|
|
||||||
if (typeof navigator === 'undefined') return false
|
|
||||||
const ua = navigator.userAgent || ''
|
|
||||||
return /Android/i.test(ua) && !/CrOS/i.test(ua)
|
|
||||||
})()
|
|
||||||
let androidPreloadStarted = false
|
|
||||||
const preloadAllForAndroid = () => {
|
|
||||||
if (!isAndroidClient || androidPreloadStarted) return
|
|
||||||
const urls = [
|
|
||||||
props.page?.img1,
|
|
||||||
props.page?.img2,
|
|
||||||
...(props.page?.images || []),
|
|
||||||
].filter(Boolean)
|
|
||||||
if (!urls.length) return
|
|
||||||
androidPreloadStarted = true
|
|
||||||
// 错开下载,避免同时抢占首屏网络
|
|
||||||
urls.forEach((url, i) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const img = new Image()
|
|
||||||
img.decoding = 'async'
|
|
||||||
img.src = url
|
|
||||||
}, 800 + i * 120)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const runNineSequential = async () => {
|
const runNineSequential = async () => {
|
||||||
const total = (props.page.images || []).length
|
const total = (props.page.images || []).length
|
||||||
|
console.log('[PG] runNineSequential start, total=', total)
|
||||||
const { cancelled } = await seq.run({
|
const { cancelled } = await seq.run({
|
||||||
total,
|
total,
|
||||||
stepMs: NINE_STEP_MS,
|
stepMs: NINE_STEP_MS,
|
||||||
settleMs: NINE_SETTLE_MS,
|
settleMs: NINE_SETTLE_MS,
|
||||||
setCount: (n) => { shownCount.value = n },
|
setCount: (n) => { console.log('[PG] nine setCount', n); shownCount.value = n },
|
||||||
})
|
})
|
||||||
|
console.log('[PG] runNineSequential end, cancelled=', cancelled)
|
||||||
if (!cancelled) emitReady()
|
if (!cancelled) emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showNineInstant = async () => {
|
const showNineInstant = async () => {
|
||||||
|
console.log('[PG] showNineInstant, count=', (props.page.images || []).length)
|
||||||
shownCount.value = (props.page.images || []).length
|
shownCount.value = (props.page.images || []).length
|
||||||
await nextTick()
|
await nextTick()
|
||||||
emitReady()
|
emitReady()
|
||||||
@@ -287,6 +278,7 @@ const showNineInstant = async () => {
|
|||||||
const runOlfsSequential = async () => {
|
const runOlfsSequential = async () => {
|
||||||
const token = ++olfsToken
|
const token = ++olfsToken
|
||||||
const total = Math.min(6, (props.page.images || []).length)
|
const total = Math.min(6, (props.page.images || []).length)
|
||||||
|
console.log('[PG] runOlfsSequential start, total=', total, 'token=', token)
|
||||||
if (!total) {
|
if (!total) {
|
||||||
emitReady()
|
emitReady()
|
||||||
return
|
return
|
||||||
@@ -294,24 +286,27 @@ const runOlfsSequential = async () => {
|
|||||||
|
|
||||||
shownCount.value = 0
|
shownCount.value = 0
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) { console.log('[PG] olfs cancelled at start'); return }
|
||||||
|
|
||||||
await sleep(OLFS_STEP_MS)
|
await sleep(OLFS_STEP_MS)
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) { console.log('[PG] olfs cancelled before 1'); return }
|
||||||
shownCount.value = 1
|
shownCount.value = 1
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
console.log('[PG] olfs shownCount=1, olfsLargeImgRef=', !!olfsLargeImgRef.value)
|
||||||
await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS)
|
await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS)
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) { console.log('[PG] olfs cancelled after large'); return }
|
||||||
|
|
||||||
for (let n = 2; n <= total; n += 1) {
|
for (let n = 2; n <= total; n += 1) {
|
||||||
await sleep(OLFS_STEP_MS)
|
await sleep(OLFS_STEP_MS)
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) { console.log('[PG] olfs cancelled at n=', n); return }
|
||||||
shownCount.value = n
|
shownCount.value = n
|
||||||
|
console.log('[PG] olfs shownCount=', n)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) return
|
||||||
await sleep(OLFS_SETTLE_MS)
|
await sleep(OLFS_SETTLE_MS)
|
||||||
|
console.log('[PG] olfs done')
|
||||||
emitReady()
|
emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +319,7 @@ const runMultiSequential = async () => {
|
|||||||
total,
|
total,
|
||||||
stepMs: MULTI_STEP_MS,
|
stepMs: MULTI_STEP_MS,
|
||||||
settleMs: MULTI_SETTLE_MS,
|
settleMs: MULTI_SETTLE_MS,
|
||||||
setCount: (n) => { shownCount.value = n },
|
setCount: (n) => { console.log('[PG] multi setCount', n, 'type=', type); shownCount.value = n },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,30 +328,71 @@ const unbindMultiObserver = () => {
|
|||||||
multiObserver = null
|
multiObserver = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindMultiObserver = async () => {
|
/** 按 page.type 调度对应的串行挂载函数(iOS 上九宫格/一大五小也走此路径) */
|
||||||
unbindMultiObserver()
|
const runSequentialByType = async () => {
|
||||||
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return
|
if (multiStarted) {
|
||||||
await nextTick()
|
console.log('[PG] runSequentialByType skip (multiStarted)')
|
||||||
const el = multiRootRef.value
|
|
||||||
if (!el || typeof IntersectionObserver === 'undefined') {
|
|
||||||
// 无观察器时直接串行,避免永不出现
|
|
||||||
if (!multiStarted) runMultiSequential()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 露出约 30% 再串行,避免进屏前播完
|
const type = props.page?.type
|
||||||
|
console.log('[PG] runSequentialByType start', { type, nineGridAnimate: props.nineGridAnimate, imgCount: (props.page?.images || []).length })
|
||||||
|
if (type === 'nine-grid') {
|
||||||
|
if (props.nineGridAnimate) await runNineSequential()
|
||||||
|
else await showNineInstant()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'one-large-five-small') {
|
||||||
|
await runOlfsSequential()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// masonry / polaroid
|
||||||
|
await runMultiSequential()
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindMultiObserver = async () => {
|
||||||
|
unbindMultiObserver()
|
||||||
|
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
||||||
|
console.warn('[PG] bindMultiObserver skip, type not in set:', props.page?.type, 'isIOS:', isIOSClient)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await nextTick()
|
||||||
|
const el = multiRootRef.value
|
||||||
|
console.log('[PG] bindMultiObserver', { type: props.page?.type, hasEl: !!el, imgCount: (props.page?.images || []).length, isIOS: isIOSClient })
|
||||||
|
if (!el || typeof IntersectionObserver === 'undefined') {
|
||||||
|
if (!multiStarted) {
|
||||||
|
multiStarted = true
|
||||||
|
console.log('[PG] no el/IO → runSequentialByType')
|
||||||
|
runSequentialByType()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// iOS 上九宫格/一大五小:跳过观察器直接串行挂载(反正不停滚,等到进视口才挂反而突兀)
|
||||||
|
if (isIOSClient && (props.page?.type === 'nine-grid' || props.page?.type === 'one-large-five-small')) {
|
||||||
|
if (!multiStarted) {
|
||||||
|
multiStarted = true
|
||||||
|
console.log('[PG] iOS direct → runSequentialByType')
|
||||||
|
runSequentialByType()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// masonry/polaroid 仍用观察器:露出 30% 再串行,避免进屏前播完
|
||||||
multiObserver = new IntersectionObserver(
|
multiObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting || entry.intersectionRatio < 0.3) continue
|
console.log('[PG] IO entry', { isIntersecting: entry.isIntersecting, ratio: entry.intersectionRatio, type: props.page?.type })
|
||||||
|
if (!entry.isIntersecting) continue
|
||||||
multiObserver?.disconnect()
|
multiObserver?.disconnect()
|
||||||
multiObserver = null
|
multiObserver = null
|
||||||
runMultiSequential()
|
multiStarted = true
|
||||||
|
console.log('[PG] IO triggered → runSequentialByType')
|
||||||
|
runSequentialByType()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: [0.3, 0.5, 0.7], rootMargin: '0px' }
|
{ threshold: [0, 0.1, 0.2, 0.3, 0.5], rootMargin: '0px 0px -10% 0px' }
|
||||||
)
|
)
|
||||||
multiObserver.observe(el)
|
multiObserver.observe(el)
|
||||||
|
console.log('[PG] observer bound')
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -387,9 +423,15 @@ watch(
|
|||||||
readyEmitted = false
|
readyEmitted = false
|
||||||
if (props.page?.type === 'nine-grid') {
|
if (props.page?.type === 'nine-grid') {
|
||||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||||
|
// iOS 自观测路径:仍需绑定观察器
|
||||||
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) await bindMultiObserver()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.page?.type === 'one-large-five-small') {
|
||||||
|
// iOS 自观测路径:仍需绑定观察器
|
||||||
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) await bindMultiObserver()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (props.page?.type === 'one-large-five-small') return
|
|
||||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
||||||
await bindMultiObserver()
|
await bindMultiObserver()
|
||||||
}
|
}
|
||||||
@@ -405,8 +447,8 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
console.log('[PG] onMounted', { type: props.page?.type, imgCount: (props.page?.images || []).length, isIOS: isIOSClient, inSelfSet: SEQUENTIAL_SELF_TYPES.has(props.page?.type) })
|
||||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
if (!props.nineGridAnimate) preloadNineGridImages()
|
||||||
preloadAllForAndroid()
|
|
||||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,19 @@ const isAndroidClient = (() => {
|
|||||||
return /Android/i.test(ua) && !/CrOS/i.test(ua)
|
return /Android/i.test(ua) && !/CrOS/i.test(ua)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const isIOSClient = (() => {
|
||||||
|
if (typeof navigator === 'undefined') return false
|
||||||
|
const ua = navigator.userAgent || ''
|
||||||
|
const platform = navigator.platform || ''
|
||||||
|
// iPhone / iPad (旧 UA) / iPod
|
||||||
|
if (/iPhone|iPad|iPod/i.test(ua)) return true
|
||||||
|
// iPadOS 13+ 伪装为桌面 Mac,通过触控判断
|
||||||
|
if (platform === 'MacIntel' && typeof navigator.maxTouchPoints === 'number' && navigator.maxTouchPoints > 1) return true
|
||||||
|
return false
|
||||||
|
})()
|
||||||
|
|
||||||
export const isAndroid = isAndroidClient
|
export const isAndroid = isAndroidClient
|
||||||
|
export const isIOS = isIOSClient
|
||||||
|
|
||||||
const OSS_HOST = 'aliyuncs.com'
|
const OSS_HOST = 'aliyuncs.com'
|
||||||
const PROC_750 = 'image/resize,w_750,lfit/format,webp/q,85'
|
const PROC_750 = 'image/resize,w_750,lfit/format,webp/q,85'
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ import { toast } from '@/composables/useToast'
|
|||||||
import { pickMusicUrl, rememberMusicPick } from '@/utils/musicPick'
|
import { pickMusicUrl, rememberMusicPick } from '@/utils/musicPick'
|
||||||
import { getClientId } from '@/utils/clientId'
|
import { getClientId } from '@/utils/clientId'
|
||||||
import { sendVisitBeacon } from '@/utils/visitBeacon'
|
import { sendVisitBeacon } from '@/utils/visitBeacon'
|
||||||
import { optimizeImageUrl } from '@/composables/useAndroidOptimize'
|
import { optimizeImageUrl, isIOS } from '@/composables/useAndroidOptimize'
|
||||||
import { DANMAKU_COLORS, DANMAKU_GRADIENT_COLORS, DEFAULT_DANMAKU_COLOR, BLESSING_STYLES } from '@/utils/danmakuColors'
|
import { DANMAKU_COLORS, DANMAKU_GRADIENT_COLORS, DEFAULT_DANMAKU_COLOR, BLESSING_STYLES } from '@/utils/danmakuColors'
|
||||||
import { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc'
|
import { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc'
|
||||||
import CanvasEffects from '@/components/CanvasEffects.vue'
|
import CanvasEffects from '@/components/CanvasEffects.vue'
|
||||||
@@ -748,9 +748,10 @@ const likeBusy = ref(false)
|
|||||||
let likePulseTimer = null
|
let likePulseTimer = null
|
||||||
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
||||||
const albumPreviewImages = ref([])
|
const albumPreviewImages = ref([])
|
||||||
/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 */
|
/** 九宫格 / 一大五小:进视口中线后停滚再串行加载
|
||||||
|
* iOS 性能足够,跳过停滚,让组件进视口自动挂载(保留滚动连续性) */
|
||||||
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
||||||
const isHoldGalleryType = (type) => HOLD_GALLERY_TYPES.has(type)
|
const isHoldGalleryType = (type) => !isIOS && HOLD_GALLERY_TYPES.has(type)
|
||||||
const galleryActivate = reactive({})
|
const galleryActivate = reactive({})
|
||||||
const galleryHold = ref(false)
|
const galleryHold = ref(false)
|
||||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||||
@@ -1047,8 +1048,9 @@ const beginGalleryGate = (pIdx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGalleryReady = (pIdx) => {
|
const onGalleryReady = (pIdx) => {
|
||||||
if (!galleryDone.has(pIdx)) return
|
// iOS 跳过 hold,但仍需刷新文档高度(图挂载后 scrollMax 可能变化)
|
||||||
refreshScrollMax()
|
refreshScrollMax()
|
||||||
|
if (!galleryDone.has(pIdx)) return
|
||||||
clearTimeout(galleryRevealTimer)
|
clearTimeout(galleryRevealTimer)
|
||||||
galleryRevealTimer = setTimeout(() => {
|
galleryRevealTimer = setTimeout(() => {
|
||||||
galleryHold.value = false
|
galleryHold.value = false
|
||||||
@@ -1066,7 +1068,7 @@ const bindGalleryHoldObserver = () => {
|
|||||||
galleryObserver = new IntersectionObserver(
|
galleryObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
const vh = window.innerHeight || 1
|
const vh = window.innerHeight || 1
|
||||||
const lineY = vh * (1 - 0.75) // 从下往上 75%
|
const lineY = vh * (1 - 0.9) // 从下往上 75%
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting) continue
|
if (!entry.isIntersecting) continue
|
||||||
const rect = entry.boundingClientRect
|
const rect = entry.boundingClientRect
|
||||||
|
|||||||
Reference in New Issue
Block a user