318 lines
7.9 KiB
Vue
318 lines
7.9 KiB
Vue
<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>
|