文件
fenleixinxi/uniapp/pages/auth/login.vue
MiAn b0eb1134a5 feat: 新增微信小程序虚拟支付提审防审模式及相关功能
本次提交主要实现微信小程序虚拟支付提审防审机制,包括:
1. 后台配置虚拟支付防审开关,支持开启/关闭模式
2. 前端根据开关状态及设备类型,iOS设备自动触发合规提示
3. 分销模块调整提现规则,无门槛即时提现
4. 登录页面优化,增加返回和暂不登录入口,符合微信审核规范
5. 新增提现记录页面,优化分销中心界面布局

该功能确保微信提审期间能顺利通过审核,审核通过后可一键切换为正常运营模式,无需重新发布小程序代码。
2026-11-01 22:41:27 +08:00

680 行
16 KiB
Vue

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

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

<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>