优化首页返回定位与发布在线时间表单
移动端首页用列表缓存和内部滚动恢复浏览位置;发布页在线时间改为全天+起止时间单行布局。
This commit is contained in:
@@ -1,15 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
import { ensureSupportChat } from '@/features/chats/api/chats'
|
||||
@@ -58,13 +49,17 @@ import {
|
||||
hasAcceleratedSaleRatio,
|
||||
hasGiftResources,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
import {
|
||||
buildMobileHomeListSignature,
|
||||
useMobileHomeCacheStore,
|
||||
} from '@/stores/mobileHomeCache'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
defineOptions({ name: 'MobileHomeView' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
const homeCache = useMobileHomeCacheStore()
|
||||
const scrollEl = ref<HTMLElement | null>(null)
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const loadFailed = ref(false)
|
||||
@@ -87,12 +82,9 @@ const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
||||
const supportLoading = ref(false)
|
||||
const showBackTop = ref(false)
|
||||
const mobilePageSize = 10
|
||||
/** KeepAlive 缓存期间为 false,避免详情页路由变化误触发首页筛选/刷新 */
|
||||
const pageActive = ref(true)
|
||||
let listingRequestSeq = 0
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let savedScrollY = 0
|
||||
let homeConfigLoaded = false
|
||||
|
||||
|
||||
const sortOptions = [
|
||||
{ key: 'recommended', label: '综合推荐' },
|
||||
@@ -301,77 +293,101 @@ const visibleZoneOptions = computed(() =>
|
||||
|
||||
applyRouteQueryToMobileState()
|
||||
|
||||
function isMobileHomeRoute() {
|
||||
return (
|
||||
route.name === 'mobile-home' ||
|
||||
route.name === 'mobile-listings' ||
|
||||
route.path === '/m' ||
|
||||
route.path === '/m/listings'
|
||||
)
|
||||
function currentListSignature() {
|
||||
return buildMobileHomeListSignature({
|
||||
searchValue: searchValue.value,
|
||||
activeSort: activeSort.value,
|
||||
activeZone: activeZone.value,
|
||||
selectedFilters: selectedFilters.value,
|
||||
rangeFilters: rangeFilters.value,
|
||||
})
|
||||
}
|
||||
|
||||
function bindScrollListener() {
|
||||
window.addEventListener('scroll', handleWindowScroll, { passive: true })
|
||||
function persistListCache() {
|
||||
homeCache.saveList({
|
||||
signature: currentListSignature(),
|
||||
listings: listings.value.slice(),
|
||||
totalListings: totalListings.value,
|
||||
zoneCounts: { ...zoneCounts.value },
|
||||
currentPage: currentPage.value,
|
||||
hasMoreListings: hasMoreListings.value,
|
||||
})
|
||||
}
|
||||
|
||||
function unbindScrollListener() {
|
||||
window.removeEventListener('scroll', handleWindowScroll)
|
||||
function persistScrollTop() {
|
||||
if (!scrollEl.value) return
|
||||
homeCache.saveScrollTop(scrollEl.value.scrollTop)
|
||||
}
|
||||
|
||||
function restoreFromCache() {
|
||||
const signature = currentListSignature()
|
||||
if (!homeCache.matchList(signature)) return false
|
||||
// 拷贝快照,避免页面内变更直接写穿 store
|
||||
listings.value = homeCache.listings.slice()
|
||||
totalListings.value = homeCache.totalListings
|
||||
zoneCounts.value = { ...homeCache.zoneCounts }
|
||||
currentPage.value = homeCache.currentPage
|
||||
hasMoreListings.value = homeCache.hasMoreListings
|
||||
loadFailed.value = false
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
return true
|
||||
}
|
||||
|
||||
function restoreHomeConfigFromCache() {
|
||||
if (!homeCache.homeConfigLoaded) return false
|
||||
announcements.value = homeCache.announcements.slice()
|
||||
bannerSlides.value = homeCache.bannerSlides.map(slide => ({ ...slide }))
|
||||
publishOptions.value = homeCache.publishOptions
|
||||
return true
|
||||
}
|
||||
|
||||
function restoreScrollPosition() {
|
||||
const top = savedScrollY
|
||||
const apply = () => window.scrollTo({ top, left: 0, behavior: 'auto' })
|
||||
const top = homeCache.scrollTop
|
||||
const apply = () => {
|
||||
if (scrollEl.value) scrollEl.value.scrollTop = top
|
||||
}
|
||||
apply()
|
||||
// 布局/图片回流后再校正一次,避免回顶
|
||||
nextTick(() => {
|
||||
apply()
|
||||
requestAnimationFrame(() => {
|
||||
apply()
|
||||
handleWindowScroll()
|
||||
requestAnimationFrame(() => {
|
||||
apply()
|
||||
handleListScroll()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 首次进入加载;KeepAlive 返回时走 onActivated,不再整页重拉
|
||||
if (!listings.value.length && !loadFailed.value) {
|
||||
const restoredList = restoreFromCache()
|
||||
if (!restoredList) {
|
||||
loadListings()
|
||||
}
|
||||
if (!homeConfigLoaded) {
|
||||
if (!restoreHomeConfigFromCache()) {
|
||||
loadHomeConfig()
|
||||
}
|
||||
if (restoredList) {
|
||||
restoreScrollPosition()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
onBeforeRouteLeave(() => {
|
||||
persistScrollTop()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindScrollListener()
|
||||
persistScrollTop()
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filterQuerySignature(),
|
||||
() => {
|
||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
||||
syncMobileHomeQuery()
|
||||
// 筛选变化时清空旧列表缓存,避免返回时命中过期数据
|
||||
homeCache.clearList()
|
||||
loadListings(true)
|
||||
}
|
||||
)
|
||||
@@ -379,17 +395,15 @@ watch(
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
||||
applyRouteQueryToMobileState()
|
||||
}
|
||||
)
|
||||
|
||||
watch(searchValue, () => {
|
||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
||||
syncMobileHomeQuery()
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
||||
homeCache.clearList()
|
||||
loadListings(true)
|
||||
}, 300)
|
||||
})
|
||||
@@ -412,7 +426,12 @@ async function loadListings(reset = true) {
|
||||
zoneCounts.value = page.zone_counts
|
||||
hasMoreListings.value = listings.value.length < page.total
|
||||
currentPage.value = page.page + 1
|
||||
requestAnimationFrame(handleWindowScroll)
|
||||
persistListCache()
|
||||
if (reset) {
|
||||
homeCache.saveScrollTop(0)
|
||||
if (scrollEl.value) scrollEl.value.scrollTop = 0
|
||||
}
|
||||
requestAnimationFrame(handleListScroll)
|
||||
} catch {
|
||||
if (reset) {
|
||||
listings.value = []
|
||||
@@ -420,6 +439,7 @@ async function loadListings(reset = true) {
|
||||
zoneCounts.value = {}
|
||||
hasMoreListings.value = false
|
||||
loadFailed.value = true
|
||||
homeCache.clearList()
|
||||
}
|
||||
} finally {
|
||||
if (requestSeq === listingRequestSeq) {
|
||||
@@ -435,7 +455,11 @@ async function loadHomeConfig() {
|
||||
announcements.value = config.announcements
|
||||
bannerSlides.value = config.banners
|
||||
publishOptions.value = config.publish_options
|
||||
homeConfigLoaded = true
|
||||
homeCache.saveHomeConfig({
|
||||
announcements: config.announcements,
|
||||
bannerSlides: config.banners,
|
||||
publishOptions: config.publish_options,
|
||||
})
|
||||
} catch {
|
||||
announcements.value = defaultHomeAnnouncements
|
||||
bannerSlides.value = defaultHomeBanners
|
||||
@@ -446,10 +470,16 @@ async function loadHomeConfig() {
|
||||
async function onRefresh() {
|
||||
refreshing.value = true
|
||||
try {
|
||||
homeCache.clearList()
|
||||
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
|
||||
announcements.value = nextHomeConfig.announcements
|
||||
bannerSlides.value = nextHomeConfig.banners
|
||||
publishOptions.value = nextHomeConfig.publish_options
|
||||
homeCache.saveHomeConfig({
|
||||
announcements: nextHomeConfig.announcements,
|
||||
bannerSlides: nextHomeConfig.banners,
|
||||
publishOptions: nextHomeConfig.publish_options,
|
||||
})
|
||||
showToast({ message: '刷新成功', icon: 'passed' })
|
||||
} catch {
|
||||
// 静默处理
|
||||
@@ -578,7 +608,7 @@ function applyRouteQueryToMobileState() {
|
||||
decodeHomeQueryJson<Record<string, unknown>>(query, 'ranges', {})
|
||||
)
|
||||
|
||||
// 内容未变时避免赋值,防止 KeepAlive 激活或路由回写时误触发列表刷新
|
||||
// 内容未变时避免赋值,防止 URL 回写时误触发列表刷新
|
||||
if (
|
||||
searchValue.value === nextSearch &&
|
||||
activeSort.value === nextSort &&
|
||||
@@ -637,14 +667,18 @@ function normalizeRangeFilters(value: Record<string, unknown>) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
function handleWindowScroll() {
|
||||
showBackTop.value = window.scrollY > 520
|
||||
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return
|
||||
function handleListScroll() {
|
||||
const el = scrollEl.value
|
||||
if (!el) return
|
||||
homeCache.saveScrollTop(el.scrollTop)
|
||||
showBackTop.value = el.scrollTop > 520
|
||||
if (el.scrollTop + el.clientHeight < el.scrollHeight - 360) return
|
||||
loadListings(false)
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
scrollEl.value?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
homeCache.saveScrollTop(0)
|
||||
}
|
||||
|
||||
function isSkinGroupKey(key: string) {
|
||||
@@ -699,6 +733,7 @@ syncMobileHomeQuery()
|
||||
|
||||
<template>
|
||||
<main class="mobile-shell">
|
||||
<div ref="scrollEl" class="mobile-scroll" @scroll.passive="handleListScroll">
|
||||
<!-- ========== Hero 区域:顶部搜索与公告 ========== -->
|
||||
<section class="mobile-hero">
|
||||
<div class="mobile-topbar">
|
||||
@@ -937,6 +972,18 @@ syncMobileHomeQuery()
|
||||
</section>
|
||||
</van-pull-refresh>
|
||||
|
||||
<footer class="home-footer">
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="home-footer-icp"
|
||||
>湘ICP备2025146668号</a>
|
||||
<span class="home-footer-divider">|</span>
|
||||
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<MobileHomeFilterSheet
|
||||
v-model:show="filterOpen"
|
||||
v-model:selected-filters="selectedFilters"
|
||||
@@ -956,17 +1003,6 @@ syncMobileHomeQuery()
|
||||
<van-icon name="arrow-up" :size="20" />
|
||||
</button>
|
||||
|
||||
<footer class="home-footer">
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="home-footer-icp"
|
||||
>湘ICP备2025146668号</a>
|
||||
<span class="home-footer-divider">|</span>
|
||||
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
||||
</footer>
|
||||
|
||||
<MobileBottomNav />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user