vlambda博客
学习文章列表

VueblogServer项目短信验证码登录功能前端实现

前言

今天这篇文章的主要目的就是带大家实现在前端登录页面实现添加加短信验证码登录功能。我们的前端项目仍然使用之前经过笔者二次开发过的开源项目vue-element-admin

1 修改登录组件源码

vue组件方面主要涉及到src/views/login/index.vue文件的修改

1.1 修改template模板

登录界面页头增加选择用户名密码登录和手机验证码登录的选择标签页,同时使用一v-if指令控制显示用户名密码登录表单或者手机验证码登录表单。修改后的template部分源码如下:

<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">
<div class="title-container">
<h3 class="title">登录表单</h3>
<el-tabs v-model="activeLoginType" @tab-click="changeLoginType">
<el-tab-pane label="用户名密码登录" name="usernamePassword" />
<el-tab-pane label="手机验证码登录" name="mobilePhoneCode" />
</el-tabs>
</div>
<div v-if="activeLoginType==1" class="username-login">
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
<el-input
ref="username"
v-model="loginForm.username"
placeholder="Username"
name="username"
type="text"
tabindex="1"
autocomplete="on"
/>

</el-form-item>
<el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="Password"
name="password"
tabindex="2"
autocomplete="on"
@keyup.native="checkCapslock"
@blur="capsTooltip = false"
@keyup.enter.native="handleLogin"
/>

<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
</el-tooltip>
</div>
<div v-else class="phoneCodeLogin">
<div class="one-line">
<el-form-item prop="phoneNo">
<el-select v-model="activeAreaCode" filterable placeholder="选择国家/地区" @change="changeArea">
<el-option
v-for="item in areaCodes"
:key="item.label"
:label="item.label"
:value="item.value"
>

<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.value }}</span>
</el-option>
</el-select>
<el-input
ref="phoneNo"
v-model="loginForm.phoneNo"
name="phoneNo"
class="phone-no"
tabindex="3"
autocomplete="on"
placeholder="请输入手机号码"
/>

<el-button type="info" :disabled="disabled" @click="sendPhoneCode">{{ sendCodeText }}</el-button>
</el-form-item>
</div>
<div class="one-line">
<el-form-item prop="phoneCode">
<el-input
ref="phoneCode"
v-model="loginForm.phoneCode"
name="phoneCode"
class="phone-code"
tabindex="4"
placeholder="请输入验证码"
@keyup.enter.native="handleLogin"
/>

</el-form-item>
</div>
</div>
<el-button
:loading="loading"
type="primary"
style="width:100%;margin-bottom:30px;"
@click.native.prevent="handleLogin"
>

登录
</el-button>
</el-form>
</div>
</template>

1.2 增加手机号码和验证码的校验器

在vue组件的data函数部分添加两个校验器,分别校验手机号码和验证码

import { validUsername, isNumber } from '@/utils/validate'
export default {
name: 'Login',
data() {
const validatePhoneNo = (rule, value, callback) => {
if (!(value.length === 11 && isNumber(value))) {
callback(new Error('手机号码必须是11位数字'))
} else {
if (value.charAt(0) !== '1' || parseInt(value.charAt(1)) < 3) {
callback(new Error('输入的手机号码不是有效的手机号'))
} else {
callback()
}
}
}
const validatePhoneCode = (rule, value, callback) => {
if (!value) {
callback(new Error('验证码不能为空'))
} else {
if (!(value.length === 6 && isNumber(value))) {
callback(new Error('验证码必须是6位数字'))
} else {
callback()
}
}
}
}

return {
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }],
phoneNo: [{ required: true, trigger: 'blur', validator: validatePhoneNo }],
phoneCode: [{ required: true, trigger: 'blur', validator: validatePhoneCode }]
}
// 其他返回对象在此省略
}
}

src/utils/validate.js中的 isNumber方法定义如下:

/**
* 是否数字
* @param {String} val
* @returns {Boolean}
*/

export function isNumber(val) {
for (let i = 0; i < val.length; i++) {
if (val.charCodeAt(i) < 48 || val.charCodeAt(i) > 57) {
return false
}
}
return true
}

1.3 增加与手机验证码登录有关的返回数据

在登录组件index.vue文件的data函数中增加如下模型数据

return {
// 1-用户名密码登录;2-手机号验证码登录
activeLoginType: '1',
activeAreaCode: '+86',
sendCodeText: '发送验证码',
disabled: false,
// areaCodes数据也可以从后台获取,这里为简化流程写成静态数据
areaCodes: [{ label: '+86', value: '中国大陆' },
{ label: '+1', value: '美国&加拿大' },
{ label: '+852', value: '中国香港' },
{ label: '+886', value: '中国台湾' },
{ label: '+81', value: '日本' }
],
loginForm: {
username: '',
password: '',
phoneNo: '',
phoneCode: ''
}
// 其他返回数据此处省略
}

1.4 增加切换登录类型和发送手机验证码方法

methods对象中增加切换的登录类型和发送手机验证码的方法

import { sendMessageCode } from '@/api/user'
import { Message } from 'element-ui'
export default {
name: 'Login',
methods: {
changeLoginType(val) {
if (val.index === '0') {
this.activeLoginType = '1'
} else {
this.activeLoginType = '2'
}
},
changeArea(val) {
console.log('val', val)
},
sendPhoneCode() {
if (this.disabled) {
return
}
const phoneNo = this.activeAreaCode.substr(1) + this.loginForm.phoneNo
console.log('phoneNo', phoneNo)
sendMessageCode(phoneNo).then(res => {
if (res.data.status === 200) {
Message({
message: '发送验证码成功',
type: 'success'
})
} else {
Message.error('发送验证码失败')
}
})
let time = 60
const myInterval = setInterval(() => {
if (time > 0) {
time--
this.sendCodeText = time + 's后重发'
this.disabled = true
} else {
this.sendCodeText = '发送验证码'
this.disabled = false
clearInterval(myInterval)
}
}, 1000)
}
// 其他方法此处省略
}
}

参照大多数验证码的前端要求,发送验证码的功能使用了一个定时器,每次发送要等待60秒之后才能再次获取验证码

1.5 修改登录方法逻辑

修改methods对象中的handleLogin方法,增加判断登录类型和手机验证码登录的逻辑

handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
if (this.activeLoginType === '1') {
const username = this.loginForm.username
const password = this.loginForm.password
this.$store.dispatch('user/login', { username: username, password: password })
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
} else {
const phoneNo = this.loginForm.phoneNo
const phoneCode = this.loginForm.phoneCode
this.$store.dispatch('user/mobileLogin', { phoneNo: phoneNo, phoneCode: phoneCode })
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
}
}
})
}

2 其他文件源码修改

2.1 增加返送短信验证码和验证码登录接口

src/api/user.js文件中增加sendMessageCodephoneCodeLogin两个方法

export function sendMessageCode(phoneNo) {
return request({
// 这里需要注意后台发送短信验证码的接口笔者作了修改,将手机号参数改为放在了查询参数里
url: `sendLoginVerifyCode?phoneNo=${phoneNo}`,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}

export function phoneCodeLogin(data) {
return request({
url: `mobile/login`,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data,
transformRequest: [function(data) {
// Do whatever you want to transform the data
let ret = ''
for (const it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}]
})
}

2.2 修改store中的user.js文件源码

修改src/store/modules/user.js文件中的源码,主要针对actions对象中的login请求做出部分修改,同时增加用于手机验证码登录的mobileLogin请求

import { login, logout, phoneCodeLogin } from '@/api/user'

const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username, password: password }).then(response => {
if (response.status === 200 && response.data) {
const data = response.data.userInfo
const useBaseInfo = {
username: data.username,
nickname: data.nickname,
email: data.email,
phoneNum: data.phoneNum
}
window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
const { roles, currentRole } = data
roles[0] = currentRole
commit('SET_TOKEN', useBaseInfo)
commit('SET_NAME', useBaseInfo.username)
setToken(currentRole.id)
commit('SET_ROLES', roles)
window.sessionStorage.setItem('roles', JSON.stringify(roles))
commit('SET_CURRENT_ROLE', currentRole)
window.sessionStorage.setItem('currentRole', currentRole)
// commit('SET_AVATAR', avtar)
getRouteIds(currentRole.id).then(response => {
if (response.status === 200 && response.data.status === 200) {
const routeIds = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve(useBaseInfo)
} else {
Message.error('user login failed')
resolve()
}
}).catch(error => {
console.error(error)
reject(error)
})
})
},
// phone code login
mobileLogin({ commit }, phoneParam) {
const { phoneNo, phoneCode } = phoneParam
return new Promise((resolve, reject) => {
phoneCodeLogin({ phoneNo: phoneNo, phoneCode: phoneCode }).then(res => {
if (res.status === 200 && res.data) {
const data = res.data.userInfo
const useBaseInfo = {
username: data.username,
nickname: data.nickname,
phoneNum: data.phoneNum,
email: data.email
}
window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
const { roles, currentRole } = data
roles[0] = currentRole
commit('SET_TOKEN', useBaseInfo)
commit('SET_NAME', useBaseInfo.username)
setToken(currentRole.id)
commit('SET_ROLES', roles)
window.sessionStorage.setItem('roles', JSON.stringify(roles))
commit('SET_CURRENT_ROLE', currentRole)
window.sessionStorage.setItem('currentRole', currentRole)
// commit('SET_AVATAR', avtar)
getRouteIds(currentRole.id).then(response => {
if (response.status === 200 && response.data.status === 200) {
const routeIds = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve(useBaseInfo)
} else {
Message.error('phone code login failed')
resolve()
}
}).catch(error => {
console.error(error)
reject(error)
})
})
},
// 其他请求此处省略
}

3 修改样式

样式的修改是基于在前端页面调试样式的基础上修改的,关于如何调试样式属于基础的前端问题,这里不做赘述。不会调样式的读者可去网上搜索专门讲解如何调样式的文章学一学,很容易上手的。

src/views/login/index.vue文件中增加修改样式的源码

<style lang="scss">
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
.one-line .el-select.el-select--medium {
width: 30%;
float: left;
.el-input.el-input--medium.el-input--suffix {
width: 100%;
border-style: solid;
border-width: 2px;
border-color: #FFF;
}
}
.one-line .phone-no.el-input.el-input--medium {
float: left;
width: 40%;
border-style: solid;
border-width: 2px;
border-color: #FFF;
border-left-width: 0;
}
.one-line .phone-code.el-input.el-input--medium {
width: 100%;
border: solid 2px #fff;
margin-top: 10px;
margin-bottom: 10px;
}
.one-line .el-button.el-button--info.el-button--medium {
height: 47px;
margin-left: 10px;
width: 27%;
}
// 其他样式代码此处省略
}
</style>

<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;

#app {
height: 100%;
width: 100%;
background-color: #2d3a4b;
}

.login-container {
width: 50%;
height: 600px;
margin-left: 20%;
margin-top: 150px;
background-color: #00DDDD;
overflow: hidden;
.el-tabs__nav > .el-tabs__item {
font-weight: 800;
cursor: pointer;
}
// 其他样式代码此处省略
</style>

4 效果测试

4.1 启动后台应用

blogserver后台项目启动mysqlredis服务后,然后在IDEA中打开blogserver项目,以debug模式启动BlogserverApplication启动类

控制台出现如下日志信息表示后台应用启动成功

04-10 21:49:57.097  INFO 19924 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path '/blog'
2022-04-10 21:49:57.097 INFO 19924 --- [ main] org.sang.BlogserverApplication : Started BlogserverApplication in 11.462 seconds (JVM running for 14.286)

4.2 启动前台应用

vue-element-admin项目的根目录下打开一个控制台(可以执行右键->Git Bash Here打开控制台)

执行本地运行前台应用的指令:npm run dev  然后回车,控制台出现如下日志显示前台应用启动成功

App running at:
- Local: http://localhost:3000/
- Network: http://192.168.1.235:3000/

Note that the development build is not optimized.
To create a production build, run npm run build.

4.3 前端效果体验

前端应用启动成功后打开谷歌浏览器,在导航栏中输入http://localhost:3000/ 然后回车进入如下登录界面

默认使用用户名密码登录

VueblogServer项目短信验证码登录功能前端实现

选中手机验证码登录, 登录表单切换为如下所示的手机验证码登录表单

VueblogServer项目短信验证码登录功能前端实现

发送成功后手机上会受到6位验证码,在验证码输入框输入6位验证码后点击登录按钮进行登录操作。

登录成功后会进入如下所示的系统首页,到这里也就代表使用短信验证码登录的功能实现了。

5 写在最后

本文的完整代码已提交到本人的gitee个人仓库,需要完整代码的读者可以克隆下来参考。

https://gitee.com/heshengfu1211/blogserver.git

https://gitee.com/heshengfu1211/vue-element-admin.git

原创不易,看到这里的老铁们麻烦帮忙点亮文章底部右下角的【在看】,如果能帮忙点赞、转发那就更感谢了。

6 往期原创推荐

【1】

【2】

【3】

【4】

【5】

---END--