1. 登录/验证码改判业务码 data.code(外层 errcode 恒为0),失败不再写入 phone 造成假登录态,并透出后端真实原因;倒计时改为发送成功后才启动;token 按 accessToken 兜底读取,不再写入 undefined 2. 权益包三 Tab 由 pageSize=1000 的假分页改为真实分页 + 页面级触底加载,各 Tab 独立游标与请求序号,合并去重兜底;中奖名单页码改为按期维护,修复切期后重复追加 3. 首页去掉整页 overflow:hidden,小屏不再裁切活动指南卡片;接口表去重并清理 7 个废弃端点;基址/客服电话集中到 common/config.js;删除权益详情不可达的假成功分支
329 lines
8.4 KiB
Vue
329 lines
8.4 KiB
Vue
<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>
|