Files
a_long_vue/src/views/withdraw/Audit.vue
2026-07-15 06:03:53 +08:00

976 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="withdraw-audit">
<!-- 顶部统计卡片 -->
<el-row :gutter="16" class="stats-cards">
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4">
<el-card class="stat-card" shadow="hover">
<div class="stat-item">
<div class="stat-label">总记录</div>
<div class="stat-value">{{ stats.total || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4">
<el-card class="stat-card" shadow="hover" @click="changeDetailStatus(1)">
<div class="stat-item">
<div class="stat-label">待审核</div>
<div class="stat-value warning">{{ stats.awaiting || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4" v-if="isAutoMode">
<el-card class="stat-card" shadow="hover" @click="changeDetailStatus(6)">
<div class="stat-item">
<div class="stat-label">待收款</div>
<div class="stat-value warning">{{ stats.pending_collect || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4">
<el-card class="stat-card" shadow="hover" @click="changeDetailStatus(2)">
<div class="stat-item">
<div class="stat-label">提现成功</div>
<div class="stat-value success">{{ stats.success || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4">
<el-card class="stat-card" shadow="hover" @click="changeDetailStatus(3)">
<div class="stat-item">
<div class="stat-label">提现失败</div>
<div class="stat-value danger">{{ stats.fail || 0 }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4" :lg="4" :xl="4">
<el-card class="stat-card" shadow="hover" @click="changeStatus(2)">
<div class="stat-item">
<div class="stat-label">已处理(全部)</div>
<div class="stat-value success">{{ stats.processed || 0 }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 总金额卡片 -->
<el-row :gutter="20" style="margin-bottom: 20px;">
<el-col :span="24">
<el-card shadow="hover">
<div class="total-amount">
<span class="total-label">筛选总金额</span>
<span class="total-value">¥{{ stats.total_amount || '0.00' }}</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选区域 -->
<el-card class="filter-card" shadow="hover">
<el-row :gutter="20">
<el-col :xs="24" :sm="6" :md="6" :lg="6" :xl="6">
<el-input
v-model="filters.searchUid"
placeholder="用户ID"
clearable
@keyup.enter="handleSearch"
/>
</el-col>
<el-col :xs="24" :sm="6" :md="6" :lg="6" :xl="6">
<el-date-picker
v-model="filters.date"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
@change="handleSearch"
style="width: 100%"
/>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="filter-actions">
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
<el-button @click="resetFilters">重置</el-button>
<el-button :icon="Refresh" @click="refreshList" circle />
</el-col>
</el-row>
<!-- 打款方式切换 -->
<el-row :gutter="20" class="filter-second-row">
<el-col :span="24">
<div class="mode-switch">
<span class="mode-switch-label">打款方式</span>
<el-radio-group v-model="filters.dakuanMode" @change="handleDakuanModeChange">
<el-radio-button :label="1">手动打款</el-radio-button>
<el-radio-button :label="2">自动打款</el-radio-button>
</el-radio-group>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="filter-second-row">
<el-col :span="24">
<div class="status-switch">
<el-button
:type="isStatusActive(1) ? 'primary' : 'default'"
@click="changeDetailStatus(1)"
size="large"
>
待审核 <el-tag size="small" type="warning" v-if="stats.awaiting">{{ stats.awaiting }}</el-tag>
</el-button>
<el-button
v-if="isAutoMode"
:type="isStatusActive(6) ? 'warning' : 'default'"
@click="changeDetailStatus(6)"
size="large"
>
待收款 <el-tag size="small" type="warning" v-if="stats.pending_collect">{{ stats.pending_collect }}</el-tag>
</el-button>
<el-button
:type="isStatusActive(2) ? 'success' : 'default'"
@click="changeDetailStatus(2)"
size="large"
>
提现成功 <el-tag size="small" type="success" v-if="stats.success">{{ stats.success }}</el-tag>
</el-button>
<el-button
:type="isStatusActive(3) ? 'danger' : 'default'"
@click="changeDetailStatus(3)"
size="large"
>
提现失败
</el-button>
<el-button
v-if="isAutoMode"
:type="isStatusActive(4) ? 'success' : 'default'"
@click="changeDetailStatus(4)"
size="large"
>
审核成功
</el-button>
<el-button
v-if="isAutoMode"
:type="isStatusActive(5) ? 'danger' : 'default'"
@click="changeDetailStatus(5)"
size="large"
>
审核失败
</el-button>
<el-button
:type="isProcessedActive ? 'success' : 'default'"
@click="changeStatus(2)"
size="large"
>
已处理 <el-tag size="small" type="success" v-if="stats.processed">{{ stats.processed }}</el-tag>
</el-button>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="filter-second-row">
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-select v-model="filters.type" placeholder="提现类型" clearable @change="handleSearch">
<el-option label="打手佣金" :value="1" />
<el-option label="管事分红" :value="2" />
<el-option label="组长分红" :value="3" />
<el-option label="审核官分佣" :value="4" />
<el-option label="打手押金" :value="5" />
<el-option label="商家余额" :value="6" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-select v-model="filters.payment" placeholder="收款方式" clearable @change="handleSearch">
<el-option label="全部" :value="0" />
<el-option label="微信收款" :value="1" />
<el-option label="支付宝收款" :value="2" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-input-number
v-model="filters.minAmount"
placeholder="最小金额"
:min="0"
:precision="2"
controls-position="right"
@change="handleSearch"
style="width: 100%"
/>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
<el-input-number
v-model="filters.maxAmount"
placeholder="最大金额"
:min="0"
:precision="2"
controls-position="right"
@change="handleSearch"
style="width: 100%"
/>
</el-col>
</el-row>
</el-card>
<!-- 自动打款批量操作栏 -->
<div v-if="isAutoMode && canBatchOperate" class="batch-bar">
<span class="batch-tip">已选 {{ selectedRows.length }} </span>
<el-button
type="success"
:disabled="selectedRows.length === 0"
:loading="batchOperating"
@click="handleBatchApprove"
>
批量同意打款
</el-button>
<el-button
type="danger"
:disabled="selectedRows.length === 0"
:loading="batchOperating"
@click="showBatchRejectDialog"
>
批量驳回
</el-button>
</div>
<!-- 提现记录表格 -->
<el-card class="table-card" shadow="hover">
<el-table
ref="tableRef"
:data="list"
v-loading="loading"
border
stripe
style="width: 100%"
row-key="id"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="isAutoMode && canBatchOperate"
type="selection"
width="48"
fixed="left"
:selectable="isRowSelectable"
/>
<el-table-column prop="yonghuid" label="用户ID" width="120" />
<el-table-column label="用户信息" min-width="200">
<template #default="{ row }">
<div style="display: flex; align-items: center;">
<el-avatar :size="40" :src="getFullImageUrl(row.avatar)" />
<div style="margin-left: 12px;">
<div>{{ row.nicheng || '--' }}</div>
<div style="font-size: 12px; color: #909399;">{{ row.phone }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="jine" label="申请金额" width="110">
<template #default="{ row }">¥{{ row.jine }}</template>
</el-table-column>
<el-table-column v-if="isAutoMode" label="手续费" width="90">
<template #default="{ row }">{{ row.shouxufei ? `¥${row.shouxufei}` : '--' }}</template>
</el-table-column>
<el-table-column v-if="isAutoMode" label="实际到账" width="100">
<template #default="{ row }">{{ row.shijidaozhang ? `¥${row.shijidaozhang}` : '--' }}</template>
</el-table-column>
<el-table-column label="提现类型" width="110">
<template #default="{ row }">{{ getTypeText(row.leixing) }}</template>
</el-table-column>
<el-table-column v-if="isAutoMode" prop="shenhe_danhao" label="审核单号" min-width="160" show-overflow-tooltip />
<el-table-column v-if="isAutoMode" prop="dakuan_mch_id" label="打款商户" min-width="120" show-overflow-tooltip />
<el-table-column label="打款方式" width="100">
<template #default="{ row }">
<el-tag :type="row.dakuan_mode === 2 ? 'success' : 'info'" size="small">
{{ getDakuanModeText(row.dakuan_mode) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="收款方式" width="100">
<template #default="{ row }">{{ getPaymentText(row.fangshi) }}</template>
</el-table-column>
<el-table-column label="状态" width="110">
<template #default="{ row }">
<el-tag :type="getStatusType(row.zhuangtai)">
{{ row.zhuangtai_text || getStatusText(row.zhuangtai) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="失败原因" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.fail_reason || row.bhliyou || '--' }}</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="160" :formatter="formatTime" />
<el-table-column label="操作" :width="isAutoMode && canBatchOperate ? 200 : 80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click.stop="goToDetail(row)">详情</el-button>
<template v-if="isAutoMode && canBatchOperate && row.zhuangtai === 1">
<el-button type="success" link @click.stop="handleInlineApprove(row)">同意</el-button>
<el-button type="danger" link @click.stop="showInlineRejectDialog(row)">驳回</el-button>
</template>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 列表页驳回弹窗自动打款单条/批量 -->
<el-dialog
v-model="rejectDialogVisible"
:title="rejectMode === 'batch' ? `批量驳回(${selectedRows.length} 条)` : '驳回提现'"
width="420px"
>
<el-form>
<el-form-item label="驳回理由" required>
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleRejectSubmit" :loading="rowOperating || batchOperating">
确认驳回
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import request from '@/utils/request'
const router = useRouter()
const STORAGE_KEY = 'withdraw_audit_filters'
const loading = ref(false)
const batchOperating = ref(false)
const rowOperating = ref(false)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const tableRef = ref(null)
const selectedRows = ref([])
const stats = ref({
total: 0,
awaiting: 0,
processed: 0,
pending_collect: 0,
success: 0,
fail: 0,
total_amount: '0.00'
})
const filters = reactive({
searchUid: '',
date: '',
type: '',
payment: 0,
minAmount: null,
maxAmount: null,
dakuanMode: 1
})
// statusFilter: 1=待审核粗粒度 2=已处理粗粒度detailZhuangtai: 1~6 精确态(优先生效)
const statusFilter = ref(1)
const detailZhuangtai = ref(null)
const rejectDialogVisible = ref(false)
const rejectReason = ref('')
const rejectTargetRow = ref(null)
const rejectMode = ref('single')
const isAutoMode = computed(() => filters.dakuanMode === 2)
const canBatchOperate = computed(() => {
if (detailZhuangtai.value != null) return Number(detailZhuangtai.value) === 1
return Number(statusFilter.value) === 1
})
const isProcessedActive = computed(() => (
detailZhuangtai.value == null && Number(statusFilter.value) === 2
))
const isStatusActive = (code) => Number(detailZhuangtai.value) === Number(code)
const getFullImageUrl = (url) => {
if (!url) return ''
const ossUrl = window.$ossURL || ''
return url.startsWith('http') ? url : ossUrl + url
}
const formatTime = (row, column, cellValue) => {
if (!cellValue) return '--'
const date = new Date(cellValue)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`
}
const getPaymentText = (val) => {
const map = { 1: '微信', 2: '支付宝' }
return map[val] || '未知'
}
const getTypeText = (val) => {
const map = {
1: '打手佣金',
2: '管事分红',
3: '组长分红',
4: '审核官分佣',
5: '打手押金',
6: '商家余额'
}
return map[val] || '未知'
}
const getDakuanModeText = (val) => {
if (val === 2) return '自动'
return '手动'
}
const statusMap = {
1: '审核中',
2: '提现成功',
3: '提现失败',
4: '审核成功',
5: '审核失败',
6: '待收款'
}
const getStatusText = (code) => statusMap[code] || '未知'
const getStatusType = (code) => {
if (code === 1 || code === 6) return 'warning'
if (code === 2 || code === 4) return 'success'
if (code === 3 || code === 5) return 'danger'
return ''
}
const isRowSelectable = (row) => row.zhuangtai === 1
/**
* 与后端 kefu_txsh_action 对齐(自动打款必传四项):
* tixian_id / yonghuid / leixing / shenhe_danhao
*/
const buildActionItem = (row) => ({
tixian_id: Number(row.id),
yonghuid: String(row.yonghuid || '').trim(),
leixing: Number(row.leixing),
shenhe_danhao: String(row.shenhe_danhao || '').trim()
})
const isAutoRow = (row) => Number(row?.dakuan_mode) === 2 || isAutoMode.value
const validateActionRows = (rows) => {
for (const row of rows) {
if (!row?.id) {
return '记录缺少 tixian_id提现记录ID'
}
if (!row.yonghuid) {
return `记录缺少 yonghuid用户ID记录ID=${row.id}`
}
if (row.leixing == null || row.leixing === '') {
return `记录缺少 leixing提现类型用户 ${row.yonghuid}`
}
if (isAutoRow(row) && !row.shenhe_danhao) {
return `记录缺少 shenhe_danhao审核单号用户 ${row.yonghuid},请确认列表接口已返回该字段`
}
}
return ''
}
const submitWithdrawAction = async (payload) => {
const res = await request.post('/yonghu/kefu_txsh_action', {
phone: localStorage.getItem('username'),
...payload
})
if (parseApiCode(res) === 0) {
ElMessage.success(res?.msg || '处理成功')
rejectDialogVisible.value = false
rejectTargetRow.value = null
selectedRows.value = []
fetchData()
return true
}
ElMessage.error(getApiErrorMsg(res, '操作失败'))
return false
}
const saveFiltersToStorage = () => {
const toStore = {
searchUid: filters.searchUid,
date: filters.date,
type: filters.type,
payment: filters.payment,
minAmount: filters.minAmount,
maxAmount: filters.maxAmount,
statusFilter: statusFilter.value,
detailZhuangtai: detailZhuangtai.value,
dakuanMode: filters.dakuanMode
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore))
}
const loadFiltersFromStorage = () => {
const stored = localStorage.getItem(STORAGE_KEY)
if (!stored) return
try {
const parsed = JSON.parse(stored)
if (parsed.searchUid !== undefined) filters.searchUid = parsed.searchUid
if (parsed.date !== undefined) filters.date = parsed.date
if (parsed.type !== undefined && [1, 2, 3, 4, 5, 6].includes(parsed.type)) filters.type = parsed.type
if (parsed.payment !== undefined && [0, 1, 2].includes(parsed.payment)) filters.payment = parsed.payment
if (parsed.minAmount !== undefined) filters.minAmount = parsed.minAmount
if (parsed.maxAmount !== undefined) filters.maxAmount = parsed.maxAmount
if (parsed.statusFilter !== undefined && [1, 2].includes(parsed.statusFilter)) statusFilter.value = parsed.statusFilter
if (parsed.detailZhuangtai !== undefined && parsed.detailZhuangtai !== null
&& [1, 2, 3, 4, 5, 6].includes(Number(parsed.detailZhuangtai))) {
detailZhuangtai.value = Number(parsed.detailZhuangtai)
}
if (parsed.dakuanMode !== undefined && [1, 2].includes(parsed.dakuanMode)) filters.dakuanMode = parsed.dakuanMode
} catch (e) {
console.error('恢复筛选条件失败', e)
}
}
const buildListParams = () => {
const params = {
phone: localStorage.getItem('username'),
page: page.value,
page_size: pageSize.value,
dakuan_mode: filters.dakuanMode,
type: filters.type || undefined,
payment: filters.payment === 0 ? undefined : filters.payment,
search_uid: filters.searchUid || undefined,
date: filters.date || undefined
}
// 明细状态优先;否则用粗粒度 status
if (detailZhuangtai.value != null && [1, 2, 3, 4, 5, 6].includes(Number(detailZhuangtai.value))) {
params.zhuangtai = Number(detailZhuangtai.value)
} else {
params.status = statusFilter.value
}
if (filters.minAmount !== null && filters.minAmount !== '') {
params.min_amount = filters.minAmount
}
if (filters.maxAmount !== null && filters.maxAmount !== '') {
params.max_amount = filters.maxAmount
}
Object.keys(params).forEach(key => params[key] === undefined && delete params[key])
return params
}
const parseApiCode = (res) => Number(res?.code)
const getApiErrorMsg = (res, fallback = '获取失败') => {
return res?.msg || res?.message || fallback
}
const extractRequestError = (error, fallback = '获取失败') => {
return error?.response?.data?.msg || error?.response?.data?.message || error?.message || fallback
}
const fetchData = async () => {
const phone = localStorage.getItem('username')
if (!phone) {
ElMessage.error('未登录或缺少客服手机号')
return
}
loading.value = true
selectedRows.value = []
try {
const res = await request.post('/yonghu/kefu_txsh_list', buildListParams())
const code = parseApiCode(res)
if (code === 0) {
const data = res?.data || {}
list.value = Array.isArray(data.list) ? data.list : []
total.value = Number(data.total) || 0
stats.value = {
total: 0,
awaiting: 0,
processed: 0,
pending_collect: 0,
success: 0,
fail: 0,
total_amount: '0.00',
...(data.stats || {})
}
return
}
if (code === 403) {
ElMessage.error(getApiErrorMsg(res, '您无权查看提现列表'))
return
}
ElMessage.error(getApiErrorMsg(res))
} catch (error) {
console.error('获取提现列表失败', error, error?.response?.data)
ElMessage.error(extractRequestError(error))
} finally {
loading.value = false
}
}
const handleDakuanModeChange = () => {
page.value = 1
selectedRows.value = []
// 手动打款无 4/5/6若当前卡在这些态回退待审核
if (!isAutoMode.value && [4, 5, 6].includes(Number(detailZhuangtai.value))) {
detailZhuangtai.value = 1
statusFilter.value = 1
}
fetchData()
saveFiltersToStorage()
}
const changeStatus = (status) => {
// 粗粒度已处理 / 待审核(清掉明细态)
statusFilter.value = status
detailZhuangtai.value = null
page.value = 1
selectedRows.value = []
fetchData()
saveFiltersToStorage()
}
const changeDetailStatus = (code) => {
detailZhuangtai.value = Number(code)
statusFilter.value = Number(code) === 1 ? 1 : 2
page.value = 1
selectedRows.value = []
fetchData()
saveFiltersToStorage()
}
const handleSearch = () => {
page.value = 1
fetchData()
saveFiltersToStorage()
}
const resetFilters = () => {
filters.searchUid = ''
filters.date = ''
filters.type = ''
filters.payment = 0
filters.minAmount = null
filters.maxAmount = null
filters.dakuanMode = 1
statusFilter.value = 1
detailZhuangtai.value = null
page.value = 1
localStorage.removeItem(STORAGE_KEY)
fetchData()
}
const refreshList = () => {
fetchData()
}
const handleSizeChange = (val) => {
pageSize.value = val
page.value = 1
fetchData()
}
const handlePageChange = (val) => {
page.value = val
fetchData()
}
const handleRowClick = (row) => {
goToDetail(row)
}
const goToDetail = (row) => {
const ctx = {
dakuan_mode: row.dakuan_mode ?? filters.dakuanMode,
shenhe_danhao: row.shenhe_danhao || '',
yonghuid: row.yonghuid || '',
leixing: row.leixing
}
sessionStorage.setItem(`withdraw_detail_ctx_${row.id}`, JSON.stringify(ctx))
router.push({
path: `/withdraw/detail/${row.id}`,
query: {
dakuan_mode: String(ctx.dakuan_mode),
shenhe_danhao: ctx.shenhe_danhao
}
})
}
const handleSelectionChange = (rows) => {
selectedRows.value = rows
}
const handleInlineApprove = async (row) => {
const validateMsg = validateActionRows([row])
if (validateMsg) {
ElMessage.error(validateMsg)
return
}
try {
await ElMessageBox.confirm(
`确认同意用户 ${row.nicheng || row.yonghuid} 的提现申请(¥${row.jine})吗?`,
'确认打款',
{ confirmButtonText: '确认同意', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
rowOperating.value = true
try {
await submitWithdrawAction({
action: 1,
...buildActionItem(row)
})
} catch (error) {
ElMessage.error('操作失败')
} finally {
rowOperating.value = false
}
}
const showInlineRejectDialog = (row) => {
rejectMode.value = 'single'
rejectTargetRow.value = row
rejectReason.value = ''
rejectDialogVisible.value = true
}
const showBatchRejectDialog = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择待审核记录')
return
}
const validateMsg = validateActionRows(selectedRows.value)
if (validateMsg) {
ElMessage.error(validateMsg)
return
}
rejectMode.value = 'batch'
rejectTargetRow.value = null
rejectReason.value = ''
rejectDialogVisible.value = true
}
const handleRejectSubmit = async () => {
if (!rejectReason.value.trim()) {
ElMessage.warning('请输入驳回理由')
return
}
const reason = rejectReason.value.trim()
const rows = rejectMode.value === 'batch' ? selectedRows.value : [rejectTargetRow.value]
const validateMsg = validateActionRows(rows.filter(Boolean))
if (validateMsg) {
ElMessage.error(validateMsg)
return
}
const loadingRef = rejectMode.value === 'batch' ? batchOperating : rowOperating
loadingRef.value = true
try {
if (rejectMode.value === 'batch') {
await submitWithdrawAction({
action: 2,
reason,
batch_list: selectedRows.value.map(buildActionItem)
})
} else {
await submitWithdrawAction({
action: 2,
reason,
...buildActionItem(rejectTargetRow.value)
})
}
} catch (error) {
ElMessage.error('操作失败')
} finally {
loadingRef.value = false
}
}
const handleBatchApprove = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择待审核记录')
return
}
const validateMsg = validateActionRows(selectedRows.value)
if (validateMsg) {
ElMessage.error(validateMsg)
return
}
try {
await ElMessageBox.confirm(
`确认批量同意 ${selectedRows.value.length} 条提现申请吗?`,
'批量同意打款',
{ confirmButtonText: '确认同意', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
batchOperating.value = true
try {
await submitWithdrawAction({
action: 1,
batch_list: selectedRows.value.map(buildActionItem)
})
} catch (error) {
ElMessage.error('操作失败')
} finally {
batchOperating.value = false
}
}
onMounted(() => {
loadFiltersFromStorage()
page.value = 1
fetchData()
})
</script>
<style scoped>
.withdraw-audit {
padding: 20px;
background-color: #f5f9fc;
min-height: 100%;
}
.stats-cards {
margin-bottom: 20px;
}
.stat-card {
border-radius: 12px;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.05);
}
.stat-item {
text-align: center;
padding: 10px 0;
}
.stat-label {
font-size: 14px;
color: #6b7a88;
margin-bottom: 8px;
}
.stat-value {
font-size: 32px;
font-weight: 600;
color: #1a2634;
}
.stat-value.warning {
color: #e6a23c;
}
.stat-value.success {
color: #67c23a;
}
.stat-value.danger {
color: #f56c6c;
}
.total-amount {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 8px 0;
}
.total-label {
font-size: 16px;
font-weight: 500;
color: #1a2634;
}
.total-value {
font-size: 28px;
font-weight: bold;
color: #f56c6c;
}
.filter-card {
margin-bottom: 20px;
border-radius: 12px;
}
.filter-second-row {
margin-top: 16px;
}
.filter-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.mode-switch {
display: flex;
align-items: center;
gap: 16px;
}
.mode-switch-label {
font-size: 14px;
font-weight: 500;
color: #1a2634;
flex-shrink: 0;
}
.status-switch {
display: flex;
gap: 20px;
margin: 8px 0;
}
.status-switch .el-button {
flex: 1;
}
.batch-bar {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.batch-tip {
font-size: 14px;
color: #6b7a88;
}
.table-card {
border-radius: 12px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
@media screen and (max-width: 768px) {
.withdraw-audit {
padding: 12px;
}
.filter-actions {
margin-top: 12px;
justify-content: flex-start;
}
.mode-switch {
flex-direction: column;
align-items: flex-start;
}
.status-switch {
flex-direction: column;
gap: 10px;
}
.pagination {
justify-content: center;
}
}
</style>