Files
hfb_sys/frontend/src/features/admin/views/AdminMohongOrdersView.vue
T

345 lines
10 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 { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Money, Refresh } from '@element-plus/icons-vue'
import {
cancelAdminMohongOrder,
completeAdminMohongOrder,
fetchAdminMohongOrders,
mohongOrderStatusLabel,
receiveAdminMohongOrder,
refundAdminMohongOrder,
type MohongOrder,
} from '@/features/mohong/api/mohong'
import { centToYuan, formatCent, yuanToCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false)
const items = ref<MohongOrder[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const keyword = ref('')
const status = ref('')
const detail = ref<MohongOrder | null>(null)
const refundingId = ref<number | null>(null)
onMounted(load)
async function load() {
loading.value = true
try {
const result = await fetchAdminMohongOrders({
keyword: keyword.value || undefined,
status: status.value || undefined,
page: page.value,
page_size: pageSize.value,
})
items.value = result.items || []
total.value = result.total || 0
} catch (error) {
ElMessage.error(readError(error, '加载失败'))
} finally {
loading.value = false
}
}
async function handleReceive(row: MohongOrder) {
try {
await ElMessageBox.confirm(`确认接待订单 ${row.order_no}?标记后状态变为「接待中」。`, '提示', {
type: 'info',
})
await receiveAdminMohongOrder(row.id)
ElMessage.success('已标记为接待中')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
}
}
async function handleComplete(row: MohongOrder) {
try {
await ElMessageBox.confirm(`确认将订单 ${row.order_no} 标记为已完成?`, '提示', {
type: 'warning',
})
await completeAdminMohongOrder(row.id)
ElMessage.success('已完成')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
}
}
async function handleCancel(row: MohongOrder) {
try {
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消订单', {
inputPlaceholder: '可选',
confirmButtonText: '确认取消',
cancelButtonText: '返回',
})
await cancelAdminMohongOrder(row.id, value || '后台取消')
ElMessage.success('已取消')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
}
}
async function handleRefund(row: MohongOrder) {
const defaultAmount = centToYuan(row.amount_cent).toFixed(2)
try {
const { value } = await ElMessageBox.prompt(
`请输入退款金额(元),默认全额 ¥${defaultAmount}`,
'人工退款',
{
inputValue: defaultAmount,
inputPlaceholder: defaultAmount,
confirmButtonText: '确认退款',
cancelButtonText: '返回',
type: 'warning',
inputValidator(input: string) {
const amount = Number(input)
if (!Number.isFinite(amount) || amount <= 0) return '请输入大于 0 的退款金额'
if (yuanToCent(amount) > row.amount_cent) return '退款金额不能超过订单金额'
return true
},
}
)
const amountCent = yuanToCent(Number(value))
refundingId.value = row.id
const result = await refundAdminMohongOrder(row.id, amountCent, '后台撞车订单人工退款')
ElMessage.success(result?.status === 'refunded' ? '退款成功' : '退款已提交')
await load()
} catch (error) {
if (error !== 'cancel') ElMessage.error(readError(error, '退款失败'))
} finally {
refundingId.value = null
}
}
function canReceive(row: MohongOrder) {
return row.status === 'paid'
}
function canComplete(row: MohongOrder) {
return row.status === 'paid' || row.status === 'receiving'
}
function canCancel(row: MohongOrder) {
return row.status === 'pending_payment' || row.status === 'paid' || row.status === 'receiving'
}
function canRefund(row: MohongOrder) {
return row.status === 'cancelled' && Boolean(row.paid_at) && Number(row.amount_cent || 0) > 0
}
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制')
} catch {
ElMessage.error('复制失败')
}
}
</script>
<template>
<div class="admin-page">
<div class="page-head">
<div>
<h2>撞车订单</h2>
<p>查看订单接待买家完成履约复制用户订单信息</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</div>
<div class="filters">
<el-input
v-model="keyword"
clearable
placeholder="订单号"
style="width: 220px"
@keyup.enter="load"
/>
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
<el-option label="待支付" value="pending_payment" />
<el-option label="已支付" value="paid" />
<el-option label="接待中" value="receiving" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已退款" value="refunded" />
</el-select>
<el-button type="primary" @click=";((page = 1), load())">查询</el-button>
</div>
<el-table v-loading="loading" :data="items" border stripe>
<el-table-column prop="order_no" label="订单号" min-width="170" />
<el-table-column prop="product_title" label="商品" min-width="140" />
<el-table-column label="数量" width="70" prop="quantity" />
<el-table-column label="金额" width="100">
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
</el-table-column>
<el-table-column label="买家" min-width="140">
<template #default="{ row }">
{{ row.buyer_nickname || '-' }}
<div class="sub">{{ row.buyer_phone || '' }}</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">{{ mohongOrderStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column label="下单时间" min-width="160">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="310" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="detail = row">详情</el-button>
<el-button v-if="canReceive(row)" link type="warning" @click="handleReceive(row)">
接待
</el-button>
<el-button v-if="canComplete(row)" link type="success" @click="handleComplete(row)">
完成
</el-button>
<el-button v-if="canCancel(row)" link type="danger" @click="handleCancel(row)">
取消
</el-button>
<el-button
v-if="canRefund(row)"
link
type="warning"
:icon="Money"
:loading="refundingId === row.id"
@click="handleRefund(row)"
>
退款
</el-button>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
:current-page="page"
:page-size="pageSize"
:total="total"
@update:current-page="page = $event"
@update:page-size="pageSize = $event"
@page-change="load"
/>
<el-drawer
:model-value="Boolean(detail)"
size="420px"
title="订单详情"
destroy-on-close
@update:model-value="(v: boolean) => !v && (detail = null)"
>
<template v-if="detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="订单号">{{ detail.order_no }}</el-descriptions-item>
<el-descriptions-item label="状态">
{{ mohongOrderStatusLabel(detail.status) }}
</el-descriptions-item>
<el-descriptions-item label="商品">{{ detail.product_title }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
<el-descriptions-item label="金额">
¥{{ detail.amount || formatCent(detail.amount_cent) }}
</el-descriptions-item>
<el-descriptions-item label="买家">
{{ detail.buyer_nickname }} {{ detail.buyer_phone }}
</el-descriptions-item>
</el-descriptions>
<div v-if="detail.items?.length" class="items-box">
<strong>商品明细</strong>
<div v-for="item in detail.items" :key="item.product_id" class="item-line">
<span>{{ item.title }} × {{ item.quantity }}</span>
<em>¥{{ item.amount || formatCent(item.amount_cent) }}</em>
</div>
</div>
<div v-if="detail.copy_text" class="copy-box">
<div class="copy-head">
<strong>复制文案</strong>
<el-button size="small" @click="copyText(detail.copy_text)">复制</el-button>
</div>
<pre>{{ detail.copy_text }}</pre>
</div>
<div v-if="detail.qrcode_url_snapshot" class="qr-box">
<strong>二维码快照</strong>
<el-image
:src="detail.qrcode_url_snapshot"
fit="contain"
style="width: 180px; height: 180px"
/>
</div>
</template>
</el-drawer>
</div>
</template>
<style scoped>
.admin-page {
display: grid;
gap: 16px;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-head h2 {
margin: 0 0 4px;
}
.page-head p,
.sub {
margin: 0;
color: #6b7a90;
font-size: 12px;
}
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.items-box {
margin-top: 14px;
display: grid;
gap: 8px;
}
.item-line {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 13px;
color: #334155;
}
.item-line em {
font-style: normal;
color: #ff6a00;
font-weight: 600;
}
.copy-box,
.qr-box {
margin-top: 16px;
}
.copy-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.copy-box pre {
margin: 0;
padding: 12px;
background: #f7f9fc;
border-radius: 8px;
white-space: pre-wrap;
font-size: 13px;
line-height: 1.6;
}
.qr-box {
display: grid;
gap: 8px;
}
</style>