优化详情返回首页停留
This commit is contained in:
+1
-1
@@ -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,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() {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user