|
@@ -0,0 +1,247 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="pending-refund-audit">
|
|
|
|
|
+ <div class="filter-bar">
|
|
|
|
|
+ <el-select v-model="statusFilter" placeholder="退款状态" @change="loadData">
|
|
|
|
|
+ <el-option label="全部状态" value="" />
|
|
|
|
|
+ <el-option label="待处理" value="pending" />
|
|
|
|
|
+ <el-option label="处理中" value="processing" />
|
|
|
|
|
+ <el-option label="已成功" value="success" />
|
|
|
|
|
+ <el-option label="已失败" value="failed" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-select v-model="typeFilter" placeholder="订单类型" @change="loadData">
|
|
|
|
|
+ <el-option label="全部类型" value="" />
|
|
|
|
|
+ <el-option label="提现退款" value="withdrawal" />
|
|
|
|
|
+ <el-option label="商品退款" value="product" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="summary.totalPendingAmount > 0" class="summary-bar">
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ :title="'待退款总金额:¥' + (summary.totalPendingAmount / 100).toFixed(2) + '(' + summary.pendingCount + ' 笔待处理)'"
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ show-icon
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="adm-table-wrap">
|
|
|
|
|
+ <el-table :data="list" v-loading="loading" border stripe height="100%">
|
|
|
|
|
+ <el-table-column prop="id" label="ID" width="70">
|
|
|
|
|
+ <template slot="header">
|
|
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="类型" width="100">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="row.orderType === 'product' ? 'primary' : 'warning'" size="small">
|
|
|
|
|
+ {{ row.orderType === 'product' ? '商品退款' : '提现退款' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="订单号" width="180">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.orderNo || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="提现申请ID" width="100">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.withdrawalRequestId || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="用户" width="120">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.userName || '用户' + row.userId }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="退款金额" width="120">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <span class="amount">{{ formatPriceWithSymbol(row.amount) }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="实退金额" width="120">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <span class="amount">{{ row.refundedAmount ? formatPriceWithSymbol(row.refundedAmount) : '-' }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="状态" width="100">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="尝试次数" width="80">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.attemptCount }}/{{ row.maxAttempts }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="失败原因" min-width="180" show-overflow-tooltip>
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ row.failureReason || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="创建时间" width="160">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ {{ formatTime(row.createdAt) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template slot="header">
|
|
|
|
|
+ <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="120" fixed="right">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-button v-if="row.status === 'pending' || row.status === 'processing'" type="danger" size="mini" @click="handleCancel(row)">
|
|
|
|
|
+ 中止退款
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <span v-else style="color:#999">-</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ v-if="total > 0"
|
|
|
|
|
+ :current-page="page"
|
|
|
|
|
+ :page-size="pageSize"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ layout="total, prev, pager, next"
|
|
|
|
|
+ @current-change="loadData"
|
|
|
|
|
+ style="margin-top:12px;text-align:right;"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import SortMixin from '@/mixins/SortMixin'
|
|
|
|
|
+import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'PendingRefundAudit',
|
|
|
|
|
+ mixins: [SortMixin],
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ list: [],
|
|
|
|
|
+ sortableColumns: { id: 'ID', createdAt: '创建时间' },
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ statusFilter: '',
|
|
|
|
|
+ typeFilter: '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 20,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ summary: {
|
|
|
|
|
+ totalPendingAmount: 0,
|
|
|
|
|
+ pendingCount: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ this.loadData()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async loadData() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.loadSummary()
|
|
|
|
|
+ const reqParams = {
|
|
|
|
|
+ status: this.statusFilter || undefined,
|
|
|
|
|
+ orderType: this.typeFilter || undefined,
|
|
|
|
|
+ page: this.page,
|
|
|
|
|
+ size: this.pageSize
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.sortState.length > 0) reqParams.sort = this.sortState
|
|
|
|
|
+ const res = await getPendingRefundList(reqParams)
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ this.list = res.data.records || []
|
|
|
|
|
+ this.total = res.data.total || 0
|
|
|
|
|
+ await this.resolveUserNames(this.list, 'userId')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.$message.error('加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async loadSummary() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getPendingRefundSummary()
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ this.summary = res.data || { totalPendingAmount: 0, pendingCount: 0 }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载待退款摘要失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async resolveUserNames(items, idField) {
|
|
|
|
|
+ if (!items || items.length === 0) return Promise.resolve()
|
|
|
|
|
+ var ids = []
|
|
|
|
|
+ items.forEach(function(item) {
|
|
|
|
|
+ if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
|
|
|
|
|
+ })
|
|
|
|
|
+ var nameMap = {}
|
|
|
|
|
+ return Promise.all(ids.map(function(id) {
|
|
|
|
|
+ return searchUsers({ keyword: String(id) })
|
|
|
|
|
+ .then(function(res) {
|
|
|
|
|
+ if (res.data && res.data.length > 0) {
|
|
|
|
|
+ var u = res.data[0]
|
|
|
|
|
+ nameMap[id] = u.nickname || u.realName || '用户' + id
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nameMap[id] = '用户' + id
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(function() {
|
|
|
|
|
+ nameMap[id] = '用户' + id
|
|
|
|
|
+ })
|
|
|
|
|
+ })).then(function() {
|
|
|
|
|
+ items.forEach(function(item) {
|
|
|
|
|
+ item.userName = nameMap[item[idField]] || '用户' + item[idField]
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleCancel(row) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.$confirm('确认中止该退款?订单将回到退款审核状态。', '确认中止', { type: 'warning' })
|
|
|
|
|
+ const res = await cancelPendingRefund({ refundId: row.id })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ this.$message.success('已中止退款')
|
|
|
|
|
+ this.loadData()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.$message.error(res.message || '操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ if (e !== 'cancel') {
|
|
|
|
|
+ this.$message.error('操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ formatTime(time) {
|
|
|
|
|
+ if (!time) return '-'
|
|
|
|
|
+ const d = new Date(time)
|
|
|
|
|
+ const pad = n => n < 10 ? '0' + n : n
|
|
|
|
|
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
|
|
|
|
|
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
|
|
|
|
|
+ },
|
|
|
|
|
+ formatPriceWithSymbol(amount) {
|
|
|
|
|
+ if (amount == null) return '-'
|
|
|
|
|
+ return '¥' + (amount / 100).toFixed(2)
|
|
|
|
|
+ },
|
|
|
|
|
+ statusType(status) {
|
|
|
|
|
+ const map = { pending: 'warning', processing: 'primary', success: 'success', failed: 'danger', cancelled: 'info' }
|
|
|
|
|
+ return map[status] || 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+ statusLabel(status) {
|
|
|
|
|
+ const map = { pending: '待处理', processing: '处理中', success: '已成功', failed: '已失败', cancelled: '已中止' }
|
|
|
|
|
+ return map[status] || status
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); }
|
|
|
|
|
+.summary-bar { margin-bottom: 12px; }
|
|
|
|
|
+.sort-header { cursor: pointer; user-select: none; font-weight: 500; font-size: 12px; }
|
|
|
|
|
+.sort-header:hover { color: #409eff; }
|
|
|
|
|
+.sort-index { color: #fff; border-radius: 8px; font-size: 10px; padding: 0 4px; margin: 0 2px; }
|
|
|
|
|
+.sort-asc-badge { background: #f56c6c; }
|
|
|
|
|
+.sort-desc-badge { background: #409eff; }
|
|
|
|
|
+.amount { font-weight: 500; color: #f56c6c; }
|
|
|
|
|
+</style>
|