优化摸大红前台交互与主题按钮样式

列表改为滚动触底自动分页,详情与订单页铺满主内容区,按钮统一为橙色主题描边/实心样式。
This commit is contained in:
yml2213
2026-07-16 19:40:37 +08:00
parent 6d61027318
commit 094a91c985
5 changed files with 427 additions and 99 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search, Tickets } from '@element-plus/icons-vue'
@@ -23,12 +23,21 @@ const page = ref(1)
const hasMore = ref(true)
const keyword = ref('')
const activeCategoryId = ref<number | null>(null)
const loadMoreSentinel = ref<HTMLElement | null>(null)
let searchTimer: ReturnType<typeof setTimeout> | null = null
let loadMoreObserver: IntersectionObserver | null = null
onMounted(async () => {
await loadCategories()
applyRouteQuery()
await loadProducts(true)
setupLoadMoreObserver()
})
onBeforeUnmount(() => {
loadMoreObserver?.disconnect()
loadMoreObserver = null
if (searchTimer) clearTimeout(searchTimer)
})
watch(
@@ -64,13 +73,13 @@ async function loadCategories() {
}
async function loadProducts(reset = true) {
if (loadingMore.value) return
if (reset) {
if (loading.value) return
loading.value = true
page.value = 1
hasMore.value = true
} else {
if (!hasMore.value) return
if (loading.value || loadingMore.value || !hasMore.value) return
loadingMore.value = true
}
try {
@@ -93,6 +102,26 @@ async function loadProducts(reset = true) {
}
}
function setupLoadMoreObserver() {
loadMoreObserver?.disconnect()
loadMoreObserver = new IntersectionObserver(
entries => {
if (!entries.some(item => item.isIntersecting)) return
if (loading.value || loadingMore.value || !hasMore.value) return
loadProducts(false)
},
{ root: null, rootMargin: '200px 0px', threshold: 0 }
)
nextTick(() => {
if (loadMoreSentinel.value) loadMoreObserver?.observe(loadMoreSentinel.value)
})
}
watch(loadMoreSentinel, el => {
loadMoreObserver?.disconnect()
if (el) loadMoreObserver?.observe(el)
})
function selectCategory(id: number | null) {
activeCategoryId.value = id
syncQuery()
@@ -138,10 +167,13 @@ const activeCategoryName = () => {
@input="onKeywordInput"
/>
</div>
<el-button size="small" :icon="Tickets" @click="router.push('/mohong/orders')">
我的订单
</el-button>
<el-button size="small" plain @click="router.push('/')">租号大厅</el-button>
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong/orders')">
<el-icon><Tickets /></el-icon>
<span>我的订单</span>
</button>
<button type="button" class="toolbar-btn outline" @click="router.push('/')">
租号大厅
</button>
</div>
</header>
@@ -176,11 +208,17 @@ const activeCategoryName = () => {
<div v-else v-loading="loading" class="product-grid">
<MohongProductCard v-for="item in products" :key="item.id" :product="item" />
</div>
<div v-if="!loading && products.length" class="load-state">
<el-button v-if="hasMore" :loading="loadingMore" @click="loadProducts(false)">
加载更多
</el-button>
<span v-else class="end">已经到底了</span>
<div
v-if="products.length"
ref="loadMoreSentinel"
class="load-sentinel"
aria-hidden="true"
/>
<div v-if="loadingMore" class="load-state">
<span class="loading-more">加载中...</span>
</div>
<div v-else-if="!loading && products.length && !hasMore" class="load-state">
<span class="end">已经到底了</span>
</div>
</div>
</div>
@@ -244,12 +282,20 @@ const activeCategoryName = () => {
align-items: center;
gap: 6px;
width: 220px;
height: 32px;
padding: 0 10px;
border: 1px solid #e4e7ed;
border-radius: 8px;
height: 36px;
padding: 0 12px;
border: 1px solid #eef1f5;
border-radius: 10px;
background: #fff;
color: #94a3b8;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.search-box:focus-within {
border-color: rgba(255, 106, 0, 0.45);
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.08);
}
.search-box input {
@@ -262,6 +308,36 @@ const activeCategoryName = () => {
font-size: 13px;
}
/* 对齐顶栏客服/发布账号的主题按钮风格 */
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.toolbar-btn.outline {
border-color: rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
}
.toolbar-btn.outline:hover {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
transform: translateY(-1px);
}
.shop-layout {
display: grid;
grid-template-columns: 140px minmax(0, 1fr);
@@ -357,6 +433,11 @@ const activeCategoryName = () => {
color: #17233d;
}
.load-sentinel {
width: 100%;
height: 1px;
}
.load-state {
display: flex;
justify-content: center;
@@ -364,6 +445,7 @@ const activeCategoryName = () => {
padding: 12px 0 8px;
}
.loading-more,
.end {
color: #b0b8c4;
font-size: 12px;