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,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> |