feat(pc): 详情页增加吸顶返回列表按钮,首页按筛选签名恢复滚动位置
This commit is contained in:
@@ -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;
|
||||||
/* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */
|
/* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */
|
||||||
|
|||||||
@@ -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('/')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user