1. App.vue 启动时立即预加载 uiConfig,杜绝首屏网络延时导致瞬间闪现付费会员入口 2. 会员中心 (member/index.vue):在提审防审模式下直接展示纯净【便民服务指南】卡片,杜绝任何 VIP 皇冠、会员标价、折扣与充值入口 3. 个人中心 (mine/index.vue):提审防审模式下彻底隐藏会员卡片与我的余额,紧凑上移卡片布局 4. 订单明细、充值页与消息中心增加防审合规兼容,避免误触发任何虚拟支付诱导
427 行
11 KiB
Vue
427 行
11 KiB
Vue
<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>
|