976 lines
29 KiB
Vue
976 lines
29 KiB
Vue
<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>
|