diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 1503f92..486aa75 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -30,6 +30,7 @@
- -
+ +
{ + 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 props = defineProps({ @@ -238,46 +250,25 @@ const preloadNineGridImages = () => { } } -/** 安卓专项:把当前 page 的所有图都预热(不创建 DOM,仅触发浏览器缓存下载) - * 让浏览器在空闲时下载+解码,避免滑到时才下载造成卡顿。 - * 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) - }) -} +/** 注:曾尝试给安卓在 onMounted 阶段后台预热所有图, + * 实测反而更卡顿(下载集中在画廊接近视口时)。已移除。 + * 若需预热,应只针对单张且错开更长时间间隔,且仅在用户即将到达时触发。 */ const runNineSequential = async () => { const total = (props.page.images || []).length + console.log('[PG] runNineSequential start, total=', total) const { cancelled } = await seq.run({ total, stepMs: NINE_STEP_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() } const showNineInstant = async () => { + console.log('[PG] showNineInstant, count=', (props.page.images || []).length) shownCount.value = (props.page.images || []).length await nextTick() emitReady() @@ -287,6 +278,7 @@ const showNineInstant = async () => { const runOlfsSequential = async () => { const token = ++olfsToken const total = Math.min(6, (props.page.images || []).length) + console.log('[PG] runOlfsSequential start, total=', total, 'token=', token) if (!total) { emitReady() return @@ -294,24 +286,27 @@ const runOlfsSequential = async () => { shownCount.value = 0 await nextTick() - if (token !== olfsToken) return + if (token !== olfsToken) { console.log('[PG] olfs cancelled at start'); return } await sleep(OLFS_STEP_MS) - if (token !== olfsToken) return + if (token !== olfsToken) { console.log('[PG] olfs cancelled before 1'); return } shownCount.value = 1 await nextTick() + console.log('[PG] olfs shownCount=1, olfsLargeImgRef=', !!olfsLargeImgRef.value) 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) { await sleep(OLFS_STEP_MS) - if (token !== olfsToken) return + if (token !== olfsToken) { console.log('[PG] olfs cancelled at n=', n); return } shownCount.value = n + console.log('[PG] olfs shownCount=', n) await nextTick() } if (token !== olfsToken) return await sleep(OLFS_SETTLE_MS) + console.log('[PG] olfs done') emitReady() } @@ -324,7 +319,7 @@ const runMultiSequential = async () => { total, stepMs: MULTI_STEP_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 } -const bindMultiObserver = async () => { - unbindMultiObserver() - if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return - await nextTick() - const el = multiRootRef.value - if (!el || typeof IntersectionObserver === 'undefined') { - // 无观察器时直接串行,避免永不出现 - if (!multiStarted) runMultiSequential() +/** 按 page.type 调度对应的串行挂载函数(iOS 上九宫格/一大五小也走此路径) */ +const runSequentialByType = async () => { + if (multiStarted) { + console.log('[PG] runSequentialByType skip (multiStarted)') 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( (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 = null - runMultiSequential() + multiStarted = true + console.log('[PG] IO triggered → runSequentialByType') + runSequentialByType() 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) + console.log('[PG] observer bound') } watch( @@ -387,9 +423,15 @@ watch( readyEmitted = false if (props.page?.type === 'nine-grid') { 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 } - if (props.page?.type === 'one-large-five-small') return if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) { await bindMultiObserver() } @@ -405,8 +447,8 @@ watch( ) 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() - preloadAllForAndroid() if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver() }) diff --git a/vite-tailwindcss/src/composables/useAndroidOptimize.js b/vite-tailwindcss/src/composables/useAndroidOptimize.js index c8e9033..657f91e 100644 --- a/vite-tailwindcss/src/composables/useAndroidOptimize.js +++ b/vite-tailwindcss/src/composables/useAndroidOptimize.js @@ -13,7 +13,19 @@ const isAndroidClient = (() => { 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 isIOS = isIOSClient const OSS_HOST = 'aliyuncs.com' const PROC_750 = 'image/resize,w_750,lfit/format,webp/q,85' diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index d8fb7cb..005e9ae 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -628,7 +628,7 @@ import { toast } from '@/composables/useToast' import { pickMusicUrl, rememberMusicPick } from '@/utils/musicPick' import { getClientId } from '@/utils/clientId' 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 { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc' import CanvasEffects from '@/components/CanvasEffects.vue' @@ -748,9 +748,10 @@ const likeBusy = ref(false) let likePulseTimer = null const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false) const albumPreviewImages = ref([]) -/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 */ +/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 + * iOS 性能足够,跳过停滚,让组件进视口自动挂载(保留滚动连续性) */ 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 galleryHold = ref(false) const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' }) @@ -1047,8 +1048,9 @@ const beginGalleryGate = (pIdx) => { } const onGalleryReady = (pIdx) => { - if (!galleryDone.has(pIdx)) return + // iOS 跳过 hold,但仍需刷新文档高度(图挂载后 scrollMax 可能变化) refreshScrollMax() + if (!galleryDone.has(pIdx)) return clearTimeout(galleryRevealTimer) galleryRevealTimer = setTimeout(() => { galleryHold.value = false @@ -1066,7 +1068,7 @@ const bindGalleryHoldObserver = () => { galleryObserver = new IntersectionObserver( (entries) => { const vh = window.innerHeight || 1 - const lineY = vh * (1 - 0.75) // 从下往上 75% + const lineY = vh * (1 - 0.9) // 从下往上 75% for (const entry of entries) { if (!entry.isIntersecting) continue const rect = entry.boundingClientRect