文件
fenleixinxi/uniapp/pages/auth/login.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

633 行
15 KiB
Vue

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

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

<template>
<view class="login fc-page">
<!-- 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 = 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) || '')
// 若上一页是必须登录的受限页面,返回上一页会导致受限页面再次触发登录拦截造成死循环,因此直接回首页
const restrictedPages = ['pages/member/orders', 'pages/member/coupons', 'pages/distribution', 'pages/post/create']
const isRestricted = restrictedPages.some(r => prevRoute.includes(r))
if (!isRestricted && !prevRoute.includes('pages/auth/login')) {
uni.navigateBack({
delta: 1,
fail: () => {
uni.switchTab({
url: '/pages/index/index',
fail: () => uni.reLaunch({ url: '/pages/index/index' })
})
},
})
return
}
}
// 兜底返回首页 TabBar(必须使用 switchTab)
uni.switchTab({
url: '/pages/index/index',
fail: () => {
uni.reLaunch({ url: '/pages/index/index' })
},
})
},
/**
* 暂不登录,直接返回首页以游客身份浏览无需登录公开内容(符合微信审核规范)
*/
handleSkipLogin() {
uni.switchTab({
url: '/pages/index/index',
fail: () => {
uni.reLaunch({ 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;
}
/* 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>