From 63f46da2b44bed8a7239fbcfa0dcf3406b9cf01b Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Sat, 9 May 2026 14:38:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/manifest.json | 2 +- .../pages/square/components/WaterfallGrid.vue | 258 +++++++++++++++--- 2 files changed, 227 insertions(+), 33 deletions(-) diff --git a/frontend/manifest.json b/frontend/manifest.json index c62c3d1..912e6cb 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -3,7 +3,7 @@ "appid" : "__UNI__F199FF4", "description" : "", "versionName" : "1.0.4", - "versionCode" : 101, + "versionCode" : 102, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { diff --git a/frontend/pages/square/components/WaterfallGrid.vue b/frontend/pages/square/components/WaterfallGrid.vue index bdb5737..91668ac 100644 --- a/frontend/pages/square/components/WaterfallGrid.vue +++ b/frontend/pages/square/components/WaterfallGrid.vue @@ -10,7 +10,11 @@ @touchend="onTouchEnd" @touchcancel="onTouchEnd" > - + { @@ -107,7 +112,71 @@ const cafFn = (id) => { clearTimeout(id) } -// ========== 自动滚动 ========== +// ========== iOS 原生自动滚动 ========== +// iOS 使用 setInterval + scroll-left 属性实现流畅滚动 +let iosScrollTimer = null +let iosScrollPaused = false +let iosLastUpdateTime = 0 +let iosLastScrollValue = 0 // 上次实际设置到 scrollLeft 的值 +const IOS_SCROLL_INTERVAL = 16 // ~60fps +const IOS_MIN_DELTA = 0.5 // 最小变化量,避免频繁更新 + +const startIOSAutoScroll = () => { + console.log('[iOS] startIOSAutoScroll called, isIOS:', isIOS, 'isComponentMounted:', isComponentMounted) + if (!isComponentMounted) return + if (!isIOS) return + stopIOSAutoScroll() + iosScrollPaused = false + iosLastUpdateTime = 0 + iosLastScrollValue = autoScrollPos + + iosScrollTimer = setInterval(() => { + if (!isComponentMounted || iosScrollPaused) return + + const now = Date.now() + // 节流:每 16ms 更新一次 + if (now - iosLastUpdateTime >= IOS_SCROLL_INTERVAL) { + iosLastUpdateTime = now + const newPos = autoScrollPos + AUTO_SCROLL_SPEED + + // 只在变化足够大时更新,减少抖动 + if (Math.abs(newPos - iosLastScrollValue) >= IOS_MIN_DELTA) { + autoScrollPos = newPos + iosLastScrollValue = newPos + scrollLeft.value = newPos + } else { + autoScrollPos = newPos // 保持 autoScrollPos 增长但不同步到 scrollLeft + } + + // 预加载 + const remainingScroll = totalWidth.value - autoScrollPos - props.screenWidth + if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) { + if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) { + appendMore() + } + } + } + }, 16) +} + +const stopIOSAutoScroll = () => { + console.log('[iOS] stopIOSAutoScroll') + clearInterval(iosScrollTimer) + iosScrollTimer = null +} + +const pauseIOSAutoScroll = () => { + console.log('[iOS] pauseIOSAutoScroll') + iosScrollPaused = true +} + +const resumeIOSAutoScroll = () => { + console.log('[iOS] resumeIOSAutoScroll') + iosScrollPaused = false + iosLastUpdateTime = 0 + autoScrollPos = currentScrollLeft + iosLastScrollValue = currentScrollLeft +} let rafId = null let userInteracting = false let resumeTimer = null @@ -115,9 +184,10 @@ let appendTimer = null // 防抖定时器 let autoScrollPos = 0 // 自动滚动目标位置 let momentumTimer = null // iOS 惯性滚动检测定时器 let scrollUpdateTimer = null // 定期同步 scrollLeft 的定时器 +let iosAutoScrollTimer = null // iOS 原生滚动定时器(检测惯性结束) const startAutoScroll = () => { - if (!isComponentMounted) return + if (!isComponentMounted || isIOS) return if (rafId) { cafFn(rafId) rafId = null @@ -155,11 +225,13 @@ const stopAutoScroll = () => { resumeTimer = null clearTimeout(momentumTimer) momentumTimer = null + clearTimeout(iosAutoScrollTimer) + iosAutoScrollTimer = null userInteracting = false isLoadingMore = false } -// 检测 iOS 惯性滚动是否结束 +// 检测 Android 惯性滚动是否结束 let lastTouchEndPos = 0 // touchend 时的位置快照 const detectMomentumEnd = () => { @@ -169,6 +241,7 @@ const detectMomentumEnd = () => { momentumTimer = setTimeout(() => { // 比较的是 lastTouchEndPos(touchend 时的位置),不受后续惯性影响 if (Math.abs(currentScrollLeft - lastTouchEndPos) < 3) { + // 安卓:恢复自动滚动 autoScrollPos = currentScrollLeft startAutoScroll() } else { @@ -194,21 +267,60 @@ const scheduleAppend = () => { } // ========== 触摸 / 滚动事件 ========== -const onTouchStart = () => { - userInteracting = true - clearTimeout(momentumTimer) - clearInterval(scrollUpdateTimer) +let isManualScrolling = false // 是否正在手动滚动 + +const onTouchStart = (e) => { + if (isIOS) { + // iOS 设备:完全停止自动滚动 + stopIOSAutoScroll() + isManualScrolling = true + clearTimeout(momentumTimer) + } else { + // 安卓设备:自动滚动停止 + userInteracting = true + clearTimeout(momentumTimer) + clearInterval(scrollUpdateTimer) + } } const onTouchEnd = () => { - // 等待 iOS 惯性滚动自然结束,再决定是否恢复自动滚动 - detectMomentumEnd() + if (isIOS) { + // iOS 设备:检测惯性滚动是否结束 + clearTimeout(momentumTimer) + lastTouchEndPos = currentScrollLeft + const tick = () => { + momentumTimer = setTimeout(() => { + if (Math.abs(currentScrollLeft - lastTouchEndPos) < 2) { + // 惯性真正结束,延迟 300ms 再恢复自动滚动(确保惯性完全停止) + clearTimeout(momentumTimer) + momentumTimer = setTimeout(() => { + isManualScrolling = false + autoScrollPos = currentScrollLeft + startIOSAutoScroll() + }, 300) + } else { + // 惯性中,更新快照继续检测 + lastTouchEndPos = currentScrollLeft + tick() + } + }, 80) + } + tick() + } else { + // 安卓设备:检测惯性滚动是否结束 + detectMomentumEnd() + } } const onScroll = (e) => { if (!isComponentMounted) return currentScrollLeft = e.detail.scrollLeft + // iOS 手动滚动时,不再更新自动滚动相关变量 + if (isIOS && isManualScrolling) { + return + } + const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) { if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) { @@ -668,60 +780,134 @@ onMounted(() => { userInteracting = false currentScrollLeft = 0 scrollLeft.value = 0 + // 获取设备平台信息,iOS 不使用自动滚动 + const sysInfo = uni.getSystemInfoSync() + isIOS = sysInfo.platform === 'ios' + console.log('[WaterfallGrid] onMounted, platform:', sysInfo.platform, 'isIOS:', isIOS) const containerH = props.screenHeight - props.bannerBottom layout = new WaterfallLayout(containerH, props.category) loadUsers().then(() => { isInitialLoading = false - nextTick(() => startAutoScroll()) + nextTick(() => { + console.log('[WaterfallGrid] loadUsers done, calling auto scroll, isIOS:', isIOS) + if (isIOS) { + startIOSAutoScroll() + } else { + startAutoScroll() + } + }) }) }) -// 监听页面可见性变化 -const handleVisibilityChange = () => { +// ========== 可见性控制 ========== +const stopAllAutoScroll = () => { + stopAutoScroll() + stopIOSAutoScroll() +} + +const resumeAllAutoScroll = () => { + // 清理所有残留的滚动/惯性定时器,确保干净启动 + clearTimeout(momentumTimer) + momentumTimer = null + stopAllAutoScroll() if (!isComponentMounted) return - if (document.hidden) { - stopAutoScroll() + // 使用 scrollLeft.value 而非 currentScrollLeft,因为手动滚动时 + // currentScrollLeft 不更新(onScroll 在 isManualScrolling 时直接 return) + autoScrollPos = scrollLeft.value + if (isIOS) { + startIOSAutoScroll() } else { - // 页面重新可见时,延迟启动自动滚动,等待视图稳定 - stopAutoScroll() - setTimeout(() => { - if (isComponentMounted) { - autoScrollPos = currentScrollLeft - startAutoScroll() - } - }, 150) + startAutoScroll() } } +// H5 页面可见性监听 +const handleVisibilityChange = () => { + if (!isComponentMounted) return + if (document.hidden) { + stopAllAutoScroll() + } else { + // 页面从隐藏恢复时,momentum 已在 stopAllAutoScroll 中清掉,不需要等待 + resumeAllAutoScroll() + } +} + +// uni-app 生命周期:App 进入前台/后台 +let appShowListener = null +let appHideListener = null + +// iOS/Android 小程序可见性监听(通过页面 onShow/onHide) +// uni-app 生命周期:App 进入前台/后台 +const onAppShowHandler = () => { + if (!isComponentMounted) return + // 页面从隐藏恢复时,momentum 已在 stopAllAutoScroll 中清掉,不需要等待 + // 直接 resume,iOS 靠自己的 RAF 驱动,Android 靠 setInterval + resumeAllAutoScroll() +} + +const onAppHideHandler = () => { + if (!isComponentMounted) return + stopAllAutoScroll() +} + +if (typeof uni !== 'undefined') { + uni.onAppShow(onAppShowHandler) + uni.onAppHide(onAppHideHandler) +} + +// H5 环境额外监听 visibilitychange if (typeof document !== 'undefined') { document.addEventListener('visibilitychange', handleVisibilityChange) } +// 供父组件调用的可见性控制方法(通过 defineExpose 暴露) +const handleAppShow = () => { + if (!isComponentMounted) return + setTimeout(() => { + resumeAllAutoScroll() + }, AUTO_RESUME_DELAY) +} + +const handleAppHide = () => { + if (!isComponentMounted) return + stopAllAutoScroll() +} + +// 暴露方法给父组件 +defineExpose({ + handleAppShow, + handleAppHide, +}) + // 监听 isActive 属性变化(父组件控制) watch(() => props.isActive, (active) => { if (!isComponentMounted) return if (active) { - stopAutoScroll() + stopAllAutoScroll() setTimeout(() => { - if (isComponentMounted) { - autoScrollPos = currentScrollLeft - startAutoScroll() - } + resumeAllAutoScroll() }, 150) } else { - stopAutoScroll() + stopAllAutoScroll() } }, { immediate: false }) onUnmounted(() => { isComponentMounted = false - stopAutoScroll() + stopAllAutoScroll() clearTimeout(resumeTimer) clearTimeout(appendTimer) clearTimeout(momentumTimer) if (typeof document !== 'undefined') { document.removeEventListener('visibilitychange', handleVisibilityChange) } + // 移除 uni-app 全局监听 + if (typeof uni !== 'undefined') { + try { + uni.offAppShow(onAppShowHandler) + uni.offAppHide(onAppHideHandler) + } catch (_) {} + } }) watch(() => [props.screenHeight, props.bannerBottom], () => { @@ -736,7 +922,7 @@ watch(() => [props.screenHeight, props.bannerBottom], () => { // 监听分类变化,重新加载数据 watch(() => props.category, (newCategory) => { if (isComponentMounted) { - stopAutoScroll() + stopAllAutoScroll() // 取消待执行的追加 if (appendTimer) { @@ -766,7 +952,11 @@ watch(() => props.category, (newCategory) => { loadUsers().then(() => { nextTick(() => { - startAutoScroll() + if (isIOS) { + startIOSAutoScroll() + } else { + startAutoScroll() + } }) }) } @@ -785,6 +975,10 @@ watch(() => props.category, (newCategory) => { top: 32rpx; } +.ios-animate { + will-change: transform; +} + .wf-card { position: absolute; cursor: pointer;