first commit

This commit is contained in:
2026-09-11 01:46:36 +08:00
commit df4332a2a3
58 changed files with 9771 additions and 0 deletions
+192
View File
@@ -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>
+231
View File
@@ -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>