一. 登录页静态布局
1.准备工作
(1). src新建 'styles/common.less',重置默认样式
// 重置默认样式
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
// 文字溢出省略号
.text-ellipsis-2{
overflow:hidden;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
(2). main.js导入 common.less
import '@/styles/common.less'
(3) 图片素材拷贝到 assets 目录[备用]
2.登录页静态布局编写
(1). 头部组件说明(NavBar)
①. utils / vant-ui.js 按需引入组件
import { NavBar } from 'vant'
Vue.use(NavBar)
②. 页面使用NavBar组件
<!--login / index.vue文件-->
<div class="login">
<!-- 头部 NavBar-->
<!-- $router.go(-1): 返回上一页 -->
<van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" />
...
</div>
(2). 通用样式覆盖
// common.less 增加导航样式css
// 添加导航的通用样式
.van-nav-bar{
.van-nav-bar__arrow{
color: #333;
}
}
(3). 登录静态结构编写
<!-- login / index.vue -->
<template>
<div class="login">
<!-- 头部 NavBar-->
<!-- $router.go(-1): 返回上一页 -->
<van-nav-bar title="会员登录" left-arrow @click-left="$router.go(-1)" />
<!-- 主题 -->
<div class="container">
<div class="title">
<h3>手机号登录</h3>
<p>未注册的手机号登录后将自动登录</p>
</div>
<div class="form">
<div class="form-item">
<input class="inp" maxlength="11" placeholder="请输入手机号" type="text">
</div>
<div class="form-item">
<input class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
<img src="@/assets/code.jpg" alt="">
</div>
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button>获取验证码</button>
</div>
</div>
<div class="login-btn">登录</div>
</div>
</div>
</template>
<script>
export default {
name: 'LoginIndex'
}
</script>
<style lang="less" scoped>
.container {
padding: 49px 29px;
.title {
margin-bottom: 20px;
h3 {
font-size: 26px;
font-weight: normal;
}
p {
line-height: 40px;
font-size: 14px;
color: #b8b8b8;
}
}
.form-item {
border-bottom: 1px solid #f3f1f2;
padding: 8px;
margin-bottom: 14px;
display: flex;
align-items: center;
.inp {
display: block;
border: none;
outline: none;
height: 32px;
font-size: 14px;
flex: 1;
}
img {
width: 94px;
height: 31px;
}
button {
height: 31px;
border: none;
font-size: 13px;
color: #cea26a;
background-color: transparent;
padding-right: 9px;
}
}
.login-btn {
width: 100%;
height: 42px;
margin-top: 39px;
background: linear-gradient(90deg, #ecb53c, #ff9211);
color: #fff;
border-radius: 39px;
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .1);
letter-spacing: 2px;
display: flex;
justify-content: center;
align-items: center;
}
}</style>
(4). 代码示例
二. request模块-axios封装
目标:将axios 请求方法,封装到 request 模块
使用 axios 来请求后端接口,一般都会对 axios 进行 一些配置(比如:配置基础地址,请求响应拦截器等
所以项目开发中,都会对 axios 进行基本的二次封装,单独封装到一个request 模块中,便于维护使用
1. 安装axios
npm i axios
// 报错使用 npm i axios --legacy-peer-deps
2. 新建request模块
// utils/request.js
import axios from 'axios'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处: 不会污染原始的 axios 实例
const instance = axios.create({
baseURL: 'http://cba.itlike.com/public/index.php?s=/api/', // 主机地址
timeout: 5000
})
// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截其中处理一下)
return response.data
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error)
})
// 导出配置好的实例
export default instance
3. 页面中使用
// login/index.vue
<template>
...
</template>
<script>
// 导入封装的axios
import request from '@/utils/request'
export default {
name: 'LoginIndex',
// 发送请求
async created () {
const res = await request.get('/captcha/image')// 接口地址
console.log(res)
}
}
</script>
<style lang="less" scoped>
...
</style>
4. 代码示例
三. 图形验证码功能完成
目标:基于请求回来的 base64 图片,实现图形验证码功能
说明:
(1).图形验证码,本质就是一个请求回来的图片
(2).用户将来输入图形验证码,用于强制人机交互,可以抵御机器自动化攻击(例如:避免批量请求获取短信)
需求:
(1).动态将请求回来的 base64 图片,解析染出来
(2).点击验证码图片盒子,要刷新验证码
1. 发送请求,获取验证码信息
// login/index.vue
<template>
...
<!-- v-model绑定用户输入的验证码 -->
<input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
<!-- 使用图形验证码,点击调用请求方法 -->
<img v-if="picUrl" :src="picUrl" @click="getPicCode" alt="">
...
</template>
<script>
import request from '@/utils/request'
export default {
name: 'LoginIndex',
data () {
return {
picCode: '', // 用户输入的图形验证码
picKey: '', // 将来请求传递的图形验证码唯一标识
picUrl: ''// 存储请求渲染的图片地址
}
},
created () {
this.getPicCode()// 调用发送请求方法
},
methods: {
// 发送请求封装为一个方法
async getPicCode () {
const { data: { base64, key } } = await request.get('/captcha/image')// 接口地址
this.picUrl = base64 // 存储地址
this.picKey = key // 存储唯一标识
}
}
}
</script>
<style lang="less" scoped>
...
</style>
2. 代码示例
四. 封装图片验证码接口
目标:将请求封装成方法,统一存放到 api 模块,与页面分离
以前的模式:
(1).页面中充斥着请求代码,可阅读性不高
(2).相同的请求没有复用
(3).请求没有统一管理
封装api模块的好处:
(1). 请求与页面逻辑分离
(2).相同的请求可以直接复用
(3).请求进行了统一管理
整体步骤:
1. 封装请求函数
// src/api/login.js
// 此处用于存放所有登录相关的接口请求
import request from '@/utils/request'
// 1. 获取图形验证码
// 按需导出
export const getPicCode = () => {
return request.get('/captcha/image') // 接口地址
}
2. 页面导入调用
// src/views/login/index.vue
<template>
...
</template>
<script>
// 按需导入
import { getPicCode } from '@/api/login'
export default {
name: 'LoginIndex',
data () {
return {
...
}
},
created () {
...
},
methods: {
// 发送请求封装为一个方法
async getPicCode () {
const { data: { base64, key } } = await getPicCode()// 调用封装方法
this.picKey = key // 存储唯一标识
}
}
}
</script>
<style lang="less" scoped>
...
</style>
3. 代码示例
五. toast轻提示
目标:阅读vant文档,掌握toast 轻提示
注册安装:
import { Toast } from 'vant'
Vue.use(Toast)
1. 两种使用方式
(1). 导入使用 (组件内 或 非组件中均可)
import { Toast } from 'vant'
Toast('提示内容')
(2). 通过this直接调用(必须组件内)
本质: 将方法,注册挂载到了Vue原型上 Vue.prototype.$toast=xxx
this.$toast('提示内容')
2. 代码示例
(1). 导入使用
(2). 通过this直接调用
六. 短信验证功能
目标: 实现短信验证倒计时功能
步骤分析:
①.点击按钮,实现 倒计时 效果
②.倒计时之前的 校验处理(手机号、验证码)
③.封装短信验证请求接口,发送请求添加提示
1. 倒计时基础效果
(1). 准备data数据
data () {
return {
totalSecond: 60, // 总秒数
second: 60, // 当前秒数,开定时器对 second--
timer: null // 定时器 id
}
}
(2). 给按钮注册点击事件
<button @click="getCode">
{{ second === totalSecond ? '获取验证码' : second + '秒后重新发送'}}
</button>
(3). 开启倒计时
// 获取短信验证码
getCode () {
// 当目前没有定时器开着,且 totalSecond 和 second 一致(秒数归位) 才可以倒计时
if (!this.timer && this.second === this.totalSecond) {
// 开启倒计时,每隔一秒减1
this.timer = setInterval(() => {
this.second--
if (this.second <= 0) {
clearInterval(this.timer) // 停止定时器
this.timer = null // 重置定时器 id
this.second = this.totalSecond // 归位
}
}, 1000)
// 发送请求,获取验证码
this.$toast('发送成功,请注意查收')
}
}
(4). 离开页面销毁定时器
// 离开页面清除定时器
destroyed () {
clearInterval(this.timer)
}
}
(5). 代码示例
2. 验证码请求校验处理
(1). 输入框v-model绑定变量
data () {
return {
mobile: '', // 手机号
picCode: '' // 用户输入的图形验证码
}
},
<input class="inp" v-model='mobile' maxlength="11" placeholder="请输入手机号" type="text">
<input v-model="picCode" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
(2). methods中封装校验方法
// 校验 手机号 和 图形验证码是否合法
validFn () {
if (!/^1[3-9]\d{9}$/.test(this.mobile)) {
this.$toast('请输入正确的手机号')
return false
}
if (!/^\w{4}$/.test(this.picCode)) {
this.$toast('请输入正确的图形验证码')
return false
}
return true
},
(3). 请求倒计时前 进行校验
// 获取短信验证码
getCode () {
if (!this.validFn()) {
// 如果没通过校验,没必要往下走
return
}
...
}
(4). 代码示例
3. 封装接口,请求获取验证码
(1). 封装接口 api / login.js
// 获取短信验证码
export const getMsgCode = (captchaCode, captchaKey, mobile) => {
return request.post('/captcha/sendSmsCaptcha', {
form: {
captchaCode,
captchaKey,
mobile
}
})
}
(2). 调用接口,添加提示
// 获取短信验证码
async getCode () {
if (!this.validFn()) {
// 如果没通过校验,没必要往下走
return
}
// 当目前没有定时器开着,且 totalSecond 和 second 一致(秒数归位) 才可以倒计时
if (!this.timer && this.second === this.totalSecond) {
// 发送请求
await getMsgCode(this.picCode, this.picKey, this.mobile)
this.$toast('短信发送成功,请注意查收')
// 开启倒计时
...
}
(3). 代码示例
七. 登录功能
目标:封装api登录接口,实现登录功能
步骤分析:
①.阅读接口文档,封装登录接口
②.登录前的校验(手机号,图形验证码,短信验证码)
③.调用方法,发送请求,成功添加提示并跳转
1. api / login.js 封装登录接口
//登录接口
export const codeLogin = (mobile, smsCode) => {
return request.post('/passport/login', {
form: {
isParty: false,
partyData: {},
mobile,
smsCode
}
})
}
2. 短信验证码绑定v-mode
data () {
return {
msgCode: '' // 短信验证码
}
},
<input class="inp" v-model="msgCode" placeholder="请输入短信验证码" type="text">
<div class="login-btn" @click="login">登录</div>
3. 页面中校验和调用登录接口
// 登录
async login () {
if (!this.validFn()) {
return
}
if (!/^\d{6}$/.test(this.msgCode)) {
this.$toast('请输入正确的手机验证码')
return
}
const res = await codeLogin(this.mobile, this.msgCode)
console.log(res)
this.$toast('登陆成功')
this.$router.push('/')
}
八. 响应拦截器统一处理错误
目标:通过响应拦截器,统一处理接口的错误提示
问题:每次请求,都会有可能会错误,就都需要错误提示
说明:响应拦截器是咱们拿到数据的 第一个 数据流转站,可以在里面统一处理错误
只要不是200,就默认提示,抛出错误
1. 响应拦截器统一处理错误
src /util /request.js
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处: 不会污染原始的 axios 实例
...
// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
...
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截其中处理一下)
const res = response.data
// 希望如果响应的status非200,最好抛出一个promise错误,await只会等待成功的promise
if (res.status !== 200) {
// 给提示
Toast(res.message)
// 控制台抛出一个错误的promise
return Promise.reject(res.message)
}
return res
}, function (error) {
// 对响应错误做点什么
...
// 导出配置好的实例
export default instance
九. 登录权限信息存储
目标: vuex 构建 user 模块存储登录权证(token & userld)
1. 新建vuex 的 user.js模块
src / store / modules / user.js
export default {
namespaced: true, // 开启命名空间
state () {
return {
// 个人权证相关
userInfo: {
token: '',
userId: ''
}
}
},
mutations: {}
},
actions: {},
getters: {}
}
2. 挂载在vuex上
src / store / index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
user // 挂载
}
})
3. 提供 mutations
src / store / modules / user.js
mutations: {
// 所有mutations的第一个参数都是state
setUserInfo (state, obj) {
state.userInfo = obj
}
},
4. 页面中commit调用
login / index.vue
// 登录(校验 & 提交)
async login () {
if (!this.validFn()) {
return
}
...
const res = await codeLogin(this.mobile, this.msgCode)
this.$store.commit('user/setUserInfo', res.data)
console.log(res)
this.$toast('登陆成功')
this.$router.push('/')
}
5. 代码示例
十. storage存储模块-vuex持久化处理
目标: 封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理
将token 和 userId存储到本地,刷新页面也不会丢失
1. 定义storage.js模块
src / utils / storage.js
// 约定一个通用键名
const INFO_KEY = 'cc_shopping_info'
// 获取个人信息
export const getInfo = () => {
const defaultObj = { token: '', userId: '' } // 默认值
const result = localStorage.getItem(INFO_KEY)
return result ? JSON.parse(result) : defaultObj // 判断有值就序列化
}
// 设置个人信息
export const setInfo = (obj) => {
localStorage.setItem(INFO_KEY, JSON.stringify(obj))
}
// 移除个人信息
export const removeInfo = () => {
localStorage.removeItem(INFO_KEY)
}
2. vuex中调用
src / store / modules / user.js
// 导入封装的方法
import { getInfo, setInfo } from '@/utils/storage'
export default {
namespaced: true, // 开启命名空间
state () {
return {
// 个人权证相关
userInfo: getInfo() // 直接调用get方法
}
},
mutations: {
// 所有mutations的第一个参数都是state
setUserInfo (state, obj) {
state.userInfo = obj
setInfo(obj) // 调用set方法
}
},
actions: {},
getters: {}
}
3. 代码示例
十一. 添加请求loading效果
utils / request.js 添加loading
1. 请求和响应拦截器添加loading逻辑
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
...
// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 开启loading,禁止背景点击(节流处理,防止多次无效点击)
Toast.loading({
message: '加载中...',
forbidClick: true, // 禁止背景点击
loadingType: 'spinner', // 配置loading图标
duration: 0 // loading不会自动消失
})
return config
}...
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么(默认axios会多包装一层data,需要响应拦截其中处理一下)
...
if (res.status !== 200) {
// 给错误提示,Toast是单例模式,后面的 Toast调用了,会将前一个 Toast 效果覆盖
// 同时只能存在一个 Toast
...
// 控制台抛出一个错误的promise
...
} else {
// 正确情况,直接走业务核心逻辑,清除loading效果
Toast.clear()
}
return res
}...