🎨 新增提现管理功能

This commit is contained in:
2025-09-10 02:32:11 +08:00
parent 856338950e
commit 36921300df
2 changed files with 696 additions and 0 deletions

20
src/api/user/with.js Normal file
View File

@@ -0,0 +1,20 @@
import service from '@/utils/request'
// list 获取提现列表
export const list = (params) => {
return service({
url: '/sys/with/list',
method: 'get',
params
})
}
// update 更新提现状态
export const update = (data) => {
return service({
url: '/sys/with',
method: 'put',
data
})
}

676
src/view/user/user/with.vue Normal file
View File

@@ -0,0 +1,676 @@
<template>
<div>
<!-- 搜索区域 -->
<div class="gva-search-box">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="用户名">
<el-input v-model="searchForm.userName" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="提现类型">
<el-select v-model="searchForm.withType" placeholder="请选择提现类型" clearable>
<el-option label="微信" :value="1" />
<el-option label="支付宝" :value="2" />
<el-option label="银行卡" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待处理" :value="1" />
<el-option label="已完成" :value="2" />
<el-option label="已拒绝" :value="3" />
<el-option label="已取消" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="提现时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchData">查询</el-button>
<el-button @click="resetData">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计信息 -->
<div class="stats-box">
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-item">
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">总申请数</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item pending">
<div class="stat-value">{{ stats.pending }}</div>
<div class="stat-label">待处理</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item completed">
<div class="stat-value">{{ stats.completed }}</div>
<div class="stat-label">已完成</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item rejected">
<div class="stat-value">{{ stats.rejected }}</div>
<div class="stat-label">已拒绝</div>
</div>
</el-col>
</el-row>
</div>
<!-- 表格区域 -->
<div class="gva-table-box">
<el-table
:data="tableData"
v-loading="tableLoading"
style="width: 100%"
class="gva-table"
stripe
>
<el-table-column prop="ID" label="ID" width="80" />
<el-table-column prop="userName" label="用户名" width="120" />
<el-table-column prop="withType" label="提现类型" width="100">
<template #default="{ row }">
<el-tag :type="getWithTypeTagType(row.withType)">
{{ getWithTypeText(row.withType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="申请金额" width="120">
<template #default="{ row }">
<span style="color: #f56c6c; font-weight: bold;">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="realAmount" label="实际到账" width="120">
<template #default="{ row }">
<span style="color: #67c23a; font-weight: bold;">
{{ row.realAmount > 0 ? `¥${row.realAmount}` : '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="accountInfo" label="收款信息" width="120">
<template #default="{ row }">
<el-button type="text" @click="viewAccountInfo(row)">查看</el-button>
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="申请时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column prop="arriveTime" label="到账时间" width="180">
<template #default="{ row }">
{{ row.arriveTime ? formatDate(row.arriveTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 1"
type="text"
@click="handleApprove(row)"
>
通过
</el-button>
<el-button
v-if="row.status === 1"
type="text"
@click="handleReject(row)"
style="color: #f56c6c"
>
拒绝
</el-button>
<el-button type="text" @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="gva-pagination"
/>
</div>
<!-- 审核弹窗 -->
<el-dialog
v-model="approveDialogVisible"
:title="approveType === 'approve' ? '通过提现申请' : '拒绝提现申请'"
width="500px"
:before-close="handleApproveClose"
>
<el-form
ref="approveFormRef"
:model="approveForm"
:rules="approveRules"
label-width="120px"
>
<el-form-item label="用户名">
<span>{{ currentWithdraw.userName }}</span>
</el-form-item>
<el-form-item label="申请金额">
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
</el-form-item>
<el-form-item v-if="approveType === 'approve'" label="实际到账金额" prop="realAmount">
<el-input-number
v-model="approveForm.realAmount"
:precision="2"
:step="0.01"
:min="0"
:max="currentWithdraw.amount"
placeholder="请输入实际到账金额"
style="width: 100%"
/>
<div style="font-size: 12px; color: #999; margin-top: 5px;">
可扣除手续费等费用不能超过申请金额
</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="approveForm.remark"
type="textarea"
:rows="3"
:placeholder="approveType === 'approve' ? '请输入通过备注(可选)' : '请输入拒绝原因'"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleApproveClose">取消</el-button>
<el-button
type="primary"
@click="submitApprove"
:class="approveType === 'reject' ? 'reject-btn' : ''"
>
{{ approveType === 'approve' ? '确认通过' : '确认拒绝' }}
</el-button>
</span>
</template>
</el-dialog>
<!-- 收款信息弹窗 -->
<el-dialog
v-model="accountInfoDialogVisible"
title="收款信息"
width="600px"
:before-close="handleAccountInfoClose"
>
<div class="account-info-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="提现类型">
<el-tag :type="getWithTypeTagType(currentWithdraw.withType)">
{{ getWithTypeText(currentWithdraw.withType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请金额">
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 20px;">
<h4>收款信息</h4>
<div v-if="isImageUrl(currentWithdraw.accountInfo)" class="image-info">
<el-image
:src="currentWithdraw.accountInfo"
:preview-src-list="[currentWithdraw.accountInfo]"
fit="contain"
style="max-width: 300px; max-height: 300px;"
/>
<p style="margin-top: 10px; color: #666;">{{ getWithTypeText(currentWithdraw.withType) }}收款码</p>
</div>
<div v-else class="text-info">
<el-input
:value="currentWithdraw.accountInfo"
readonly
type="textarea"
:rows="3"
/>
<p style="margin-top: 10px; color: #666;">银行卡号或账户信息</p>
</div>
</div>
</div>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="提现详情"
width="600px"
:before-close="handleDetailClose"
>
<el-descriptions :column="2" border>
<el-descriptions-item label="提现ID">{{ currentWithdraw.ID }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentWithdraw.userName }}</el-descriptions-item>
<el-descriptions-item label="提现类型">
<el-tag :type="getWithTypeTagType(currentWithdraw.withType)">
{{ getWithTypeText(currentWithdraw.withType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请金额">
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
</el-descriptions-item>
<el-descriptions-item label="实际到账">
<span style="color: #67c23a; font-weight: bold;">
{{ currentWithdraw.realAmount > 0 ? `¥${currentWithdraw.realAmount}` : '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(currentWithdraw.status)">
{{ getStatusText(currentWithdraw.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ formatDate(currentWithdraw.CreatedAt) }}</el-descriptions-item>
<el-descriptions-item label="到账时间">
{{ currentWithdraw.arriveTime ? formatDate(currentWithdraw.arriveTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { list, update } from '@/api/user/with.js'
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'WithdrawManagement'
})
// 表格相关数据
const tableLoading = ref(false)
const tableData = ref([])
const queryParams = ref({
page: 1,
pageSize: 10
})
const total = ref(0)
// 搜索表单
const searchForm = ref({
userName: '',
withType: '',
status: '',
dateRange: []
})
// 统计数据
const stats = computed(() => {
const pending = tableData.value.filter(item => item.status === 1).length
const completed = tableData.value.filter(item => item.status === 2).length
const rejected = tableData.value.filter(item => item.status === 3).length
return {
total: total.value,
pending,
completed,
rejected
}
})
// 审核弹窗
const approveDialogVisible = ref(false)
const approveFormRef = ref(null)
const approveType = ref('approve') // 'approve' 或 'reject'
const currentWithdraw = ref({})
const approveForm = ref({
id: '',
status: 2,
realAmount: 0,
remark: ''
})
// 收款信息弹窗
const accountInfoDialogVisible = ref(false)
// 详情弹窗
const detailDialogVisible = ref(false)
// 表单验证规则
const approveRules = {
realAmount: [
{ required: true, message: '请输入实际到账金额', trigger: 'blur' },
{ type: 'number', min: 0, message: '金额不能小于0', trigger: 'blur' }
],
remark: [
{
validator: (rule, value, callback) => {
if (approveType.value === 'reject' && !value) {
callback(new Error('拒绝时必须填写拒绝原因'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 获取提现类型文本
const getWithTypeText = (type) => {
const typeMap = {
1: '微信',
2: '支付宝',
3: '银行卡'
}
return typeMap[type] || '未知'
}
// 获取提现类型标签类型
const getWithTypeTagType = (type) => {
const typeMap = {
1: 'success',
2: 'primary',
3: 'warning'
}
return typeMap[type] || 'info'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
1: '待处理',
2: '已完成',
3: '已拒绝',
4: '已取消'
}
return statusMap[status] || '未知'
}
// 获取状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
1: 'warning',
2: 'success',
3: 'danger',
4: 'info'
}
return typeMap[status] || 'info'
}
// 判断是否为图片URL
const isImageUrl = (url) => {
if (!url) return false
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
return imageExtensions.some(ext => url.toLowerCase().includes(ext))
}
// 搜索数据
const searchData = () => {
queryParams.value.page = 1
getList()
}
// 重置搜索
const resetData = () => {
searchForm.value = {
userName: '',
withType: '',
status: '',
dateRange: []
}
queryParams.value.page = 1
getList()
}
// 分页处理
const handleSizeChange = (size) => {
queryParams.value.pageSize = size
getList()
}
const handleCurrentChange = (page) => {
queryParams.value.page = page
getList()
}
// 获取提现列表
async function getList() {
tableLoading.value = true
try {
const params = {
...queryParams.value,
...searchForm.value
}
// 处理日期范围
if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
params.startTime = searchForm.value.dateRange[0]
params.endTime = searchForm.value.dateRange[1]
}
delete params.dateRange
const res = await list(params)
if (res.code === 0) {
tableData.value = res.data.list || []
total.value = res.data.total || 0
} else {
ElMessage.error(res.msg || '获取数据失败')
}
} catch (error) {
console.error('获取提现列表失败:', error)
ElMessage.error('获取数据失败')
} finally {
tableLoading.value = false
}
}
// 通过申请
const handleApprove = (row) => {
currentWithdraw.value = row
approveType.value = 'approve'
approveForm.value = {
id: row.ID,
status: 2,
realAmount: row.amount,
remark: ''
}
approveDialogVisible.value = true
}
// 拒绝申请
const handleReject = (row) => {
currentWithdraw.value = row
approveType.value = 'reject'
approveForm.value = {
id: row.ID,
status: 3,
realAmount: 0,
remark: ''
}
approveDialogVisible.value = true
}
// 查看收款信息
const viewAccountInfo = (row) => {
currentWithdraw.value = row
accountInfoDialogVisible.value = true
}
// 查看详情
const viewDetail = (row) => {
currentWithdraw.value = row
detailDialogVisible.value = true
}
// 关闭审核弹窗
const handleApproveClose = () => {
approveDialogVisible.value = false
approveFormRef.value?.resetFields()
}
// 关闭收款信息弹窗
const handleAccountInfoClose = () => {
accountInfoDialogVisible.value = false
}
// 关闭详情弹窗
const handleDetailClose = () => {
detailDialogVisible.value = false
}
// 提交审核
const submitApprove = async () => {
if (!approveFormRef.value) return
await approveFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await update(approveForm.value)
if (res.code === 0) {
ElMessage.success(approveType.value === 'approve' ? '审核通过成功' : '拒绝成功')
handleApproveClose()
getList()
} else {
ElMessage.error(res.msg || '操作失败')
}
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
// 页面初始化
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.gva-search-box {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats-box {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.stat-item {
text-align: center;
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
&.pending {
background: #fef9e7;
color: #e6a23c;
}
&.completed {
background: #f0f9ff;
color: #67c23a;
}
&.rejected {
background: #fef2f2;
color: #f56c6c;
}
.stat-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
opacity: 0.8;
}
}
}
.gva-table-box {
background: #fff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.gva-table {
:deep(.el-table__header) {
background-color: #f5f7fa;
}
:deep(.el-table__row:hover > td) {
background-color: #f5f7fa !important;
}
}
.gva-pagination {
margin-top: 20px;
text-align: right;
}
.demo-form-inline {
.el-form-item {
margin-right: 20px;
}
}
.account-info-content {
.image-info {
text-align: center;
.el-image {
border: 1px solid #dcdfe6;
border-radius: 4px;
}
}
.text-info {
.el-textarea {
:deep(.el-textarea__inner) {
background-color: #f5f7fa;
}
}
}
}
.reject-btn {
background-color: #f56c6c !important;
border-color: #f56c6c !important;
&:hover {
background-color: #f78989 !important;
border-color: #f78989 !important;
}
}
</style>