Files
hfb_sys/frontend/src/features/mohong/views/MohongOrderDetailView.vue
T
yml2213 d93795a4b4 统一订单列表混排租赁与撞车,后台撞车订单展示真实手机号
PC/移动端「我的订单」并行拉取两类订单并按时间混排展示;后台撞车订单买家手机号不再脱敏,便于履约联系。
2026-07-19 16:01:45 +08:00

516 lines
12 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 { ElMessage } from 'element-plus'
import {
cancelMyMohongOrder,
fetchMyMohongOrder,
mohongOrderStatusLabel,
queryMohongPayment,
startMohongPayment,
type MohongOrder,
} from '@/features/mohong/api/mohong'
import PayWaySelectDialog from '@/features/orders/components/PayWaySelectDialog.vue'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useOrderPaymentCashier } from '@/features/orders/composables/useOrderPaymentCashier'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
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 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,
})
const statusText = computed(() =>
order.value ? mohongOrderStatusLabel(order.value.status) : ''
)
const activePayWayLabel = computed(() => {
const map: Record<string, string> = { WXZF: '微信', ZFBZF: '支付宝' }
return map[cashier.activePayment.value?.pay_way || ''] || '微信/支付宝'
})
onMounted(loadOrder)
async function loadOrder() {
loading.value = true
try {
order.value = await fetchMyMohongOrder(String(route.params.id))
} catch (error) {
ElMessage.error(readError(error, '订单不存在'))
} finally {
loading.value = false
}
}
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
}
async function handlePay() {
if (!order.value || acting.value) return
const payWay = await selectPayWay()
if (!payWay) return
acting.value = true
try {
const payment = await startMohongPayment(order.value.id, payWay)
if (payment.paid || payment.status === 'paid') {
ElMessage.success('支付成功')
await loadOrder()
return
}
await cashier.openPaymentCashier(payment, async () => {
await loadOrder()
})
} catch (error) {
ElMessage.error(readError(error, '支付失败'))
} finally {
acting.value = false
}
}
async function handleCancel() {
if (!order.value || acting.value) return
acting.value = true
try {
order.value = await cancelMyMohongOrder(order.value.id)
ElMessage.success('已取消')
} catch (error) {
ElMessage.error(readError(error, '取消失败'))
} finally {
acting.value = false
}
}
async function copyText() {
if (!order.value?.copy_text) {
ElMessage.warning('暂无可复制信息')
return
}
try {
await navigator.clipboard.writeText(order.value.copy_text)
ElMessage.success('已复制订单信息')
} catch {
ElMessage.error('复制失败')
}
}
</script>
<template>
<section v-loading="loading" class="order-detail-page">
<template v-if="order">
<header class="page-header">
<div>
<p class="eyebrow">订单详情</p>
<h1>{{ statusText }}</h1>
<p class="sub">订单号 {{ order.order_no }} · {{ formatDateTime(order.created_at) }}</p>
</div>
<button type="button" class="toolbar-btn outline" @click="router.push('/orders')">
返回列表
</button>
</header>
<div class="layout">
<div class="main-card">
<div class="product-row">
<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>
</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>
<button type="button" class="btn outline sm" @click="copyText">一键复制</button>
</div>
<pre>{{ order.copy_text }}</pre>
</div>
<div v-if="order.qrcode_url_snapshot" class="qr-box">
<h3>客服二维码</h3>
<img :src="order.qrcode_url_snapshot" alt="客服二维码" />
</div>
</div>
<aside class="side-card">
<h3>操作</h3>
<button
v-if="order.status === 'pending_payment'"
type="button"
class="btn solid"
:disabled="acting"
@click="handlePay"
>
{{ acting ? '处理中...' : '去支付' }}
</button>
<button
v-if="order.status === 'pending_payment'"
type="button"
class="btn outline"
:disabled="acting"
@click="handleCancel"
>
取消订单
</button>
<button v-if="order.copy_text" type="button" class="btn outline" @click="copyText">
复制订单信息
</button>
<button type="button" class="btn outline" @click="router.push('/crash')">
继续选购
</button>
</aside>
</div>
</template>
<el-empty v-else-if="!loading" description="订单不存在" />
<PayWaySelectDialog
v-model="payWayDialogVisible"
@choose="choosePayWay"
@closed="handlePayWayDialogClosed"
/>
<el-dialog
v-model="cashier.paymentPopupVisible.value"
title="订单支付"
width="520px"
append-to-body
class="mohong-pay-dialog"
@closed="cashier.stopPaymentPolling"
>
<div v-if="cashier.activePayment.value" class="pay-dialog-body">
<div class="pay-amount">
支付金额
<strong>¥{{ formatCent(cashier.activePayment.value.amount_cent) }}</strong>
</div>
<div v-if="cashier.paymentQRCodeURL.value" class="pay-qr">
<img :src="cashier.paymentQRCodeURL.value" alt="支付二维码" />
<p>请使用{{ activePayWayLabel }}扫码支付</p>
</div>
</div>
<template #footer>
<button
type="button"
class="btn solid"
:disabled="cashier.checkingPayment.value"
@click="cashier.refreshPaymentStatus(false)"
>
{{ cashier.checkingPayment.value ? '查询中...' : '我已支付,刷新状态' }}
</button>
</template>
</el-dialog>
</section>
</template>
<style scoped>
.order-detail-page {
width: 100%;
display: grid;
gap: 18px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.eyebrow {
margin: 0 0 6px;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
}
.page-header h1 {
margin: 0 0 6px;
font-size: 24px;
font-weight: 800;
color: #17233d;
}
.sub {
margin: 0;
color: #8a94a6;
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) 280px;
gap: 16px;
align-items: start;
width: 100%;
}
.main-card,
.side-card {
background: #fff;
border: 1px solid #eef1f5;
border-radius: 16px;
padding: 18px;
}
.product-row {
display: grid;
grid-template-columns: 88px 1fr;
gap: 14px;
margin-bottom: 18px;
}
.cover {
width: 88px;
height: 88px;
border-radius: 12px;
overflow: hidden;
background: #f3f6fb;
display: flex;
align-items: center;
justify-content: center;
color: #a0aec0;
}
.cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-row h2 {
margin: 0 0 8px;
font-size: 18px;
}
.product-row p {
margin: 0 0 8px;
color: #8a94a6;
}
.product-row strong {
color: #ff6a00;
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;
padding-top: 16px;
border-top: 1px solid #eef1f5;
}
.copy-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.copy-box h3,
.qr-box h3,
.side-card h3 {
margin: 0;
font-size: 15px;
color: #17233d;
}
.copy-box pre {
margin: 0;
padding: 12px;
border-radius: 10px;
background: #f7f9fc;
white-space: pre-wrap;
font-size: 13px;
line-height: 1.6;
color: #334155;
}
.qr-box {
text-align: center;
}
.qr-box img {
margin-top: 12px;
width: 180px;
height: 180px;
object-fit: contain;
border-radius: 12px;
background: #f7f9fc;
}
.side-card {
display: grid;
gap: 10px;
align-content: start;
}
.side-card .btn {
width: 100%;
}
.mohong-pay-dialog :deep(.el-dialog__footer) {
display: flex;
justify-content: center;
}
.pay-dialog-body {
display: grid;
gap: 14px;
justify-items: center;
text-align: center;
}
.pay-amount strong {
display: block;
margin-top: 6px;
color: #ff6a00;
font-size: 28px;
}
.pay-qr img {
width: 220px;
height: 220px;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
}
</style>