优化详情返回首页停留

This commit is contained in:
yml2213
2026-07-11 23:25:51 +08:00
parent f81842d46e
commit 7614c0980e
4 changed files with 120 additions and 13 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>哈夫币租号平台</title> <title>大锤商行</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" href="/apple-touch-icon.png" /> <link rel="icon" type="image/png" href="/apple-touch-icon.png" />
</head> </head>
+8 -1
View File
@@ -8,6 +8,9 @@ import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
import AdminLayout from './layouts/AdminLayout.vue' import AdminLayout from './layouts/AdminLayout.vue'
import AppLayout from './layouts/AppLayout.vue' import AppLayout from './layouts/AppLayout.vue'
/** 移动端从详情返回时保留列表状态与滚动位置 */
const mobileKeepAliveIncludes = ['MobileHomeView']
const route = useRoute() const route = useRoute()
const layout = computed(() => { const layout = computed(() => {
if (route.path === '/m' || route.path.startsWith('/m/')) { if (route.path === '/m' || route.path.startsWith('/m/')) {
@@ -23,7 +26,11 @@ const layout = computed(() => {
<AdminLayout v-if="layout === 'admin'"> <AdminLayout v-if="layout === 'admin'">
<RouterView /> <RouterView />
</AdminLayout> </AdminLayout>
<RouterView v-else-if="layout === 'blank'" /> <RouterView v-else-if="layout === 'blank'" v-slot="{ Component }">
<KeepAlive :include="mobileKeepAliveIncludes" :max="5">
<component :is="Component" />
</KeepAlive>
</RouterView>
<AppLayout v-else> <AppLayout v-else>
<RouterView /> <RouterView />
</AppLayout> </AppLayout>
@@ -1,9 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import {
computed,
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
ref,
watch,
} from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router' import { RouterLink, useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant' import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue' import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { ensureSupportChat } from '@/features/chats/api/chats' import { ensureSupportChat } from '@/features/chats/api/chats'
import { formatCent, formatMoney } from '@/shared/utils/money' import { formatCent, formatMoney } from '@/shared/utils/money'
import { import {
@@ -52,6 +60,8 @@ import {
} from '@/shared/utils/listingDisplay' } from '@/shared/utils/listingDisplay'
import { useSessionStore } from '@/stores/session' import { useSessionStore } from '@/stores/session'
defineOptions({ name: 'MobileHomeView' })
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const session = useSessionStore() const session = useSessionStore()
@@ -77,8 +87,12 @@ const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
const supportLoading = ref(false) const supportLoading = ref(false)
const showBackTop = ref(false) const showBackTop = ref(false)
const mobilePageSize = 10 const mobilePageSize = 10
/** KeepAlive 缓存期间为 false,避免详情页路由变化误触发首页筛选/刷新 */
const pageActive = ref(true)
let listingRequestSeq = 0 let listingRequestSeq = 0
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
let savedScrollY = 0
let homeConfigLoaded = false
const sortOptions = [ const sortOptions = [
{ key: 'recommended', label: '综合推荐' }, { key: 'recommended', label: '综合推荐' },
@@ -287,20 +301,76 @@ const visibleZoneOptions = computed(() =>
applyRouteQueryToMobileState() applyRouteQueryToMobileState()
onMounted(() => { function isMobileHomeRoute() {
loadListings() return (
loadHomeConfig() route.name === 'mobile-home' ||
route.name === 'mobile-listings' ||
route.path === '/m' ||
route.path === '/m/listings'
)
}
function bindScrollListener() {
window.addEventListener('scroll', handleWindowScroll, { passive: true }) window.addEventListener('scroll', handleWindowScroll, { passive: true })
}
function unbindScrollListener() {
window.removeEventListener('scroll', handleWindowScroll)
}
function restoreScrollPosition() {
const top = savedScrollY
const apply = () => window.scrollTo({ top, left: 0, behavior: 'auto' })
apply()
// 布局/图片回流后再校正一次,避免回顶
nextTick(() => {
apply()
requestAnimationFrame(() => {
apply()
handleWindowScroll()
})
})
}
onMounted(() => {
// 首次进入加载;KeepAlive 返回时走 onActivated,不再整页重拉
if (!listings.value.length && !loadFailed.value) {
loadListings()
}
if (!homeConfigLoaded) {
loadHomeConfig()
}
})
onActivated(() => {
pageActive.value = true
bindScrollListener()
// 缓存实例仍持有列表数据;只把筛选写回 URL,避免整表重拉
if (isMobileHomeRoute()) {
syncMobileHomeQuery()
}
restoreScrollPosition()
})
onDeactivated(() => {
savedScrollY = window.scrollY || window.pageYOffset || 0
pageActive.value = false
unbindScrollListener()
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer)
searchDebounceTimer = null
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('scroll', handleWindowScroll) unbindScrollListener()
if (searchDebounceTimer) clearTimeout(searchDebounceTimer) if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
}) })
watch( watch(
() => filterQuerySignature(), () => filterQuerySignature(),
() => { () => {
if (!pageActive.value || !isMobileHomeRoute()) return
syncMobileHomeQuery() syncMobileHomeQuery()
loadListings(true) loadListings(true)
} }
@@ -309,14 +379,17 @@ watch(
watch( watch(
() => route.query, () => route.query,
() => { () => {
if (!pageActive.value || !isMobileHomeRoute()) return
applyRouteQueryToMobileState() applyRouteQueryToMobileState()
} }
) )
watch(searchValue, () => { watch(searchValue, () => {
if (!pageActive.value || !isMobileHomeRoute()) return
syncMobileHomeQuery() syncMobileHomeQuery()
if (searchDebounceTimer) clearTimeout(searchDebounceTimer) if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
searchDebounceTimer = setTimeout(() => { searchDebounceTimer = setTimeout(() => {
if (!pageActive.value || !isMobileHomeRoute()) return
loadListings(true) loadListings(true)
}, 300) }, 300)
}) })
@@ -362,6 +435,7 @@ async function loadHomeConfig() {
announcements.value = config.announcements announcements.value = config.announcements
bannerSlides.value = config.banners bannerSlides.value = config.banners
publishOptions.value = config.publish_options publishOptions.value = config.publish_options
homeConfigLoaded = true
} catch { } catch {
announcements.value = defaultHomeAnnouncements announcements.value = defaultHomeAnnouncements
bannerSlides.value = defaultHomeBanners bannerSlides.value = defaultHomeBanners
@@ -494,15 +568,32 @@ function applyRouteQueryToMobileState() {
const query = hasHomeQueryValues(route.query, mobileHomeFilterQueryKeys) const query = hasHomeQueryValues(route.query, mobileHomeFilterQueryKeys)
? route.query ? route.query
: storedQuery : storedQuery
searchValue.value = readQueryString(query, 'keyword') const nextSearch = readQueryString(query, 'keyword')
activeSort.value = readQueryString(query, 'sort') || 'recommended' const nextSort = readQueryString(query, 'sort') || 'recommended'
activeZone.value = readQueryString(query, 'zone') || 'all' const nextZone = readQueryString(query, 'zone') || 'all'
selectedFilters.value = normalizeSelectedFilters( const nextFilters = normalizeSelectedFilters(
decodeHomeQueryJson<Record<string, unknown>>(query, 'filters', {}) decodeHomeQueryJson<Record<string, unknown>>(query, 'filters', {})
) )
rangeFilters.value = normalizeRangeFilters( const nextRanges = normalizeRangeFilters(
decodeHomeQueryJson<Record<string, unknown>>(query, 'ranges', {}) decodeHomeQueryJson<Record<string, unknown>>(query, 'ranges', {})
) )
// 内容未变时避免赋值,防止 KeepAlive 激活或路由回写时误触发列表刷新
if (
searchValue.value === nextSearch &&
activeSort.value === nextSort &&
activeZone.value === nextZone &&
JSON.stringify(selectedFilters.value) === JSON.stringify(nextFilters) &&
JSON.stringify(rangeFilters.value) === JSON.stringify(nextRanges)
) {
return
}
searchValue.value = nextSearch
activeSort.value = nextSort
activeZone.value = nextZone
selectedFilters.value = nextFilters
rangeFilters.value = nextRanges
} }
function syncMobileHomeQuery() { function syncMobileHomeQuery() {
+9
View File
@@ -14,6 +14,15 @@ import { sellerRoutes } from './sellerRoutes'
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [...adminRoutes, ...mobileRoutes, ...publicRoutes, ...accountRoutes, ...sellerRoutes], routes: [...adminRoutes, ...mobileRoutes, ...publicRoutes, ...accountRoutes, ...sellerRoutes],
scrollBehavior(to, _from, savedPosition) {
// 浏览器前进/后退优先用历史滚动位置
if (savedPosition) return savedPosition
// 移动端首页由 KeepAlive + 组件内自行恢复滚动,避免被强制拉回顶部
if (to.name === 'mobile-home' || to.name === 'mobile-listings') {
return false
}
return { left: 0, top: 0 }
},
}) })
router.beforeEach(async to => { router.beforeEach(async to => {