campus-errand/miniapp/pages/mine/earnings.vue
2026-04-01 12:50:17 +08:00

659 lines
14 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>
<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="最低1元支持小数点两位"
/>
</view>
<view class="form-item">
<text class="form-label">收款方式</text>
<view class="payment-options">
<view
class="payment-option"
:class="{ active: withdrawForm.paymentMethod === 'WeChat' }"
@click="withdrawForm.paymentMethod = 'WeChat'"
><text>微信</text></view>
<view
class="payment-option"
:class="{ active: withdrawForm.paymentMethod === 'Alipay' }"
@click="withdrawForm.paymentMethod = 'Alipay'"
><text>支付宝</text></view>
</view>
</view>
<view class="form-item">
<text class="form-label">收款二维码</text>
<view class="upload-area" @click="chooseQrImage">
<image v-if="withdrawForm.qrImage" class="qr-preview" :src="withdrawForm.qrImage" mode="aspectFit"></image>
<text v-else class="upload-placeholder">点击上传</text>
</view>
</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 } 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
}
},
onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadData()
},
methods: {
goBack() { uni.navigateBack() },
/** 加载收益和提现数据 */
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(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 < 1) {
uni.showToast({ title: '提现金额最低1元', 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
}
if (!this.withdrawForm.qrImage) {
uni.showToast({ title: '请上传收款二维码', icon: 'none' })
return
}
this.withdrawSubmitting = true
try {
// 上传二维码图片
let qrCodeImage = this.withdrawForm.qrImageUrl
if (this.withdrawForm.qrImage && !qrCodeImage) {
const uploadRes = await uploadFile(this.withdrawForm.qrImage)
qrCodeImage = uploadRes.url
}
await applyWithdraw({
amount,
paymentMethod: this.withdrawForm.paymentMethod,
qrCodeImage
})
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;
}
.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.cancel {
background-color: #f5f5f5;
color: #666666;
}
.modal-btn.cancel::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>