目录
登录-表单校验实现
表单如何进行校验
表单校验步骤
自定义校验规则
整个表单的内容验证
登录-基础登录业务实现
登录业务流程
Pinia管理用户数据
如何使用Pinia管理数据
关键代码总结
登录-Pinia用户数据持久化
持久化用户数据说明
编辑关键步骤总结和插件运行机制
登录-请求拦截器携带Token
为什么要在请求拦截器携带Token
如何配置
登录-退出登录功能实现
退出登录业务实现
购物车功能实现
购物车业务逻辑梳理拆解
本地购物车 - 加入购物车实现
1. 添加购物车
2. 删除功能实现
3列表购物车-单选功能实现
4.列表购物车-全选功能实现
5.列表购物车-统计数据功能实现
总结
登录-表单校验实现
为什么需要校验
作用:前端提前校验可以省去一些错误的请求提交,为后端节省接口压力
表单如何进行校验
ElementPlus表单组件内置了表单校验功能,只需要按照组件要求配置必要参数即可(直接看文档)
思想:当功能很复杂时,通过多个组件各自负责某个小功能,再组合成一个大功能是组件设计中的常用方法 高级软件人才培训
表单校验步骤
自定义校验规则
ElementPlus表单组件内置了初始的校验配置,应付简单的校验只需要通过配置即可,如果想要定制一些特殊的校验需 求,可以使用自定义校验规则,格式如下:
校验逻辑:如果勾选了协议框,通过校验,如果没有勾选,不通过校验
整个表单的内容验证
思考:每个表单域都有自己的校验触发事件,如果用户一上来就点击登录怎么办呢? 答:在点击登录时需要对所有需要校验的表单进行统一校验
<script setup>
import { ref } from 'vue'
// 表单数据对象
const userInfo = ref({
account: '1311111111',
password: '123456',
agree: true
})
// 规则数据对象
const rules = {
account: [
{ required: true, message: '用户名不能为空' }
],
password: [
{ required: true, message: '密码不能为空' },
{ min: 6, max: 24, message: '密码长度要求6-14个字符' }
],
agree: [
{
validator: (rule, val, callback) => {
return val ? callback() : new Error('请先同意协议')
}
}
]
}
</script>
<template>
<div class="form">
<el-form ref="formRef" :model="userInfo" :rules="rules" status-icon>
<el-form-item prop="account" label="账户">
<el-input v-model="userInfo.account" />
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input v-model="userInfo.password" />
</el-form-item>
<el-form-item prop="agree" label-width="22px">
<el-checkbox v-model="userInfo.agree" size="large">
我已同意隐私条款和服务条款
</el-checkbox>
</el-form-item>
<el-button size="large" class="subBtn" >点击登录</el-button>
</el-form>
</div>
</template>
登录-基础登录业务实现
登录业务流程
Pinia管理用户数据
基本思想:Pinia负责用户数据相关的state和action,组件中只负责触发action函数并传递参数
如何使用Pinia管理数据
遵循理念:和数据相关的所有操作(state + action)都放到Pinia中,组件只负责触发action函数
关键代码总结
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/apis/login'
import { mergeCartAPI } from '@/apis/cart'
import { useCartStore } from '@/stores/cart'
export const useUserStore = defineStore('user', () => {
// 1. 定义管理用户数据的state
const userInfo = ref({})
const CartStore = useCartStore()
// 2. 定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
userInfo.value = res.result
// 合并购物车
let result = CartStore.cartList.map(item => {
return {
skuId: item.skuId,
selected: item.selected,
count: item.count
}
})
mergeCartAPI(result)
//重新获取数据
CartStore.updateNewList()
}
// 退出时清除用户信息
const clearUserInfo = () => {
userInfo.value = {}
CartStore.clearCart()
}
// 3. 以对象的格式把state和action return
return {
getUserInfo,
userInfo,
clearUserInfo
}
}, {
persist: true,
})
登录-Pinia用户数据持久化
持久化用户数据说明
1. 用户数据中有一个关键的数据叫做Token (用来标识当前用户是否登录),而Token持续一段时间才会过期 2. Pinia的存储是基于内存的,刷新就丢失,为了保持登录状态就要做到刷新不丢失,需要配合持久化进行存储
目的:保持token不丢失,保持登录状态 最终效果:操作state时会自动把用户数据在本地的localStorage也存一份,刷新的时候会从localStorage中先取
关键步骤总结和插件运行机制
//下载
npm i pinia-plugin-persistedstate
//注册
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
//使用
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
persist: true,
}
)
登录-请求拦截器携带Token
为什么要在请求拦截器携带Token
Token作为用户标识,在很多个接口中都需要携带Token才可以正确获取数据,所以需要在接口调用时携带Token。另 外,为了统一控制采取请求拦截器携带的方案
如何配置
Axios请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token数据会被注入到请求header中,格式按 照后端要求的格式进行拼接处理
// axios请求拦截器
http.interceptors.request.use(config => {
const UserStore = useUserStore();
const token = UserStore.userInfo.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, e => Promise.reject(e))
登录-退出登录功能实现
退出登录业务实现
基础思想:
- 清除用户信息
- 跳转到登录页
1- 新增清除用户信息action
// 退出时清除用户信息
const clearUserInfo = () => {
userInfo.value = {}
}
2- 组件中执行业务逻辑
<script setup>
import { useUserStore } from '@/stores/userStore'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const confirm = () => {
console.log('用户要退出登录了')
// 退出登录业务逻辑实现
// 1.清除用户信息 触发action
userStore.clearUserInfo()
// 2.跳转到登录页
router.push('/login')
}
</script>
购物车功能实现
购物车业务逻辑梳理拆解
1. 整个购物车的实现分为俩个大分支,本地购物车操作和接口购物车操作
2. 由于购物车数据的特殊性,采取Pinia管理购物车列表数据并添加持久化缓存
本地购物车 - 加入购物车实现
1. 添加购物车
基础思想:如果已经添加过相同的商品,就在其数量count上加一,如果没有添加过,就直接push到购物车列表中
// 封装购物车模块
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore = defineStore('cart', () => {
// 1. 定义state - cartList
const cartList = ref([])
// 2. 定义action - addCart
const addCart = (goods) => {
console.log('添加', goods)
// 添加购物车操作
// 已添加过 - count + 1
// 没有添加过 - 直接push
// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
const item = cartList.value.find((item) => goods.skuId === item.skuId)
if (item) {
// 找到了
item.count++
} else {
// 没找到
cartList.value.push(goods)
}
}
return {
cartList,
addCart
}
}, {
persist: true,
})
2. 删除功能实现
1- 添加删除action函数
// 删除购物车
const delCart = async (skuId) => {
// 思路:
// 1. 找到要删除项的下标值 - splice
// 2. 使用数组的过滤方法 - filter
const idx = cartList.value.findIndex((item) => skuId === item.skuId)
cartList.value.splice(idx, 1)
}
2- 组件触发action函数并传递参数
<i class="iconfont icon-close-new" @click="cartStore.delCart(i.skuId)"></i>
3。列表购物车-单选功能实现
基本思想:通过skuId找到要进行单选操作的商品,把控制是否选中的selected字段修改为当前单选框的状态
1- 添加单选action
// 单选功能
const singleCheck = (skuId, selected) => {
// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selected
const item = cartList.value.find((item) => item.skuId === skuId)
item.selected = selected
}
2- 触发action函数
<script setup>
// 单选回调
const singleCheck = (i, selected) => {
console.log(i, selected)
// store cartList 数组 无法知道要修改谁的选中状态?
// 除了selected补充一个用来筛选的参数 - skuId
cartStore.singleCheck(i.skuId, selected)
}
</script>
<template>
<td>
<!-- 单选框 -->
<el-checkbox :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" />
</td>
</template>
4.列表购物车-全选功能实现
基础思想:
- 全选状态决定单选框状态 - 遍历cartList把每一项的selected都设置为何全选框状态一致
- 单选框状态决定全选状态 - 只有所有单选框的selected都为true, 全选框才为true
1- store中定义action和计算属性
// 全选功能action
const allCheck = (selected) => {
// 把cartList中的每一项的selected都设置为当前的全选框状态
cartList.value.forEach(item => item.selected = selected)
}
// 是否全选计算属性
const isAll = computed(() => cartList.value.every((item) => item.selected))
2- 组件中触发aciton和使用计算属性 、
<script setup>
const allCheck = (selected) => {
cartStore.allCheck(selected)
}
</script>
<template>
<!-- 全选框 -->
<el-checkbox :model-value="cartStore.isAll" @change="allCheck" />
</template>
5.列表购物车-统计数据功能实现
// 3. 已选择数量
const selectedCount = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count, 0))
// 4. 已选择商品价钱合计
const selectedPrice = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count * c.price, 0))
总结
本地购物车与线上购物车合并
判断有没有token 如果有就走线上 如果没有就走本地
退出登录时清除本地购物车
登录时 跟购物车合并并重新查询
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useUserStore } from '@/stores/user'
import { insertCartAPI, findNewChartList, delCartAPI } from '@/apis/cart'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const isLogin = computed(() => userStore.userInfo.token)
// 1. 定义state - cartList
const cartList = ref([])
// 获取购物车列表
const updateNewList = async () => {
let res = await findNewChartList()
console.log(res, '购物车列表');
cartList.value = res.result
}
// 2. 定义action - addCart
const addCart = async (goods) => {
const { skuId, count } = goods
console.log('添加', goods)
if (isLogin.value) {
// 登录之后的加入购车逻辑
await insertCartAPI({ skuId, count })
updateNewList()
} else {
// 添加购物车操作
// 已添加过 - count + 1
// 没有添加过 - 直接push
// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
const item = cartList.value.find((item) => goods.skuId === item.skuId)
if (item) {
// 找到了
item.count++
} else {
// 没找到
cartList.value.push(goods)
}
}
}
// 删除购物车
const delCart = async (skuId) => {
if (isLogin.value) {
// 调用接口实现接口购物车中的删除功能
await delCartAPI([skuId])
updateNewList()
} else {
// 思路:
// 1. 找到要删除项的下标值 - splice
// 2. 使用数组的过滤方法 - filter
const idx = cartList.value.findIndex((item) => skuId === item.skuId)
cartList.value.splice(idx, 1)
}
};
// 单选功能
const singleCheck = (skuId, selected) => {
// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selected
const item = cartList.value.find((item) => item.skuId === skuId)
item.selected = selected
}
// 全选功能action
const allCheck = (selected) => {
// 把cartList中的每一项的selected都设置为当前的全选框状态
cartList.value.forEach(item => item.selected = selected)
}
// 清除购物车
const clearCart = () => {
cartList.value = []
}
const allCount = computed(() => cartList.value.reduce((acc, item) => acc + item.count, 0))
const allPrice = computed(() => cartList.value.reduce((acc, item) => acc + item.price * item.count, 0))
const isAll = computed(() => cartList.value.every((item) => item.selected))
// 3. 已选择数量
const selectedCount = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count, 0))
// 4. 已选择商品价钱合计
const selectedPrice = computed(() => cartList.value.filter(item => item.selected).reduce((a, c) => a + c.count * c.price, 0))
return {
cartList,
addCart,
delCart,
allCount,
allPrice,
singleCheck,
allCheck,
isAll,
selectedCount,
selectedPrice,
clearCart,
updateNewList
}
}, {
persist: true,
})