Commit 5033c0632cbc2f95b19d54e8e7b85a4b2b8afa71

Authored by shaofan
1 parent caab2d9a

Refactor: 新增骑手提现审核功能,扩展提现审核相关接口并完善路由与审核页面交互。

src/api/index.ts
@@ -162,6 +162,17 @@ export const riderLevelApi = { @@ -162,6 +162,17 @@ export const riderLevelApi = {
162 request.delete('/api/admin/rider/level/del', { params: { id, cityId } }), 162 request.delete('/api/admin/rider/level/del', { params: { id, cityId } }),
163 } 163 }
164 164
  165 +export const riderWithdrawApi = {
  166 + list: (params: any) => request.get('/api/admin/rider/withdraw/list', { params }),
  167 + detail: (id: number) => request.get(`/api/admin/rider/withdraw/${id}`),
  168 + approve: (id: number, data: { remark?: string }) =>
  169 + request.post(`/api/admin/rider/withdraw/${id}/approve`, data),
  170 + reject: (id: number, data: { remark?: string }) =>
  171 + request.post(`/api/admin/rider/withdraw/${id}/reject`, data),
  172 + markPaid: (id: number, data: { transferNo: string; remark?: string }) =>
  173 + request.post(`/api/admin/rider/withdraw/${id}/mark-paid`, data),
  174 +}
  175 +
165 // 退款管理 176 // 退款管理
166 export const refundApi = { 177 export const refundApi = {
167 reasons: (role = 0) => request.get('/api/admin/refund/reasons', { params: { role } }), 178 reasons: (role = 0) => request.get('/api/admin/refund/reasons', { params: { role } }),
src/router/index.ts
@@ -100,6 +100,12 @@ const router = createRouter({ @@ -100,6 +100,12 @@ const router = createRouter({
100 meta: { title: '骑手等级' }, 100 meta: { title: '骑手等级' },
101 }, 101 },
102 { 102 {
  103 + path: 'rider/withdraw',
  104 + name: 'RiderWithdrawAudit',
  105 + component: () => import('@/views/rider/RiderWithdrawAudit.vue'),
  106 + meta: { title: '骑手提现审核' },
  107 + },
  108 + {
103 path: 'open', 109 path: 'open',
104 name: 'OpenApp', 110 name: 'OpenApp',
105 component: () => import('@/views/open/OpenAppList.vue'), 111 component: () => import('@/views/open/OpenAppList.vue'),
src/views/rider/RiderWithdrawAudit.vue 0 → 100644
  1 +<template>
  2 + <div>
  3 + <a-card title="骑手提现审核" :bordered="false" class="list-table-card">
  4 + <div class="soft-page-stack">
  5 + <div class="soft-note-card">
  6 + <strong>审核说明</strong>
  7 + <p>骑手申请提现时会先冻结可用余额;审核拒绝后解冻,审核通过后进入待打款,标记已打款后扣减冻结金额并写入提现流水。</p>
  8 + </div>
  9 +
  10 + <div class="list-toolbar">
  11 + <div class="list-toolbar-left">
  12 + <a-select
  13 + v-if="isAdmin"
  14 + v-model:value="filterCityId"
  15 + placeholder="租户"
  16 + allowClear
  17 + class="list-filter"
  18 + @change="handleFilterChange"
  19 + >
  20 + <a-select-option v-for="item in cityList" :key="item.id" :value="item.id">{{ item.name }}</a-select-option>
  21 + </a-select>
  22 + <a-select v-model:value="filterStatus" placeholder="提现状态" allowClear class="list-filter" @change="handleFilterChange">
  23 + <a-select-option :value="0">待审核</a-select-option>
  24 + <a-select-option :value="1">待打款</a-select-option>
  25 + <a-select-option :value="2">已拒绝</a-select-option>
  26 + <a-select-option :value="3">已打款</a-select-option>
  27 + <a-select-option :value="4">打款失败</a-select-option>
  28 + </a-select>
  29 + <a-date-picker v-model:value="startDate" value-format="YYYY-MM-DD" placeholder="申请开始日期" class="list-filter" @change="handleFilterChange" />
  30 + <a-date-picker v-model:value="endDate" value-format="YYYY-MM-DD" placeholder="申请结束日期" class="list-filter" @change="handleFilterChange" />
  31 + <a-input-search v-model:value="keyword" placeholder="搜索提现单号/骑手姓名/手机号" class="list-search" @search="handleKeywordSearch" />
  32 + </div>
  33 + </div>
  34 +
  35 + <a-table
  36 + :dataSource="list"
  37 + :columns="columns"
  38 + :loading="loading"
  39 + rowKey="id"
  40 + :pagination="pagination"
  41 + :scroll="{ x: 1500 }"
  42 + @change="handleTableChange"
  43 + >
  44 + <template #bodyCell="{ column, record }">
  45 + <template v-if="column.key === 'cityName'">
  46 + {{ record.cityName || getCityName(record.cityId) }}
  47 + </template>
  48 + <template v-else-if="column.key === 'amount' || column.key === 'riderBalance' || column.key === 'riderFrozenBalance'">
  49 + ¥{{ formatMoney(record[column.key]) }}
  50 + </template>
  51 + <template v-else-if="column.key === 'status'">
  52 + <a-tag :color="statusColor[record.status] || 'default'">{{ statusMap[record.status] || `状态${record.status}` }}</a-tag>
  53 + </template>
  54 + <template v-else-if="column.key === 'accountType'">
  55 + {{ accountTypeMap[record.accountType] || '-' }}
  56 + </template>
  57 + <template v-else-if="column.key === 'accountNo'">
  58 + {{ maskAccountNo(record.accountNo) }}
  59 + </template>
  60 + <template v-else-if="column.key === 'applyTime' || column.key === 'auditTime' || column.key === 'payTime'">
  61 + {{ formatTime(record[column.key]) }}
  62 + </template>
  63 + <template v-else-if="column.key === 'action'">
  64 + <a-space>
  65 + <a @click="openDetail(record)">详情</a>
  66 + <template v-if="record.status === 0">
  67 + <a @click="openAction('approve', record)" style="color:green">通过</a>
  68 + <a @click="openAction('reject', record)" style="color:red">拒绝</a>
  69 + </template>
  70 + <template v-else-if="record.status === 1">
  71 + <a @click="openAction('paid', record)">标记已打款</a>
  72 + </template>
  73 + </a-space>
  74 + </template>
  75 + </template>
  76 + </a-table>
  77 + </div>
  78 + </a-card>
  79 +
  80 + <a-modal v-model:open="detailVisible" title="提现申请详情" :footer="null" width="860px">
  81 + <a-descriptions v-if="detail" bordered :column="2" size="small">
  82 + <a-descriptions-item label="提现单号">{{ detail.withdrawNo }}</a-descriptions-item>
  83 + <a-descriptions-item label="状态">
  84 + <a-tag :color="statusColor[detail.status] || 'default'">{{ statusMap[detail.status] || `状态${detail.status}` }}</a-tag>
  85 + </a-descriptions-item>
  86 + <a-descriptions-item label="骑手">{{ detail.riderName || '-' }} / {{ detail.mobile || '-' }}</a-descriptions-item>
  87 + <a-descriptions-item label="租户">{{ detail.cityName || getCityName(detail.cityId) }}</a-descriptions-item>
  88 + <a-descriptions-item label="可用余额">¥{{ formatMoney(detail.riderBalance) }}</a-descriptions-item>
  89 + <a-descriptions-item label="冻结余额">¥{{ formatMoney(detail.riderFrozenBalance) }}</a-descriptions-item>
  90 + <a-descriptions-item label="提现金额">¥{{ formatMoney(detail.amount) }}</a-descriptions-item>
  91 + <a-descriptions-item label="申请时间">{{ formatTime(detail.applyTime) }}</a-descriptions-item>
  92 + <a-descriptions-item label="账户类型">{{ accountTypeMap[detail.accountType] || '-' }}</a-descriptions-item>
  93 + <a-descriptions-item label="收款人">{{ detail.accountName || '-' }}</a-descriptions-item>
  94 + <a-descriptions-item label="开户行">{{ detail.bankName || '-' }}</a-descriptions-item>
  95 + <a-descriptions-item label="开户支行">{{ detail.bankBranch || '-' }}</a-descriptions-item>
  96 + <a-descriptions-item label="收款账号" :span="2">{{ maskAccountNo(detail.accountNo) }}</a-descriptions-item>
  97 + <a-descriptions-item label="申请备注" :span="2">{{ detail.applyRemark || '-' }}</a-descriptions-item>
  98 + <a-descriptions-item label="审核人">{{ detail.auditorName || '-' }}</a-descriptions-item>
  99 + <a-descriptions-item label="审核时间">{{ formatTime(detail.auditTime) }}</a-descriptions-item>
  100 + <a-descriptions-item label="审核/打款备注" :span="2">{{ detail.auditRemark || '-' }}</a-descriptions-item>
  101 + <a-descriptions-item label="打款流水号">{{ detail.transferNo || '-' }}</a-descriptions-item>
  102 + <a-descriptions-item label="打款时间">{{ formatTime(detail.payTime) }}</a-descriptions-item>
  103 + </a-descriptions>
  104 + </a-modal>
  105 +
  106 + <a-modal v-model:open="actionVisible" :title="actionTitle" @ok="handleAction" :confirmLoading="actionSaving">
  107 + <div class="soft-page-stack">
  108 + <div class="soft-note-card">
  109 + <strong>{{ actionTitle }}</strong>
  110 + <p>{{ actionTip }}</p>
  111 + </div>
  112 + <a-form layout="vertical">
  113 + <a-form-item v-if="actionType === 'paid'" label="打款流水号" required>
  114 + <a-input v-model:value="actionForm.transferNo" placeholder="请输入线下打款流水号" />
  115 + </a-form-item>
  116 + <a-form-item :label="actionType === 'reject' ? '拒绝原因' : '备注'" :required="actionType === 'reject'">
  117 + <a-input v-model:value="actionForm.remark" :placeholder="actionType === 'reject' ? '请输入拒绝原因' : '请输入备注(选填)'" />
  118 + </a-form-item>
  119 + </a-form>
  120 + </div>
  121 + </a-modal>
  122 + </div>
  123 +</template>
  124 +
  125 +<script setup lang="ts">
  126 +import { computed, onMounted, reactive, ref } from 'vue'
  127 +import type { TablePaginationConfig } from 'ant-design-vue'
  128 +import { message } from 'ant-design-vue'
  129 +import { riderWithdrawApi } from '@/api'
  130 +import { useRoleCityList } from '@/composables/useRoleCityList'
  131 +
  132 +const PAGE_SIZE = 20
  133 +type ActionType = 'approve' | 'reject' | 'paid'
  134 +
  135 +const loading = ref(false)
  136 +const actionSaving = ref(false)
  137 +const list = ref<any[]>([])
  138 +const detail = ref<any | null>(null)
  139 +const detailVisible = ref(false)
  140 +const actionVisible = ref(false)
  141 +const actionType = ref<ActionType>('approve')
  142 +const actionTargetId = ref<number>(0)
  143 +const total = ref(0)
  144 +const currentPage = ref(1)
  145 +const pageSize = ref(PAGE_SIZE)
  146 +const filterCityId = ref<number | undefined>()
  147 +const filterStatus = ref<number | undefined>()
  148 +const startDate = ref<string | undefined>()
  149 +const endDate = ref<string | undefined>()
  150 +const keyword = ref('')
  151 +const actionForm = reactive({ remark: '', transferNo: '' })
  152 +const { isAdmin, managedCityId, cityList, loadCities, getCityName } = useRoleCityList()
  153 +
  154 +const statusMap: Record<number, string> = {
  155 + 0: '待审核',
  156 + 1: '待打款',
  157 + 2: '已拒绝',
  158 + 3: '已打款',
  159 + 4: '打款失败',
  160 +}
  161 +
  162 +const statusColor: Record<number, string> = {
  163 + 0: 'orange',
  164 + 1: 'blue',
  165 + 2: 'red',
  166 + 3: 'green',
  167 + 4: 'red',
  168 +}
  169 +
  170 +const accountTypeMap: Record<number, string> = {
  171 + 1: '银行卡',
  172 + 2: '支付宝',
  173 + 3: '微信',
  174 +}
  175 +
  176 +const columns = computed(() => {
  177 + const base = [
  178 + { title: '提现单号', dataIndex: 'withdrawNo', ellipsis: true, width: 180 },
  179 + { title: '骑手', dataIndex: 'riderName', width: 120 },
  180 + { title: '手机号', dataIndex: 'mobile', width: 130 },
  181 + { title: '提现金额', key: 'amount', width: 110 },
  182 + { title: '状态', key: 'status', width: 100 },
  183 + { title: '账户类型', key: 'accountType', width: 100 },
  184 + { title: '收款人', dataIndex: 'accountName', width: 110 },
  185 + { title: '收款账号', key: 'accountNo', width: 140 },
  186 + { title: '可用余额', key: 'riderBalance', width: 110 },
  187 + { title: '冻结余额', key: 'riderFrozenBalance', width: 110 },
  188 + { title: '申请时间', key: 'applyTime', width: 160 },
  189 + { title: '审核人', dataIndex: 'auditorName', width: 120 },
  190 + { title: '审核时间', key: 'auditTime', width: 160 },
  191 + { title: '操作', key: 'action', fixed: 'right', width: 180 },
  192 + ]
  193 + return isAdmin.value ? [{ title: '租户', key: 'cityName', width: 120 }, ...base] : base
  194 +})
  195 +
  196 +const pagination = computed<TablePaginationConfig>(() => ({
  197 + current: currentPage.value,
  198 + pageSize: pageSize.value,
  199 + total: total.value,
  200 + showSizeChanger: false,
  201 + showTotal: (count) => `共 ${count} 条`,
  202 +}))
  203 +
  204 +const actionTitle = computed(() => {
  205 + if (actionType.value === 'reject') return '拒绝提现申请'
  206 + if (actionType.value === 'paid') return '标记已打款'
  207 + return '审核通过提现申请'
  208 +})
  209 +
  210 +const actionTip = computed(() => {
  211 + if (actionType.value === 'reject') return '拒绝后会解冻本次提现金额并退回骑手可用余额。'
  212 + if (actionType.value === 'paid') return '请确认线下已完成打款,提交后会扣减冻结余额并写入提现流水。'
  213 + return '通过后申请进入待打款状态,冻结余额暂不扣减,后续需手动标记已打款。'
  214 +})
  215 +
  216 +function buildParams() {
  217 + return {
  218 + cityId: isAdmin.value ? filterCityId.value : managedCityId.value,
  219 + status: filterStatus.value,
  220 + keyword: keyword.value || undefined,
  221 + startDate: startDate.value,
  222 + endDate: endDate.value,
  223 + page: currentPage.value,
  224 + }
  225 +}
  226 +
  227 +async function loadList() {
  228 + loading.value = true
  229 + try {
  230 + const res: any = await riderWithdrawApi.list(buildParams())
  231 + const data = res?.data ?? {}
  232 + list.value = Array.isArray(data.list) ? data.list : []
  233 + total.value = Number(data.total ?? 0)
  234 + currentPage.value = Number(data.page ?? currentPage.value)
  235 + pageSize.value = Number(data.pageSize ?? PAGE_SIZE)
  236 + } finally {
  237 + loading.value = false
  238 + }
  239 +}
  240 +
  241 +async function openDetail(record: any) {
  242 + const res: any = await riderWithdrawApi.detail(record.id)
  243 + detail.value = res?.data ?? record
  244 + detailVisible.value = true
  245 +}
  246 +
  247 +function openAction(type: ActionType, record: any) {
  248 + actionType.value = type
  249 + actionTargetId.value = record.id
  250 + actionForm.remark = ''
  251 + actionForm.transferNo = ''
  252 + actionVisible.value = true
  253 +}
  254 +
  255 +async function handleAction() {
  256 + if (actionType.value === 'reject' && !actionForm.remark.trim()) {
  257 + message.error('请输入拒绝原因')
  258 + return
  259 + }
  260 + if (actionType.value === 'paid' && !actionForm.transferNo.trim()) {
  261 + message.error('请输入打款流水号')
  262 + return
  263 + }
  264 + actionSaving.value = true
  265 + try {
  266 + if (actionType.value === 'approve') {
  267 + await riderWithdrawApi.approve(actionTargetId.value, { remark: actionForm.remark })
  268 + } else if (actionType.value === 'reject') {
  269 + await riderWithdrawApi.reject(actionTargetId.value, { remark: actionForm.remark })
  270 + } else {
  271 + await riderWithdrawApi.markPaid(actionTargetId.value, {
  272 + transferNo: actionForm.transferNo,
  273 + remark: actionForm.remark,
  274 + })
  275 + }
  276 + message.success('操作成功')
  277 + actionVisible.value = false
  278 + await loadList()
  279 + } finally {
  280 + actionSaving.value = false
  281 + }
  282 +}
  283 +
  284 +function handleFilterChange() {
  285 + currentPage.value = 1
  286 + loadList()
  287 +}
  288 +
  289 +function handleKeywordSearch() {
  290 + currentPage.value = 1
  291 + loadList()
  292 +}
  293 +
  294 +function handleTableChange(page: TablePaginationConfig) {
  295 + const nextPage = page.current ?? 1
  296 + if (nextPage === currentPage.value) return
  297 + currentPage.value = nextPage
  298 + loadList()
  299 +}
  300 +
  301 +function formatMoney(value: number | string | null | undefined) {
  302 + return Number(value ?? 0).toFixed(2)
  303 +}
  304 +
  305 +function formatTime(value: number | null | undefined) {
  306 + if (!value) return '-'
  307 + return new Date(Number(value) * 1000).toLocaleString('zh-CN', { hour12: false })
  308 +}
  309 +
  310 +function maskAccountNo(value?: string) {
  311 + const text = String(value || '')
  312 + if (!text) return '-'
  313 + if (text.length <= 4) return text
  314 + return `${text.slice(0, 4)}****${text.slice(-4)}`
  315 +}
  316 +
  317 +onMounted(async () => {
  318 + await loadCities()
  319 + if (!isAdmin.value) {
  320 + filterCityId.value = managedCityId.value
  321 + }
  322 + await loadList()
  323 +})
  324 +</script>