From ff4babc2be1809f6ce03eb48c23f55920f447125 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期二, 04 八月 2026 11:50:38 +0800
Subject: [PATCH] feat: align withdrawal management with admin prototype
---
talent-card-admin/src/views/MerchantSettlementView.vue | 309 ++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 291 insertions(+), 18 deletions(-)
diff --git a/talent-card-admin/src/views/MerchantSettlementView.vue b/talent-card-admin/src/views/MerchantSettlementView.vue
index c261f6d..e6a50cb 100644
--- a/talent-card-admin/src/views/MerchantSettlementView.vue
+++ b/talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,26 +1,299 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
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'), loading = ref(false), rows = ref<any[]>([])
-const labels: Record<string,string> = { pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '取现处理中', paid: '已到账', failed: '失败' }
-const filters = reactive({ merchantName: '', applicationNo: '', status: '' })
-const filteredRows = computed(() => rows.value.filter((row) => {
- const merchantMatched = !filters.merchantName || String(row.merchantName || '').includes(filters.merchantName.trim())
- const applicationMatched = !filters.applicationNo || String(row.applicationNo || '').includes(filters.applicationNo.trim())
- return merchantMatched && applicationMatched && (!filters.status || row.status === filters.status)
-}))
-const load = async () => { loading.value = true; try { rows.value = (await getMerchantSettlement(kind.value)).records || [] } finally { loading.value = false } }
-const resetFilters = () => { filters.merchantName = ''; filters.applicationNo = ''; filters.status = '' }
-const review = async (row: any, status: string) => {
- let reason = '', providerValue = ''
- if (status === 'approved') { const isOnboarding = kind.value === 'onboardings'; const result = await ElMessageBox.prompt(isOnboarding ? '请输入汇付进件成功后返回的商户 huifu_id' : '请输入汇付绑卡成功后返回的取现卡 token_no', isOnboarding ? '配置汇付商户号' : '配置汇付取现卡序列号', { inputPattern: /^[A-Za-z0-9]{1,32}$/, inputErrorMessage: '请输入有效的汇付标识' }); providerValue = result.value }
- else if (status === 'rejected' || status === 'failed') { const result = await ElMessageBox.prompt('请输入处理原因', '确认处理'); reason = result.value || '' }
- else { await ElMessageBox.confirm('确认发起汇付取现?取现结果将以汇付同步返回或签名回调为准。', '发起取现', { type: 'warning' }) }
- await reviewMerchantSettlement(kind.value, row.id, status, reason, providerValue); ElMessage.success(status === 'paid' ? '已提交汇付取现' : '处理成功'); load()
+
+const kind = ref<Kind>('withdrawals')
+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 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>) {
+ 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="page-stack"><el-card><div class="toolbar"><el-radio-group v-model="kind" @change="load"><el-radio-button label="onboardings">商户进件</el-radio-button><el-radio-button label="bank-cards">银行卡审核</el-radio-button><el-radio-button label="withdrawals">提现审核</el-radio-button></el-radio-group><el-button @click="load">刷新</el-button></div><el-form class="settlement-filters" inline><el-form-item label="商户"><el-input v-model="filters.merchantName" clearable placeholder="请输入商户名称" /></el-form-item><el-form-item label="申请单号"><el-input v-model="filters.applicationNo" clearable placeholder="请输入申请单号" /></el-form-item><el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="待审核" value="pending" /><el-option label="已通过" value="approved" /><el-option label="已驳回" value="rejected" /><el-option label="处理中" value="processing" /><el-option label="已到账" value="paid" /><el-option label="失败" value="failed" /></el-select></el-form-item><el-form-item><el-button @click="resetFilters">重置</el-button></el-form-item></el-form><el-table v-loading="loading" :data="filteredRows" border><el-table-column prop="merchantName" label="商户"/><el-table-column prop="storeName" label="门店"/><el-table-column prop="applicationNo" label="申请单号"/><el-table-column prop="bankName" label="银行"/><el-table-column label="金额"><template #default="{row}">{{row.amountCents == null ? '-' : '¥' + (Number(row.amountCents)/100).toFixed(2)}}</template></el-table-column><el-table-column label="状态"><template #default="{row}">{{labels[row.status] || row.status}}</template></el-table-column><el-table-column label="操作" width="180"><template #default="{row}"><template v-if="row.status==='pending'"><el-button size="small" type="primary" @click="review(row,kind==='withdrawals'?'paid':'approved')">{{kind==='withdrawals'?'发起取现':'通过'}}</el-button><el-button size="small" type="danger" @click="review(row,kind==='withdrawals'?'failed':'rejected')">驳回</el-button></template></template></el-table-column></el-table></el-card></div></template>
-<style scoped>.toolbar{display:flex;justify-content:space-between;margin-bottom:18px}.settlement-filters{padding:14px 0 4px}</style>
+
+<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 prop="subjectType" label="主体类型" min-width="105" />
+ </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="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="90" fixed="right" align="center">
+ <template #default="{ row }">
+ <el-button v-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>
--
Gitblit v1.7.1