campus-errand/miniapp/pages/mine/earnings.vue
18631081161 681d2b5fe8
All checks were successful
continuous-integration/drone/push Build is passing
提现
2026-04-02 16:55:18 +08:00

640 lines
13 KiB
Vue

<template>
<view class="earnings-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
</view>
<text class="navbar-title">我的收益</text>
<view class="nav-placeholder"></view>
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 四种金额状态展示 -->
<view class="amount-section">
<view class="amount-grid">
<view class="amount-item">
<text class="amount-value">¥{{ earnings.frozen || '0.00' }}</text>
<text class="amount-label">冻结中</text>
</view>
<view class="amount-item">
<text class="amount-value highlight">¥{{ earnings.available || '0.00' }}</text>
<text class="amount-label">待提现</text>
</view>
<view class="amount-item">
<text class="amount-value">¥{{ earnings.withdrawing || '0.00' }}</text>
<text class="amount-label">提现中</text>
</view>
<view class="amount-item">
<text class="amount-value">¥{{ earnings.withdrawn || '0.00' }}</text>
<text class="amount-label">已提现</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<view class="action-btn primary" @click="openWithdraw">
<text>申请提现</text>
</view>
<view class="action-btn secondary" @click="goEarningsRecord">
<text>收益记录</text>
</view>
</view>
<!-- 提现说明 -->
<view class="guide-link" @click="openGuide">
<text>点击查看提现说明</text>
</view>
<!-- 提现记录 -->
<view class="record-section">
<view class="section-title"><text>提现记录</text></view>
<view v-if="withdrawals.length === 0" class="empty-tip">
<text>暂无提现记录</text>
</view>
<view v-for="item in withdrawals" :key="item.id" class="record-item">
<view class="record-left">
<text class="record-method">{{ item.paymentMethod === 'WeChat' ? '微信' : '支付宝' }}</text>
<text class="record-time">{{ formatTime(item.createdAt) }}</text>
</view>
<view class="record-right">
<text class="record-amount">-¥{{ item.amount }}</text>
<text class="record-status" :class="getWithdrawStatusClass(item.status)">{{ getWithdrawStatusLabel(item.status) }}</text>
</view>
</view>
</view>
<!-- 申请提现弹窗 -->
<view class="modal-mask" v-if="showWithdrawModal" @click="showWithdrawModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">申请提现</text>
<view class="withdraw-available">
<text>可提现金额:</text>
<text class="available-amount">¥{{ earnings.available || '0.00' }}</text>
</view>
<view class="form-item">
<text class="form-label">提现金额</text>
<input
class="form-input"
v-model="withdrawForm.amount"
type="digit"
:placeholder="`最低${minWithdrawal}元,支持小数点两位`"
/>
</view>
<view class="withdraw-tip">
<text>提现将自动转入您的微信零钱</text>
</view>
<view class="modal-actions">
<button class="modal-btn cancel" @click="showWithdrawModal = false">取消</button>
<button class="modal-btn confirm" @click="submitWithdraw" :loading="withdrawSubmitting">申请提现</button>
</view>
</view>
</view>
<!-- 提现说明弹窗 -->
<view class="modal-mask" v-if="showGuideModal" @click="showGuideModal = false">
<view class="modal-content guide-modal" @click.stop>
<text class="modal-title">提现说明</text>
<scroll-view class="guide-content" scroll-y>
<rich-text :nodes="guideContent"></rich-text>
</scroll-view>
<view class="modal-actions">
<button class="modal-btn confirm full" @click="showGuideModal = false">我知道了</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { getEarnings, getWithdrawals, applyWithdraw, getWithdrawalGuide, getMinWithdrawal } from '../../utils/api'
import { uploadFile } from '../../utils/request'
export default {
data() {
return {
earnings: {
frozen: '0.00',
available: '0.00',
withdrawing: '0.00',
withdrawn: '0.00'
},
withdrawals: [],
// 提现弹窗
showWithdrawModal: false,
withdrawForm: {
amount: '',
paymentMethod: 'WeChat',
qrImage: '',
qrImageUrl: ''
},
withdrawSubmitting: false,
// 提现说明弹窗
showGuideModal: false,
guideContent: '',
statusBarHeight: 0,
minWithdrawal: 1
}
},
onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadData()
this.loadMinWithdrawal()
},
methods: {
goBack() { uni.navigateBack() },
/** 加载最低提现金额配置 */
async loadMinWithdrawal() {
try {
const res = await getMinWithdrawal()
if (res?.value) this.minWithdrawal = parseFloat(res.value) || 1
} catch (e) {}
},
/** 加载收益和提现数据 */
async loadData() {
try {
const [earningsRes, withdrawalsRes] = await Promise.all([
getEarnings(),
getWithdrawals()
])
if (earningsRes) {
this.earnings = {
frozen: (earningsRes.frozenAmount || 0).toFixed(2),
available: (earningsRes.availableAmount || 0).toFixed(2),
withdrawing: (earningsRes.withdrawingAmount || 0).toFixed(2),
withdrawn: (earningsRes.withdrawnAmount || 0).toFixed(2)
}
}
this.withdrawals = withdrawalsRes?.items || withdrawalsRes || []
} catch (e) {
// 静默处理
}
},
formatTime(dateStr) {
if (!dateStr) return '-'
const d = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
},
getWithdrawStatusLabel(status) {
const map = { Pending: '待处理', Processing: '处理中', Completed: '已完成' }
return map[status] || status
},
getWithdrawStatusClass(status) {
const map = { Pending: 'ws-pending', Processing: 'ws-processing', Completed: 'ws-done' }
return map[status] || ''
},
/** 跳转收益记录 */
goEarningsRecord() {
uni.navigateTo({ url: '/pages/mine/earnings-record' })
},
/** 打开提现弹窗 */
openWithdraw() {
this.withdrawForm = { amount: '', paymentMethod: 'WeChat', qrImage: '', qrImageUrl: '' }
this.showWithdrawModal = true
},
/** 选择收款二维码图片 */
chooseQrImage() {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
this.withdrawForm.qrImage = res.tempFilePaths[0]
}
})
},
/** 提交提现申请 */
async submitWithdraw() {
const amount = parseFloat(this.withdrawForm.amount)
// 金额校验
if (isNaN(amount) || amount < this.minWithdrawal) {
uni.showToast({ title: `提现金额最低${this.minWithdrawal}`, icon: 'none' })
return
}
// 小数位校验
const parts = this.withdrawForm.amount.split('.')
if (parts.length > 1 && parts[1].length > 2) {
uni.showToast({ title: '请输入正确的提现金额', icon: 'none' })
return
}
const available = parseFloat(this.earnings.available) || 0
if (amount > available) {
uni.showToast({ title: '超出可提现范围', icon: 'none' })
return
}
this.withdrawSubmitting = true
try {
await applyWithdraw({
amount,
paymentMethod: 'WeChat'
})
uni.showToast({ title: '提现申请已提交', icon: 'success' })
this.showWithdrawModal = false
this.loadData()
} catch (e) {
// 错误已在 request 中处理
} finally {
this.withdrawSubmitting = false
}
},
/** 打开提现说明弹窗 */
async openGuide() {
try {
if (!this.guideContent) {
const res = await getWithdrawalGuide()
this.guideContent = res?.value || res?.content || '暂无提现说明'
}
this.showGuideModal = true
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
}
}
}
</script>
<style scoped>
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background: #FFB700;
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.navbar-title {
font-size: 34rpx;
font-weight: bold;
color: #363636;
}
.nav-placeholder {
width: 60rpx;
}
.earnings-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 金额状态区域 */
.amount-section {
background-color: #FFB700;
padding: 40rpx 24rpx 50rpx;
}
.amount-grid {
display: flex;
justify-content: space-around;
}
.amount-item {
display: flex;
flex-direction: column;
align-items: center;
}
.amount-value {
font-size: 40rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: bold;
margin-bottom: 8rpx;
}
.amount-value.highlight {
color: #ffffff;
}
.amount-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
}
/* 操作按钮 */
.action-section {
display: flex;
gap: 20rpx;
margin: -20rpx 24rpx 0;
position: relative;
z-index: 1;
}
.action-btn {
flex: 1;
text-align: center;
padding: 24rpx 0;
border-radius: 12rpx;
font-size: 28rpx;
}
.action-btn.primary {
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.action-btn.primary text {
color: #FFB700;
font-weight: 500;
}
.action-btn.secondary {
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.action-btn.secondary text {
color: #333333;
}
/* 提现说明链接 */
.guide-link {
text-align: center;
padding: 24rpx 0;
}
.guide-link text {
font-size: 26rpx;
color: #FFB700;
}
/* 提现记录 */
.record-section {
margin: 0 24rpx;
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx 30rpx;
}
.section-title {
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 16rpx;
}
.section-title text {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
.empty-tip {
text-align: center;
padding: 40rpx 0;
}
.empty-tip text {
font-size: 26rpx;
color: #999999;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.record-item:last-child {
border-bottom: none;
}
.record-left {
display: flex;
flex-direction: column;
}
.record-method {
font-size: 28rpx;
color: #333333;
margin-bottom: 6rpx;
}
.record-time {
font-size: 24rpx;
color: #999999;
}
.record-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.record-amount {
font-size: 28rpx;
color: #e64340;
font-weight: 500;
margin-bottom: 6rpx;
}
.record-status {
font-size: 22rpx;
}
.ws-pending { color: #faad14; }
.ws-processing { color: #FFB700; }
.ws-done { color: #52c41a; }
/* 弹窗通用 */
.modal-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 620rpx;
background-color: #ffffff;
border-radius: 20rpx;
padding: 40rpx;
max-height: 80vh;
}
.modal-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
text-align: center;
display: block;
margin-bottom: 30rpx;
}
/* 提现弹窗 */
.withdraw-available {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
font-size: 28rpx;
color: #666666;
}
.available-amount {
color: #e64340;
font-weight: bold;
font-size: 32rpx;
margin-left: 8rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
font-size: 28rpx;
color: #333333;
display: block;
margin-bottom: 12rpx;
}
.form-input {
height: 72rpx;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.withdraw-tip {
text-align: center;
padding: 16rpx 0;
}
.withdraw-tip text {
font-size: 24rpx;
color: #999;
}
.payment-options {
display: flex;
gap: 20rpx;
}
.payment-option {
flex: 1;
text-align: center;
padding: 16rpx 0;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
}
.payment-option.active {
border-color: #FFB700;
background-color: rgba(255, 183, 0, 0.05);
}
.payment-option text {
font-size: 28rpx;
color: #333333;
}
.payment-option.active text {
color: #FFB700;
}
.upload-area {
width: 200rpx;
height: 200rpx;
border: 2rpx dashed #dddddd;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.qr-preview {
width: 100%;
height: 100%;
}
.upload-placeholder {
font-size: 26rpx;
color: #999999;
}
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
}
.modal-btn::after {
border: none;
}
.modal-btn.confirm {
background-color: #FAD146;
color: #ffffff;
}
.modal-btn.full {
flex: none;
width: 100%;
}
/* 提现说明弹窗 */
.guide-modal {
max-height: 70vh;
}
.guide-content {
max-height: 50vh;
margin-bottom: 20rpx;
font-size: 26rpx;
color: #333333;
line-height: 1.8;
}
</style>