Files
jurong_circle_frontdesk/src/views/AgentWithdrawals.vue
2025-08-21 10:21:57 +08:00

1099 lines
27 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="agent-withdrawals">
<!-- 顶部导航 -->
<div class="page-header">
<button class="btn-back" @click="goBack">
<i class="icon-arrow-left"></i>
返回仪表盘
</button>
<h2 class="page-title">佣金提现</h2>
</div>
<!-- 顶部统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-icon">
<i class="icon-wallet"></i>
</div>
<div class="stat-content">
<div class="stat-value">¥{{ commissionStats.total_commission || '0.00' }}</div>
<div class="stat-label">总佣金</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="icon-money"></i>
</div>
<div class="stat-content">
<div class="stat-value">¥{{ commissionStats.available_amount || '0.00' }}</div>
<div class="stat-label">可提现金额</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="icon-pending"></i>
</div>
<div class="stat-content">
<div class="stat-value">¥{{ commissionStats.pending_withdrawal || '0.00' }}</div>
<div class="stat-label">提现中</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="icon-success"></i>
</div>
<div class="stat-content">
<div class="stat-value">¥{{ commissionStats.withdrawn_amount || '0.00' }}</div>
<div class="stat-label">已提现</div>
</div>
</div>
</div>
<!-- 提现申请 -->
<div class="withdrawal-section">
<div class="section-header">
<h3>申请提现</h3>
</div>
<form @submit.prevent="submitWithdrawal" class="withdrawal-form">
<div class="form-group">
<label>提现金额</label>
<div class="amount-input">
<input
v-model="withdrawalForm.amount"
type="number"
step="0.01"
min="1"
:max="commissionStats.available_amount"
placeholder="请输入提现金额"
required
>
<span class="currency"></span>
</div>
<div class="amount-info">
<span class="amount-tips">
可提现金额¥{{ commissionStats.available_amount || '0.00' }}
</span>
<button class="btn-edit" @click="openPaymentDialog">
更改收款方式
</button>
</div>
</div>
<div class="form-actions">
<button
type="submit"
class="btn-primary"
:disabled="submitting || !canWithdraw"
>
{{ submitting ? '提交中...' : '申请提现' }}
</button>
</div>
</form>
</div>
<!-- 提现记录 -->
<div class="records-section">
<div class="section-header">
<h3>提现记录</h3>
<div class="filter-controls">
<select v-model="recordsFilter" @change="loadWithdrawalRecords">
<option value="">全部状态</option>
<option value="pending">待审核</option>
<option value="approved">已通过</option>
<option value="completed">已完成</option>
<option value="rejected">已拒绝</option>
</select>
</div>
</div>
<div class="records-list">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="withdrawalRecords.length === 0" class="empty">
暂无提现记录
</div>
<div v-else>
<div
v-for="record in withdrawalRecords"
:key="record.id"
class="record-item"
>
<div class="record-header">
<span class="amount">¥{{ record.amount }}</span>
<span class="status" :class="`status-${record.status}`">
{{ getStatusText(record.status) }}
</span>
</div>
<div class="record-details">
<div class="detail-item">
<span class="label">申请时间</span>
<span class="value">{{ formatDate(record.created_at) }}</span>
</div>
<div class="detail-item">
<span class="label">收款信息</span>
<span class="value">
{{ getPaymentTypeText(record.payment_type || 'bank') }}
<template v-if="record.payment_type === 'bank' || !record.payment_type">
- {{ record.bank_name }} {{ maskAccount(record.account_number || record.bank_account) }}
</template>
<template v-else>
- {{ record.account_holder }} ({{ maskAccount(record.account_number) }})
</template>
</span>
</div>
<div v-if="record.admin_note" class="detail-item">
<span class="label">备注</span>
<span class="value">{{ record.admin_note }}</span>
</div>
<div v-if="record.completed_at" class="detail-item">
<span class="label">完成时间</span>
<span class="value">{{ formatDate(record.completed_at) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 编辑收款方式的对话框 -->
<el-dialog
v-model="paymentDialogVisible"
title="编辑收款方式"
width="600px"
:before-close="handleDialogClose"
>
<el-form
ref="paymentFormRef"
:model="paymentForm"
label-width="120px"
label-position="top"
:rules="paymentFormRules"
>
<el-form-item label="收款方式" prop="payment_type">
<el-select
v-model="paymentForm.payment_type"
placeholder="请选择收款方式"
@change="onPaymentTypeChange"
style="width: 100%"
>
<el-option label="银行卡" value="bank"></el-option>
<el-option label="微信收款码" value="wechat"></el-option>
<el-option label="支付宝收款码" value="alipay"></el-option>
<el-option label="云闪付收款码" value="unionpay"></el-option>
</el-select>
</el-form-item>
<el-form-item
v-if="paymentForm.payment_type === 'bank'"
label="银行名称"
prop="bank_name"
>
<el-input
v-model="paymentForm.bank_name"
placeholder="请输入银行名称"
></el-input>
</el-form-item>
<el-form-item
:label="getAccountLabel(paymentForm.payment_type)"
prop="account_number"
>
<el-input
v-model="paymentForm.account_number"
:placeholder="getAccountPlaceholder(paymentForm.payment_type)"
></el-input>
</el-form-item>
<el-form-item
:label="getHolderLabel(paymentForm.payment_type)"
prop="account_holder"
>
<el-input
v-model="paymentForm.account_holder"
:placeholder="getHolderPlaceholder(paymentForm.payment_type)"
></el-input>
</el-form-item>
<el-form-item
v-if="paymentForm.payment_type && paymentForm.payment_type !== 'bank'"
label="收款码图片"
prop="qr_code_url"
>
<el-upload
class="qr-code-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="handleQrCodeUpload"
accept="image/*"
>
<div v-if="paymentForm.qr_code_url" class="qr-code-preview">
<img :src="paymentForm.qr_code_url" class="qr-code-image" />
<div class="qr-code-mask">
<span>点击更换图片</span>
</div>
</div>
<div v-else class="upload-placeholder">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-text">点击上传收款码</div>
<div class="upload-hint">支持 JPGPNG 格式大小不超过5MB</div>
</div>
</el-upload>
<div v-if="uploadingQrCode" class="upload-progress">
<el-progress :percentage="uploadProgress" :show-text="false"></el-progress>
<span>上传中...</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="paymentDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="submitPaymentForm"
:loading="updating"
>
保存
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import api from '@/utils/api'
export default {
name: 'AgentWithdrawals',
components: {
Plus
},
setup() {
const router = useRouter()
const userStore = useUserStore()
const paymentFormRef = ref(null)
// 响应式数据
const loading = ref(false)
const updating = ref(false)
const submitting = ref(false)
const paymentDialogVisible = ref(false)
const recordsFilter = ref('')
const uploadingQrCode = ref(false)
const uploadProgress = ref(0)
const commissionStats = reactive({
total_commission: '0.00',
available_amount: '0.00',
pending_withdrawal: '0.00',
withdrawn_amount: '0.00'
})
const paymentInfo = reactive({
payment_type: '',
bank_name: '',
account_number: '',
account_holder: '',
qr_code_url: ''
})
const paymentForm = reactive({
payment_type: '',
bank_name: '',
account_number: '',
account_holder: '',
qr_code_url: ''
})
const withdrawalForm = reactive({
amount: ''
})
const withdrawalRecords = ref([])
// 表单验证规则
const paymentFormRules = reactive({
payment_type: [
{ required: true, message: '请选择收款方式', trigger: 'change' }
],
bank_name: [
{ required: true, message: '请输入银行名称', trigger: 'blur' }
],
account_number: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
account_holder: [
{ required: true, message: '请输入持有人姓名', trigger: 'blur' }
],
qr_code_url: [
{ required: true, message: '请上传收款码图片', trigger: 'change' }
]
})
// 计算属性
const canWithdraw = computed(() => {
const amount = parseFloat(withdrawalForm.amount)
const available = parseFloat(commissionStats.available_amount)
const hasBasicInfo = amount > 0 && amount <= available && paymentInfo.account_number && paymentInfo.payment_type
// 银行卡只需要基本信息
if (paymentInfo.payment_type === 'bank') {
return hasBasicInfo
}
// 收款码需要额外验证图片
return hasBasicInfo && paymentInfo.qr_code_url
})
// 方法
/**
* 加载佣金统计数据
*/
const loadCommissionStats = async () => {
try {
const response = await api.get('/agent-withdrawals/stats')
if (response.data.success) {
// 更新佣金统计数据
Object.assign(commissionStats, {
total_commission: response.data.data.total_commission || '0.00',
available_amount: response.data.data.available_amount || '0.00',
pending_withdrawal: response.data.data.pending_withdrawal || '0.00',
withdrawn_amount: response.data.data.withdrawn_amount || '0.00'
})
// 处理收款方式信息
if (response.data.data.paymentInfo) {
Object.assign(paymentInfo, response.data.data.paymentInfo)
Object.assign(paymentForm, response.data.data.paymentInfo)
} else if (response.data.data.bank_account) {
// 兼容旧的银行信息字段
const bankInfo = {
payment_type: 'bank',
// 移除 bank_name 字段,因为后端数据库中不存在该字段
account_number: response.data.data.bank_account || '',
account_holder: response.data.data.account_holder || '',
qr_code_url: ''
}
Object.assign(paymentInfo, bankInfo)
Object.assign(paymentForm, bankInfo)
}
}
} catch (error) {
console.error('加载佣金统计失败:', error)
ElMessage.error('加载佣金统计失败')
}
}
/**
* 打开收款方式对话框
*/
const openPaymentDialog = () => {
// 重置表单为当前信息
Object.assign(paymentForm, paymentInfo)
paymentDialogVisible.value = true
}
/**
* 提交收款方式表单
*/
const submitPaymentForm = () => {
paymentFormRef.value.validate(async (valid) => {
if (!valid) return
if (updating.value) return
updating.value = true
try {
const response = await api.put('/agent-withdrawals/payment-info', paymentForm)
if (response.data.success) {
Object.assign(paymentInfo, paymentForm)
paymentDialogVisible.value = false
ElMessage.success('收款方式更新成功')
} else {
ElMessage.error(response.data.message || '更新失败')
}
} catch (error) {
console.error('更新收款方式失败:', error)
ElMessage.error('更新失败,请重试')
} finally {
updating.value = false
}
})
}
/**
* 处理对话框关闭
*/
const handleDialogClose = (done) => {
if (JSON.stringify(paymentForm) !== JSON.stringify(paymentInfo)) {
ElMessageBox.confirm('您有未保存的更改,确定要关闭吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
done()
}).catch(() => {})
} else {
done()
}
}
/**
* 提交提现申请
*/
const submitWithdrawal = async () => {
if (submitting.value || !canWithdraw.value) return
if (!paymentInfo.account_number || !paymentInfo.payment_type) {
ElMessage.warning('请先设置收款方式')
return
}
// 收款码类型需要验证图片
if (paymentInfo.payment_type !== 'bank' && !paymentInfo.qr_code_url) {
ElMessage.warning('请上传收款码图片')
return
}
submitting.value = true
try {
const response = await api.post('/agent-withdrawals/apply', {
amount: parseFloat(withdrawalForm.amount)
})
if (response.data.success) {
ElMessage.success('提现申请提交成功,请等待审核')
withdrawalForm.amount = ''
await loadCommissionStats()
await loadWithdrawalRecords()
} else {
ElMessage.error(response.data.message || '申请失败')
}
} catch (error) {
console.error('提现申请失败:', error)
ElMessage.error('申请失败,请重试')
} finally {
submitting.value = false
}
}
/**
* 加载提现记录
*/
const loadWithdrawalRecords = async () => {
loading.value = true
try {
const params = {}
if (recordsFilter.value) {
params.status = recordsFilter.value
}
const response = await api.get('/agent-withdrawals/records', { params })
if (response.data.success) {
withdrawalRecords.value = response.data.data.records || []
}
} catch (error) {
console.error('加载提现记录失败:', error)
ElMessage.error('加载提现记录失败')
} finally {
loading.value = false
}
}
/**
* 格式化日期
*/
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
/**
* 获取状态文本
*/
const getStatusText = (status) => {
const statusMap = {
pending: '待审核',
approved: '已通过',
completed: '已完成',
rejected: '已拒绝'
}
return statusMap[status] || status
}
/**
* 掩码显示账号信息
*/
const maskAccount = (account) => {
if (!account) return ''
if (account.length <= 8) return account
return account.slice(0, 4) + '****' + account.slice(-4)
}
/**
* 处理收款码图片上传
*/
const handleQrCodeUpload = async (file) => {
// 验证文件类型
if (!file.raw.type.startsWith('image/')) {
ElMessage.error('请选择图片文件')
return false
}
// 验证文件大小限制为5MB
if (file.raw.size > 5 * 1024 * 1024) {
ElMessage.error('图片大小不能超过5MB')
return false
}
uploadingQrCode.value = true
uploadProgress.value = 0
try {
const formData = new FormData()
formData.append('qrCode', file.raw)
const response = await api.post('/agent-withdrawals/upload-qr-code', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
}
}
})
if (response.data.success) {
paymentForm.qr_code_url = response.data.data.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.data.message || '上传失败')
}
} catch (error) {
console.error('上传收款码失败:', error)
ElMessage.error('上传失败,请重试')
} finally {
uploadingQrCode.value = false
uploadProgress.value = 0
}
}
/**
* 获取收款方式显示文本
*/
const getPaymentTypeText = (type) => {
const types = {
'bank': '银行卡',
'wechat': '微信收款码',
'alipay': '支付宝收款码',
'unionpay': '云闪付收款码'
}
return types[type] || ''
}
/**
* 获取账号标签
*/
const getAccountLabel = (type) => {
const labels = {
'bank': '银行账号',
'wechat': '微信号',
'alipay': '支付宝账号',
'unionpay': '云闪付账号'
}
return labels[type] || '账号'
}
/**
* 获取持有人标签
*/
const getHolderLabel = (type) => {
const labels = {
'bank': '开户人',
'wechat': '微信持有人姓名',
'alipay': '支付宝持有人姓名',
'unionpay': '云闪付持有人姓名'
}
return labels[type] || '持有人'
}
/**
* 获取账号输入占位符
*/
const getAccountPlaceholder = (type) => {
const placeholders = {
'bank': '请输入银行账号',
'wechat': '请输入微信号',
'alipay': '请输入支付宝账号',
'unionpay': '请输入云闪付账号'
}
return placeholders[type] || '请输入账号'
}
/**
* 获取持有人输入占位符
*/
const getHolderPlaceholder = (type) => {
const placeholders = {
'bank': '请输入开户人姓名',
'wechat': '请输入微信持有人姓名',
'alipay': '请输入支付宝持有人姓名',
'unionpay': '请输入云闪付持有人姓名'
}
return placeholders[type] || '请输入持有人姓名'
}
/**
* 收款方式类型改变时的处理
*/
const onPaymentTypeChange = () => {
// 清空非通用字段
if (paymentForm.payment_type !== 'bank') {
paymentForm.bank_name = ''
} else {
paymentForm.qr_code_url = ''
}
// 重置验证状态
nextTick(() => {
paymentFormRef.value.clearValidate(['bank_name', 'qr_code_url'])
})
}
/**
* 返回代理仪表盘
*/
const goBack = () => {
router.push('/agent/dashboard')
}
// 生命周期
onMounted(async () => {
await loadCommissionStats()
await loadWithdrawalRecords()
})
return {
// 引用
paymentFormRef,
// 状态
loading,
updating,
submitting,
paymentDialogVisible,
recordsFilter,
uploadingQrCode,
uploadProgress,
// 数据
commissionStats,
paymentInfo,
paymentForm,
withdrawalForm,
withdrawalRecords,
// 计算属性
canWithdraw,
// 方法
loadCommissionStats,
openPaymentDialog,
submitPaymentForm,
handleDialogClose,
submitWithdrawal,
loadWithdrawalRecords,
formatDate,
getStatusText,
maskAccount,
handleQrCodeUpload,
getPaymentTypeText,
getAccountLabel,
getHolderLabel,
getAccountPlaceholder,
getHolderPlaceholder,
onPaymentTypeChange,
goBack,
// 表单规则
paymentFormRules
}
}
}
</script>
<style scoped>
.agent-withdrawals {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
position: relative;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.page-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin: 0;
font-size: 24px;
color: white;
font-weight: 600;
}
/* 返回按钮 */
.btn-back {
gap: 8px;
padding: 10px 16px;
background: none;
border: none;
border-radius: 8px;
font-size: 14px;
color: white;
cursor: pointer;
}
.btn-back:hover {
border-color: #adb5bd;
}
.btn-back .icon-arrow-left::before {
content: '<';
font-size: 16px;
}
/* 统计卡片 */
.stats-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 30px;
width: 343px;
}
.stat-card {
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.stat-card:nth-child(1) .stat-icon {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-card:nth-child(2) .stat-icon {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card:nth-child(3) .stat-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card:nth-child(4) .stat-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
}
/* 通用区块样式 */
.withdrawal-section,
.records-section {
border-radius: 12px;
padding: 24px;
margin: 0 auto 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 343px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
}
.section-header h3 {
margin: 0;
font-size: 18px;
color: #333;
line-height: 24px;
}
/* 表单样式 */
.withdrawal-form {
display: grid;
gap: 20px;
}
.form-group {
display: grid;
gap: 8px;
}
.form-group label {
font-size: 14px;
color: #333;
font-weight: 500;
}
.amount-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.amount-input {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.amount-input input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
padding-right: 40px;
}
.amount-input input:focus {
outline: none;
border-color: #007bff;
}
.currency {
position: absolute;
right: 12px;
color: #666;
font-size: 14px;
}
.amount-tips {
font-size: 12px;
color: #666;
margin-right: 10px;
}
.form-actions {
display: flex;
justify-content: center; /* 将右对齐改为居中对齐 */
}
/* 按钮样式 */
.btn-primary,
.btn-edit {
padding: 10px 20px;
border: none;
border-radius: 1000px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #007bff;
color: white;
width: 300px;
/* 保持申请提现按钮原有样式不变 */
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-edit {
background: #28a745;
color: white;
padding: 8px 16px;
font-size: 12px;
white-space: nowrap;
}
.btn-edit:hover {
background: #1e7e34;
}
/* 筛选控件 */
.filter-controls {
height: 24px;
display: flex;
align-items: center;
}
.filter-controls select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
height: 24px;
}
/* 记录列表 */
.records-list {
display: grid;
gap: 16px;
}
.loading,
.empty {
text-align: center;
padding: 40px;
color: #666;
font-size: 14px;
}
.record-item {
border: 1px solid black;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.3s;
}
.record-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.record-header .amount {
font-size: 18px;
font-weight: bold;
color: #333;
}
.status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-approved {
background: #d1ecf1;
color: #0c5460;
}
.status-completed {
background: #d4edda;
color: #155724;
}
.status-rejected {
background: #f8d7da;
color: #721c24;
}
.record-details {
display: grid;
gap: 8px;
}
.detail-item {
display: flex;
font-size: 14px;
}
.detail-item .label {
width: 80px;
color: #666;
flex-shrink: 0;
}
.detail-item .value {
color: #333;
flex: 1;
}
/* 图标 */
.icon-wallet::before { content: '💰'; }
.icon-money::before { content: '💵'; }
.icon-pending::before { content: '⏳'; }
.icon-success::before { content: '✅'; }
/* 响应式设计 */
@media (max-width: 768px) {
.agent-withdrawals {
padding: 16px;
}
.stat-card {
padding: 20px;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.record-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>