摸大红支持购物车多选一起结账
下单支持多商品行快照与一次支付;支付后整单复制文案,前台提供购物车加购与结账。
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Delete, ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { createMohongOrder, startMohongPayment } from '@/features/mohong/api/mohong'
|
||||
import { useMohongCart } from '@/features/mohong/composables/useMohongCart'
|
||||
import type { PaymentPayWay } from '@/features/orders/api/orders'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { formatCent } from '@/shared/utils/money'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mobile?: boolean
|
||||
selectPayWay: () => Promise<PaymentPayWay | null>
|
||||
startCashier: (
|
||||
payment: Awaited<ReturnType<typeof startMohongPayment>>,
|
||||
onPaid: () => void | Promise<void>
|
||||
) => Promise<void>
|
||||
}>(),
|
||||
{ mobile: false }
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
const {
|
||||
lines,
|
||||
drawerOpen,
|
||||
totalCount,
|
||||
totalAmountCent,
|
||||
isEmpty,
|
||||
closeCart,
|
||||
setQuantity,
|
||||
removeLine,
|
||||
clearCart,
|
||||
checkoutItems,
|
||||
} = useMohongCart()
|
||||
const checkingOut = ref(false)
|
||||
|
||||
async function ensureAuthReady() {
|
||||
const loginPath = props.mobile ? '/m/login' : '/login'
|
||||
const realnamePath = props.mobile ? '/m/realname' : '/realname'
|
||||
if (!session.isLoggedIn) {
|
||||
router.push({ path: loginPath, query: { redirect: router.currentRoute.value.fullPath } })
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await session.loadMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (session.realnameStatus !== 'verified') {
|
||||
router.push({ path: realnamePath, query: { redirect: router.currentRoute.value.fullPath } })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
if (isEmpty.value || checkingOut.value) return
|
||||
if (!(await ensureAuthReady())) return
|
||||
const payWay = await props.selectPayWay()
|
||||
if (!payWay) return
|
||||
|
||||
checkingOut.value = true
|
||||
try {
|
||||
const order = await createMohongOrder(checkoutItems())
|
||||
clearCart()
|
||||
closeCart()
|
||||
const payment = await startMohongPayment(order.id, payWay)
|
||||
const orderPath = props.mobile ? `/m/mohong/orders/${order.id}` : `/mohong/orders/${order.id}`
|
||||
if (payment.paid || payment.status === 'paid') {
|
||||
ElMessage.success('支付成功')
|
||||
await router.push(orderPath)
|
||||
return
|
||||
}
|
||||
await props.startCashier(payment, async () => {
|
||||
await router.push(orderPath)
|
||||
})
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '结账失败'))
|
||||
} finally {
|
||||
checkingOut.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer
|
||||
v-if="!mobile"
|
||||
v-model="drawerOpen"
|
||||
title="购物车"
|
||||
size="420px"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="isEmpty" class="empty">
|
||||
<el-icon :size="36"><ShoppingCart /></el-icon>
|
||||
<p>还没有选购商品</p>
|
||||
<el-button type="primary" color="#ff6a00" @click="closeCart">去选购</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="cart-list">
|
||||
<div v-for="line in lines" :key="line.product_id" class="cart-line">
|
||||
<div class="cover">
|
||||
<img v-if="line.cover_url" :src="line.cover_url" :alt="line.title" />
|
||||
<span v-else>{{ line.title.slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<strong>{{ line.title }}</strong>
|
||||
<p>¥{{ line.price || formatCent(line.price_cent) }} / {{ line.unit }}</p>
|
||||
<div class="line-actions">
|
||||
<el-input-number
|
||||
:model-value="line.quantity"
|
||||
:min="1"
|
||||
:max="line.stock > 0 ? line.stock : 99"
|
||||
size="small"
|
||||
@update:model-value="(v: number | undefined) => setQuantity(line.product_id, Number(v || 1))"
|
||||
/>
|
||||
<el-button text type="danger" :icon="Delete" @click="removeLine(line.product_id)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-amount">¥{{ formatCent(line.price_cent * line.quantity) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cart-footer">
|
||||
<div>
|
||||
共 {{ totalCount }} 件
|
||||
<strong>¥{{ formatCent(totalAmountCent) }}</strong>
|
||||
</div>
|
||||
<el-button type="primary" color="#ff6a00" :loading="checkingOut" @click="handleCheckout">
|
||||
一起结账
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<van-popup
|
||||
v-else
|
||||
v-model:show="drawerOpen"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ maxHeight: '78vh' }"
|
||||
>
|
||||
<div class="m-cart">
|
||||
<header class="m-head">
|
||||
<h3>购物车</h3>
|
||||
<button type="button" @click="closeCart">关闭</button>
|
||||
</header>
|
||||
<div v-if="isEmpty" class="empty">
|
||||
<p>还没有选购商品</p>
|
||||
</div>
|
||||
<div v-else class="m-list">
|
||||
<div v-for="line in lines" :key="line.product_id" class="m-line">
|
||||
<div class="cover">
|
||||
<img v-if="line.cover_url" :src="line.cover_url" :alt="line.title" />
|
||||
<span v-else>{{ line.title.slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<strong>{{ line.title }}</strong>
|
||||
<p>¥{{ line.price || formatCent(line.price_cent) }}</p>
|
||||
<div class="line-actions">
|
||||
<van-stepper
|
||||
:model-value="line.quantity"
|
||||
:min="1"
|
||||
:max="line.stock > 0 ? line.stock : 99"
|
||||
@change="(v: number) => setQuantity(line.product_id, Number(v || 1))"
|
||||
/>
|
||||
<button type="button" class="rm" @click="removeLine(line.product_id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="!isEmpty" class="m-footer">
|
||||
<div>
|
||||
共{{ totalCount }}件
|
||||
<strong>¥{{ formatCent(totalAmountCent) }}</strong>
|
||||
</div>
|
||||
<van-button
|
||||
type="primary"
|
||||
color="#ff6a00"
|
||||
round
|
||||
:loading="checkingOut"
|
||||
@click="handleCheckout"
|
||||
>
|
||||
一起结账
|
||||
</van-button>
|
||||
</footer>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
place-items: center;
|
||||
padding: 48px 16px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.cart-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
.cart-line {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.cover {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f1f5f9;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.info strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.info p {
|
||||
margin: 4px 0 8px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.line-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.line-amount {
|
||||
color: #ff6a00;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cart-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid #eef1f5;
|
||||
background: #fff;
|
||||
}
|
||||
.cart-footer strong {
|
||||
margin-left: 8px;
|
||||
color: #ff6a00;
|
||||
font-size: 18px;
|
||||
}
|
||||
.m-cart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 78vh;
|
||||
}
|
||||
.m-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #eef1f5;
|
||||
}
|
||||
.m-head h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.m-head button {
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #64748b;
|
||||
}
|
||||
.m-list {
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
flex: 1;
|
||||
}
|
||||
.m-line {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.rm {
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
}
|
||||
.m-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #eef1f5;
|
||||
}
|
||||
.m-footer strong {
|
||||
margin-left: 6px;
|
||||
color: #ff6a00;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user