本次提交主要实现微信小程序虚拟支付提审防审机制,包括: 1. 后台配置虚拟支付防审开关,支持开启/关闭模式 2. 前端根据开关状态及设备类型,iOS设备自动触发合规提示 3. 分销模块调整提现规则,无门槛即时提现 4. 登录页面优化,增加返回和暂不登录入口,符合微信审核规范 5. 新增提现记录页面,优化分销中心界面布局 该功能确保微信提审期间能顺利通过审核,审核通过后可一键切换为正常运营模式,无需重新发布小程序代码。
680 行
16 KiB
Vue
680 行
16 KiB
Vue
<template>
|
||
<view class="login fc-page">
|
||
<!-- 0. 顶部极简返回胶囊(自适应状态栏与胶囊避让,符合微信登录审核规范) -->
|
||
<view
|
||
class="login__nav-back"
|
||
:style="{ top: navTop + 'px', height: navBarHeight + 'px' }"
|
||
@click="handleCancelOrBack"
|
||
>
|
||
<view class="login__nav-back-inner">
|
||
<view class="login__back-arrow" />
|
||
<text class="login__nav-back-text">返回</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 1. 顶部沉浸天幕插画(底部自然羽化融入纯净背景) -->
|
||
<view class="login__hero-wrap">
|
||
<image
|
||
class="login__hero-img"
|
||
src="/static/banners/login-hero-planet.svg"
|
||
mode="aspectFill"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 2. 主体流式交互区:左对齐大标题 -> 核心操作按钮 -> 底部协议行 -->
|
||
<view class="login__content">
|
||
<!-- 大标题区 -->
|
||
<view class="login__header">
|
||
<text class="login__title">欢迎登录</text>
|
||
<text class="login__subtitle">{{ siteName }} · 本地生活便民服务</text>
|
||
</view>
|
||
|
||
<!-- 核心操作区 -->
|
||
<view class="login__action">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 微信小程序端:手机号快捷登录(按钮与权限一致,符合微信审核规范) -->
|
||
<button
|
||
class="login__pill-btn"
|
||
:class="{ 'login__pill-btn--disabled': loading }"
|
||
open-type="getPhoneNumber"
|
||
@getphonenumber="onGetPhoneNumber"
|
||
>
|
||
<text class="login__pill-btn-text">{{ loading ? '正在安全登录…' : '手机号快捷登录' }}</text>
|
||
</button>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<!-- 非微信环境:手机号 + 密码表单 -->
|
||
<view class="login__h5-form">
|
||
<view class="login__input-wrap">
|
||
<image class="login__input-icon" :src="iconUrl('phone')" mode="aspectFit" />
|
||
<input
|
||
class="login__input"
|
||
type="number"
|
||
v-model="h5Form.phone"
|
||
maxlength="11"
|
||
placeholder="请输入11位手机号"
|
||
placeholder-class="login__placeholder"
|
||
/>
|
||
</view>
|
||
<view class="login__input-wrap">
|
||
<image class="login__input-icon" :src="iconUrl('lock')" mode="aspectFit" />
|
||
<input
|
||
class="login__input"
|
||
type="password"
|
||
v-model="h5Form.password"
|
||
maxlength="32"
|
||
placeholder="请输入登录密码(默认 123456)"
|
||
placeholder-class="login__placeholder"
|
||
/>
|
||
</view>
|
||
<button
|
||
class="login__pill-btn"
|
||
:class="{ 'login__pill-btn--disabled': loading }"
|
||
@click="onPasswordLoginClick"
|
||
>
|
||
<text class="login__pill-btn-text">{{ loading ? '正在登录…' : '登 录' }}</text>
|
||
</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- 暂不登录辅助入口(极简呼吸感,符合微信审核规范:不得强制阻断) -->
|
||
<view class="login__skip-wrap" @click.stop="handleSkipLogin">
|
||
<text class="login__skip-text">暂不登录,先去逛逛 ›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 3. 协议主动勾选行(支持未勾选抖动微动画提示,杜绝生硬弹窗) -->
|
||
<view
|
||
class="login__agreement-row"
|
||
:class="{ 'login__agreement-row--shake': shakeAgreement }"
|
||
>
|
||
<view
|
||
class="login__checkbox"
|
||
:class="{ 'login__checkbox--active': agreed }"
|
||
@click="toggleAgree"
|
||
>
|
||
<text v-if="agreed" class="login__check-tick">✓</text>
|
||
</view>
|
||
<view class="login__agreement-text">
|
||
<text class="login__text-plain" @click="toggleAgree">已阅读并同意</text>
|
||
<text class="login__text-link" @click.stop="goAgreement('service')">《服务条款》</text>
|
||
<text class="login__text-plain" @click="toggleAgree">和</text>
|
||
<text class="login__text-link" @click.stop="goAgreement('privacy')">《隐私权政策》</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 登录页面
|
||
* 微信小程序合规与交互规范:
|
||
* 1. 按钮明确展示“手机号快捷登录”,调用 open-type="getPhoneNumber" 授权获取手机号;
|
||
* 2. 授权后通过后端微信官方 API 实时换取手机号并建档登录;
|
||
* 3. 遵照微信审核规范提供显著的“返回”和“暂不登录,先去逛逛”通道;
|
||
* 4. 优化未勾选协议时的交互:采用自然抖动微动效与 Toast 提示,取代生硬阻断的弹窗。
|
||
*/
|
||
import { useUserStore } from '@/common/store/index.js'
|
||
import authApi from '@/common/api/auth.js'
|
||
import { iconUrl, logoUrl, loadUiConfig, uiState } from '@/common/store/ui.js'
|
||
import { getNavMetrics } from '@/common/utils/navbar.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
agreed: false, // 协议勾选状态
|
||
shakeAgreement: false, // 未勾选时的微摇晃动画
|
||
redirectUrl: '', // 登录成功后回跳地址
|
||
navTop: 24, // 导航栏顶部安全距离(根据设备动态计算)
|
||
navBarHeight: 44, // 导航栏标准高度
|
||
h5Form: {
|
||
phone: '',
|
||
password: '',
|
||
},
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 站点品牌名称
|
||
* @returns {string}
|
||
*/
|
||
siteName() {
|
||
return uiState.siteName || '同城分类信息'
|
||
},
|
||
},
|
||
/**
|
||
* 页面加载
|
||
* @param {Object} query 路由参数(支持携带 redirect 回跳地址)
|
||
*/
|
||
onLoad(query) {
|
||
const metrics = getNavMetrics()
|
||
if (metrics) {
|
||
this.navTop = metrics.statusBarHeight || 24
|
||
this.navBarHeight = metrics.navBarHeight || 44
|
||
}
|
||
if (query && query.redirect) {
|
||
try {
|
||
this.redirectUrl = decodeURIComponent(query.redirect)
|
||
} catch (e) {
|
||
this.redirectUrl = query.redirect
|
||
}
|
||
}
|
||
loadUiConfig()
|
||
},
|
||
methods: {
|
||
iconUrl,
|
||
logoUrl,
|
||
/**
|
||
* 取消登录或返回上一页(若无页面栈历史则安全重定向至首页)
|
||
*/
|
||
handleCancelOrBack() {
|
||
const pages = getCurrentPages()
|
||
if (pages && pages.length > 1) {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
},
|
||
})
|
||
} else {
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
}
|
||
},
|
||
/**
|
||
* 暂不登录,返回来源页或首页以游客身份浏览
|
||
*/
|
||
handleSkipLogin() {
|
||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
|
||
if (pages && pages.length > 1) {
|
||
const prevPage = pages[pages.length - 2]
|
||
const prevRoute = prevPage && (prevPage.route || (prevPage.$page && prevPage.$page.route) || '')
|
||
if (prevRoute && !prevRoute.includes('pages/auth/login')) {
|
||
uni.navigateBack({
|
||
fail: () => {
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
},
|
||
})
|
||
return
|
||
}
|
||
}
|
||
uni.reLaunch({
|
||
url: '/pages/index/index',
|
||
fail: () => {
|
||
uni.redirectTo({ url: '/pages/index/index' })
|
||
},
|
||
})
|
||
},
|
||
/**
|
||
* 切换协议勾选
|
||
*/
|
||
toggleAgree() {
|
||
this.agreed = !this.agreed
|
||
if (this.agreed) {
|
||
this.shakeAgreement = false
|
||
}
|
||
},
|
||
/**
|
||
* 跳转协议独立页面
|
||
* @param {'service'|'privacy'} type 协议类型
|
||
*/
|
||
goAgreement(type) {
|
||
uni.navigateTo({
|
||
url: `/pages/auth/agreement?type=${type}`,
|
||
})
|
||
},
|
||
/**
|
||
* 检查协议勾选状态(未勾选时触发平滑震动提示)
|
||
* @returns {boolean} 是否已勾选协议
|
||
*/
|
||
checkAgreement() {
|
||
if (this.agreed) {
|
||
return true
|
||
}
|
||
this.shakeAgreement = false
|
||
this.$nextTick(() => {
|
||
this.shakeAgreement = true
|
||
setTimeout(() => {
|
||
this.shakeAgreement = false
|
||
}, 600)
|
||
})
|
||
uni.showToast({
|
||
title: '请先阅读并勾选协议',
|
||
icon: 'none',
|
||
duration: 2000,
|
||
})
|
||
return false
|
||
},
|
||
/**
|
||
* 获取微信登录临时凭证 code
|
||
* @returns {Promise<string>}
|
||
*/
|
||
getWxCode() {
|
||
return new Promise((resolve, reject) => {
|
||
const fail = () => reject(new Error('微信授权失败,请重试'))
|
||
// #ifdef MP-WEIXIN
|
||
try {
|
||
if (typeof uni !== 'undefined' && uni.login) {
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (r) => (r && r.code ? resolve(r.code) : fail()),
|
||
fail,
|
||
})
|
||
return
|
||
}
|
||
} catch (e) {}
|
||
// #endif
|
||
fail()
|
||
})
|
||
},
|
||
/**
|
||
* 微信手机号授权回调处理
|
||
* @param {Object} e 微信原生组件回调
|
||
*/
|
||
async onGetPhoneNumber(e) {
|
||
if (this.loading) return
|
||
|
||
// 优先检查协议勾选
|
||
if (!this.checkAgreement()) {
|
||
return
|
||
}
|
||
|
||
const phoneCode = (e && e.detail && e.detail.code) || ''
|
||
if (!phoneCode) {
|
||
uni.showToast({ title: '已取消授权手机号', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
await this.executeWxLogin(phoneCode)
|
||
},
|
||
/**
|
||
* 执行快捷登录(凭证换取账号及手机号)
|
||
* @param {string} phoneCode 微信手机号凭证
|
||
*/
|
||
async executeWxLogin(phoneCode) {
|
||
this.loading = true
|
||
const store = useUserStore()
|
||
try {
|
||
const wxCode = await this.getWxCode()
|
||
await store.login({
|
||
code: wxCode,
|
||
phoneCode,
|
||
})
|
||
try {
|
||
await store.fetchProfile()
|
||
} catch (e) {}
|
||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||
|
||
setTimeout(() => {
|
||
this.redirectAfterLogin()
|
||
}, 400)
|
||
} catch (err) {
|
||
uni.showToast({ title: (err && err.message) || '登录失败,请重试', icon: 'none' })
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
/**
|
||
* 手机号+密码登录提交处理(供非小程序端调试使用)
|
||
*/
|
||
async onPasswordLoginClick() {
|
||
if (this.loading) return
|
||
const phone = String(this.h5Form.phone || '').trim()
|
||
const password = String(this.h5Form.password || '')
|
||
|
||
if (!phone) {
|
||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||
return
|
||
}
|
||
if (!/^1\d{10}$/.test(phone)) {
|
||
uni.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||
return
|
||
}
|
||
if (!password) {
|
||
uni.showToast({ title: '请输入登录密码', icon: 'none' })
|
||
return
|
||
}
|
||
if (password.length < 6) {
|
||
uni.showToast({ title: '密码长度不能少于6位', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
if (!this.checkAgreement()) return
|
||
|
||
this.loading = true
|
||
try {
|
||
const res = await authApi.passwordLogin({ phone, password })
|
||
const store = useUserStore()
|
||
store.state.token = res.token
|
||
store.state.userInfo = res.userInfo
|
||
store.state.userType = Number(res.userInfo.userType || 1)
|
||
uni.setStorageSync('fc_token', res.token)
|
||
uni.setStorageSync('fc_user_info', res.userInfo)
|
||
|
||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||
setTimeout(() => {
|
||
this.redirectAfterLogin()
|
||
}, 400)
|
||
} catch (err) {
|
||
uni.showToast({ title: (err && err.message) || '登录失败', icon: 'none' })
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
/**
|
||
* 登录成功后智能跳转
|
||
*/
|
||
redirectAfterLogin() {
|
||
if (this.redirectUrl && !this.redirectUrl.includes('pages/auth/login')) {
|
||
uni.redirectTo({
|
||
url: this.redirectUrl,
|
||
fail: () => {
|
||
uni.reLaunch({ url: this.redirectUrl })
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
|
||
if (pages && pages.length > 1) {
|
||
const prevPage = pages[pages.length - 2]
|
||
const prevRoute = prevPage && (prevPage.route || (prevPage.$page && prevPage.$page.route) || '')
|
||
if (prevRoute && !prevRoute.includes('pages/auth/login')) {
|
||
uni.navigateBack()
|
||
return
|
||
}
|
||
}
|
||
|
||
uni.reLaunch({ url: '/pages/mine/index' })
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 页面根容器:纯净底色,通透轻盈 */
|
||
.login {
|
||
min-height: 100vh;
|
||
position: relative;
|
||
background: #ffffff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 0. 顶部极简返回胶囊按钮 */
|
||
.login__nav-back {
|
||
position: fixed;
|
||
left: 24rpx;
|
||
z-index: 100;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 16rpx 0 8rpx;
|
||
}
|
||
|
||
.login__nav-back-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 64rpx;
|
||
padding: 0 20rpx 0 14rpx;
|
||
border-radius: 32rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
border: 1rpx solid rgba(220, 235, 226, 0.9);
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.login__back-arrow {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-left: 3rpx solid #334155;
|
||
border-bottom: 3rpx solid #334155;
|
||
transform: rotate(45deg);
|
||
margin-right: 10rpx;
|
||
margin-left: 4rpx;
|
||
}
|
||
|
||
.login__nav-back-text {
|
||
font-size: 26rpx;
|
||
color: #334155;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 1. 顶部沉浸天幕背景:羽化融入底部纯白 */
|
||
.login__hero-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 540rpx;
|
||
overflow: hidden;
|
||
background: #ffffff;
|
||
mask-image: linear-gradient(to bottom, #000000 68%, transparent 100%);
|
||
-webkit-mask-image: linear-gradient(to bottom, #000000 68%, transparent 100%);
|
||
}
|
||
|
||
.login__hero-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
/* 2. 主体交互内容区 */
|
||
.login__content {
|
||
position: relative;
|
||
z-index: 2;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10rpx 56rpx calc(60rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 大标题区(左对齐,现代高级字色搭配微光) */
|
||
.login__header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
margin-bottom: 56rpx;
|
||
}
|
||
|
||
.login__title {
|
||
font-size: 56rpx;
|
||
font-weight: 800;
|
||
letter-spacing: 2rpx;
|
||
line-height: 1.2;
|
||
color: #1e293b; /* 规范现代高级黑灰 */
|
||
}
|
||
|
||
.login__subtitle {
|
||
margin-top: 14rpx;
|
||
font-size: 26rpx;
|
||
color: #64748b; /* 规范中性辅助灰,杜绝暗墨绿 */
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
/* 核心操作区 */
|
||
.login__action {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 饱满流光大药丸主按钮 */
|
||
.login__pill-btn {
|
||
width: 100%;
|
||
height: 104rpx;
|
||
border-radius: 52rpx;
|
||
background: linear-gradient(90deg, #00e070 0%, #00d668 50%, #00d26a 100%);
|
||
box-shadow: 0 14rpx 36rpx rgba(0, 224, 112, 0.32);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
box-sizing: border-box;
|
||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.985);
|
||
opacity: 0.92;
|
||
}
|
||
}
|
||
|
||
.login__pill-btn--disabled {
|
||
opacity: 0.55;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.login__pill-btn-text {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #04301e;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
/* 暂不登录辅助入口(极简轻盈,扩大点击判定热区) */
|
||
.login__skip-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 28rpx;
|
||
padding: 24rpx 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.login__skip-text {
|
||
font-size: 28rpx;
|
||
color: #64748b;
|
||
font-weight: 500;
|
||
letter-spacing: 1rpx;
|
||
transition: color 0.15s ease;
|
||
|
||
&:active {
|
||
color: #00d26a;
|
||
}
|
||
}
|
||
|
||
/* H5 调试模式表单 */
|
||
.login__h5-form {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.login__input-wrap {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
background: #f7faf8;
|
||
border-radius: 24rpx;
|
||
border: 1rpx solid #e2eee6;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
margin-bottom: 24rpx;
|
||
box-sizing: border-box;
|
||
transition: border-color 0.2s ease, background 0.2s ease;
|
||
|
||
&:focus-within {
|
||
background: #ffffff;
|
||
border-color: #00e070;
|
||
}
|
||
}
|
||
|
||
.login__input-icon {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
margin-right: 18rpx;
|
||
opacity: 0.55;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.login__input {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #1e293b;
|
||
height: 100%;
|
||
}
|
||
|
||
.login__placeholder {
|
||
font-size: 26rpx;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
/* 协议主动勾选行(支持 Shake 微抖动动画) */
|
||
.login__agreement-row {
|
||
margin-top: 50rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8rpx 0;
|
||
transition: transform 0.15s ease;
|
||
|
||
&--shake {
|
||
animation: shake-anim 0.5s ease-in-out;
|
||
}
|
||
}
|
||
|
||
@keyframes shake-anim {
|
||
0%, 100% {
|
||
transform: translateX(0);
|
||
}
|
||
20%, 60% {
|
||
transform: translateX(-10rpx);
|
||
}
|
||
40%, 80% {
|
||
transform: translateX(10rpx);
|
||
}
|
||
}
|
||
|
||
.login__checkbox {
|
||
width: 30rpx;
|
||
height: 30rpx;
|
||
border-radius: 8rpx;
|
||
border: 2rpx solid #99c2aa;
|
||
background: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 12rpx;
|
||
flex-shrink: 0;
|
||
transition: all 0.15s ease;
|
||
cursor: pointer;
|
||
|
||
&--active {
|
||
background: #00d26a;
|
||
border-color: #00d26a;
|
||
}
|
||
}
|
||
|
||
.login__check-tick {
|
||
color: #ffffff;
|
||
font-size: 20rpx;
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
}
|
||
|
||
.login__agreement-text {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.login__text-plain {
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.login__text-link {
|
||
font-size: 24rpx;
|
||
color: #00a854; /* 规范强调绿,高对比度清爽 */
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
}
|
||
</style> |