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