1.创建组件并配置路由
1.创建 src/views/login/index.vue
<template> <div class="login-container">登录页面</div> </template> <script> export default { name: 'LoginPage', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped lang="less"></style>
2.在 src/router/index.js`中配置登录页的路由表
{ path: '/login', name: 'login', component: () => import('@/views/login') }
2.布局结构及样式
1.html结构布局主要使用到两个 Vant 组件: [NavBar 导航栏] 和 [Form 表单]
2.公共样式写到全局(src/styles/index.less),将局部样式写到组件内部
src/styles/index.less:
body { background-color: #f5f7f9; } .page-nav-bar { background-color: #3296fa; .van-nav-bar__title { color: #fff; } }
src/views/login/index.vue:
<template> <div class="login-container"> <!-- 导航栏 --> <van-nav-bar class="page-nav-bar" title="登录" /> <!-- /导航栏 --> <!-- 登录表单 --> <van-form @submit="onSubmit"> <van-field name="用户名" placeholder="请输入手机号" > <i slot="left-icon" class="toutiao toutiao-shouji"></i> </van-field> <van-field type="password" name="验证码" placeholder="请输入验证码" > <i slot="left-icon" class="toutiao toutiao-yanzhengma"></i> <template #button> <van-button class="send-sms-btn" round size="small" type="default">发送验证码</van-button> </template> </van-field> <div class="login-btn-wrap"> <van-button class="login-btn" block type="info" native-type="submit"> 登录 </van-button> </div> </van-form> <!-- /登录表单 --> </div> </template> <script> export default { name: 'LoginIndex', components: {}, props: {}, data () { return { } }, computed: {}, watch: {}, created () {}, mounted () {}, methods: { onSubmit (values) { console.log('submit', values) } } } </script> <style scoped lang="less"> .login-container { .toutiao { font-size: 37px; } .send-sms-btn { width: 152px; height: 46px; line-height: 46px; background-color: #ededed; font-size: 22px; color: #666; } .login-btn-wrap { padding: 53px 33px; .login-btn { background-color: #6db4fb; border: none; } } } </style>
其中用到了vant组件中的field输入框:
<i slot="left-icon" class="toutiao toutiao-shouji"></i>
3.实现基本登录功能
3.1.根据接口要求绑定获取表单数据
先查看接口文档:
在登录页面组件的实例选项 data 中添加 user 数据字段:
data () { return { user: { mobile: '', code: '' } } }
在表单中使用 v-model 绑定对应数据:
<!-- van-cell-group 仅仅是提供了一个上下外边框,能看到包裹的区域 --> <van-cell-group> <van-field v-model="user.mobile" required clearable label="手机号" placeholder="请输入手机号" /> <van-field v-model="user.code" type="number" label="验证码" placeholder="请输入验证码" required /> </van-cell-group>
使用 VueDevtools 调试工具查看是否绑定成功
3.2将数据发送给后台
创建 src/api/user.js封装请求方法
/** * 用户相关的请求模块 */ import request from "@/utils/request" /** * 用户登录 */ export const login = data => { return request({ method: 'POST', url: '/app/v1_0/authorizations', data }) }
给登录按钮注册点击事件
async onLogin () { try { const res = await login(this.user) console.log('登录成功', res) } catch (err) { if (err.response.status === 400) { console.log('登录失败', err) } } }
4.登录状态提示
在组件中可以直接通过this.$toast调用!!!
Toast 默认采用单例模式,即同一时间只会存在一个 Toast
async onLogin () { // 开始转圈圈 this.$toast.loading({ duration: 0, // 持续时间,0表示持续展示不停止,默认为2000 forbidClick: true, // 是否禁止背景点击 message: '登录中...' // 提示消息 }) try { const res = await request({ method: 'POST', url: '/app/v1_0/authorizations', data: this.user }) console.log('登录成功', res) // 提示 success 或者 fail 的时候,会先把其它的 toast 先清除 this.$toast.success('登录成功') } catch (err) { console.log('登录失败', err) this.$toast.fail('登录失败,手机号或验证码错误') } }
5.表单验证
1、给 van-field 组件配置 rules 验证规则
2、当表单提交的时候会自动触发表单验证
如果验证通过,会触发 submit 事件
如果验证失败,不会触发 submit<van-form @submit="onSubmit"> <van-field v-model="user.mobile" name="手机号" placeholder="请输入手机号" :rules="userFormRules.mobile" type="number" maxlength="11" > <i slot="left-icon" class="toutiao toutiao-shouji"></i> </van-field> <van-field v-model="user.code" name="验证码" placeholder="请输入验证码" :rules="userFormRules.code" type="number" maxlength="6" > <i slot="left-icon" class="toutiao toutiao-yanzhengma"></i> <template #button> <van-button class="send-sms-btn" round size="small" type="default">发送验证码</van-button> </template> </van-field> <div class="login-btn-wrap"> <van-button class="login-btn" block type="info" native-type="submit"> 登录 </van-button> </div> </van-form> <!-- /登录表单 --> </div> </template> <script> import { login } from '@/api/user' export default { name: 'LoginIndex', components: {}, props: {}, data () { return { user: { mobile: '', // 手机号 code: '' // 验证码 }, userFormRules: { mobile: [{ required: true, message: '手机号不能为空' }, { pattern: /^1[3|5|7|8]\d{9}$/, message: '手机号格式错误' }], code: [{ required: true, message: '验证码不能为空' }, { pattern: /^\d{6}$/, message: '验证码格式错误' }] } } }, }
其中使用了html5的 两个属性来优化表单验证(限制数字类型+限制字符串长度)
type="number"
maxlength="11"
6. 验证码处理
6.1.验证手机号
点击获取验证码按钮需要单独验证手机号,此时会触发表单提交,因此需要给按钮添加
native-type="button"
此时利用validate方法来验证单个表单(手机号表单):
<van-form ref="loginForm" @submit="onSubmit"> <van-field v-model="user.mobile" name="mobile" placeholder="请输入手机号" :rules="userFormRules.mobile" type="number" maxlength="11" > <i slot="left-icon" class="iconfont icon-shouji"></i> </van-field> <van-field v-model="user.code" name="code" placeholder="请输入验证码" :rules="userFormRules.code" type="number" maxlength="6" > <i slot="left-icon" class="iconfont icon-yanzhengma"></i> <template #button> <!-- time:倒计时事件 --> <van-count-down v-if="isCountDownShow" :time="1000 * 60" format="ss s" @finish="isCountDownShow = false" /> <van-button v-else native-type="button" class="send-sms-btn" round size="small" type="default" @click="onSendSms" >发送验证码</van-button > </template> </van-field> <div class="login-btn-wrap"> <van-button class="login-btn" round block type="info" native-type="submit" >登录</van-button > </div> </van-form>
async onSendSms () { console.log('onSendSms') // 1. 校验手机号 try { await this.$refs.loginForm.validate('mobile') } catch (err) { return console.log('验证失败', err) }
6.2 验证通过显示倒计时
此时用v-if和v-else来达到倒计时的显示与隐藏,在倒计时结束时结合finish事件停止倒计时
<van-field v-model="user.code" placeholder="请输入验证码" > <i class="icon icon-mima" slot="left-icon"></i> <van-count-down v-if="isCountDownShow" slot="button" :time="1000 * 5" format="ss s" @finish="isCountDownShow = false" /> <van-button v-else slot="button" size="small" type="primary" round @click="onSendSmsCode" >发送验证码</van-button> </van-field>
6.3发送验证码
1、在 api/user.js 中添加封装数据接口
export const getSmsCode = mobile => { return request({ method: 'GET', url: `/app/v1_0/sms/codes/${mobile}` }) }
2、给发送验证码按钮注册点击事件
3、发送处理
import { login, sendSms } from '@/api/user'
async onSendSms () { // 1. 校验手机号 try { await this.$refs.loginForm.validate('mobile') } catch (err) { return console.log('验证失败', err) } // 2. 验证通过,显示倒计时 this.isCountDownShow = true // 3. 请求发送验证码 try { await sendSms(this.user.mobile) this.$toast('发送成功') } catch (err) { // 发送失败,关闭倒计时 this.isCountDownShow = false if (err.response.status === 429) { this.$toast('发送太频繁了,请稍后重试') } else { this.$toast('发送失败,请稍后重试') } } }
7. 处理用户Token
Token 是用户登录成功之后服务端返回的身份令牌,在项目中的多个业务中需要使用到:
- 访问需要授权的 API 接口
- 校验页面的访问权限
- ...但是我们只有在第一次用户登录成功之后才能拿到 Token。
所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。- 本地存储:获取麻烦,数据不是响应式
- Vuex 容器(推荐):获取方便,响应式的登录成功,将 Token 存储到 Vuex 容器中:获取方便/响应式
为了持久化,还需要把 Token 放到本地存储:持久化1.在 src/store/index.js中:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const TOKEN_KEY = 'TOUTIAO_USER' export default new Vuex.Store({ state: { // 用户的登录状态信息 user: JSON.parse(window.localStorage.getItem('TOUTIAO_USER')) // user: null }, mutations: { setUser (state, user) { state.user = user window.localStorage.setItem('TOUTIAO_USER', JSON.stringify(user)) } }, actions: { }, modules: { } })
2.登录成功以后将后端返回的 token 相关数据存储到容器中
async onLogin () { // const loginToast = this.$toast.loading({ this.$toast.loading({ duration: 0, // 持续时间,0表示持续展示不停止 forbidClick: true, // 是否禁止背景点击 message: '登录中...' // 提示消息 }) try { const res = await login(this.user) // res.data.data => { token: 'xxx', refresh_token: 'xxx' } + this.$store.commit('setUser', res.data.data) // 提示 success 或者 fail 的时候,会先把其它的 toast 先清除 this.$toast.success('登录成功') } catch (err) { console.log('登录失败', err) this.$toast.fail('登录失败,手机号或验证码错误') } // 停止 loading,它会把当前页面中所有的 toast 都给清除 // loginToast.clear() }
3.优化封装本地存储操作模块
src/utils/storage.js:
export const getItem = name => { const data = window.localStorage.getItem(name) try { return JSON.parse(data) } catch (err) { return data } } export const setItem = (name, value) => { if (typeof value === 'object') { value = JSON.stringify(value) } window.localStorage.setItem(name, value) } export const removeItem = name => { window.localStorage.removeItem(name) }
修改src/store/index.js:
import Vue from 'vue' import Vuex from 'vuex' import { getItem, setItem } from '@/utils/storage' Vue.use(Vuex) const TOKEN_KEY = 'TOUTIAO_USER' export default new Vuex.Store({ state: { // 一个对象,存储当前登录用户信息(token等数据) user: getItem(TOKEN_KEY) }, mutations: { setUser(state, data) { state.user = data //为了防止刷新丢失,需要把数据备份到本地存储 setItem(TOKEN_KEY, state.user) } }, actions: {}, modules: {} })