fix: 支付/钱包
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 44s

This commit is contained in:
sexygoat
2026-04-14 12:12:57 +08:00
parent 1db7ea9b7c
commit 5e8ff94b7a
2 changed files with 306 additions and 5 deletions

View File

@@ -53,7 +53,7 @@
{{ alreadyBindPhone ? '已绑定' : '绑定手机号' }}
</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'change-phone')" role="button" tabindex="0">
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'change-phone')" role="button" tabindex="0">
<view class="function-icon">
<image src="/static/change-phone.png" mode="aspectFit" alt="更换手机号"></image>
</view>
@@ -65,7 +65,7 @@
</view>
<view class="function-name">设备换货</view>
</view>
<view class="function-item interactive card-interactive" v-if="isDevice" @tap="$emit('enter', 'wallet')" role="button" tabindex="0">
<view class="function-item interactive card-interactive" @tap="$emit('enter', 'wallet')" role="button" tabindex="0">
<view class="function-icon">
<image src="/static/wallet.png" mode="aspectFit" alt="我的钱包"></image>
</view>

View File

@@ -2,7 +2,10 @@
<view class="container">
<view class="wallet-card">
<view class="balance-section">
<view class="balance-label">账户余额()</view>
<view class="balance-header">
<view class="balance-label">账户余额()</view>
<button class="btn-recharge" @tap="showRechargeModal = true">充值</button>
</view>
<view class="balance-amount">{{ formatMoney(walletDetail.balance) }}</view>
</view>
<view class="wallet-footer">
@@ -13,11 +16,55 @@
<view class="footer-divider"></view>
<view class="footer-item">
<view class="footer-label">更新时间</view>
<view class="footer-value">{{ walletDetail.updated_at }}</view>
<view class="footer-value">{{ formatDateTime(walletDetail.updated_at) }}</view>
</view>
</view>
</view>
<!-- 充值弹窗 -->
<up-popup :show="showRechargeModal" mode="center" @close="showRechargeModal=false">
<view class="recharge-popup">
<view class="popup-header">
<view class="popup-title">账户充值</view>
<view class="popup-close" @tap="showRechargeModal=false">×</view>
</view>
<view class="recharge-amounts">
<view
class="amount-item"
v-for="item in rechargeAmounts"
:key="item.value"
:class="{ active: selectedAmount === item.value }"
@tap="selectAmount(item.value)"
>
<text class="amount-value">¥{{ item.label }}</text>
</view>
<view
class="amount-item custom"
:class="{ active: isCustomAmount }"
@tap="selectCustomAmount"
>
<text class="amount-value">自定义</text>
</view>
</view>
<view class="custom-input" v-if="isCustomAmount">
<view class="input-label">请输入充值金额</view>
<up-input
v-model="customAmount"
type="number"
placeholder="最低0.01元"
border="surround"
/>
</view>
<view class="popup-footer">
<button class="btn-apple btn-secondary" @tap="showRechargeModal=false">取消</button>
<button class="btn-apple btn-primary" @tap="confirmRecharge">确认充值</button>
</view>
</view>
</up-popup>
<view class="tab-section">
<view class="tab-header">
<view class="tab-indicator" :style="{ left: currentTab === 0 ? '8rpx' : '50%' }"></view>
@@ -142,6 +189,17 @@
return (amount / 100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const formatDateTime = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
};
let walletDetail = reactive({
balance: 0,
frozen_balance: 0,
@@ -160,6 +218,129 @@
let transactionPage = ref(1);
const transactionPageSize = 5;
let showRechargeModal = ref(false);
let selectedAmount = ref(null);
let isCustomAmount = ref(false);
let customAmount = ref('');
const rechargeAmounts = [
{ value: 1000, label: '10' },
{ value: 2000, label: '20' },
{ value: 5000, label: '50' },
{ value: 10000, label: '100' },
{ value: 20000, label: '200' },
{ value: 50000, label: '500' }
];
const selectAmount = (value) => {
selectedAmount.value = value;
isCustomAmount.value = false;
customAmount.value = '';
};
const selectCustomAmount = () => {
isCustomAmount.value = true;
selectedAmount.value = null;
};
const confirmRecharge = async () => {
let amount = 0;
if (isCustomAmount.value) {
// 自定义金额
const customAmountNum = parseFloat(customAmount.value);
if (!customAmountNum || customAmountNum < 0.01) {
uni.showToast({ title: '请输入正确的充值金额', icon: 'none' });
return;
}
amount = Math.round(customAmountNum * 100); // 转换为分
} else if (selectedAmount.value) {
// 预设金额
amount = selectedAmount.value;
} else {
uni.showToast({ title: '请选择充值金额', icon: 'none' });
return;
}
uni.showLoading({ title: '处理中...', mask: true });
try {
// 第一步:充值前校验
const checkData = await walletApi.rechargeCheck(userStore.state.identifier);
// 如果需要强制充值且金额小于强制充值金额
if (checkData.need_force_recharge && amount < checkData.force_recharge_amount) {
uni.hideLoading();
uni.showModal({
title: '提示',
content: `${checkData.message || '当前需充值最低金额'} ¥${(checkData.force_recharge_amount / 100).toFixed(2)}`,
showCancel: false
});
return;
}
// 检查金额范围
if (amount < checkData.min_amount || amount > checkData.max_amount) {
uni.hideLoading();
uni.showToast({
title: `充值金额范围:¥${(checkData.min_amount / 100).toFixed(2)} - ¥${(checkData.max_amount / 100).toFixed(2)}`,
icon: 'none',
duration: 2500
});
return;
}
// 第二步:创建充值订单
const rechargeData = await walletApi.recharge(
userStore.state.identifier,
amount,
'wechat'
);
uni.hideLoading();
showRechargeModal.value = false;
// 验证支付配置
if (!rechargeData.pay_config || !rechargeData.pay_config.package) {
uni.showToast({ title: '支付参数获取失败', icon: 'none' });
return;
}
// 第三步:调起微信支付
uni.requestPayment({
timeStamp: rechargeData.pay_config.timestamp,
nonceStr: rechargeData.pay_config.nonce_str,
package: rechargeData.pay_config.package,
signType: rechargeData.pay_config.sign_type,
paySign: rechargeData.pay_config.pay_sign,
success: (res) => {
console.log('支付成功', res);
uni.showToast({ title: '充值成功', icon: 'success' });
setTimeout(() => {
// 刷新钱包余额和充值记录
loadWalletDetail();
rechargePage.value = 1;
rechargeNoMore.value = false;
loadRechargeList();
}, 1500);
},
fail: (err) => {
console.error('支付失败', err);
if (err.errMsg && (err.errMsg.includes('cancel') || err.errMsg.includes('用户取消'))) {
uni.showToast({ title: '已取消支付', icon: 'none' });
} else {
uni.showToast({ title: '支付失败', icon: 'none' });
}
}
});
} catch (e) {
uni.hideLoading();
console.error('充值失败', e);
const errorMsg = e.msg || e.message || '充值失败,请稍后重试';
uni.showToast({ title: errorMsg, icon: 'none' });
}
};
const getRechargeStatusClass = (status) => {
const classMap = {
'0': 'tag-warning',
@@ -284,7 +465,23 @@
.balance-section {
margin-bottom: 28rpx;
.balance-label { font-size: 24rpx; opacity: 0.75; margin-bottom: 8rpx; }
.balance-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.balance-label { font-size: 24rpx; opacity: 0.75; }
.btn-recharge {
background: rgba(255, 255, 255, 0.25);
border: 1rpx solid rgba(255, 255, 255, 0.5);
color: #fff;
font-size: 24rpx;
padding: 8rpx 24rpx;
border-radius: 40rpx;
font-weight: 500;
&::after { border: none; }
}
}
.balance-amount { font-size: 72rpx; font-weight: 700; letter-spacing: -2rpx; }
}
@@ -394,4 +591,108 @@
color: var(--text-tertiary);
font-size: 24rpx;
}
.recharge-popup {
width: 600rpx;
background: var(--bg-primary);
border-radius: var(--radius-large);
overflow: hidden;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid var(--border-light);
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: var(--text-tertiary);
cursor: pointer;
}
}
.recharge-amounts {
padding: 30rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
.amount-item {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid var(--border-light);
border-radius: var(--radius-medium);
cursor: pointer;
transition: all 0.3s;
&.active {
border-color: var(--primary);
background: rgba(0, 122, 255, 0.05);
.amount-value {
color: var(--primary);
font-weight: 600;
}
}
.amount-value {
font-size: 28rpx;
color: var(--text-primary);
}
}
}
.custom-input {
padding: 0 30rpx 30rpx;
.input-label {
font-size: 26rpx;
color: var(--text-secondary);
margin-bottom: 16rpx;
}
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1rpx solid var(--border-light);
.btn-apple {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-medium);
font-size: 28rpx;
font-weight: 500;
border: none;
&.btn-secondary {
background: var(--gray-200);
color: var(--text-primary);
}
&.btn-primary {
background: var(--primary);
color: #fff;
}
}
}
}
</style>