| New file |
| | |
| | | <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, verifyOrder } 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 discountAmount = computed(() => Math.max(0, Number(detail.value?.orderAmount || 0) - Number(detail.value?.subsidyAmount || 0) - Number(detail.value?.payAmount || 0))) |
| | | |
| | | function money(value: unknown) { |
| | | if (value === null || value === undefined || value === '') return '-' |
| | | return `¥${(Number(value) / 100).toFixed(2)}` |
| | | } |
| | | |
| | | 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 paymentStatus(value: string) { |
| | | return ({ pending: '待支付', processing: '支付中', paid: '支付成功', failed: '支付失败', closed: '已关闭' } as Record<string, string>)[value] || value || '-' |
| | | } |
| | | |
| | | function refundStatus(value: number) { |
| | | return ({ 1: '退款中', 2: '退款成功', 3: '退款失败' } as Record<number, string>)[Number(value)] || '-' |
| | | } |
| | | |
| | | function canPay() { |
| | | return detail.value?.orderStatus === 0 |
| | | } |
| | | |
| | | function canVerify() { |
| | | return detail.value?.orderStatus === 1 && detail.value?.verifyStatus !== 1 |
| | | } |
| | | |
| | | 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 verify() { |
| | | if (!detail.value?.orderNo) return |
| | | await ElMessageBox.confirm('确认核销该订单?', '订单核销', { type: 'warning' }) |
| | | detail.value = await verifyOrder(detail.value.orderNo, String(detail.value.orderType || '').includes('STORE') ? 'STORE_CONSUME' : 'GROUP', detail.value.merchantId) |
| | | 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-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> |
| | | </section> |
| | | |
| | | <section class="detail-section"> |
| | | <h3>订单信息</h3> |
| | | <div class="info-card info-grid order-grid"> |
| | | <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><span>优惠信息</span><strong>{{ discountAmount ? `优惠 ¥${(discountAmount / 100).toFixed(2)}` : '-' }}</strong></div> |
| | | <div><span>优惠金额</span><strong>{{ discountAmount ? `-¥${(discountAmount / 100).toFixed(2)}` : '-' }}</strong></div> |
| | | <div><span>政府补贴</span><strong>{{ money(detail.subsidyAmount) }}</strong></div> |
| | | <div><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>{{ dateTime(payment.paidAt || payment.createdAt) }}</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><span>支付状态</span><strong>{{ paymentStatus(payment.status) }}</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="canVerify()" type="primary" @click="verify">核销</el-button> |
| | | <el-button v-if="canRefund()" type="danger" plain @click="refund">退款</el-button> |
| | | </div> |
| | | </template> |
| | | </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; } |
| | | .empty-cover { color: #86909c; } |
| | | .order-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } |
| | | .order-grid div:nth-child(5), .order-grid div:nth-child(9) { grid-column: span 2; } |
| | | .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> |