first commit
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import SearchFormCard from '@/components/SearchFormCard.vue'
|
||||
import PaginationBar from '@/components/PaginationBar.vue'
|
||||
import { getOrderDetail, getOrderList } from '@/api/order'
|
||||
import type { OrderItem } from '@/types/order'
|
||||
import { ORDER_STATUS, PAY_CHANNEL, XPAY_ENV, enumLabel, enumOptions, enumTag } from '@/utils/constants'
|
||||
import { formatDate, formatMoney, formatText } from '@/utils/format'
|
||||
|
||||
/* ---------------- 列表与筛选 ---------------- */
|
||||
const loading = ref(false)
|
||||
const list = ref<OrderItem[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const query = reactive({
|
||||
order_no: '',
|
||||
user_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined,
|
||||
})
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getOrderList({
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
order_no: query.order_no || undefined,
|
||||
user_id: query.user_id || undefined,
|
||||
status: query.status,
|
||||
})
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} catch {
|
||||
// 错误提示已由请求层处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
query.order_no = ''
|
||||
query.user_id = undefined
|
||||
query.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
|
||||
/* ---------------- 详情抽屉 ---------------- */
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref<OrderItem | null>(null)
|
||||
|
||||
async function openDetail(row: OrderItem) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
detail.value = await getOrderDetail(row.order_no)
|
||||
} catch {
|
||||
// 错误提示已由请求层处理
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearchFormCard @search="handleSearch" @reset="handleReset">
|
||||
<el-input
|
||||
v-model="query.order_no"
|
||||
placeholder="订单号(精确)"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-input
|
||||
v-model.number="query.user_id"
|
||||
placeholder="用户 ID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="query.status" placeholder="订单状态" clearable>
|
||||
<el-option
|
||||
v-for="opt in enumOptions(ORDER_STATUS)"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</SearchFormCard>
|
||||
|
||||
<div class="zt-card">
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column prop="order_no" label="订单号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="user_id" label="用户 ID" width="90" />
|
||||
<el-table-column prop="goods_name" label="商品" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="实付金额" width="110" align="right">
|
||||
<template #default="{ row }: { row: OrderItem }">
|
||||
<span class="zt-money">¥{{ formatMoney(row.pay_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }: { row: OrderItem }">
|
||||
<el-tag :type="enumTag(ORDER_STATUS, row.order_status)" size="small">
|
||||
{{ enumLabel(ORDER_STATUS, row.order_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付渠道" width="100">
|
||||
<template #default="{ row }: { row: OrderItem }">
|
||||
{{ enumLabel(PAY_CHANNEL, row.pay_channel) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付时间" width="150">
|
||||
<template #default="{ row }: { row: OrderItem }">{{ formatDate(row.pay_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="150">
|
||||
<template #default="{ row }: { row: OrderItem }">{{ formatDate(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }: { row: OrderItem }">
|
||||
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无订单数据" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
<PaginationBar v-model:page="page" v-model:page-size="pageSize" :total="total" @change="fetchList" />
|
||||
</div>
|
||||
|
||||
<!-- 订单详情抽屉 -->
|
||||
<el-drawer v-model="detailVisible" title="订单详情" size="480px">
|
||||
<div v-loading="detailLoading" class="detail-body">
|
||||
<el-descriptions v-if="detail" :column="1" border size="small">
|
||||
<el-descriptions-item label="订单号">{{ detail.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户 ID">{{ detail.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称">{{ formatText(detail.goods_name) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="enumTag(ORDER_STATUS, detail.order_status)" size="small">
|
||||
{{ enumLabel(ORDER_STATUS, detail.order_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付渠道">
|
||||
{{ enumLabel(PAY_CHANNEL, detail.pay_channel) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信交易号">{{ formatText(detail.transaction_id) }}</el-descriptions-item>
|
||||
<template v-if="detail.xpay_product_id">
|
||||
<el-descriptions-item label="虚拟支付道具 ID">{{ formatText(detail.xpay_product_id) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="虚拟支付平台">{{ formatText(detail.xpay_platform) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="虚拟支付环境">{{ enumLabel(XPAY_ENV, detail.xpay_env) }}</el-descriptions-item>
|
||||
</template>
|
||||
<el-descriptions-item label="单价">
|
||||
<span class="zt-money">¥{{ formatMoney(detail.unit_price) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单总额">
|
||||
<span class="zt-money">¥{{ formatMoney(detail.total_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="实付金额">
|
||||
<span class="zt-money">¥{{ formatMoney(detail.pay_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="退款金额">
|
||||
<span class="zt-money">¥{{ formatMoney(detail.refund_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="退款时间">{{ formatDate(detail.refund_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会员天数">{{ detail.member_days }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上传额度">{{ detail.upload_quota }}</el-descriptions-item>
|
||||
<el-descriptions-item label="AI 推荐额度">{{ detail.ai_recommend_quota }}</el-descriptions-item>
|
||||
<el-descriptions-item label="衣柜分享额度">{{ detail.closet_share_quota }}</el-descriptions-item>
|
||||
<el-descriptions-item label="搭配分享额度">{{ detail.outfit_share_quota }}</el-descriptions-item>
|
||||
<el-descriptions-item label="权益到期">{{ formatDate(detail.expire_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">{{ formatDate(detail.pay_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(detail.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ formatText(detail.remark) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-body {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import SearchFormCard from '@/components/SearchFormCard.vue'
|
||||
import PaginationBar from '@/components/PaginationBar.vue'
|
||||
import { approveRefund, getRefundList, rejectRefund } from '@/api/order'
|
||||
import type { RefundItem } from '@/types/order'
|
||||
import { REFUND_STATUS, enumLabel, enumTag } from '@/utils/constants'
|
||||
import { formatDate, formatMoney, formatText } from '@/utils/format'
|
||||
|
||||
/* ---------------- 列表与筛选 ---------------- */
|
||||
const loading = ref(false)
|
||||
const list = ref<RefundItem[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
/**
|
||||
* 状态筛选:undefined = 全部(不传),0 = 待处理(显式传 0)
|
||||
* 后端用指针区分「不传」与「传 0」,前端用 -1 作为「全部」的占位值
|
||||
*/
|
||||
const query = reactive({
|
||||
status: -1 as number,
|
||||
order_no: '',
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ value: -1, label: '全部状态' },
|
||||
{ value: 0, label: '待处理' },
|
||||
{ value: 1, label: '退款中' },
|
||||
{ value: 2, label: '退款成功' },
|
||||
{ value: 3, label: '退款失败' },
|
||||
]
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRefundList({
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
status: query.status === -1 ? undefined : query.status,
|
||||
order_no: query.order_no || undefined,
|
||||
})
|
||||
list.value = res.list
|
||||
total.value = res.total
|
||||
} catch {
|
||||
// 错误提示已由请求层处理
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
query.status = -1
|
||||
query.order_no = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
|
||||
/* ---------------- 审核(通过 / 驳回) ---------------- */
|
||||
const auditVisible = ref(false)
|
||||
const auditLoading = ref(false)
|
||||
const auditMode = ref<'approve' | 'reject'>('approve')
|
||||
const auditTarget = ref<RefundItem | null>(null)
|
||||
const auditFormRef = ref<FormInstance>()
|
||||
|
||||
const auditForm = reactive({
|
||||
reason: '',
|
||||
})
|
||||
|
||||
const auditRules: FormRules = {
|
||||
reason: [
|
||||
{ required: true, message: '请填写审核备注', trigger: 'blur' },
|
||||
{ max: 200, message: '备注不超过 200 字', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
function openAudit(mode: 'approve' | 'reject', row: RefundItem) {
|
||||
auditMode.value = mode
|
||||
auditTarget.value = row
|
||||
auditForm.reason = ''
|
||||
auditVisible.value = true
|
||||
}
|
||||
|
||||
async function submitAudit() {
|
||||
const valid = await auditFormRef.value?.validate().catch(() => false)
|
||||
if (!valid || !auditTarget.value) return
|
||||
auditLoading.value = true
|
||||
try {
|
||||
if (auditMode.value === 'approve') {
|
||||
await approveRefund(auditTarget.value.refund_no, { reason: auditForm.reason })
|
||||
ElMessage.success('已通过,退款单进入「退款中」状态')
|
||||
} else {
|
||||
await rejectRefund(auditTarget.value.refund_no, { reason: auditForm.reason })
|
||||
ElMessage.success('已驳回')
|
||||
}
|
||||
auditVisible.value = false
|
||||
fetchList()
|
||||
} catch {
|
||||
// 错误提示已由请求层处理
|
||||
} finally {
|
||||
auditLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmReject(row: RefundItem) {
|
||||
const confirmed = await ElMessageBox.confirm(
|
||||
`驳回后该退款单将标记为「退款失败」,确认驳回订单 ${row.order_no} 的退款吗?`,
|
||||
'驳回确认',
|
||||
{ type: 'warning', confirmButtonText: '继续驳回' },
|
||||
).catch(() => false)
|
||||
if (!confirmed) return
|
||||
openAudit('reject', row)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearchFormCard @search="handleSearch" @reset="handleReset">
|
||||
<el-select v-model="query.status" placeholder="退款状态">
|
||||
<el-option
|
||||
v-for="opt in statusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="query.order_no"
|
||||
placeholder="关联订单号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</SearchFormCard>
|
||||
|
||||
<div class="zt-card">
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column prop="refund_no" label="退款单号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="order_no" label="关联订单号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="user_id" label="用户 ID" width="90" />
|
||||
<el-table-column label="退款金额" width="110" align="right">
|
||||
<template #default="{ row }: { row: RefundItem }">
|
||||
<span class="zt-money">¥{{ formatMoney(row.refund_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }: { row: RefundItem }">
|
||||
<el-tag :type="enumTag(REFUND_STATUS, row.status)" size="small">
|
||||
{{ enumLabel(REFUND_STATUS, row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请原因" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }: { row: RefundItem }">{{ formatText(row.refund_reason) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请时间" width="150">
|
||||
<template #default="{ row }: { row: RefundItem }">{{ formatDate(row.apply_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }: { row: RefundItem }">{{ formatText(row.fail_reason) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }: { row: RefundItem }">
|
||||
<template v-if="row.status === 0">
|
||||
<el-button link type="success" size="small" @click="openAudit('approve', row)">通过</el-button>
|
||||
<el-button link type="danger" size="small" @click="confirmReject(row)">驳回</el-button>
|
||||
</template>
|
||||
<span v-else class="no-action">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无退款单数据" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
<PaginationBar v-model:page="page" v-model:page-size="pageSize" :total="total" @change="fetchList" />
|
||||
</div>
|
||||
|
||||
<!-- 审核对话框 -->
|
||||
<el-dialog
|
||||
v-model="auditVisible"
|
||||
:title="auditMode === 'approve' ? '退款审核通过' : '退款审核驳回'"
|
||||
width="460px"
|
||||
>
|
||||
<el-alert
|
||||
v-if="auditTarget"
|
||||
:title="`退款单 ${auditTarget.refund_no} · 金额 ¥${formatMoney(auditTarget.refund_amount)}`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="audit-alert"
|
||||
/>
|
||||
<el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="88px">
|
||||
<el-form-item :label="auditMode === 'approve' ? '审核备注' : '驳回原因'" prop="reason">
|
||||
<el-input
|
||||
v-model="auditForm.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
:placeholder="auditMode === 'approve' ? '如:已核实,同意退款(必填)' : '请填写驳回原因(必填)'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditVisible = false">取消</el-button>
|
||||
<el-button
|
||||
:type="auditMode === 'approve' ? 'success' : 'danger'"
|
||||
:loading="auditLoading"
|
||||
@click="submitAudit"
|
||||
>
|
||||
{{ auditMode === 'approve' ? '确认通过' : '确认驳回' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.audit-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.no-action {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user