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)