<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Refresh, Search } from '@element-plus/icons-vue'
|
import { getMerchantSettlement, reviewMerchantSettlement } from '../api/admin'
|
|
type Kind = 'onboardings' | 'bank-cards' | 'withdrawals'
|
|
const kind = ref<Kind>('onboardings')
|
const router = useRouter()
|
const loading = ref(false)
|
const rows = ref<Record<string, any>[]>([])
|
const total = ref(0)
|
const page = ref(1)
|
const pageSize = ref(20)
|
const reviewVisible = ref(false)
|
const reviewing = ref<Record<string, any> | null>(null)
|
|
const filters = reactive({
|
merchantName: '',
|
contactMobile: '',
|
merchantType: '',
|
status: '',
|
dateRange: [] as string[],
|
})
|
|
const tabOptions: Array<{ value: Kind; label: string }> = [
|
{ value: 'withdrawals', label: '提现管理' },
|
{ value: 'onboardings', label: '商户进件' },
|
{ value: 'bank-cards', label: '银行卡申请' },
|
]
|
|
const merchantTypes = ['餐饮服务', '住宿服务', '购房服务', '购车服务', '体检服务', '金融服务', '行政审批', '景区服务']
|
const statusOptions = computed(() => kind.value === 'withdrawals'
|
? [
|
{ value: 'pending', label: '待审核' },
|
{ value: 'processing', label: '处理中' },
|
{ value: 'paid', label: '已到账' },
|
{ value: 'failed', label: '提现失败' },
|
]
|
: [
|
{ value: 'pending', label: '待审核' },
|
{ value: 'approved', label: '已通过' },
|
{ value: 'rejected', label: '已驳回' },
|
])
|
|
function statusLabel(value: string) {
|
return ({ pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '处理中', paid: '已到账', failed: '提现失败' } as Record<string, string>)[value] || value || '-'
|
}
|
|
function statusType(value: string) {
|
return ({ pending: 'warning', approved: 'success', paid: 'success', rejected: 'danger', failed: 'danger', processing: 'primary' } as Record<string, 'warning' | 'success' | 'danger' | 'primary'>)[value] || 'primary'
|
}
|
|
function subjectTypeLabel(value: unknown) {
|
const normalized = String(value || '').toLowerCase()
|
if (normalized === 'enterprise' || normalized === 'company' || normalized === '企业') return '企业'
|
return normalized === 'individual' || normalized === 'person' || normalized === '个人' ? '个人' : String(value || '-')
|
}
|
|
function formatAmount(value: unknown) {
|
if (value === null || value === undefined || value === '') return '-'
|
return `¥${(Number(value) / 100).toFixed(2)}`
|
}
|
|
function formatDate(value: unknown) {
|
if (!value) return '-'
|
return String(value).replace('T', ' ').replace(/\.\d+$/, '')
|
}
|
|
async function load() {
|
loading.value = true
|
try {
|
const result = await getMerchantSettlement(kind.value, {
|
page: page.value,
|
size: pageSize.value,
|
merchantName: filters.merchantName.trim() || undefined,
|
contactMobile: filters.contactMobile.trim() || undefined,
|
merchantType: filters.merchantType || undefined,
|
status: filters.status || undefined,
|
startDate: filters.dateRange[0],
|
endDate: filters.dateRange[1],
|
})
|
rows.value = result.records || []
|
total.value = Number(result.total || 0)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
function search() {
|
page.value = 1
|
load()
|
}
|
|
function reset() {
|
filters.merchantName = ''
|
filters.contactMobile = ''
|
filters.merchantType = ''
|
filters.status = ''
|
filters.dateRange = []
|
page.value = 1
|
load()
|
}
|
|
function changeKind(value: string | number | boolean | undefined) {
|
kind.value = value as Kind
|
page.value = 1
|
filters.status = ''
|
load()
|
}
|
|
function openReview(row: Record<string, any>) {
|
if (kind.value === 'onboardings') {
|
router.push({ name: 'merchant-onboarding-detail', params: { id: row.id } })
|
return
|
}
|
reviewing.value = row
|
reviewVisible.value = true
|
}
|
|
async function review(status: 'paid' | 'failed' | 'approved' | 'rejected') {
|
const row = reviewing.value
|
if (!row) return
|
let reason = ''
|
let providerValue = ''
|
try {
|
if (status === 'paid') {
|
await ElMessageBox.confirm('确认发起该笔提现的汇付代付?代付结果以汇付同步响应或签名回调为准。', '确认审核通过', { type: 'warning' })
|
} else if (status === 'approved') {
|
const onboarding = kind.value === 'onboardings'
|
const result = await ElMessageBox.prompt(
|
onboarding ? '请输入汇付进件成功后返回的商户 huifu_id' : '请输入汇付绑卡成功后返回的提现卡 token_no',
|
onboarding ? '配置汇付商户号' : '配置汇付提现卡序列号',
|
{ inputPattern: /^[A-Za-z0-9]{1,32}$/, inputErrorMessage: '请输入有效的汇付标识' },
|
)
|
providerValue = result.value
|
} else {
|
const result = await ElMessageBox.prompt('请输入处理原因', '驳回申请', {
|
inputValidator: (value) => value.trim() ? true : '请填写驳回原因',
|
})
|
reason = result.value.trim()
|
}
|
await reviewMerchantSettlement(kind.value, row.id, status, reason, providerValue)
|
ElMessage.success(status === 'paid' ? '已提交汇付代付' : '审核处理成功')
|
reviewVisible.value = false
|
load()
|
} catch {
|
// Element Plus confirmation and prompt cancellations do not need a user-facing error.
|
}
|
}
|
|
function handlePageChange(value: number) {
|
page.value = value
|
load()
|
}
|
|
onMounted(load)
|
</script>
|
|
<template>
|
<div class="withdrawal-page">
|
<div class="settlement-tabs">
|
<el-radio-group v-model="kind" @change="changeKind">
|
<el-radio-button v-for="tab in tabOptions" :key="tab.value" :label="tab.value">
|
{{ tab.label }}
|
</el-radio-button>
|
</el-radio-group>
|
</div>
|
|
<section class="query-panel">
|
<div class="panel-title">查询表格</div>
|
<el-form class="query-form" label-width="70px" @submit.prevent="search">
|
<el-form-item label="商家">
|
<el-input v-model="filters.merchantName" clearable placeholder="请输入商家名称" />
|
</el-form-item>
|
<el-form-item label="联系电话">
|
<el-input v-model="filters.contactMobile" clearable placeholder="请输入商家联系电话" />
|
</el-form-item>
|
<el-form-item label="申请时间">
|
<el-date-picker
|
v-model="filters.dateRange"
|
type="daterange"
|
value-format="YYYY-MM-DD"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
unlink-panels
|
/>
|
</el-form-item>
|
<el-form-item label="商家类型">
|
<el-select v-model="filters.merchantType" clearable placeholder="全部">
|
<el-option v-for="item in merchantTypes" :key="item" :label="item" :value="item" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-select v-model="filters.status" clearable placeholder="全部">
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
</el-select>
|
</el-form-item>
|
<el-form-item class="query-actions">
|
<el-button type="primary" :icon="Search" @click="search">查询</el-button>
|
<el-button :icon="Refresh" @click="reset">重置</el-button>
|
</el-form-item>
|
</el-form>
|
</section>
|
|
<section class="table-panel">
|
<el-table v-loading="loading" :data="rows" border class="settlement-table" empty-text="暂无数据">
|
<template v-if="kind === 'withdrawals'">
|
<el-table-column prop="withdrawalNo" label="提现id" min-width="156" />
|
<el-table-column prop="merchantName" label="商家名称" min-width="130" show-overflow-tooltip />
|
<el-table-column prop="contactMobile" label="联系方式" min-width="118" />
|
<el-table-column prop="merchantType" label="商家类型" min-width="110" />
|
<el-table-column label="可提现金额" min-width="116" align="right">
|
<template #default="{ row }">{{ formatAmount(row.availableAmountCents) }}</template>
|
</el-table-column>
|
<el-table-column label="提现金额" min-width="104" align="right">
|
<template #default="{ row }">{{ formatAmount(row.amountCents) }}</template>
|
</el-table-column>
|
<el-table-column prop="bankName" label="提现银行" min-width="130" show-overflow-tooltip />
|
<el-table-column prop="cardMasked" label="提现卡号" min-width="150" />
|
</template>
|
<template v-else-if="kind === 'onboardings'">
|
<el-table-column prop="applicationNo" label="进件id" min-width="156" />
|
<el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="contactMobile" label="联系方式" min-width="118" />
|
<el-table-column prop="merchantType" label="商家类型" min-width="110" />
|
<el-table-column prop="subjectName" label="进件主体" min-width="150" show-overflow-tooltip />
|
<el-table-column label="主体类型" min-width="105"><template #default="{ row }">{{ subjectTypeLabel(row.subjectType) }}</template></el-table-column>
|
</template>
|
<template v-else>
|
<el-table-column prop="id" label="申请id" min-width="100" />
|
<el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="contactMobile" label="联系方式" min-width="118" />
|
<el-table-column prop="merchantType" label="商家类型" min-width="110" />
|
<el-table-column prop="bankName" label="开户银行" min-width="145" show-overflow-tooltip />
|
<el-table-column prop="branchName" label="开户支行" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="cityName" label="开户城市" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="cardMasked" label="银行卡号" min-width="150" />
|
</template>
|
<el-table-column label="申请时间" min-width="165">
|
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
|
</el-table-column>
|
<el-table-column label="状态" min-width="100" align="center">
|
<template #default="{ row }">
|
<el-tag :type="statusType(row.status)" effect="light">{{ statusLabel(row.status) }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="110" fixed="right" align="center">
|
<template #default="{ row }">
|
<el-button v-if="kind === 'onboardings'" link type="primary" @click="openReview(row)">查看信息</el-button>
|
<el-button v-else-if="row.status === 'pending'" link type="primary" @click="openReview(row)">审核</el-button>
|
<span v-else class="muted">-</span>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div class="pagination-bar">
|
<span>共 {{ total }} 条</span>
|
<el-pagination
|
v-model:current-page="page"
|
v-model:page-size="pageSize"
|
background
|
layout="prev, pager, next"
|
:total="total"
|
@current-change="handlePageChange"
|
/>
|
</div>
|
</section>
|
|
<el-dialog v-model="reviewVisible" title="提现审核" width="400px" :close-on-click-modal="false">
|
<template v-if="reviewing">
|
<div class="review-summary">
|
<div><span>商家名称</span><strong>{{ reviewing.merchantName || '-' }}</strong></div>
|
<div><span>提现金额</span><strong>{{ formatAmount(reviewing.amountCents) }}</strong></div>
|
<div><span>提现银行</span><strong>{{ reviewing.bankName || '-' }} {{ reviewing.cardMasked || '' }}</strong></div>
|
</div>
|
</template>
|
<template #footer>
|
<el-button @click="reviewVisible = false">取消</el-button>
|
<el-button type="danger" plain @click="review(kind === 'withdrawals' ? 'failed' : 'rejected')">驳回</el-button>
|
<el-button type="primary" @click="review(kind === 'withdrawals' ? 'paid' : 'approved')">审核通过</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped>
|
.withdrawal-page { min-width: 1040px; }
|
.settlement-tabs { margin: 0 0 16px; }
|
.settlement-tabs :deep(.el-radio-button__inner) { min-width: 96px; }
|
.query-panel, .table-panel { background: #fff; border: 1px solid #edf0f5; }
|
.query-panel { padding: 18px 20px 6px; }
|
.panel-title { margin-bottom: 16px; color: #1d2129; font-size: 15px; font-weight: 600; }
|
.query-form { display: grid; grid-template-columns: repeat(3, minmax(250px, 1fr)); column-gap: 24px; }
|
.query-form :deep(.el-form-item) { margin-bottom: 16px; }
|
.query-form :deep(.el-form-item__content) { min-width: 0; }
|
.query-form :deep(.el-input), .query-form :deep(.el-select), .query-form :deep(.el-date-editor) { width: 100%; }
|
.query-actions { justify-content: flex-end; }
|
.query-actions :deep(.el-form-item__content) { justify-content: flex-end; }
|
.table-panel { margin-top: 16px; padding: 0 20px 12px; }
|
.settlement-table { width: 100%; }
|
.settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; }
|
.settlement-table :deep(.el-table__cell) { padding: 10px 0; }
|
.pagination-bar { display: flex; align-items: center; justify-content: flex-end; gap: 18px; padding-top: 14px; color: #606266; font-size: 13px; }
|
.muted { color: #909399; }
|
.review-summary { display: grid; gap: 14px; }
|
.review-summary div { display: flex; justify-content: space-between; gap: 16px; }
|
.review-summary span { color: #909399; }
|
.review-summary strong { color: #303133; font-weight: 500; text-align: right; }
|
@media (max-width: 1280px) { .query-form { grid-template-columns: repeat(2, minmax(280px, 1fr)); } }
|
</style>
|