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

列表改为滚动触底自动分页,详情与订单页铺满主内容区,按钮统一为橙色主题描边/实心样式。
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 { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
@@ -24,12 +24,22 @@ const loadingMore = ref(false)
const keyword = ref('')
const activeCategoryId = ref<number | null>(null)
const failedCover = ref<Record<number, boolean>>({})
const listPanel = ref<HTMLElement | 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 +74,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 {
@@ -92,6 +102,25 @@ 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: listPanel.value, rootMargin: '160px 0px', threshold: 0 }
)
nextTick(() => {
if (loadMoreSentinel.value) loadMoreObserver?.observe(loadMoreSentinel.value)
})
}
watch([listPanel, loadMoreSentinel], () => {
setupLoadMoreObserver()
})
function selectCategory(id: number | null) {
activeCategoryId.value = id
syncQuery()
@@ -166,7 +195,7 @@ function showCover(item: MohongProduct) {
</button>
</aside>
<section class="list-panel">
<section ref="listPanel" class="list-panel">
<van-loading v-if="loading" class="state-loading" size="24px" vertical>加载中...</van-loading>
<van-empty v-else-if="products.length === 0" description="暂无商品" />
<div v-else class="product-list">
@@ -196,17 +225,14 @@ function showCover(item: MohongProduct) {
<span class="buy-icon"></span>
</button>
</div>
<van-button
v-if="hasMore && products.length"
size="small"
plain
block
:loading="loadingMore"
class="load-more"
@click="loadProducts(false)"
>
加载更多
</van-button>
<div
v-if="products.length"
ref="loadMoreSentinel"
class="load-sentinel"
aria-hidden="true"
/>
<div v-if="loadingMore" class="load-tip">加载中...</div>
<div v-else-if="!loading && products.length && !hasMore" class="load-tip">已经到底了</div>
</section>
</div>
<MobileBottomNav />
@@ -379,8 +405,14 @@ function showCover(item: MohongProduct) {
font-size: 12px;
font-weight: 700;
}
.load-more {
margin: 12px;
width: calc(100% - 24px);
.load-sentinel {
width: 100%;
height: 1px;
}
.load-tip {
padding: 12px;
text-align: center;
color: #b0b8c4;
font-size: 12px;
}
</style>
@@ -214,7 +214,9 @@ async function handleBuy() {
>
{{ canBuy ? '立即购买' : '暂时缺货' }}
</el-button>
<el-button size="large" @click="router.push('/mohong')">返回列表</el-button>
<el-button size="large" class="btn-outline" @click="router.push('/mohong')">
返回列表
</el-button>
</div>
<div class="tips">
@@ -288,14 +290,15 @@ async function handleBuy() {
<style scoped>
.mohong-detail-page {
width: 100%;
max-width: 1200px;
}
.detail-layout {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
gap: 28px;
/* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */
grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
gap: 24px;
align-items: start;
width: 100%;
}
.gallery-panel,
@@ -304,13 +307,15 @@ async function handleBuy() {
border: 1px solid #eef1f5;
border-radius: 16px;
padding: 18px;
min-width: 0;
}
.main-image {
width: 100%;
aspect-ratio: 1;
border-radius: 12px;
overflow: hidden;
background: #f3f6fb;
background: #111;
}
.main-image img {
@@ -436,10 +441,27 @@ async function handleBuy() {
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
}
.actions :deep(.el-button--primary) {
--el-button-bg-color: #ff6a00;
--el-button-border-color: #ff6a00;
--el-button-hover-bg-color: #e55d00;
--el-button-hover-border-color: #e55d00;
}
.actions :deep(.btn-outline) {
--el-button-bg-color: #fff8f2;
--el-button-border-color: rgba(255, 106, 0, 0.32);
--el-button-text-color: #ff6a00;
--el-button-hover-bg-color: #fff3e8;
--el-button-hover-border-color: #ff6a00;
--el-button-hover-text-color: #ff6a00;
}
.tips {
padding-top: 16px;
border-top: 1px solid #eef1f5;
@@ -527,5 +549,9 @@ async function handleBuy() {
.detail-layout {
grid-template-columns: 1fr;
}
.gallery-panel {
max-width: 420px;
}
}
</style>
@@ -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;
@@ -141,7 +141,9 @@ function openChat() {
<h1>{{ statusText }}</h1>
<p class="sub">订单号 {{ order.order_no }} · {{ formatDateTime(order.created_at) }}</p>
</div>
<el-button @click="router.push('/mohong/orders')">返回列表</el-button>
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong/orders')">
返回列表
</button>
</header>
<div class="layout">
@@ -164,7 +166,7 @@ function openChat() {
<div v-if="order.copy_text" class="copy-box">
<div class="copy-head">
<h3>订单信息发给客服</h3>
<el-button type="primary" plain size="small" @click="copyText">一键复制</el-button>
<button type="button" class="btn outline sm" @click="copyText">一键复制</button>
</div>
<pre>{{ order.copy_text }}</pre>
</div>
@@ -177,27 +179,38 @@ function openChat() {
<aside class="side-card">
<h3>操作</h3>
<el-button
<button
v-if="order.status === 'pending_payment'"
type="primary"
color="#ff6a00"
:loading="acting"
type="button"
class="btn solid"
:disabled="acting"
@click="handlePay"
>
去支付
</el-button>
<el-button
{{ acting ? '处理中...' : '去支付' }}
</button>
<button
v-if="order.status === 'pending_payment'"
:loading="acting"
type="button"
class="btn outline"
:disabled="acting"
@click="handleCancel"
>
取消订单
</el-button>
<el-button v-if="order.conversation_id" type="primary" plain @click="openChat">
</button>
<button
v-if="order.conversation_id"
type="button"
class="btn outline"
@click="openChat"
>
进入订单群
</el-button>
<el-button v-if="order.copy_text" plain @click="copyText">复制订单信息</el-button>
<el-button plain @click="router.push('/mohong')">继续选购</el-button>
</button>
<button v-if="order.copy_text" type="button" class="btn outline" @click="copyText">
复制订单信息
</button>
<button type="button" class="btn outline" @click="router.push('/mohong')">
继续选购
</button>
</aside>
</div>
</template>
@@ -213,9 +226,11 @@ function openChat() {
<div class="pay-way-options">
<button type="button" class="pay-way-option" @click="choosePayWay('WXZF')">
<strong>微信支付</strong>
<small>使用微信扫码完成支付</small>
</button>
<button type="button" class="pay-way-option" @click="choosePayWay('ZFBZF')">
<strong>支付宝支付</strong>
<small>使用支付宝扫码完成支付</small>
</button>
</div>
</el-dialog>
@@ -225,6 +240,7 @@ function openChat() {
title="订单支付"
width="520px"
append-to-body
class="mohong-pay-dialog"
@closed="cashier.stopPaymentPolling"
>
<div v-if="cashier.activePayment.value" class="pay-dialog-body">
@@ -238,13 +254,14 @@ function openChat() {
</div>
</div>
<template #footer>
<el-button
type="primary"
:loading="cashier.checkingPayment.value"
<button
type="button"
class="btn solid"
:disabled="cashier.checkingPayment.value"
@click="cashier.refreshPaymentStatus(false)"
>
我已支付刷新状态
</el-button>
{{ cashier.checkingPayment.value ? '查询中...' : '我已支付,刷新状态' }}
</button>
</template>
</el-dialog>
</section>
@@ -253,7 +270,6 @@ function openChat() {
<style scoped>
.order-detail-page {
width: 100%;
max-width: 1100px;
display: grid;
gap: 18px;
}
@@ -263,6 +279,7 @@ function openChat() {
justify-content: space-between;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.eyebrow {
@@ -274,7 +291,8 @@ function openChat() {
.page-header h1 {
margin: 0 0 6px;
font-size: 28px;
font-size: 24px;
font-weight: 800;
color: #17233d;
}
@@ -284,11 +302,65 @@ function openChat() {
font-size: 13px;
}
.toolbar-btn,
.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;
transition: all 0.2s;
}
.toolbar-btn.outline,
.btn.outline {
border-color: rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
}
.toolbar-btn.outline:hover,
.btn.outline:hover:not(:disabled) {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
}
.btn.solid {
background: #ff6a00;
border-color: #ff6a00;
color: #fff;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.2);
}
.btn.solid:hover:not(:disabled) {
background: #e55d00;
border-color: #e55d00;
}
.btn.sm {
height: 30px;
padding: 0 12px;
font-size: 12px;
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 240px;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 16px;
align-items: start;
width: 100%;
}
.main-card,
@@ -388,11 +460,11 @@ function openChat() {
.side-card {
display: grid;
gap: 10px;
align-content: start;
}
.side-card :deep(.el-button) {
.side-card .btn {
width: 100%;
margin: 0;
}
.pay-way-options {
@@ -401,6 +473,8 @@ function openChat() {
}
.pay-way-option {
display: grid;
gap: 4px;
width: 100%;
padding: 14px 16px;
border: 1px solid #e7edf6;
@@ -408,6 +482,29 @@ function openChat() {
background: #fff;
text-align: left;
cursor: pointer;
transition:
border-color 0.2s,
background 0.2s;
}
.pay-way-option:hover {
border-color: #ff6a00;
background: #fff8f2;
}
.pay-way-option strong {
color: #17233d;
font-size: 15px;
}
.pay-way-option small {
color: #8a94a6;
font-size: 12px;
}
.mohong-pay-dialog :deep(.el-dialog__footer) {
display: flex;
justify-content: center;
}
.pay-dialog-body {
@@ -34,61 +34,152 @@ async function loadOrders() {
<section class="mohong-orders-page">
<header class="page-header">
<div>
<p class="eyebrow">My Orders</p>
<h1>摸大红订单</h1>
<p class="sub">查看支付状态复制订单信息进入客服群</p>
</div>
<el-button @click="router.push('/mohong')">返回商品列表</el-button>
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong')">
返回商品列表
</button>
</header>
<el-table v-loading="loading" :data="orders" border stripe empty-text="暂无订单">
<el-table-column prop="order_no" label="订单号" min-width="180" />
<el-table-column prop="product_title" label="商品" min-width="160" />
<el-table-column label="数量" width="80" prop="quantity" />
<el-table-column label="金额" width="110">
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
</el-table-column>
<el-table-column label="状态" width="110">
<template #default="{ row }">{{ mohongOrderStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column label="下单时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="router.push(`/mohong/orders/${row.id}`)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-wrap">
<el-table
v-loading="loading"
:data="orders"
border
stripe
empty-text="暂无订单"
style="width: 100%"
>
<el-table-column prop="order_no" label="订单号" min-width="200" />
<el-table-column prop="product_title" label="商品" min-width="180" />
<el-table-column label="数量" width="90" prop="quantity" align="center" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
</el-table-column>
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<span class="status-tag" :class="row.status">
{{ mohongOrderStatusLabel(row.status) }}
</span>
</template>
</el-table-column>
<el-table-column label="下单时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<button type="button" class="link-btn" @click="router.push(`/mohong/orders/${row.id}`)">
详情
</button>
</template>
</el-table-column>
</el-table>
</div>
</section>
</template>
<style scoped>
.mohong-orders-page {
display: grid;
gap: 18px;
gap: 16px;
width: 100%;
max-width: 1200px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
align-items: center;
gap: 12px;
}
.eyebrow {
margin: 0 0 8px;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
width: 100%;
}
.page-header h1 {
margin: 0;
font-size: 28px;
margin: 0 0 4px;
font-size: 22px;
font-weight: 800;
color: #17233d;
}
.sub {
margin: 0;
color: #8a94a6;
font-size: 13px;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-btn:hover {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
}
.table-wrap {
width: 100%;
background: #fff;
border: 1px solid #eef1f5;
border-radius: 14px;
overflow: hidden;
}
.status-tag {
display: inline-flex;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #f1f5f9;
color: #64748b;
}
.status-tag.pending_payment {
background: #fff7ed;
color: #ff6a00;
}
.status-tag.paid {
background: #ecfdf5;
color: #059669;
}
.status-tag.completed {
background: #eff6ff;
color: #2563eb;
}
.status-tag.cancelled,
.status-tag.refunded {
background: #f8fafc;
color: #94a3b8;
}
.link-btn {
border: 0;
background: transparent;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
cursor: pointer;
padding: 0;
}
.link-btn:hover {
color: #e55d00;
text-decoration: underline;
}
</style>