摸大红支持购物车多选一起结账

下单支持多商品行快照与一次支付;支付后整单复制文案,前台提供购物车加购与结账。
This commit is contained in:
yml2213
2026-07-16 20:58:39 +08:00
parent e241607535
commit 183351a297
17 changed files with 1159 additions and 117 deletions
@@ -13,6 +13,8 @@ import {
startMohongPayment,
type MohongProduct,
} from '@/features/mohong/api/mohong'
import MohongCartPanel from '@/features/mohong/components/MohongCartPanel.vue'
import { useMohongCart } from '@/features/mohong/composables/useMohongCart'
import { useSessionStore } from '@/stores/session'
import { formatCent } from '@/shared/utils/money'
import { readError } from '@/shared/utils/error'
@@ -20,6 +22,7 @@ import { readError } from '@/shared/utils/error'
const route = useRoute()
const router = useRouter()
const session = useSessionStore()
const { addProduct, openCart, totalCount: cartCount } = useMohongCart()
const product = ref<MohongProduct | null>(null)
const loading = ref(true)
const quantity = ref(1)
@@ -91,6 +94,19 @@ async function ensureAuthReady() {
return true
}
function handleAddCart() {
if (!product.value) return
if (!canBuy.value) {
showToast({ message: '库存不足', icon: 'cross' })
return
}
const result = addProduct(product.value, quantity.value)
showToast({
message: result.message,
icon: result.ok ? 'passed' : 'cross',
})
}
async function handleBuy() {
if (!product.value || submitting.value) return
if (!(await ensureAuthReady())) return
@@ -107,11 +123,11 @@ async function handleBuy() {
const payment = await startMohongPayment(order.id, payWay)
if (payment.paid || payment.status === 'paid') {
showToast({ message: '支付成功', icon: 'passed' })
await router.push(`/mohong/orders/${order.id}`)
await router.push(`/m/mohong/orders/${order.id}`)
return
}
await openMobilePaymentCashier(payment, async () => {
await router.push(`/mohong/orders/${order.id}`)
await router.push(`/m/mohong/orders/${order.id}`)
})
} catch (error) {
showToast({ message: readError(error, '下单失败'), icon: 'cross' })
@@ -164,9 +180,16 @@ async function handleBuy() {
</section>
<div class="buy-bar">
<button type="button" class="cart-entry" @click="openCart">
<van-icon name="shopping-cart-o" :size="20" />
<em v-if="cartCount > 0">{{ cartCount }}</em>
</button>
<div class="sum">
合计 <strong>¥{{ totalPrice }}</strong>
</div>
<van-button plain round color="#ff6a00" :disabled="!canBuy" @click="handleAddCart">
加购
</van-button>
<van-button
type="primary"
color="#ff6a00"
@@ -175,12 +198,17 @@ async function handleBuy() {
:disabled="!canBuy"
@click="handleBuy"
>
{{ canBuy ? '立即购买' : '暂时缺货' }}
{{ canBuy ? '购买' : '缺货' }}
</van-button>
</div>
</template>
<van-empty v-else description="商品不存在" />
<MohongCartPanel
mobile
:select-pay-way="() => payWaySelect.select()"
:start-cashier="(payment, onPaid) => openMobilePaymentCashier(payment, onPaid)"
/>
<MobilePayWaySelectPopup
v-model:show="payWaySelect.visible.value"
@choose="payWaySelect.choose"
@@ -297,22 +325,51 @@ async function handleBuy() {
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
gap: 8px;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: #fff;
border-top: 1px solid #eef1f5;
}
.cart-entry {
position: relative;
width: 40px;
height: 40px;
border: 0;
border-radius: 50%;
background: #fff7ed;
color: #ff6a00;
display: grid;
place-items: center;
flex-shrink: 0;
}
.cart-entry em {
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-style: normal;
line-height: 16px;
text-align: center;
}
.sum {
flex: 1;
color: #6b7a90;
font-size: 13px;
min-width: 0;
}
.sum strong {
color: #ff6a00;
font-size: 20px;
font-size: 18px;
margin-left: 4px;
}
.buy-bar :deep(.van-button) {
min-width: 128px;
min-width: 72px;
flex-shrink: 0;
}
</style>
@@ -7,9 +7,16 @@ import { ensureSupportChat } from '@/features/chats/api/chats'
import {
fetchMohongCategories,
fetchMohongProducts,
queryMohongPayment,
type MohongCategory,
type MohongProduct,
} from '@/features/mohong/api/mohong'
import MohongCartPanel from '@/features/mohong/components/MohongCartPanel.vue'
import { useMohongCart } from '@/features/mohong/composables/useMohongCart'
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
import { useSessionStore } from '@/stores/session'
import { formatCent } from '@/shared/utils/money'
import { readError } from '@/shared/utils/error'
@@ -17,6 +24,21 @@ import { readError } from '@/shared/utils/error'
const router = useRouter()
const route = useRoute()
const session = useSessionStore()
const { totalCount: cartCount, openCart } = useMohongCart()
const payWaySelect = useMobilePayWaySelect()
const {
paymentPopupVisible,
activePayment,
payURL,
paymentQRCodeURL,
qrGenerating,
checkingPayment,
openMobilePaymentCashier,
refreshPaymentStatus,
} = useMobilePaymentCashier({
queryPayment: queryMohongPayment,
paidMessage: '支付成功',
})
const supportLoading = ref(false)
const loading = ref(false)
const products = ref<MohongProduct[]>([])
@@ -191,6 +213,9 @@ async function handleSupportClick() {
@input="onSearchInput"
/>
</div>
<button type="button" class="orders-btn" @click="openCart">
购物车{{ cartCount > 0 ? `(${cartCount})` : '' }}
</button>
<button
type="button"
class="orders-btn"
@@ -264,6 +289,25 @@ async function handleSupportClick() {
<div v-else-if="!loading && products.length && !hasMore" class="load-tip">已经到底了</div>
</section>
</div>
<MohongCartPanel
mobile
:select-pay-way="() => payWaySelect.select()"
:start-cashier="(payment, onPaid) => openMobilePaymentCashier(payment, onPaid)"
/>
<MobilePayWaySelectPopup
v-model:show="payWaySelect.visible.value"
@choose="payWaySelect.choose"
@closed="payWaySelect.handleClosed"
/>
<MobilePaymentCashierPopup
v-model:show="paymentPopupVisible"
:payment="activePayment"
:pay-url="payURL"
:qr-code-url="paymentQRCodeURL"
:qr-generating="qrGenerating"
:checking="checkingPayment"
@refresh="refreshPaymentStatus(false)"
/>
<MobileBottomNav />
</main>
</template>
@@ -127,14 +127,25 @@ async function copyText() {
</div>
<div>
<h2>{{ order.product_title }}</h2>
<p>
<p v-if="!order.items?.length">
¥{{ order.unit_price || formatCent(order.unit_price_cent) }} × {{ order.quantity }}
{{ order.product_unit }}
</p>
<p v-else> {{ order.items.length }} · {{ order.quantity }} </p>
<strong>合计 ¥{{ order.amount || formatCent(order.amount_cent) }}</strong>
</div>
</section>
<section v-if="order.items?.length" class="panel">
<h3>商品明细</h3>
<div v-for="item in order.items" :key="item.product_id" class="item-row">
<span>{{ item.title }}</span>
<em
>×{{ item.quantity }} · ¥{{ item.amount || formatCent(item.amount_cent) }}</em
>
</div>
</section>
<section v-if="order.copy_text" class="panel">
<div class="copy-head">
<h3>订单信息发给客服</h3>
@@ -272,6 +283,23 @@ async function copyText() {
.product-panel strong {
color: #ff6a00;
}
.item-row {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
font-size: 13px;
color: #0f172a;
}
.item-row:last-child {
border-bottom: 0;
}
.item-row em {
font-style: normal;
color: #64748b;
white-space: nowrap;
}
.copy-head {
display: flex;
align-items: center;
@@ -9,6 +9,8 @@ import {
startMohongPayment,
type MohongProduct,
} from '@/features/mohong/api/mohong'
import MohongCartPanel from '@/features/mohong/components/MohongCartPanel.vue'
import { useMohongCart } from '@/features/mohong/composables/useMohongCart'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useOrderPaymentCashier } from '@/features/orders/composables/useOrderPaymentCashier'
import { useSessionStore } from '@/stores/session'
@@ -18,6 +20,7 @@ import { readError } from '@/shared/utils/error'
const route = useRoute()
const router = useRouter()
const session = useSessionStore()
const { addProduct, openCart, totalCount: cartCount } = useMohongCart()
const product = ref<MohongProduct | null>(null)
const loading = ref(true)
const quantity = ref(1)
@@ -112,6 +115,20 @@ async function ensureAuthReady() {
return true
}
function handleAddCart() {
if (!product.value) return
if (!canBuy.value) {
ElMessage.warning('库存不足')
return
}
const result = addProduct(product.value, quantity.value)
if (result.ok) {
ElMessage.success(result.message)
} else {
ElMessage.warning(result.message)
}
}
async function handleBuy() {
if (!product.value || submitting.value) return
if (!(await ensureAuthReady())) return
@@ -204,6 +221,9 @@ async function handleBuy() {
</div>
<div class="actions">
<el-button size="large" class="btn-outline" :disabled="!canBuy" @click="handleAddCart">
加入购物车
</el-button>
<el-button
type="primary"
size="large"
@@ -214,6 +234,9 @@ async function handleBuy() {
>
{{ canBuy ? '立即购买' : '暂时缺货' }}
</el-button>
<el-button size="large" class="btn-outline" @click="openCart">
购物车{{ cartCount > 0 ? `(${cartCount})` : '' }}
</el-button>
<el-button size="large" class="btn-outline" @click="router.push('/mohong')">
返回列表
</el-button>
@@ -223,8 +246,8 @@ async function handleBuy() {
<p>购买须知</p>
<ul>
<li>下单需登录并完成实名认证</li>
<li>可加入购物车多选一起结账支付后一键复制整单信息</li>
<li>支付成功后可在订单详情扫码联系客服</li>
<li>订单详情支持一键复制订单信息发给客服</li>
</ul>
</div>
</div>
@@ -232,6 +255,11 @@ async function handleBuy() {
</template>
<el-empty v-else-if="!loading" description="商品不存在" />
<MohongCartPanel
:select-pay-way="selectPayWay"
:start-cashier="(payment, onPaid) => cashier.openPaymentCashier(payment, onPaid)"
/>
<el-dialog
v-model="payWayDialogVisible"
title="选择支付方式"
@@ -2,9 +2,14 @@
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search, Service, Tickets } from '@element-plus/icons-vue'
import { Search, Service, ShoppingCart, Tickets } from '@element-plus/icons-vue'
import MohongCartPanel from '@/features/mohong/components/MohongCartPanel.vue'
import MohongProductCard from '@/features/mohong/components/MohongProductCard.vue'
import { useMohongCart } from '@/features/mohong/composables/useMohongCart'
import { ensureSupportChat } from '@/features/chats/api/chats'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useOrderPaymentCashier } from '@/features/orders/composables/useOrderPaymentCashier'
import { queryMohongPayment, startMohongPayment } from '@/features/mohong/api/mohong'
import {
fetchMohongCategories,
fetchMohongProducts,
@@ -17,8 +22,38 @@ import { readError } from '@/shared/utils/error'
const router = useRouter()
const route = useRoute()
const session = useSessionStore()
const { totalCount: cartCount, openCart } = useMohongCart()
const supportLoading = ref(false)
const loading = ref(false)
const payWayDialogVisible = ref(false)
let payWayResolver: ((value: PaymentPayWay | null) => void) | null = null
const cashier = useOrderPaymentCashier({
notifySuccess: msg => ElMessage.success(msg),
notifyError: msg => ElMessage.error(msg),
notifyInfo: msg => ElMessage.info(msg),
notifyFallback: msg => ElMessage.warning(msg),
paidMessage: '支付成功',
pollIntervalMs: 3000,
qrWidth: 240,
queryPayment: queryMohongPayment,
})
function selectPayWay(): Promise<PaymentPayWay | null> {
payWayDialogVisible.value = true
return new Promise(resolve => {
payWayResolver = resolve
})
}
function choosePayWay(payWay: PaymentPayWay) {
payWayResolver?.(payWay)
payWayResolver = null
payWayDialogVisible.value = false
}
function handlePayWayDialogClosed() {
payWayResolver?.(null)
payWayResolver = null
}
const loadingMore = ref(false)
const products = ref<MohongProduct[]>([])
const categories = ref<MohongCategory[]>([])
@@ -188,6 +223,10 @@ async function handleSupportClick() {
@input="onKeywordInput"
/>
</div>
<button type="button" class="toolbar-btn outline" @click="openCart">
<el-icon><ShoppingCart /></el-icon>
<span>购物车{{ cartCount > 0 ? `(${cartCount})` : '' }}</span>
</button>
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong/orders')">
<el-icon><Tickets /></el-icon>
<span>我的订单</span>
@@ -252,6 +291,29 @@ async function handleSupportClick() {
</div>
</div>
</div>
<MohongCartPanel
:select-pay-way="selectPayWay"
:start-cashier="(payment, onPaid) => cashier.openPaymentCashier(payment, onPaid)"
/>
<el-dialog
v-model="payWayDialogVisible"
title="选择支付方式"
width="420px"
append-to-body
@closed="handlePayWayDialogClosed"
>
<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>
</section>
</template>
@@ -481,6 +543,32 @@ async function handleSupportClick() {
font-size: 12px;
}
.pay-way-options {
display: grid;
gap: 10px;
}
.pay-way-option {
display: grid;
gap: 4px;
padding: 14px 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fff;
text-align: left;
cursor: pointer;
}
.pay-way-option:hover {
border-color: #ff6a00;
}
.pay-way-option strong {
color: #0f172a;
font-size: 15px;
}
.pay-way-option small {
color: #94a3b8;
font-size: 12px;
}
@media (max-width: 900px) {
.shop-layout {
grid-template-columns: 1fr;
@@ -148,14 +148,27 @@ async function copyText() {
</div>
<div>
<h2>{{ order.product_title }}</h2>
<p>
<p v-if="!order.items?.length">
¥{{ order.unit_price || formatCent(order.unit_price_cent) }} × {{ order.quantity }}
{{ order.product_unit }}
</p>
<p v-else> {{ order.items.length }} · {{ order.quantity }} </p>
<strong>合计 ¥{{ order.amount || formatCent(order.amount_cent) }}</strong>
</div>
</div>
<div v-if="order.items?.length" class="items-box">
<h3>商品明细</h3>
<div v-for="item in order.items" :key="item.product_id" class="item-row">
<span class="item-title">{{ item.title }}</span>
<span
>¥{{ item.unit_price || formatCent(item.unit_price_cent) }} × {{ item.quantity }}
{{ item.unit }}</span
>
<strong>¥{{ item.amount || formatCent(item.amount_cent) }}</strong>
</div>
</div>
<div v-if="order.copy_text" class="copy-box">
<div class="copy-head">
<h3>订单信息发给客服</h3>
@@ -396,6 +409,36 @@ async function copyText() {
font-size: 18px;
}
.items-box {
margin-top: 4px;
padding: 12px 0 4px;
border-top: 1px solid #eef1f5;
}
.items-box h3 {
margin: 0 0 10px;
font-size: 14px;
color: #17233d;
}
.item-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
font-size: 13px;
color: #475569;
}
.item-row:last-child {
border-bottom: 0;
}
.item-title {
color: #0f172a;
font-weight: 600;
}
.item-row strong {
color: #ff6a00;
}
.copy-box,
.qr-box {
margin-top: 16px;