campus-errand/miniapp/pages/mine/earnings.vue
2026-04-17 22:54:59 +08:00

756 lines
16 KiB
Vue
Raw Permalink 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>
<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 v-if="pendingConfirms.length > 0" class="confirm-section">
<view class="section-title"><text>待确认收款</text></view>
<view v-for="item in pendingConfirms" :key="item.id" class="confirm-item">
<view class="confirm-left">
<text class="confirm-amount">¥{{ item.amount }}</text>
<text class="confirm-tip">管理员已审批,请确认收款</text>
</view>
<button class="confirm-btn" @click="confirmReceive(item)" :loading="item._loading">确认收款</button>
</view>
</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>
<text v-if="item.status === 'Rejected' && item.rejectReason" class="reject-reason">{{ item.rejectReason }}</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, getPendingConfirmWithdrawals } 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: [],
pendingConfirms: [], // 待确认收款的提现记录
// 提现弹窗
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, pendingRes] = await Promise.all([
getEarnings(),
getWithdrawals(),
getPendingConfirmWithdrawals()
])
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 || []
this.pendingConfirms = (pendingRes || []).map(item => ({ ...item, _loading: false }))
} 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: '已完成', WaitConfirm: '待确认收款', Rejected: '已拒绝' }
return map[status] || status
},
getWithdrawStatusClass(status) {
const map = { Pending: 'ws-pending', Processing: 'ws-processing', Completed: 'ws-done', WaitConfirm: 'ws-confirm', Rejected: 'ws-rejected' }
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' })
}
},
/** 确认收款(调用微信 requestMerchantTransfer */
confirmReceive(item) {
if (!item.packageInfo) {
uni.showToast({ title: '收款信息异常,请稍后重试', icon: 'none' })
return
}
item._loading = true
this.confirmingReceive = true
// #ifdef MP-WEIXIN
wx.requestMerchantTransfer({
mchId: '1744231030',
appId: 'wxd62aec23fcb79bc6',
package: item.packageInfo,
success: (res) => {
console.log('[确认收款] 成功', res)
// 立即从列表移除
this.pendingConfirms = this.pendingConfirms.filter(i => i.id !== item.id)
// 显示加载提示延迟2秒等待回调再刷新数据后跳转
uni.showLoading({ title: '收款处理中...' })
setTimeout(async () => {
await this.loadData()
uni.hideLoading()
this.confirmingReceive = false
uni.showToast({ title: '收款成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/order-hall/order-hall' })
}, 1000)
}, 2000)
},
fail: (res) => {
console.error('[确认收款] 失败', res)
this.confirmingReceive = false
uni.showToast({ title: '收款取消或失败', icon: 'none' })
},
complete: () => {
item._loading = false
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: '请在微信小程序中操作', icon: 'none' })
item._loading = false
// #endif
}
}
}
</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; }
.ws-confirm { color: #e64340; }
.ws-rejected { color: #999; }
.reject-reason {
font-size: 22rpx;
color: #e64340;
margin-top: 4rpx;
}
/* 待确认收款区域 */
.confirm-section {
margin: 0 24rpx 20rpx;
background-color: #fff8e1;
border-radius: 16rpx;
padding: 24rpx 30rpx;
border: 1rpx solid #ffe082;
}
.confirm-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.confirm-left {
display: flex;
flex-direction: column;
}
.confirm-amount {
font-size: 34rpx;
color: #e64340;
font-weight: bold;
margin-bottom: 6rpx;
}
.confirm-tip {
font-size: 24rpx;
color: #999;
}
.confirm-btn {
background-color: #FFB700;
color: #fff;
font-size: 26rpx;
padding: 12rpx 30rpx;
border-radius: 30rpx;
border: none;
line-height: 1.5;
}
.confirm-btn::after {
border: none;
}
/* 弹窗通用 */
.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>