Commit 5033c0632cbc2f95b19d54e8e7b85a4b2b8afa71
1 parent
caab2d9a
Refactor: 新增骑手提现审核功能,扩展提现审核相关接口并完善路由与审核页面交互。
Showing
3 changed files
with
341 additions
and
0 deletions
src/api/index.ts
| ... | ... | @@ -162,6 +162,17 @@ export const riderLevelApi = { |
| 162 | 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 | 177 | export const refundApi = { |
| 167 | 178 | reasons: (role = 0) => request.get('/api/admin/refund/reasons', { params: { role } }), | ... | ... |
src/router/index.ts
| ... | ... | @@ -100,6 +100,12 @@ const router = createRouter({ |
| 100 | 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 | 109 | path: 'open', |
| 104 | 110 | name: 'OpenApp', |
| 105 | 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> | ... | ... |