feat(pc): 详情页增加吸顶返回列表按钮,首页按筛选签名恢复滚动位置

This commit is contained in:
yml2213
2026-08-15 10:42:09 +08:00
parent a25486201c
commit fbd59ed19f
4 changed files with 176 additions and 2 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router' import { RouterLink, onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import { import {
defaultAWRecycleConfig, defaultAWRecycleConfig,
defaultHomeAnnouncements, defaultHomeAnnouncements,
@@ -164,6 +164,7 @@ async function loadHome() {
publishOptions.value = emptyListingPublishOptions publishOptions.value = emptyListingPublishOptions
} finally { } finally {
loading.value = false loading.value = false
restorePcHomeScroll()
} }
} }
@@ -268,6 +269,63 @@ function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
} }
} }
// ========== PC 首页滚动位置持久化 ==========
// 详情页返回/浏览器后退时,首页列表异步加载导致页面高度不足,
// vue-router 的 savedPosition 恢复会被浏览器钳制到错误位置,
// 因此按「筛选签名」显式保存/恢复滚动位置,确保回到上次筛选的位置。
const pcHomeScrollStorageKey = 'hfb.home.scroll.desktop'
function savePcHomeScroll() {
try {
window.sessionStorage.setItem(
pcHomeScrollStorageKey,
JSON.stringify({ signature: desktopHomeStateSignature(), top: window.scrollY })
)
} catch {
// 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
}
}
} 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 })
return
}
// 首页无限滚动分页:已渲染高度不足时继续加载一页后重试,尽量恢复到位
if (attempts < 5 && hasMoreListings.value && !loadingMore.value) {
attempts += 1
void loadListingsPage(false).then(() => requestAnimationFrame(apply))
} else {
window.scrollTo({ top: maxTop })
}
}
requestAnimationFrame(apply)
}
onBeforeRouteLeave(() => {
savePcHomeScroll()
})
syncHomeQuery() syncHomeQuery()
loadHome() loadHome()
</script> </script>
@@ -3,7 +3,8 @@ import { readError } from '@/shared/utils/error'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, nextTick, onMounted, ref, watch } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ShoppingCart, Star, StarFilled } from '@element-plus/icons-vue' import { ShoppingCart, Star, StarFilled, ArrowLeft } from '@element-plus/icons-vue'
import { goBackOrHome } from '@/shared/utils/routerBack'
import { fetchListing, type Listing } from '@/features/listings/api/listings' import { fetchListing, type Listing } from '@/features/listings/api/listings'
import { useListingCollections } from '@/features/listings/composables/useListingCollections' import { useListingCollections } from '@/features/listings/composables/useListingCollections'
@@ -345,6 +346,10 @@ function handleToggleFavorite() {
const added = toggleFavorite(listing.value) const added = toggleFavorite(listing.value)
ElMessage.success(added ? '已收藏账号' : '已取消收藏') ElMessage.success(added ? '已收藏账号' : '已取消收藏')
} }
function goBack() {
goBackOrHome(router)
}
</script> </script>
<template> <template>
@@ -353,6 +358,13 @@ function handleToggleFavorite() {
<span>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span> <span>防骗提示下单后请按平台交接流程确认收号与归还不要私下交易</span>
</div> </div>
<div class="detail-back-bar">
<button class="detail-back-btn" type="button" @click="goBack">
<el-icon><ArrowLeft /></el-icon>
<span>返回列表</span>
</button>
</div>
<div v-if="listing" class="pc-detail-layout"> <div v-if="listing" class="pc-detail-layout">
<section class="pc-detail-main"> <section class="pc-detail-main">
<div class="detail-hero-card"> <div class="detail-hero-card">
@@ -619,6 +631,44 @@ function handleToggleFavorite() {
</template> </template>
<style scoped> <style scoped>
.detail-back-bar {
/* 吸顶固定:始终显示在顶部导航栏(64px)下方,滚动时不跟随页面移走 */
position: sticky;
top: 78px;
z-index: 20;
display: flex;
align-items: center;
margin-bottom: 14px;
}
.detail-back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border: 1px solid #eef1f5;
border-radius: 10px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 2px 8px rgba(23, 35, 61, 0.08);
transition:
border-color 0.2s ease,
color 0.2s ease,
background 0.2s ease,
box-shadow 0.2s ease;
}
.detail-back-btn:hover {
border-color: #ff6a00;
background: #fff7ed;
color: #ff6a00;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.12);
}
.pc-detail-layout { .pc-detail-layout {
grid-template-columns: minmax(0, 1fr) 420px; grid-template-columns: minmax(0, 1fr) 420px;
align-items: start; align-items: start;
@@ -2,6 +2,8 @@
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import { goBackOrHome } from '@/shared/utils/routerBack'
import { import {
createMohongOrder, createMohongOrder,
fetchMohongProduct, fetchMohongProduct,
@@ -69,6 +71,10 @@ const activePayWayLabel = computed(() => {
onMounted(loadProduct) onMounted(loadProduct)
function goBack() {
goBackOrHome(router)
}
async function loadProduct() { async function loadProduct() {
loading.value = true loading.value = true
try { try {
@@ -162,6 +168,12 @@ async function handleBuy() {
<template> <template>
<section v-loading="loading" class="mohong-detail-page"> <section v-loading="loading" class="mohong-detail-page">
<div class="detail-back-bar">
<button class="detail-back-btn" type="button" @click="goBack">
<el-icon><ArrowLeft /></el-icon>
<span>返回列表</span>
</button>
</div>
<template v-if="product"> <template v-if="product">
<div class="detail-layout"> <div class="detail-layout">
<div class="gallery-panel"> <div class="gallery-panel">
@@ -307,6 +319,44 @@ async function handleBuy() {
width: 100%; width: 100%;
} }
.detail-back-bar {
/* 吸顶固定:始终显示在顶部导航栏(64px)下方,滚动时不跟随页面移走 */
position: sticky;
top: 78px;
z-index: 20;
display: flex;
align-items: center;
margin-bottom: 14px;
}
.detail-back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border: 1px solid #eef1f5;
border-radius: 10px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 2px 8px rgba(23, 35, 61, 0.08);
transition:
border-color 0.2s ease,
color 0.2s ease,
background 0.2s ease,
box-shadow 0.2s ease;
}
.detail-back-btn:hover {
border-color: #ff6a00;
background: #fff7ed;
color: #ff6a00;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.12);
}
.detail-layout { .detail-layout {
display: grid; display: grid;
/* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */ /* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */
+16
View File
@@ -0,0 +1,16 @@
import type { Router } from 'vue-router'
/**
* 返回上一个站内页面(保留其 URL query,即筛选条件),
* 若没有站内历史(例如直接粘贴链接打开详情页),则回首页。
* vue-router 在 push 时会往 history.state 写入 back 字段记录来源页,
* 且该字段始终是站内路径(外部来源时首次进入为 null),可安全判断。
*/
export function goBackOrHome(router: Router) {
const state = window.history.state as { back?: string | null } | null
if (state?.back) {
router.back()
return
}
void router.push('/')
}