Files
hfb_sys/frontend/src/features/mohong/views/MobileMohongOrderDetailView.vue
T

346 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
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 {
cancelMyMohongOrder,
fetchMyMohongOrder,
mohongOrderStatusLabel,
queryMohongPayment,
startMohongPayment,
type MohongOrder,
} from '@/features/mohong/api/mohong'
import { formatCent } from '@/shared/utils/money'
import { readError } from '@/shared/utils/error'
const route = useRoute()
const router = useRouter()
const order = ref<MohongOrder | null>(null)
const loading = ref(true)
const acting = ref(false)
const payWaySelect = useMobilePayWaySelect()
const {
paymentPopupVisible,
activePayment,
payURL,
paymentQRCodeURL,
qrGenerating,
checkingPayment,
openMobilePaymentCashier,
refreshPaymentStatus,
} = useMobilePaymentCashier({
queryPayment: queryMohongPayment,
paidMessage: '支付成功',
})
const statusText = computed(() => (order.value ? mohongOrderStatusLabel(order.value.status) : ''))
onMounted(loadOrder)
async function loadOrder() {
loading.value = true
try {
order.value = await fetchMyMohongOrder(String(route.params.id))
} catch (error) {
showToast({ message: readError(error, '订单不存在'), icon: 'cross' })
} finally {
loading.value = false
}
}
async function handlePay() {
if (!order.value || acting.value) return
const payWay = await payWaySelect.select()
if (!payWay) return
acting.value = true
try {
const payment = await startMohongPayment(order.value.id, payWay)
if (payment.paid || payment.status === 'paid') {
showToast({ message: '支付成功', icon: 'passed' })
await loadOrder()
return
}
await openMobilePaymentCashier(payment, async () => {
await loadOrder()
})
} catch (error) {
showToast({ message: readError(error, '支付失败'), icon: 'cross' })
} finally {
acting.value = false
}
}
async function handleCancel() {
if (!order.value || acting.value) return
acting.value = true
try {
order.value = await cancelMyMohongOrder(order.value.id)
showToast({ message: '已取消', icon: 'passed' })
} catch (error) {
showToast({ message: readError(error, '取消失败'), icon: 'cross' })
} finally {
acting.value = false
}
}
async function copyText() {
if (!order.value?.copy_text) {
showToast({ message: '暂无可复制信息', icon: 'warning-o' })
return
}
try {
await navigator.clipboard.writeText(order.value.copy_text)
showToast({ message: '已复制订单信息', icon: 'passed' })
} catch {
showToast({ message: '复制失败', icon: 'cross' })
}
}
</script>
<template>
<main class="order-shell">
<header class="page-header">
<button type="button" class="back-btn" @click="router.back()">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>订单详情</h1>
</header>
<van-loading v-if="loading" class="state-loading" size="24px" vertical>加载中...</van-loading>
<template v-else-if="order">
<section class="panel status-panel">
<strong>{{ statusText }}</strong>
<p>订单号 {{ order.order_no }}</p>
</section>
<section class="panel product-panel">
<div class="cover">
<img v-if="order.product_cover_url" :src="order.product_cover_url" alt="" />
<span v-else></span>
</div>
<div>
<h2>{{ order.product_title }}</h2>
<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>
<button type="button" @click="copyText">一键复制</button>
</div>
<pre class="copy-text">{{ order.copy_text }}</pre>
</section>
<section v-if="order.qrcode_url_snapshot" class="panel qr-panel">
<h3>客服二维码</h3>
<img :src="order.qrcode_url_snapshot" alt="客服二维码" />
</section>
<div class="actions">
<van-button
v-if="order.status === 'pending_payment'"
type="primary"
color="#ff6a00"
block
round
:loading="acting"
@click="handlePay"
>
去支付
</van-button>
<van-button
v-if="order.status === 'pending_payment'"
plain
block
round
:loading="acting"
@click="handleCancel"
>
取消订单
</van-button>
<van-button v-if="order.copy_text" plain block round @click="copyText">
复制订单信息
</van-button>
</div>
</template>
<van-empty v-else description="订单不存在" />
<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)"
/>
</main>
</template>
<style scoped>
.order-shell {
min-height: 100vh;
background: #f5f7fb;
padding-bottom: 24px;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
background: #fff;
border-bottom: 1px solid #eef1f5;
}
.back-btn {
border: 0;
background: transparent;
}
.page-header h1 {
margin: 0;
font-size: 17px;
}
.state-loading {
padding: 48px 0;
}
.panel {
margin: 10px 12px;
padding: 14px;
border-radius: 14px;
background: #fff;
}
.status-panel strong {
display: block;
font-size: 20px;
color: #ff6a00;
margin-bottom: 4px;
}
.status-panel p {
margin: 0;
color: #8a94a6;
font-size: 13px;
}
.product-panel {
display: grid;
grid-template-columns: 72px 1fr;
gap: 12px;
}
.cover {
width: 72px;
height: 72px;
border-radius: 10px;
overflow: hidden;
background: #f0f3f8;
display: flex;
align-items: center;
justify-content: center;
color: #a0aec0;
}
.cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-panel h2 {
margin: 0 0 6px;
font-size: 15px;
}
.product-panel p {
margin: 0 0 6px;
color: #8a94a6;
font-size: 13px;
}
.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;
justify-content: space-between;
margin-bottom: 8px;
}
.copy-head h3,
.qr-panel h3 {
margin: 0;
font-size: 14px;
}
.copy-head button {
border: 0;
background: #fff4ea;
color: #ff6a00;
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
}
.copy-text {
margin: 0;
padding: 12px;
border-radius: 10px;
background: #f7f9fc;
color: #334155;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.qr-panel {
text-align: center;
}
.qr-panel img {
margin-top: 10px;
width: 180px;
height: 180px;
object-fit: contain;
border-radius: 12px;
background: #f7f9fc;
}
.actions {
padding: 8px 12px 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
</style>