<script setup lang="ts">
|
import { computed, onMounted, ref, watch } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { getOrderDetail, payOrder, refundOrder } from '../api/admin'
|
|
type Detail = Record<string, any>
|
|
const route = useRoute()
|
const router = useRouter()
|
const loading = ref(false)
|
const detail = ref<Detail | null>(null)
|
|
const items = computed(() => detail.value?.items || [])
|
const payments = computed(() => detail.value?.payments || [])
|
const refunds = computed(() => detail.value?.refunds || [])
|
const previewVisible = ref(false)
|
const previewUrl = ref('')
|
const talentDiscountAmount = computed(() => Math.max(0, Number(detail.value?.orderAmount || 0) - Number(detail.value?.basePayAmount ?? detail.value?.orderAmount ?? 0)))
|
const couponAmount = computed(() => Math.max(0, Number(detail.value?.couponAmount || 0)))
|
const talentDiscountRate = computed(() => {
|
const original = Number(detail.value?.orderAmount || 0)
|
const base = Number(detail.value?.basePayAmount ?? original)
|
if (!original || !base) return '-'
|
return `${(base / original * 10).toFixed(1).replace(/\.0$/, '')}折`
|
})
|
|
function money(value: unknown) {
|
if (value === null || value === undefined || value === '') return '-'
|
return `¥${(Number(value) / 100).toFixed(2)}`
|
}
|
|
function yuan(value: unknown) {
|
if (value === null || value === undefined || value === '') return '-'
|
return `${(Number(value) / 100).toFixed(2)}元`
|
}
|
|
function discountMoney(value: unknown) {
|
const amount = Math.max(0, Number(value || 0))
|
return amount > 0 ? `-${(amount / 100).toFixed(2)}元` : '0元'
|
}
|
|
function dateTime(value: unknown) {
|
if (!value) return '-'
|
return String(value).replace('T', ' ').replace(/\.\d+$/, '')
|
}
|
|
function orderStatus(value: Detail | null) {
|
if (!value) return '-'
|
if (value.orderStatus === 0) return '待支付'
|
if (value.orderStatus === 2) return '已退款'
|
return value.verifyStatus === 1 ? '已核销' : '待核销'
|
}
|
|
function previewImage(url: string) {
|
if (!url) return
|
previewUrl.value = url
|
previewVisible.value = true
|
}
|
|
function paymentTime(payment: Detail) {
|
if (detail.value?.orderStatus === 0 || payment.status === 'pending') return '-'
|
return dateTime(payment.paidAt)
|
}
|
|
function refundStatus(value: number) {
|
return ({ 1: '退款中', 2: '退款成功', 3: '退款失败' } as Record<number, string>)[Number(value)] || '-'
|
}
|
|
function canPay() {
|
return detail.value?.orderStatus === 0
|
}
|
|
function canRefund() {
|
return detail.value?.orderStatus === 1 && detail.value?.verifyStatus !== 1
|
}
|
|
async function load() {
|
const orderNo = String(route.params.orderNo || '')
|
if (!orderNo) return
|
loading.value = true
|
try {
|
detail.value = await getOrderDetail(orderNo)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
async function markPaid() {
|
if (!detail.value?.orderNo) return
|
await ElMessageBox.confirm('确认将该订单标记为已支付?', '订单支付', { type: 'warning' })
|
detail.value = await payOrder(detail.value.orderNo)
|
await load()
|
ElMessage.success('订单已支付')
|
}
|
|
async function refund() {
|
if (!detail.value?.orderNo) return
|
const defaultAmount = (Number(detail.value.payAmount || 0) / 100).toFixed(2)
|
const { value } = await ElMessageBox.prompt('请输入退款金额(元)', '订单退款', {
|
inputValue: defaultAmount,
|
inputPattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/,
|
inputErrorMessage: '请输入正确的金额',
|
})
|
await refundOrder(detail.value.orderNo, Number(value))
|
await load()
|
ElMessage.success('退款申请已提交')
|
}
|
|
watch(() => route.params.orderNo, load)
|
onMounted(load)
|
</script>
|
|
<template>
|
<div v-loading="loading" class="order-detail-page">
|
<div class="detail-toolbar">
|
<el-button :icon="ArrowLeft" @click="router.back()">返回订单列表</el-button>
|
</div>
|
|
<template v-if="detail">
|
<section class="detail-section">
|
<h3>用户信息</h3>
|
<div class="info-card info-grid user-grid">
|
<div><span>持卡人名称</span><strong>{{ detail.talentName || '-' }}</strong></div>
|
<div><span>持卡人手机号</span><strong>{{ detail.talentMobile || '-' }}</strong></div>
|
<div><span>人才卡等级</span><strong>{{ detail.talentLevel || '-' }}</strong></div>
|
<div><span>人才卡号</span><strong>{{ detail.cardNo || '-' }}</strong></div>
|
</div>
|
</section>
|
|
<section class="detail-section">
|
<h3>商品信息</h3>
|
<div class="info-card merchant-line">
|
<span>商户名称</span><strong>{{ detail.merchantName || '-' }}</strong>
|
<span>商户联系电话</span><strong>{{ detail.contactMobile || '-' }}</strong>
|
<span>商户类型</span><strong>{{ detail.merchantType || '-' }}</strong>
|
</div>
|
<el-table :data="items" border class="detail-items" empty-text="暂无商品信息">
|
<el-table-column prop="id" label="id" min-width="110" align="center" />
|
<el-table-column prop="name" label="商品名称" min-width="220" show-overflow-tooltip />
|
<el-table-column label="封面图" width="130" align="center">
|
<template #default="{ row }">
|
<el-image v-if="row.coverUrl" :src="row.coverUrl" fit="cover" class="product-cover" :preview-src-list="[row.coverUrl]" preview-teleported />
|
<span v-else class="empty-cover">-</span>
|
</template>
|
</el-table-column>
|
<el-table-column label="商品金额" min-width="130" align="right">
|
<template #default="{ row }">{{ money(row.amount) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="100" fixed="right" align="center">
|
<template #default="{ row }">
|
<el-button link type="primary" :disabled="!row.coverUrl" @click="previewImage(row.coverUrl)">查看</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</section>
|
|
<section class="detail-section">
|
<h3>订单信息</h3>
|
<div class="info-card info-grid order-grid" :class="{ 'store-consume-order-grid': detail.orderType === 'STORE_CONSUME' }">
|
<div><span>订单编号</span><strong>{{ detail.orderNo || '-' }}</strong></div>
|
<div><span>商品金额</span><strong>{{ money(detail.orderAmount) }}</strong></div>
|
<div><span>商品数量</span><strong>{{ items.length }}</strong></div>
|
<div><span>小计</span><strong>{{ money(detail.orderAmount) }}</strong></div>
|
<div class="discount-summary-row">
|
<section class="discount-prototype-grid">
|
<span class="discount-title">优惠信息</span>
|
<span class="discount-label">人才折扣</span>
|
<strong class="discount-highlight">{{ talentDiscountRate }}</strong>
|
<span class="discount-label">折扣金额:</span>
|
<strong class="discount-amount">{{ discountMoney(talentDiscountAmount) }}</strong>
|
<span class="discount-label">商品优惠</span>
|
<strong class="discount-highlight">{{ detail.couponName || '-' }}</strong>
|
<span class="discount-label">优惠金额:</span>
|
<strong class="discount-amount">{{ discountMoney(couponAmount) }}</strong>
|
<span class="discount-label">补贴金额</span>
|
<strong class="discount-amount">{{ yuan(detail.subsidyAmount) }}</strong>
|
</section>
|
</div>
|
<div class="actual-pay-row"><span>实际应付</span><strong class="amount-strong">{{ money(detail.payAmount) }}</strong></div>
|
<div><span>下单时间</span><strong>{{ dateTime(detail.createdAt) }}</strong></div>
|
<div><span>核销时间</span><strong>{{ dateTime(detail.verifyTime) }}</strong></div>
|
<div><span>订单状态</span><strong>{{ orderStatus(detail) }}</strong></div>
|
</div>
|
</section>
|
|
<section class="detail-section">
|
<h3>支付信息</h3>
|
<div v-if="payments.length" class="records-stack">
|
<div v-for="payment in payments" :key="payment.paymentNo" class="info-card info-grid payment-grid">
|
<div><span>支付时间</span><strong>{{ paymentTime(payment) }}</strong></div>
|
<div><span>支付方式</span><strong>{{ payment.provider === 'HUIFU' ? '微信支付' : payment.provider || '-' }}</strong></div>
|
<div><span>支付流水号</span><strong>{{ payment.providerTransactionNo || payment.paymentNo || '-' }}</strong></div>
|
<div><span>支付金额</span><strong class="amount-strong">{{ money(payment.amount) }}</strong></div>
|
</div>
|
</div>
|
<div v-else class="detail-empty">暂无支付流水</div>
|
</section>
|
|
<section class="detail-section">
|
<h3>退款信息</h3>
|
<div v-if="refunds.length" class="records-stack">
|
<div v-for="refundItem in refunds" :key="refundItem.refundNo" class="info-card info-grid refund-grid">
|
<div><span>退款时间</span><strong>{{ dateTime(refundItem.refundTime) }}</strong></div>
|
<div><span>退款渠道</span><strong>{{ refundItem.provider === 'HUIFU' ? '微信支付' : refundItem.provider || '-' }}</strong></div>
|
<div><span>退款流水号</span><strong>{{ refundItem.providerRefundNo || refundItem.refundNo || '-' }}</strong></div>
|
<div><span>退款金额</span><strong class="amount-strong">{{ money(refundItem.refundAmount) }}</strong></div>
|
<div><span>剩余可退金额</span><strong>{{ money(refundItem.refundableAmount) }}</strong></div>
|
<div><span>退款状态</span><strong>{{ refundStatus(refundItem.refundStatus) }}</strong></div>
|
</div>
|
</div>
|
<div v-else class="detail-empty">暂无退款记录</div>
|
</section>
|
|
<div class="detail-actions">
|
<el-button @click="router.back()">返回</el-button>
|
<el-button v-if="canPay()" type="primary" @click="markPaid">标记支付</el-button>
|
<el-button v-if="canRefund()" type="primary" @click="refund">退款</el-button>
|
</div>
|
</template>
|
<el-dialog v-model="previewVisible" title="封面图预览" width="640px" destroy-on-close :close-on-click-modal="false">
|
<el-image v-if="previewUrl" :src="previewUrl" fit="contain" class="order-preview-image" />
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped>
|
.order-detail-page { min-width: 960px; padding: 2px 0 28px; }
|
.detail-toolbar { display: flex; justify-content: flex-end; margin-bottom: 16px; }
|
.detail-section { margin-bottom: 22px; }
|
.detail-section h3 { margin: 0 0 12px; color: #1d2129; font-size: 16px; font-weight: 600; }
|
.info-card { background: #f8fafc; border-radius: 2px; }
|
.info-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 34px; padding: 18px 14px; }
|
.info-grid div, .merchant-line { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
|
.info-grid span, .merchant-line span { flex: 0 0 auto; color: #4e5969; font-size: 13px; }
|
.info-grid strong, .merchant-line strong { overflow: hidden; color: #1d2129; font-size: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
.user-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
.merchant-line { gap: 14px; padding: 16px 14px; }
|
.merchant-line strong { margin-right: 38px; }
|
.detail-items { margin-top: 10px; }
|
.detail-items :deep(th.el-table__cell) { color: #1d2129; font-weight: 600; background: #f3f4f6; }
|
.product-cover { width: 48px; height: 38px; vertical-align: middle; }
|
.order-preview-image { display: block; width: 100%; max-height: 520px; }
|
.empty-cover { color: #86909c; }
|
.order-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
.store-consume-order-grid .actual-pay-row { grid-column: 1 / -1; }
|
.info-grid .discount-summary-row { grid-column: 1 / -1; display: block !important; align-items: initial !important; gap: 0 !important; }
|
.discount-prototype-grid {
|
display: grid !important;
|
grid-template-columns: 86px 78px 142px 88px 108px;
|
grid-template-rows: repeat(3, 30px);
|
align-items: center;
|
width: 502px;
|
min-width: 502px;
|
}
|
.discount-title {
|
grid-row: 1 / span 3;
|
align-self: start;
|
padding-top: 4px;
|
color: #1d2129 !important;
|
font-size: 13px;
|
font-weight: 600;
|
line-height: 20px;
|
}
|
.discount-label {
|
color: #1d2129 !important;
|
font-size: 13px;
|
font-weight: 600;
|
line-height: 20px;
|
}
|
.discount-highlight {
|
display: block;
|
overflow: visible !important;
|
color: #f59e0b !important;
|
font-size: 13px;
|
font-weight: 600 !important;
|
line-height: 20px;
|
text-overflow: clip !important;
|
white-space: nowrap !important;
|
}
|
.discount-amount {
|
display: block;
|
overflow: visible !important;
|
color: #1d2129 !important;
|
font-size: 13px;
|
font-weight: 600 !important;
|
line-height: 20px;
|
text-align: left;
|
text-overflow: clip !important;
|
white-space: nowrap !important;
|
}
|
.payment-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
.refund-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
.amount-strong { color: #1d2129; font-weight: 600 !important; }
|
.records-stack { display: grid; gap: 10px; }
|
.detail-empty { padding: 20px; color: #86909c; text-align: center; background: #f8fafc; }
|
.detail-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 6px; }
|
@media (max-width: 1280px) { .info-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .user-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
</style>
|