diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index b806f1b..7c893c8 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -15,7 +15,7 @@ - +
{ 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() }) diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index f4f7eee..d76b753 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -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)