Files
hhrs/pages/index/login.vue
T
jacobxu666 745f4bf5f1 fix: 修复登录失败被当成功、列表假分页与首页裁切等隐患
1. 登录/验证码改判业务码 data.code(外层 errcode 恒为0),失败不再写入 phone 造成假登录态,并透出后端真实原因;倒计时改为发送成功后才启动;token 按 accessToken 兜底读取,不再写入 undefined

2. 权益包三 Tab 由 pageSize=1000 的假分页改为真实分页 + 页面级触底加载,各 Tab 独立游标与请求序号,合并去重兜底;中奖名单页码改为按期维护,修复切期后重复追加

3. 首页去掉整页 overflow:hidden,小屏不再裁切活动指南卡片;接口表去重并清理 7 个废弃端点;基址/客服电话集中到 common/config.js;删除权益详情不可达的假成功分支
2026-09-14 23:38:13 +08:00

329 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- xuchao: 顶部导航(返回+页面名称) -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<u-icon name="arrow-left" color="#1a1818" size="40"></u-icon>
</view>
<text class="nav-title">手机号登录</text>
</view>
<!-- 标题区 -->
<view class="title-area">
<text class="title">手机号登录</text>
<text class="subtitle">请使用真实手机号登录</text>
</view>
<!-- 表单区 -->
<view class="form-area">
<view class="input-item">
<input class="input-field" type="number" maxlength="11" v-model="phone" placeholder="请输入手机号码" />
</view>
<view class="input-item code-row">
<input class="input-field" type="number" maxlength="4" v-model="code" placeholder="请输入验证码" />
<text class="code-btn" :class="{ disabled: countdown > 0 }" @click="sendCode">
{{ countdown > 0 ? countdown + '秒后重新获取' : '获取验证码' }}
</text>
</view>
</view>
<!-- 登录按钮 -->
<view class="login-btn" @click="onLogin">立即登录</view>
<!-- 协议区 -->
<view class="agreement-area">
<view class="checkbox" :class="{ checked: agreed }" @click="agreed = !agreed">
<text class="check-icon" v-if="agreed">✓</text>
</view>
<text class="agreement-text" @click="agreed = !agreed">我已阅读并同意</text>
<text class="agreement-link" @click.stop="goAgreement('user')">《用户协议》</text>
<text class="agreement-text" @click="agreed = !agreed">和</text>
<text class="agreement-link" @click.stop="goAgreement('privacy')">《隐私协议》</text>
</view>
</view>
</template>
<script>
import config from '@/common/config.js'
export default {
data() {
return {
phone: '',
code: '',
countdown: 0,
timer: null,
agreed: true,
redirect: ''
}
},
onLoad(options) {
this.redirect = options.redirect || ''
},
onUnload() {
if (this.timer) {
clearInterval(this.timer)
}
},
methods: {
// xuchao: open-api/pingXiang/login、sendLoginVerifyCode 返回 OpenapiResponse{code,msg,data},
// 被网关再包一层后外层 errcode 恒为 0,真正的业务码在 data.code 里。
// 只判 errcode 会把「验证码错误/过期、短信被限流」全部当成成功。
// 宽松兜底:后端若改成不返回 data.code,仍按 errcode 判定,不会把成功误判为失败。
isBizOk(res) {
if (!res || res.errcode !== 0) return false
const d = res.data
return !d || d.code === undefined || d.code === null || d.code === 200
},
// xuchao: 失败原因优先取业务 msg,其次取网关 errmsg
bizMsg(res) {
return (res && res.data && res.data.msg) || (res && res.errmsg) || ''
},
sendCode() {
if (this.countdown > 0) return
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
this.$u.api.sendLoginVerifyCode({ smsType: 'CIB_LOGIN', mobile: this.phone }).then(res => {
if (this.isBizOk(res)) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
// xuchao: 倒计时移到发送成功之后(原来无条件启动,发送失败也会被白锁 60 秒无法重试)
this.countdown = 60
this.timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.timer)
this.timer = null
}
}, 1000)
} else {
// xuchao: 透出后端真实原因,如「操作频繁,请稍后再试」
uni.showToast({ title: this.bizMsg(res) || '发送失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '发送失败', icon: 'none' })
})
},
onLogin() {
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!this.code || this.code.length < 4) {
uni.showToast({ title: '请输入4位验证码', icon: 'none' })
return
}
if (!this.agreed) {
uni.showToast({ title: '请同意用户协议和隐私协议', icon: 'none' })
return
}
// xuchao: password 为后端要求的固定旁路口令,集中取自 config.loginFixedPassword
this.$u.api.checkerLogin({ password: config.loginFixedPassword, mobile: this.phone, verifyCode: this.code }).then(res => {
if (this.isBizOk(res)) {
// xuchao: 后端当前只回 userId/userName,并未下发 accessToken(字段名也是 accessToken)。
// 取到才存、取不到存空串,避免把 undefined 写进 storage 变成 "Bearer undefined"
const payload = (res.data && res.data.data) || {}
uni.setStorageSync('token', payload.accessToken || payload.token || '')
uni.setStorageSync('phone', this.phone)
this.$u.api.getEligibility({ phone: this.phone, identityNo: '', name: '' }).then(eRes => {
if (eRes.errcode === 0) {
uni.setStorageSync('isAuth', eRes.data.isAuth)
uni.setStorageSync('hasWin', eRes.data.hasWin)
uni.setStorageSync('cardNumber', eRes.data.cardNumber)
uni.setStorageSync('expiresTime', eRes.data.expiresTime)
}
}).finally(() => {
if (this.redirect === 'rights') {
const isAuth = uni.getStorageSync('isAuth')
const hasWin = uni.getStorageSync('hasWin')
let status = 'auth'
if (isAuth) {
status = hasWin ? 'info' : 'noWin'
}
uni.redirectTo({ url: `/pages/rights/rights?status=${status}` })
} else {
uni.navigateBack()
}
})
} else {
// xuchao: 透出后端真实原因,如「验证码错误」「验证码已过期,请重新获取」;
// 失败时绝不写 phone,避免留下“假登录态”让后续页面以为已登录
uni.showToast({ title: this.bizMsg(res) || '登录失败', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '登录失败', icon: 'none' })
})
},
goAgreement(type) {
const url = type === 'user' ? '/pages/agreement/user' : '/pages/agreement/privacy'
uni.navigateTo({ url })
},
goBack() {
// xuchao: 直接通过链接进入(无历史栈)时回到首页,避免返回无响应
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
}
</script>
<style>
.page {
height: 100vh;
background-color: #ffffff;
padding: 0 40rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 标题区 */
.title-area {
margin-top: 160rpx;
margin-bottom: 80rpx;
}
.title {
font-size: 44rpx;
font-weight: 700;
color: #1a1a1a;
display: block;
text-align: center;
margin-bottom: 16rpx;
}
.subtitle {
font-size: 26rpx;
color: #999999;
display: block;
text-align: center;
}
/* 表单区 */
.form-area {
margin-bottom: 40rpx;
}
.input-item {
border: 1px solid #e5e5e5;
border-radius: 12rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.input-field {
height: 96rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333333;
}
.code-row {
display: flex;
align-items: center;
}
.code-row .input-field {
flex: 1;
}
.code-btn {
font-size: 26rpx;
color: #0088FF;
padding: 0 24rpx;
white-space: nowrap;
flex-shrink: 0;
}
.code-btn.disabled {
color: #cccccc;
}
/* 登录按钮 */
.login-btn {
height: 96rpx;
line-height: 96rpx;
text-align: center;
background-color: #0088FF;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
/* 协议区 */
.agreement-area {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #0088FF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
}
.checkbox.checked {
background-color: #0088FF;
}
.check-icon {
font-size: 20rpx;
color: #ffffff;
}
.agreement-text {
font-size: 24rpx;
color: #999999;
}
.agreement-link {
font-size: 24rpx;
color: #0088FF;
}
/* xuchao: 顶部返回导航条(负外边距抵消页面左右 padding,通栏展示) */
.nav-bar {
display: flex;
align-items: center;
height: 56px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
position: relative;
margin: 0 -40rpx;
}
.nav-back {
position: absolute;
left: 12px;
top: 0;
bottom: 0;
display: flex;
align-items: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 600;
color: #1a1818;
padding: 0 56px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>