14 智慧商城 登录和首页
项目资料
- 接口文档:- wiki - 智慧商城 - 实战项目
- 演示地址:- 智慧商城
登录页 loginPage
- 页面:
views/login/index.vue
静态布局
- 编写静态结构代码。
NavBar 导航栏组件
- 在
utils/vant-ui.js中引入Tabbar和TabbarItem组件。 - 在
views/login/index.vue中使用NavBar组件。 - 添加通用样式:
styles/common.less设置导航条,返回箭头颜色
示例代码
js
import Vue from 'vue'
import { NavBar, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)
Vue.use(NavBar)vue
<script>
import './index.less'
export default {
name: 'LoginPage',
data() {
return {}
},
}
</script>
<template>
<!-- <div> -->
<!-- login页面 -->
<!-- </div> -->
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<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 alt="" src="@/assets/code.png">
</div>
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button>获取验证码</button>
</div>
</div>
<div class="login-btn">
登录
</div>
</div>
</div>
</template>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;
}
// 添加导航的通用样式
.van-nav-bar {
.van-nav-bar__arrow {
color: #333;
}
}二次封装 axios 为模块
- 接口文档:wiki - 智慧商城 - 实战项目
- 演示地址:智慧商城
- 接口调用基础地址:
http://cba.itlike.com/public/index.php?s=/api/
- 我们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如:配置基础地址,请求响应拦截器等等)
- 一般项目开发中,都会对 axios 进行基本的二次封装, 单独封装到一个模块中,便于维护使用
- 安装 axios 依赖
pnpm add axios - 新建
utils/request.js封装 request 模块。- 导入 axios 模块。
- 创建实例。
- 添加请求/响应拦截器。在请求或响应被 then 或 catch 处理前拦截它们。
- 导出实例。
图形验证码功能
基于请求回来的 base64 图片,实现图形验证码功能。
- 查看接口文档 获取图形验证码,了解接口的请求方式和参数。
- 使用
request模块发送请求,获取图形验证码。- 图形验证码,本质就是一个请求回来的图片
- 用户将来输入图形验证码,用于强制人机交互,可以抵御机器自动化攻击 (例如:避免批量请求获取短信)
- 在
views/login/index.vue中动态渲染图形验证码,并且点击时要重新刷新验证码。- 动态将请求回来的 base64 图片,解析渲染出来
- 点击验证码图片盒子,要刷新验证码
示例代码
js
import axios from 'axios'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({
baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
timeout: 5000,
headers: {
'platform': 'H5',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
},
})
// 在请求或响应被 then 或 catch 处理前拦截它们
// 添加请求拦截器
instance.interceptors.request.use((config) => {
// 在发送请求之前做些什么
return config
}, (error) => {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
// 导出自定义配置的 axios 实例
export default instancevue
<script>
import './index.less'
import Request from '@/utils/request'
export default {
name: 'LoginPage',
data() {
return {
picKey: '', // 将来请求传递的图形验证码唯一标识
picString: '', // 存储请求渲染的图片地址
}
},
async created() {
await this.getCaptchaImage()
},
methods: {
// 获取图形验证码
async getCaptchaImage() {
const { data: { key, base64 } } = await Request.get('/captcha/image')
this.picKey = key
this.picString = base64
},
},
}
</script>
<template>
<!-- <div> -->
<!-- login页面 -->
<!-- </div> -->
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<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 v-if="picString" :src="picString" alt="" referrerpolicy="no-referrer" @click="getCaptchaImage">
</div>
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button>获取验证码</button>
</div>
</div>
<div class="login-btn">
登录
</div>
</div>
</div>
</template>封装 api 接口
将请求封装成方法,统一存放到 api 模块,与页面分离
以前的模式:

页面中充斥着请求代码
可阅读性不高
相同的请求没有复用请求没有统一管理
期望:

- 请求与页面逻辑分离
- 相同的请求可以直接复用请求
- 进行了统一管理
步骤:
- 新建请求模块
- 封装请求函数
- 页面中导入调用
封装上一步图形验证码 Api 接口:
- 新建
api/login.js提供获取图形验证码 Api 函数 login/index.vue页面中调用
示例代码
js
// 此处用于存放所有登录相关的接口请求
import Request from '@/utils/request'
// 获取图形验证码
export const getCaptchaImage = () => Request.get('/captcha/image')vue
<script>
import './index.less'
import { getCaptchaImage } from '@/api/login'
export default {
name: 'LoginPage',
data() {
return {
picKey: '', // 将来请求传递的图形验证码唯一标识
picString: '', // 存储请求渲染的图片地址
}
},
async created() {
await this.getCaptchaImage()
},
methods: {
async getCaptchaImage() {
const { data: { key, base64 } } = await getCaptchaImage()
this.picKey = key
this.picString = base64
},
},
}
</script>
<template>
<!-- <div> -->
<!-- login页面 -->
<!-- </div> -->
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<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 v-if="picString" :src="picString" alt="" referrerpolicy="no-referrer" @click="getCaptchaImage">
</div>
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button>获取验证码</button>
</div>
</div>
<div class="login-btn">
登录
</div>
</div>
</div>
</template>Toast 轻提示组件
在页面中间弹出黑色半透明提示,用于消息通知、加载提示、操作结果提示等场景。
- 注册安装js
import { Toast } from 'vant' Vue.use(Toast) - 使用
- 导入调用 ( 组件内 或 非组件中均可 )js
import { Toast } from 'vant' Toast('提示内容') - 通过
this直接调用 ( 组件内)。引入 Toast 组件后,会自动在 Vue 的 prototype 上挂载$toast方法,便于在组件内调用。jsthis.$toast('提示内容')
- 导入调用 ( 组件内 或 非组件中均可 )
- 在
utils/vant-ui.js中引入Toast组件。 - 在
views/login/index.vue中使用Toast组件。
短信验证和倒计时功能
获取短信验证码和倒计时功能。
- 查看接口文档 获取短信验证码,了解接口的请求方式和参数。
- 点击获取验证码按钮后,在发送请求之前,需要对手机号和图形验证码进行校验。
- 手机号校验:正则校验
(!/^1[3-9]\d{9}$/.test(this.$refs.phone.value)) - 图形验证码校验:非空校验
(!this.$refs.captcha.value)格式校验(!/^\w{4}$/.test(this.$refs.captcha.value))或者(!/^[a-zA-Z0-9]{4}$/.test(this.$refs.captcha.value))
- 手机号校验:正则校验
- 校验通过后,发送请求获取短信验证码。
- 请求成功后,按钮变为不可点击状态,同时开始倒计时,按钮内容变为 "xxs 后重新获取"
- 倒计时结束后,按钮恢复可点击状态,按钮内容恢复为 "获取验证码"
如果在倒计时过程中,用户再次点击获取验证码按钮,需要提示用户等待倒计时结束无需提示,按钮不可点击即可- 如果用户在倒计时过程中,离开了当前页面,需要清除倒计时
示例代码
js
import Vue from 'vue'
import { NavBar, Tabbar, TabbarItem, Toast } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)
Vue.use(NavBar)
Vue.use(Toast)js
// 此处用于存放所有登录相关的接口请求
import Request from '@/utils/request'
// 获取图形验证码
export const getCaptchaImage = () => Request.get('/captcha/image')
// 获取短信验证码
export const getSmsCode = (captchaCode, captchaKey, mobile) => Request.post('/captcha/sendSmsCaptcha', { form: { captchaCode, captchaKey, mobile } })vue
<script>
import './index.less'
import { getCaptchaImage, getSmsCode } from '@/api/login'
export default {
name: 'LoginPage',
data() {
return {
picKey: '', // 将来请求传递的图形验证码唯一标识
picString: '', // 存储请求渲染的图片地址
isButtonDisabled: false, // 按钮禁用状态(默认为 false,即可点击状态)
timer: null, // 倒计时的定时器 id
buttonText: '获取验证码', // 按钮文字
}
},
async created() {
await this.getCaptchaImage()
},
beforeUnmount() {
if (this.timer)
clearInterval(this.timer)
},
methods: {
// 获取图形验证码
async getCaptchaImage() {
const { data: { key, base64 } } = await getCaptchaImage()
this.picKey = key
this.picString = base64
},
// 校验 手机号 和 图形验证码 是否合法
// 通过为 true,不通过为 false
validatePhoneAndCaptcha() {
// 校验手机号
if (!/^1[3-9]\d{9}$/.test(this.$refs.phone.value)) {
this.$toast('手机号格式不正确')
return false
}
// 校验图形验证码
if (!this.$refs.captcha.value) {
this.$toast('图形验证码不能为空')
return false
}
// 校验图形验证码是否正确
if (!/^[a-zA-Z0-9]{4}$/.test(this.$refs.captcha.value)) {
this.$toast('图形验证码格式不正确')
return false
}
return true
},
// 获取短信验证码 和 添加倒计时功能
// 倒计时功能:点击获取验证码后,按钮变为不可点击状态,同时开始倒计时,倒计时结束后,按钮恢复可点击状态
// 1. 点击获取验证码按钮,发送请求获取短信验证码
// 2. 请求成功后,按钮变为不可点击状态,同时开始倒计时,button 内容变为 "重新获取(60)s"
// 3. 倒计时结束后,按钮恢复可点击状态
// 4. ~~如果在倒计时过程中,用户再次点击获取验证码按钮,需要提示用户等待倒计时结束~~ // 无需提示,按钮不可点击即可
// 5. 如果用户在倒计时过程中,离开了当前页面,需要清除倒计时
async getSmsCode() {
// 校验手机号和图形验证码
if (!this.validatePhoneAndCaptcha())
return false
// 如果倒计时还在进行
// if (this.timer) {
// this.$toast('请等待倒计时结束')
// return
// }
try {
const { message } = await getSmsCode(this.$refs.captcha.value, this.picKey, this.$refs.phone.value)
this.$toast(message)
this.isButtonDisabled = true
this.countdown = 10
this.timer = setInterval(() => {
this.countdown--
this.buttonText = `${this.countdown}s 后重新获取`
if (this.countdown === 0) {
// 倒计时结束, 清除定时器, 按钮禁用状态解除,按钮文字恢复
clearInterval(this.timer)
// this.timer = null
this.isButtonDisabled = false
this.buttonText = '获取验证码'
}
}, 1000)
}
catch (error) {
this.$toast('获取短信验证码失败')
}
},
},
}
</script>
<template>
<!-- <div> -->
<!-- login页面 -->
<!-- </div> -->
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<div class="container">
<div class="title">
<h3>手机号登录</h3>
<p>未注册的手机号登录后将自动注册</p>
</div>
<div class="form">
<div class="form-item">
<input ref="phone" class="inp" maxlength="11" placeholder="请输入手机号码" type="text" value="19868686868">
</div>
<div class="form-item">
<input ref="captcha" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
<img v-if="picString" :src="picString" alt="" referrerpolicy="no-referrer" @click="getCaptchaImage">
</div>
<div class="form-item">
<input class="inp" placeholder="请输入短信验证码" type="text">
<button :disabled="isButtonDisabled" @click="getSmsCode">
{{ buttonText }}
</button>
</div>
</div>
<div class="login-btn">
登录
</div>
</div>
</div>
</template>登录功能
封装 api 登录接口,实现登录功能
- 查看接口文档 手机验证码登录,封装登录接口
- 登录前的校验 (手机号,图形验证码,短信验证码)
- 调用方法,发送请求,成功添加提示并跳转
示例代码
js
// 此处用于存放所有登录相关的接口请求
import Request from '@/utils/request'
// 获取图形验证码
export const getCaptchaImage = () => Request.get('/captcha/image')
/**
* 获取短信验证码
* @param captchaCode 图形验证码
* @param captchaKey 图形验证码的 key
* @param mobile 接收验证码手机
* @returns {Promise} 返回一个 Promise
*/
export const getSmsCode = (captchaCode, captchaKey, mobile) => Request.post('/captcha/sendSmsCaptcha', { form: { captchaCode, captchaKey, mobile } })
/**
* 登录 login
* @param mobile 手机号
* @param smsCode 短信验证码, 测试环境验证码为:246810
* @returns {Promise} 返回一个 Promise
*/
export const login = (mobile, smsCode) => Request.post('/passport/login', { form: { isParty: false, mobile, partyData: {}, smsCode } })vue
<script>
import './index.less'
import { getCaptchaImage, getSmsCode, login } from '@/api/login'
export default {
name: 'LoginPage',
data() {
return {
picKey: '', // 将来请求传递的图形验证码唯一标识
picString: '', // 存储请求渲染的图片地址
isButtonDisabled: false, // 按钮禁用状态(默认为 false,即可点击状态)
timer: null, // 倒计时的定时器 id
buttonText: '获取验证码', // 按钮文字
}
},
async created() {
await this.getCaptchaImage()
},
beforeUnmount() {
if (this.timer)
clearInterval(this.timer)
},
methods: {
// 获取图形验证码
async getCaptchaImage() {
const { data: { key, base64 } } = await getCaptchaImage()
this.picKey = key
this.picString = base64
},
// 校验 手机号 和 图形验证码 是否合法
// 通过为 true,不通过为 false
validatePhoneAndCaptcha() {
// 校验手机号
if (!/^1[3-9]\d{9}$/.test(this.$refs.phone.value)) {
this.$toast('手机号格式不正确')
return false
}
// 校验图形验证码
if (!this.$refs.captcha.value) {
this.$toast('图形验证码不能为空')
return false
}
// 校验图形验证码是否正确
if (!/^[a-zA-Z0-9]{4}$/.test(this.$refs.captcha.value)) {
this.$toast('图形验证码格式不正确')
return false
}
return true
},
// 获取短信验证码 和 添加倒计时功能
// 倒计时功能:点击获取验证码后,按钮变为不可点击状态,同时开始倒计时,倒计时结束后,按钮恢复可点击状态
// 1. 点击获取验证码按钮,发送请求获取短信验证码
// 2. 请求成功后,按钮变为不可点击状态,同时开始倒计时,button 内容变为 "重新获取 (60)s"
// 3. 倒计时结束后,按钮恢复可点击状态
// 4. ~~如果在倒计时过程中,用户再次点击获取验证码按钮,需要提示用户等待倒计时结束~~ // 无需提示,按钮不可点击即可
// 5. 如果用户在倒计时过程中,离开了当前页面,需要清除倒计时
async getSmsCode() {
// 校验手机号和图形验证码
if (!this.validatePhoneAndCaptcha())
return false
// 如果倒计时还在进行
// if (this.timer) {
// this.$toast('请等待倒计时结束')
// return
// }
try {
const { message } = await getSmsCode(this.$refs.captcha.value, this.picKey, this.$refs.phone.value)
this.$toast(message)
this.isButtonDisabled = true
this.countdown = 10
this.timer = setInterval(() => {
this.countdown--
this.buttonText = `${this.countdown}s 后重新获取`
if (this.countdown === 0) {
// 倒计时结束,清除定时器,按钮禁用状态解除,按钮文字恢复
clearInterval(this.timer)
// this.timer = null
this.isButtonDisabled = false
this.buttonText = '获取验证码'
}
}, 1000)
}
catch (error) {
this.$toast('获取短信验证码失败')
}
},
// 登录
async login() {
// 校验手机号和图形验证码
if (!this.validatePhoneAndCaptcha())
return false
// 校验短信验证码
if (!this.$refs.smsCode.value) {
this.$toast('短信验证码不能为空')
return false
}
// 登录逻辑
try {
const { data: { userId, token }, message } = await login(this.$refs.phone.value, this.$refs.smsCode.value)
this.$toast(message)
console.log(userId, token)
// 登录成功后跳转到首页
await this.$router.push('/')
}
catch (error) {
this.$toast('登录失败')
}
},
},
}
</script>
<template>
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<div class="container">
<div class="title">
<h3>手机号登录</h3>
<p>未注册的手机号登录后将自动注册</p>
</div>
<div class="form">
<div class="form-item">
<input ref="phone" class="inp" maxlength="11" placeholder="请输入手机号码" type="text" value="19868686868">
</div>
<div class="form-item">
<input ref="captcha" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
<img v-if="picString" :src="picString" alt="" referrerpolicy="no-referrer" @click="getCaptchaImage">
</div>
<div class="form-item">
<input ref="smsCode" class="inp" placeholder="请输入短信验证码" type="text">
<button :disabled="isButtonDisabled" @click="getSmsCode">
{{ buttonText }}
</button>
</div>
</div>
<div class="login-btn" @click="login">
登录
</div>
</div>
</div>
</template>响应拦截器统一处理错误提示
- 响应拦截器统一处理错误提示
- 目标:通过响应拦截器,统一处理接口的错误提示
- 问题:每次请求,都会有可能会错误,就都需要错误提示
- 说明:响应拦截器是咱们拿到数据的 第一个 数据流转站,可以在里面统一处理错误。只要不是 200, 就给默认提示,抛出错误
示例代码
js
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({
baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
timeout: 5000,
headers: {
'platform': 'H5',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
},
})
// 在请求或响应被 then 或 catch 处理前拦截它们
// 添加请求拦截器
instance.interceptors.request.use((config) => {
// 在发送请求之前做些什么
return config
}, (error) => {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回服务器的响应结果对象
// 优化 axios 响应结果:可以让逻辑页面少点一层 data 就能拿到后端返回的真正数据对象
const res = response.data
// 如果响应状态码不是 200,说明后端接口出现了错误,这时候就可以在这里统一处理错误:弹出错误提示信息 Toast 和 返回一个失败的 Promise 对象
if (res.status !== 200) {
Toast(res.message)
return Promise.reject(new Error(res.message || 'Error'))
}
return res
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
// console.dir(error);
if (error?.response?.status === 401)
Toast('身份验证失败,请重新登录')
return Promise.reject(error)
})
// 导出自定义配置的 axios 实例
export default instance将登录权证信息存入 vuex
- 目标:vuex 构建 user 模块存储登录权证 (token & userId)
- token 存入 vuex 的好处,易获取,响应式
- vuex 需要分模块 => user 模块
- 步骤:
- 创建 user 模块
- 创建 token 和 userId 状态,提供 mutations,然后挂载到 vuex 上
- 登录成功后,将 token 和 userId 存入 vuex
示例代码
js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
},
})js
const state = {
userInfo: {
token: '',
userId: '',
},
}
const getters = {}
const actions = {}
const mutations = {
setUserInfo(state, { token, userId }) {
state.userInfo = {
token,
userId,
}
},
clearUserInfo(state) {
state.userInfo = {
token: '',
userId: '',
}
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}vue
<script>
import './index.less'
import { mapMutations } from 'vuex'
import { getCaptchaImage, getSmsCode, login } from '@/api/login'
export default {
name: 'LoginPage',
data() {
return {
picKey: '', // 将来请求传递的图形验证码唯一标识
picString: '', // 存储请求渲染的图片地址
isButtonDisabled: false, // 按钮禁用状态(默认为 false,即可点击状态)
timer: null, // 倒计时的定时器 id
buttonText: '获取验证码', // 按钮文字
}
},
async created() {
await this.getCaptchaImage()
},
beforeUnmount() {
if (this.timer)
clearInterval(this.timer)
},
methods: {
...mapMutations(['user/setUserInfo', 'user/clearUserInfo']),
// 获取图形验证码
async getCaptchaImage() {
const { data: { key, base64 } } = await getCaptchaImage()
this.picKey = key
this.picString = base64
},
// 校验 手机号 和 图形验证码 是否合法
// 通过为 true,不通过为 false
validatePhoneAndCaptcha() {
// 校验手机号
if (!/^1[3-9]\d{9}$/.test(this.$refs.phone.value)) {
this.$toast('手机号格式不正确')
return false
}
// 校验图形验证码
if (!this.$refs.captcha.value) {
this.$toast('图形验证码不能为空')
return false
}
// 校验图形验证码是否正确
if (!/^[a-zA-Z0-9]{4}$/.test(this.$refs.captcha.value)) {
this.$toast('图形验证码格式不正确')
return false
}
return true
},
// 获取短信验证码 和 添加倒计时功能
// 倒计时功能:点击获取验证码后,按钮变为不可点击状态,同时开始倒计时,倒计时结束后,按钮恢复可点击状态
// 1. 点击获取验证码按钮,发送请求获取短信验证码
// 2. 请求成功后,按钮变为不可点击状态,同时开始倒计时,button 内容变为 "重新获取 (60)s"
// 3. 倒计时结束后,按钮恢复可点击状态
// 4. ~~如果在倒计时过程中,用户再次点击获取验证码按钮,需要提示用户等待倒计时结束~~ // 无需提示,按钮不可点击即可
// 5. 如果用户在倒计时过程中,离开了当前页面,需要清除倒计时
async getSmsCode() {
// 校验手机号和图形验证码
if (!this.validatePhoneAndCaptcha())
return false
// 如果倒计时还在进行
// if (this.timer) {
// this.$toast('请等待倒计时结束')
// return
// }
try {
const { message } = await getSmsCode(this.$refs.captcha.value, this.picKey, this.$refs.phone.value)
this.$toast(message)
this.isButtonDisabled = true
this.countdown = 10
this.timer = setInterval(() => {
this.countdown--
this.buttonText = `${this.countdown}s 后重新获取`
if (this.countdown === 0) {
// 倒计时结束,清除定时器,按钮禁用状态解除,按钮文字恢复
clearInterval(this.timer)
// this.timer = null
this.isButtonDisabled = false
this.buttonText = '获取验证码'
}
}, 1000)
}
catch (error) {
this.$toast('获取短信验证码失败')
}
},
// 登录
async login() {
// 校验手机号和图形验证码
if (!this.validatePhoneAndCaptcha())
return false
// 校验短信验证码
if (!this.$refs.smsCode.value) {
this.$toast('短信验证码不能为空')
return false
}
// 登录逻辑
try {
const { data: { userId, token }, message } = await login(this.$refs.phone.value, this.$refs.smsCode.value)
this.$toast(message)
// console.log(userId, token)
// 将用户信息存储到 vuex 中
this.$store.commit('user/setUserInfo', { userId, token })
// 登录成功后跳转到首页
await this.$router.push('/')
}
catch (error) {
this.$toast('登录失败')
}
},
},
}
</script>
<template>
<div class="login">
<van-nav-bar left-arrow left-text="返回" title="会员登录" @click-left="$router.back()" />
<div class="container">
<div class="title">
<h3>手机号登录</h3>
<p>未注册的手机号登录后将自动注册</p>
</div>
<div class="form">
<div class="form-item">
<input ref="phone" class="inp" maxlength="11" placeholder="请输入手机号码" type="text" value="19868686868">
</div>
<div class="form-item">
<input ref="captcha" class="inp" maxlength="5" placeholder="请输入图形验证码" type="text">
<img v-if="picString" :src="picString" alt="" referrerpolicy="no-referrer" @click="getCaptchaImage">
</div>
<div class="form-item">
<input ref="smsCode" class="inp" placeholder="请输入短信验证码" type="text">
<button :disabled="isButtonDisabled" @click="getSmsCode">
{{ buttonText }}
</button>
</div>
</div>
<div class="login-btn" @click="login">
登录
</div>
</div>
</div>
</template>vuex 持久化处理
- 目标:封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理
- 步骤:
- 新建
utils/storage.js封装方法 - 在
store/modules/user.js中引入并使用
- 新建
示例代码
js
// 设置 localStorage 的存储键名
const INFO_KEY = 'hm-shopping-userInfo'
// 获取 localStorage 中的个人信息
export const getUserInfoLocalStorage = () => JSON.parse(localStorage.getItem(INFO_KEY) || '{ "token": "", "userId": {} }')
// 设置 localStorage 中的个人信息
export const setUserInfoLocalStorage = userInfo => localStorage.setItem(INFO_KEY, JSON.stringify(userInfo))
// 删除 localStorage 中的个人信息
export const removeUserInfoLocalStorage = () => localStorage.removeItem(INFO_KEY)js
import { getUserInfoLocalStorage, removeUserInfoLocalStorage, setUserInfoLocalStorage } from '@/utils/storage'
const state = {
userInfo: getUserInfoLocalStorage(),
}
const getters = {}
const actions = {}
const mutations = {
setUserInfo(state, { token, userId }) {
state.userInfo = {
token,
userId,
}
setUserInfoLocalStorage({ token, userId })
},
clearUserInfo(state) {
state.userInfo = {
token: '',
userId: '',
}
removeUserInfoLocalStorage()
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}添加请求 loading 效果
- 目标:统一在每次请求后台时,添加 loading 效果
- 背景:有时候因为网络原因,一次请求的结果可能需要一段时间后才能回来,此时,需要给用户 添加 loading 提示。
- 添加 loading 提示的好处:
- 节流处理:防止用户在一次请求还没回来之前,多次进行点击,发送无效请求
- 友好提示:告知用户,目前是在加载中,请耐心等待,用户体验会更好
- 步骤:
- 使用
Toast.loading方法展示加载提示,通过forbidClick属性可以禁用背景点击。 - 请求拦截器中,每次请求,打开 loading
- 响应拦截器中,每次响应,关闭 loading
- 使用
示例代码
js
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例,将来对创建出来的实例,进行自定义配置
// 好处:不会污染原始的 axios 实例
const instance = axios.create({
baseURL: 'http://cba.itlike.com/public/index.php?s=/api/',
timeout: 5000,
headers: {
'platform': 'H5',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
},
})
// 在请求或响应被 then 或 catch 处理前拦截它们
// 添加请求拦截器
instance.interceptors.request.use((config) => {
// 在发送请求之前做些什么
// 使用 Toast.loading 方法展示加载提示
Toast.loading({
message: '加载中...',
forbidClick: true, // 禁止背景点击
loadingType: 'spinner', // 加载图标类型 spinner|circular
duration: 0, // 展示时长 (ms),值为 0 时,toast 不会消失,需要手动调用 Toast.clear()
})
return config
}, (error) => {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回服务器的响应结果对象
// 优化 axios 响应结果:可以让逻辑页面少点一层 data 就能拿到后端返回的真正数据对象
const res = response.data
// 如果响应状态码不是 200,说明后端接口出现了错误,这时候就可以在这里统一处理错误:弹出错误提示信息 Toast 和 返回一个失败的 Promise 对象
if (res.status !== 200) {
Toast(res.message)
return Promise.reject(new Error(res.message || 'Error'))
}
else {
// 使用 Toast.clear() 方法关闭加载提示
Toast.clear()
}
return res
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
// console.dir(error);
if (error?.response?.status === 401)
Toast('身份验证失败,请重新登录')
return Promise.reject(error)
})
// 导出自定义配置的 axios 实例
export default instance页面访问拦截
- 目标:基于全局前置守卫,进行页面访问拦截处理
- 说明:智慧商城项目,大部分页面,游客都可以直接访问, 如遇到需要登录才能进行的操作,提示并跳转到登录
- 但是:对于支付页,订单页等,必须是登录的用户才能访问的,游客不能进入该页面,需要做拦截处理

- 所有的路由一旦被匹配到,都会先经过全局前置守卫
- 只有全局前置守卫放行,才会真正解析渲染组件,才能看到页面内容
示例代码
js
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store'
import LoginPage from '@/views/login/index.vue'
import LayoutPage from '@/views/layout/index.vue'
import SearchPage from '@/views/search/index.vue'
import SearchListPage from '@/views/searchlist/index.vue'
import GoodsDetailPage from '@/views/goodsdetail/index.vue'
import PayPage from '@/views/pay/index.vue'
import OrderPage from '@/views/order/index.vue'
import HomePage from '@/views/layout/home.vue'
import CategoryPage from '@/views/layout/category.vue'
import CartPage from '@/views/layout/cart.vue'
import MyPage from '@/views/layout/my.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/login', name: 'login', component: LoginPage },
{
path: '/',
name: 'layout',
component: LayoutPage,
redirect: '/home',
children: [
{ path: '/home', name: 'home', component: HomePage },
{ path: '/category', name: 'category', component: CategoryPage },
{ path: '/cart', name: 'cart', component: CartPage },
{ path: '/my', name: 'my', component: MyPage },
],
},
{ path: '/search', name: 'search', component: SearchPage },
{ path: '/searchlist', name: 'searchlist', component: SearchListPage },
// 动态路由传参,确认将来是哪个商品,路由参数中携带 id
{
path: '/goodsdetail/:id',
name: 'goodsdetail',
component: GoodsDetailPage,
props: true,
},
{ path: '/pay', name: 'pay', component: PayPage },
{ path: '/order', name: 'order', component: OrderPage },
]
// 所有的路由在真正被访问到之前 (解析渲染对应组件页面前),都会先经过全局前置守卫
// 只有全局前置守卫放行了,才会到达对应的页面
// 全局前置导航守卫
// to: 到哪里去,到哪去的完整路由信息对象 (路径,参数)
// from: 从哪里来,从哪来的完整路由信息对象 (路径,参数)
// next(): 是否放行
// (1) next() 直接放行,放行到 to 要去的路径
// (2) next(路径) 进行拦截,拦截到 next 里面配置的路径
// 定义一个数组,专门用户存放所有需要权限访问的页面
const authUrls = ['/pay', '/order']
routes.beforeEach((to, from, next) => {
// console.log('全局前置导航守卫', to, from)
// 如果要去的页面在 authUrls 中,就需要校验登录状态
if (authUrls.includes(to.path)) {
// 校验登录状态
const token = store.getters.getToken
if (token) {
next()
}
else {
// 没有登录,跳转到登录页
next('/login')
}
}
else {
// 不需要校验登录状态的页面,直接放行
next()
}
})
const router = new VueRouter({
mode: 'history',
routes,
})
export default routerjs
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
},
getters: {
getToken(state) {
return state.user.userInfo.token
},
},
})