Files
hhrs/pages/index/login.vue
T

335 lines
8.8 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/hhService/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
if (!d || d.code === undefined || d.code === null) {
// xuchao: 没有业务码时,还要排除“网关把 404/错误页包成 HTTP 200”的情况
// (实测未部署的新地址会返回 {data:{timestamp,status:404,error:'Not Found'}} 且 errcode=0)
return !d || (d.status === undefined && d.error === undefined)
}
return 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: 只认"非空字符串"凭证。后端若把 null 序列化成 {}(实测 tokenDecode 就是这个形状),
// 用 || 兜底会把对象存进缓存,之后每个请求都变成 Bearer [object Object]
const payload = (res.data && res.data.data) || {}
const issued = [payload.accessToken, payload.token].find(v => typeof v === 'string' && v.length > 0)
uni.setStorageSync('token', issued || '')
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>