优化首页返回定位与发布在线时间表单

移动端首页用列表缓存和内部滚动恢复浏览位置;发布页在线时间改为全天+起止时间单行布局。
This commit is contained in:
yml2213
2026-07-12 23:02:01 +08:00
parent 42b250cd2c
commit acd9d9b7f1
11 changed files with 359 additions and 327 deletions
@@ -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>