perf(listings): 优化从详情页返回首页的滚动恢复体验,消除顶部闪跳
This commit is contained in:
@@ -122,14 +122,43 @@ export function useListingQuery(
|
||||
}
|
||||
)
|
||||
|
||||
function restoreSnapshot(snapshot: {
|
||||
listings: Listing[]
|
||||
totalListings: number
|
||||
zoneCounts: Record<string, number>
|
||||
currentPage: number
|
||||
hasMoreListings: boolean
|
||||
}) {
|
||||
listings.value = snapshot.listings.slice()
|
||||
totalListings.value = snapshot.totalListings
|
||||
zoneCounts.value = { ...snapshot.zoneCounts }
|
||||
currentPage.value = snapshot.currentPage
|
||||
hasMoreListings.value = snapshot.hasMoreListings
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
return {
|
||||
listings: listings.value.slice(),
|
||||
totalListings: totalListings.value,
|
||||
zoneCounts: { ...zoneCounts.value },
|
||||
currentPage: currentPage.value,
|
||||
hasMoreListings: hasMoreListings.value,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
loadingMore,
|
||||
listings,
|
||||
totalListings,
|
||||
zoneCounts,
|
||||
currentPage,
|
||||
hasMoreListings,
|
||||
loadListingsPage,
|
||||
zoneCount,
|
||||
restoreSnapshot,
|
||||
getSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink, onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { usePcHomeCacheStore } from '@/stores/pcHomeCache'
|
||||
import {
|
||||
defaultAWRecycleConfig,
|
||||
defaultCooperationFeedbackConfig,
|
||||
@@ -83,6 +84,8 @@ const {
|
||||
hasMoreListings,
|
||||
loadListingsPage,
|
||||
zoneCount,
|
||||
restoreSnapshot,
|
||||
getSnapshot,
|
||||
} = useListingQuery(filters, sortBy, activeZone)
|
||||
|
||||
watch(
|
||||
@@ -169,6 +172,7 @@ async function loadHome() {
|
||||
publishOptions.value = emptyListingPublishOptions
|
||||
} finally {
|
||||
loading.value = false
|
||||
saveToCache()
|
||||
restorePcHomeScroll()
|
||||
}
|
||||
}
|
||||
@@ -290,65 +294,118 @@ function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PC 首页滚动位置持久化 ==========
|
||||
// 详情页返回/浏览器后退时,首页列表异步加载导致页面高度不足,
|
||||
// vue-router 的 savedPosition 恢复会被浏览器钳制到错误位置,
|
||||
// 因此按「筛选签名」显式保存/恢复滚动位置,确保回到上次筛选的位置。
|
||||
// ========== PC 首页状态与滚动持久化 ==========
|
||||
// 详情页返回/浏览器后退时,利用 Pinia 缓存同步恢复全部已加载列表与高度,
|
||||
// 使首帧渲染即可完成完整布局,结合 instant 滚动定位彻底解决「从顶部闪跳」问题。
|
||||
const pcHomeScrollStorageKey = 'hfb.home.scroll.desktop'
|
||||
const pcHomeCache = usePcHomeCacheStore()
|
||||
|
||||
function restoreFromCache(): boolean {
|
||||
const signature = desktopHomeStateSignature()
|
||||
if (!pcHomeCache.matchList(signature)) return false
|
||||
|
||||
restoreSnapshot({
|
||||
listings: pcHomeCache.listings,
|
||||
totalListings: pcHomeCache.totalListings,
|
||||
zoneCounts: pcHomeCache.zoneCounts,
|
||||
currentPage: pcHomeCache.currentPage,
|
||||
hasMoreListings: pcHomeCache.hasMoreListings,
|
||||
})
|
||||
if (pcHomeCache.homeConfigLoaded) {
|
||||
announcements.value = pcHomeCache.announcements.slice()
|
||||
banners.value = pcHomeCache.banners.map(slide => ({ ...slide }))
|
||||
awRecycle.value = { ...pcHomeCache.awRecycle }
|
||||
cooperationFeedback.value = { ...pcHomeCache.cooperationFeedback }
|
||||
publishOptions.value = pcHomeCache.publishOptions
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function saveToCache() {
|
||||
const snapshot = getSnapshot()
|
||||
if (snapshot.listings.length === 0) return
|
||||
pcHomeCache.saveList({
|
||||
signature: desktopHomeStateSignature(),
|
||||
listings: snapshot.listings,
|
||||
totalListings: snapshot.totalListings,
|
||||
zoneCounts: snapshot.zoneCounts,
|
||||
currentPage: snapshot.currentPage,
|
||||
hasMoreListings: snapshot.hasMoreListings,
|
||||
})
|
||||
pcHomeCache.saveHomeConfig({
|
||||
announcements: announcements.value,
|
||||
banners: banners.value,
|
||||
awRecycle: awRecycle.value,
|
||||
cooperationFeedback: cooperationFeedback.value,
|
||||
publishOptions: publishOptions.value,
|
||||
})
|
||||
pcHomeCache.saveScrollTop(window.scrollY)
|
||||
}
|
||||
|
||||
function savePcHomeScroll() {
|
||||
saveToCache()
|
||||
try {
|
||||
window.sessionStorage.setItem(
|
||||
pcHomeScrollStorageKey,
|
||||
JSON.stringify({ signature: desktopHomeStateSignature(), top: window.scrollY })
|
||||
)
|
||||
} catch {
|
||||
// sessionStorage 不可用时忽略,仅损失滚动恢复
|
||||
// sessionStorage 不可用时忽略
|
||||
}
|
||||
}
|
||||
|
||||
function restorePcHomeScroll() {
|
||||
let savedTop = 0
|
||||
let savedSignature = ''
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(pcHomeScrollStorageKey)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as { signature?: string; top?: number }
|
||||
if (typeof parsed.signature === 'string' && typeof parsed.top === 'number') {
|
||||
savedSignature = parsed.signature
|
||||
savedTop = parsed.top
|
||||
let savedTop = pcHomeCache.scrollTop
|
||||
let savedSignature = pcHomeCache.listSignature
|
||||
if (savedTop <= 0) {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(pcHomeScrollStorageKey)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as { signature?: string; top?: number }
|
||||
if (typeof parsed.signature === 'string' && typeof parsed.top === 'number') {
|
||||
savedSignature = parsed.signature
|
||||
savedTop = parsed.top
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
// 仅当返回时的筛选条件与离开时一致才恢复,避免把滚动带到别的筛选结果上
|
||||
if (savedTop <= 0 || savedSignature !== desktopHomeStateSignature()) return
|
||||
|
||||
let attempts = 0
|
||||
const apply = () => {
|
||||
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||
if (savedTop <= maxTop) {
|
||||
window.scrollTo({ top: savedTop })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
// 首页无限滚动分页:已渲染高度不足时继续加载一页后重试,尽量恢复到位
|
||||
if (attempts < 5 && hasMoreListings.value && !loadingMore.value) {
|
||||
attempts += 1
|
||||
void loadListingsPage(false).then(() => requestAnimationFrame(apply))
|
||||
} else {
|
||||
window.scrollTo({ top: maxTop })
|
||||
}
|
||||
}
|
||||
if (savedTop <= 0 || savedSignature !== desktopHomeStateSignature()) return
|
||||
|
||||
const apply = () => {
|
||||
window.scrollTo({ top: savedTop, behavior: 'instant' })
|
||||
}
|
||||
apply()
|
||||
requestAnimationFrame(apply)
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (window.scrollY > 0) {
|
||||
pcHomeCache.saveScrollTop(window.scrollY)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
if (restored) {
|
||||
restorePcHomeScroll()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
savePcHomeScroll()
|
||||
})
|
||||
|
||||
syncHomeQuery()
|
||||
loadHome()
|
||||
const restored = restoreFromCache()
|
||||
if (!restored) {
|
||||
loadHome()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -368,13 +368,6 @@ function restoreScrollPosition() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const restoredList = restoreFromCache()
|
||||
if (!restoredList) {
|
||||
loadListings()
|
||||
}
|
||||
if (!restoreHomeConfigFromCache()) {
|
||||
loadHomeConfig()
|
||||
}
|
||||
if (restoredList) {
|
||||
restoreScrollPosition()
|
||||
}
|
||||
@@ -811,6 +804,15 @@ function getItemResourcePreview(item: Listing) {
|
||||
}
|
||||
|
||||
syncMobileHomeQuery()
|
||||
|
||||
// 同步恢复列表和配置快照,确保首帧 DOM 即包含完整卡片列表,避免高度塌陷和滚动闪跳
|
||||
const restoredList = restoreFromCache()
|
||||
if (!restoredList) {
|
||||
loadListings()
|
||||
}
|
||||
if (!restoreHomeConfigFromCache()) {
|
||||
loadHomeConfig()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readError } from '@/shared/utils/error'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast, showDialog } from 'vant'
|
||||
import { goBackOrHome } from '@/shared/utils/routerBack'
|
||||
|
||||
import { fetchListing, type Listing } from '@/features/listings/api/listings'
|
||||
import { useListingCollections } from '@/features/listings/composables/useListingCollections'
|
||||
@@ -387,13 +388,17 @@ function handleToggleFavorite() {
|
||||
const added = toggleFavorite(listing.value)
|
||||
showToast({ message: added ? '已收藏账号' : '已取消收藏', icon: added ? 'star' : 'success' })
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
goBackOrHome(router)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="mobile-detail">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="page-header">
|
||||
<button class="back-btn" @click="router.back()">
|
||||
<button class="back-btn" @click="goBack">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<h1>账号详情</h1>
|
||||
|
||||
Reference in New Issue
Block a user