文件
fenleixinxi/uniapp/pages/member/recharge.vue
MiAn 02fc28613a feat: 深度强化微信小程序端虚拟支付提审防审模式
1. App.vue 启动时立即预加载 uiConfig,杜绝首屏网络延时导致瞬间闪现付费会员入口
2. 会员中心 (member/index.vue):在提审防审模式下直接展示纯净【便民服务指南】卡片,杜绝任何 VIP 皇冠、会员标价、折扣与充值入口
3. 个人中心 (mine/index.vue):提审防审模式下彻底隐藏会员卡片与我的余额,紧凑上移卡片布局
4. 订单明细、充值页与消息中心增加防审合规兼容,避免误触发任何虚拟支付诱导
2026-11-02 17:42:49 +08:00

427 行
11 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="recharge fc-page">
<!-- 规范化顶部导航 -->
<fc-navbar title="余额中心" theme="gradient" :show-back="true" />
<!-- 余额卡(右上角:余额明细入口) -->
<view class="recharge__hero">
<view class="recharge__hero-entry" @click="goFlows">
<text>余额明细</text>
<text class="recharge__hero-entry-arrow">›</text>
</view>
<text class="recharge__hero-label">账户余额(元)</text>
<text class="recharge__hero-num">{{ assets.balance != null ? assets.balance : '0.00' }}</text>
<text class="recharge__hero-tip">充值后可用于付费发布、会员开通与联系方式解锁</text>
</view>
<!-- 充值金额 -->
<view v-if="!isIosAudit" class="fc-card">
<text class="recharge__title">选择充值金额</text>
<view class="recharge__tiers">
<view v-for="t in tiers" :key="t.value" class="recharge__tier"
:class="{ 'recharge__tier--active': Number(amount) === t.value }" @click="pickTier(t.value)">
<text class="recharge__tier-amount">¥{{ t.value }}</text>
<text v-if="t.tag" class="recharge__tier-tag">{{ t.tag }}</text>
</view>
</view>
<view class="recharge__custom">
<text class="recharge__custom-symbol">¥</text>
<input v-model="amount" class="recharge__custom-input" type="digit" placeholder="自定义充值金额(元)" />
</view>
<view class="recharge__btn" @click="submit">
<text>立即充值 ¥{{ payAmount }}</text>
</view>
<text class="recharge__note">微信支付安全付款,支付成功后余额自动到账(通常几秒内)。</text>
</view>
<!-- iOS 设备合规提示卡片 -->
<!-- 便民服务合规展示卡片(彻底杜绝客服导流) -->
<view v-else class="fc-card recharge__audit-card">
<text class="recharge__title">同城便民服务提示</text>
<text class="recharge__audit-text">当前处于同城便民服务体验周期,全站生活信息免费浏览与发布,免充值即可畅享便民体验。</text>
<button class="recharge__btn" @click="goHome">
<text>返回首页</text>
</button>
</view>
<!-- 支付密码(后台开启「余额支付密码」后显示) -->
<view v-if="payPasswordEnabled" class="fc-card">
<view class="recharge__row" @click="openPayPassword">
<view class="recharge__row-left">
<image class="fc-icon fc-icon--sm" :src="iconUrl('lock')" mode="aspectFit" />
<text class="recharge__row-title">支付密码</text>
</view>
<view class="recharge__row-right">
<text class="recharge__row-value">{{ hasPayPassword ? '已设置' : '未设置' }}</text>
<text class="recharge__row-arrow">›</text>
</view>
</view>
<text class="recharge__note">开启后,余额支付时需验证 6 位支付密码。</text>
</view>
<!-- 支付密码弹窗(设置/修改:旧密码 → 新密码 → 确认新密码) -->
<fc-pay-password :show="pwdPopup.show" :title="pwdPopup.title" :tip="pwdPopup.tip"
@close="pwdPopup.show = false" @confirm="onPwdConfirm" />
</view>
</template>
<script>
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import FcPayPassword from '@/components/fc-pay-password/fc-pay-password.vue'
import memberApi from '@/common/api/member.js'
import orderApi from '@/common/api/order.js'
import { iconUrl, loadUiConfig, uiState } from '@/common/store/ui.js'
import { pay } from '@/common/utils/pay.js'
export default {
components: { FcNavbar, FcPayPassword },
data() {
return {
assets: {},
tiers: [
{ value: 50, tag: '' },
{ value: 100, tag: '推荐' },
{ value: 200, tag: '' },
{ value: 500, tag: '' },
{ value: 1000, tag: '' },
],
amount: '',
submitting: false,
// 支付密码
hasPayPassword: false,
pwdPopup: { show: false, title: '', tip: '' },
pwdStep: '', // old / new / confirm
pwdOld: '', // 修改密码时暂存旧密码
pwdTmp: '', // 新密码暂存(两次输入一致性校验)
}
},
computed: {
/** 是否触发 iOS 虚拟支付合规机制 */
/**
* 是否处于提审防审模式(各端统一,由后台配置开关控制)
* @returns {boolean}
*/
isIosAudit() {
// #ifdef MP-WEIXIN
return uiState.virtualPayAudit !== false
// #endif
return false
},
payPasswordEnabled() {
return !!uiState.payPasswordEnabled
},
/** 待充值金额(展示用,保留两位小数) */
payAmount() {
const n = Number(this.amount)
return !n || n <= 0 ? '0.00' : n.toFixed(2)
},
},
onLoad() {
loadUiConfig()
this.loadAssets()
},
/** 从收银台返回时重新拉余额:微信回调入账后这里能刷新到最新值 */
onShow() {
this.loadAssets()
},
methods: {
goHome() {
uni.switchTab({
url: '/pages/index/index',
fail: () => {
uni.reLaunch({ url: '/pages/index/index' })
}
})
},
iconUrl,
async loadAssets() {
try {
this.assets = await memberApi.assets()
this.hasPayPassword = !!(this.assets && this.assets.hasPayPassword)
} catch (e) {
this.assets = { balance: 0 }
}
},
pickTier(v) {
this.amount = String(v)
},
/** 跳转余额明细页 */
goFlows() {
uni.navigateTo({ url: '/pages/member/flows' })
},
async submit() {
if (this.isIosAudit) {
uni.showModal({
title: '提示',
content: '根据相关规范,暂不支持线上直接充值服务。',
showCancel: false,
confirmText: '我知道了',
})
return
}
if (this.submitting) return
const num = Number(this.amount)
if (!num || num <= 0) {
uni.showToast({ title: '请输入正确的充值金额', icon: 'none' })
return
}
if (num > 100000) {
uni.showToast({ title: '单次充值不能超过 100000 元', icon: 'none' })
return
}
this.submitting = true
try {
const res = await orderApi.recharge({ amount: num, payType: 'wxpay' })
// 微信支付:下单只落 pending,付款后由 /pay/notify 回调入账(余额不会立刻变化)
await pay({ orderNo: res.orderNo, payType: 'wxpay' })
await this.loadAssets()
this.amount = ''
uni.showToast({ title: '支付成功,到账略有延迟', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message || '充值失败', icon: 'none' })
} finally {
this.submitting = false
}
},
/* ==================== 支付密码(设置/修改) ==================== */
openPayPassword() {
if (this.hasPayPassword) {
// 修改:先验证旧密码
this.pwdStep = 'old'
this.pwdOld = ''
this.pwdTmp = ''
this.pwdPopup = { show: true, title: '请输入原支付密码', tip: '验证通过后设置新密码' }
} else {
this.pwdStep = 'new'
this.pwdOld = ''
this.pwdTmp = ''
this.pwdPopup = { show: true, title: '设置支付密码', tip: '6 位数字,用于余额支付验证' }
}
},
async onPwdConfirm(password) {
this.pwdPopup.show = false
try {
if (this.pwdStep === 'old') {
// 旧密码验证通过 → 输入新密码
this.pwdOld = password
this.pwdStep = 'new'
this.pwdPopup = { show: true, title: '设置新支付密码', tip: '6 位数字,不能与原密码相同' }
return
}
if (this.pwdStep === 'new') {
this.pwdTmp = password
this.pwdStep = 'confirm'
this.pwdPopup = { show: true, title: '确认新支付密码', tip: '请再次输入相同密码' }
return
}
if (this.pwdStep === 'confirm') {
if (password !== this.pwdTmp) {
uni.showToast({ title: '两次输入不一致,请重试', icon: 'none' })
return
}
await memberApi.setPayPassword({
password,
oldPassword: this.hasPayPassword ? this.pwdOld : undefined,
})
this.hasPayPassword = true
uni.showToast({ title: '支付密码已保存', icon: 'success' })
}
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
},
},
}
</script>
<style lang="scss" scoped>
.recharge {
padding-bottom: 60rpx;
}
.recharge__hero {
position: relative;
background: $fc-gradient-hero;
padding: 48rpx 40rpx 60rpx;
border-radius: 0 0 40rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* 右上角:余额明细入口(半透明白胶囊,微信零钱同款位置) */
.recharge__hero-entry {
position: absolute;
top: 24rpx;
right: 28rpx;
display: flex;
align-items: center;
padding: 10rpx 22rpx;
border-radius: $fc-radius-pill;
background: rgba(255, 255, 255, 0.85);
font-size: $fc-font-xs;
color: $fc-text-secondary;
&:active {
opacity: 0.7;
}
}
.recharge__hero-entry-arrow {
margin-left: 4rpx;
font-size: 28rpx;
line-height: 1;
color: $fc-text-placeholder;
}
.recharge__hero-label {
color: $fc-text-secondary;
font-weight: 500;
font-size: $fc-font-sm;
}
.recharge__hero-num {
margin-top: 16rpx;
color: $fc-text;
letter-spacing: -1rpx;
font-size: 72rpx;
font-weight: 700;
}
.recharge__hero-tip {
margin-top: 16rpx;
color: $fc-text-placeholder;
font-size: $fc-font-xs;
}
.recharge__title {
font-size: $fc-font-lg;
font-weight: 700;
color: $fc-text;
}
.recharge__tiers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
margin-top: 24rpx;
}
.recharge__tier {
position: relative;
padding: 28rpx 0;
border: 2rpx solid $fc-border;
border-radius: $fc-radius;
background: $fc-bg;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
&--active {
border-color: $fc-primary-text;
background: $fc-primary-lighter;
box-shadow: 0 4rpx 14rpx rgba(4, 135, 74, 0.15);
}
}
.recharge__tier-amount {
font-size: $fc-font-lg;
font-weight: 700;
color: $fc-text;
}
.recharge__tier-tag {
position: absolute;
top: -14rpx;
right: 10rpx;
padding: 2rpx 12rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-warm;
color: #ffffff;
font-size: 18rpx;
}
.recharge__custom {
margin-top: 8rpx;
height: 96rpx;
border-radius: $fc-radius;
background: $fc-bg;
display: flex;
align-items: center;
padding: 0 28rpx;
}
.recharge__custom-symbol {
font-size: 40rpx;
font-weight: 700;
color: $fc-price;
}
.recharge__custom-input {
flex: 1;
margin-left: 12rpx;
font-size: $fc-font-lg;
color: $fc-text;
}
.recharge__btn {
margin-top: 28rpx;
height: 90rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 32rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 28rpx rgba(4, 48, 30, 0.3);
}
.recharge__note {
display: block;
margin-top: 20rpx;
font-size: $fc-font-xs;
color: $fc-text-placeholder;
line-height: $fc-leading;
}
/* 支付密码行 */
.recharge__row {
display: flex;
align-items: center;
justify-content: space-between;
}
.recharge__row-left {
display: flex;
align-items: center;
}
.recharge__row-title {
margin-left: 10rpx;
font-size: $fc-font-base;
color: $fc-text;
font-weight: 500;
}
.recharge__row-right {
display: flex;
align-items: center;
}
.recharge__row-value {
font-size: $fc-font-sm;
color: $fc-text-secondary;
}
.recharge__row-arrow {
margin-left: 8rpx;
font-size: 36rpx;
color: $fc-text-placeholder;
line-height: 1;
}
.recharge__audit-card {
padding: 40rpx 30rpx;
margin-top: 24rpx;
}
.recharge__audit-text {
display: block;
font-size: 26rpx;
color: #64748b;
line-height: 1.6;
margin: 20rpx 0 30rpx;
}
.recharge__btn--contact {
background: #00d668;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
/* 行标题(支付密码行) */
</style>