文章目录
- 一、加入购物车
- 1. 添加到购物车的接口
- 2. 点击按钮的回调函数
- 3. 请求成功后进行路由跳转
- (1)、创建路由并配置路由规则
- (2)、路由跳转并传参(本地存储)
- 二、购物车页面的业务
- 1. uuid生成用户id
- 2. 获取购物车数据
- 3. 计算打勾商品总价
- 4. 全选与商品打勾
- (1)、商品全部打勾,自动勾全选(every方法)
- (2)、打勾单个商品,更新数据
- (3)、点击全选框,对每个商品的单选框进行操作
- 5. 删除购物车
- (1)、删除单个商品
- (2)、删除选中的商品
- 6. 处理购物车的产品数量(难)
一、加入购物车
思路分析:点击加入购物车按钮
后,需要进行如下的几个操作
(1)、向服务器发送请求,将产品信息(产品Id,购买的产品数量)发给服务器进行存储。
(2)、服务器存储成功,进行路由跳转,跳转到加入购物车成功的界面。
(3)、服务器存储失败,给用户提示。
1. 添加到购物车的接口
写发送请求的接口:api/index.js
// 添加到购物车(对已有物品进行数量改动) url: /api/cart/addToCart/{ skuId }/{ skuNum } 请求方式post
export const reqAddOrUpdateShopCart = (skuId, skuNum) => {
return requests({ url: `/cart/addToCart/${skuId}/${skuNum} `, method: 'post' })
}
2. 点击按钮的回调函数
<!-- 加入购物车 -->
<div class="add">
<a href="javascript:" @click="addShopCar">加入购物车</a>
</div>
<script>
addShopCar () {
// 1. 派发action请求,将数据给服务器进行存储
this.$store.dispatch('detail/addOrUpdateShopCart', { skuId: this.$route.params.goodsId, skuNum: this.skuNum })
// 2. 服务器存储成功---进行路由跳转并携带参数
// 3. 服务器存储失败----给用户提示
}
</script>
这里需要注意的是
(1)、这里发请求不需要服务器返回数据,所以拿到服务器的返回结果时并不需要存储。服务器的返回结果只会告知请求是发送成功or发送失败。
(2)、问题: 请求的返回结果在仓库的actions里,组件中如何拿到这个返回结果以判断是请求成功还是请求失败。
解决:async
函数返回的是一个Promise对象,组件中派发请求相当于调用了这个async
函数,应该得到一个Promise对象。那我们就在这个async
函数里判断请求成功或失败来实例化一个Promise对象,返回给组件。
// detail小仓库里:
async addOrUpdateShopCart (context, { skuId, skuNum }) {
let result = await reqAddOrUpdateShopCart(skuId, skuNum)
if (result.code == 200) {
// 加入购物车成功
return 'ok'
} else {
// 加入购物车失败
return Promise.reject(new Error('faile'))
}
}
回调函数中接收到这个
addShopCar () {
// 1. 派发action请求,将数据给服务器进行存储:this.$store.dispatch('detail/addOrUpdateShopCart')就是在调用这个函数addOrUpdateShopCart
try {
this.$store.dispatch('detail/addOrUpdateShopCart', { skuId: this.$route.params.goodsId, skuNum: this.skuNum })
// 2. 服务器存储成功---进行路由跳转并携带参数
this.$router.push({ name: 'addCartSuccess', })
} catch (error) {
// 3. 存储失败----给用户提示
alert(error.message)
}
}
3. 请求成功后进行路由跳转
(1)、创建路由并配置路由规则
1. 创建路由
将路由组件添加到pages
文件夹
2. 配置路由规则
(2)、路由跳转并传参(本地存储)
如果路由跳转的时候不携带产品信息参数,则需要在跳转页面之后根据产品Id再次发送请求。这样做没必要。直接携带就好了。但是之前路由跳转传参都是简单的数字之类的。这里的产品信息(产品名称,产品属性)涉及到对象,还要传递购买的产品数量。
二、购物车页面的业务
1. uuid生成用户id
进入购物车页面时,应将用户的id传给后端,后端根据用户id检索出该用户的购物车商品信息,传递给前端用于展示及进行其他操作。在这个系统中,我们用uuid
库生成一个用户Id,在向后台发送请求时传递给后端。
1、创建文件utils/uuid_token.js
:
需要注意:一个用户只能有一个Id,所以这里采用本地存储来存储uuid生成的用户id。每次先读取本地存储中是否有用户id,有的话直接用,没有就生成一个新的。
// 用于生成用户的临时身份Id
import { v4 as uuidv4 } from 'uuid'
/*
- 先检查本地存储中有没有这个用户Id,
- 有就直接返回,
- 没有则生成新的
*/
export const getUUID = () => {
let uuid_token = localStorage.getItem('UUID_TOKEN')
// 如果没有
if (!uuid_token) {
// 生成用户id
uuid_token = uuidv4();
// 存储到
localStorage.setItem('UUID_TOKEN', uuid_token)
}
return uuid_token
}
2、在仓库里调用函数,得到userTempId
.
3、将userTempId
配置在请求头里
查看文档里的接口会发现:请求中并没有让携带参数,所以除了通过参数能将数据传递给后台,还能用什么方式呢?答:请求头。可以将用户Id放在请求头中。
2. 获取购物车数据
1、写接口
api/index.js
// 获取购物车列表 url: /api/cart/cartList 请求方式 get
export const reqShopCart = () => {
return requests({ url: '/cart/cartList ', method: 'get' })
}
2、vuex三连环
3、组件发请求
因为在购物车页面,其他的操作也会重新向服务器发请求,所以这里将放请求的操作包装成一个函数。
根据获取到的数据渲染界面
3. 计算打勾商品总价
computed: {
...mapState('shopCart', ['cartInfoList']),
// 计算总价
totalPrice () {
let sum = 0
this.cartInfoList.forEach((el) => {
if (el.isChecked === 1) { // 商品是否被选中
sum += (el.skuPrice * el.skuNum)
}
})
return sum
},
}
4. 全选与商品打勾
(1)、商品全部打勾,自动勾全选(every方法)
<div class="select-all">
<input class="chooseAll" type="checkbox" :checked="isAllChecked"/>
<span>全选</span>
</div>
<script>
computed:{
isAllChecked () {
// 统计checked的数量,是否等于数组长度
let flag = this.cartInfoList.every((item) => {
return item.isChecked == 1
})
return flag
}
}
</script>
关于every方法,回顾博客:
(2)、打勾单个商品,更新数据
选中某个商品后,商品的isChecked
属性发生变化,需要将更新后的商品信息发给后台保存。(我感觉这个业务在实际中没必要,每次勾选或取消勾选都发送请求,服务器压力增大,没必要。但是这里是为了练习一些知识点)。
接口
// 切换商品选中状态 url: /api/cart/checkCart/{skuID}/{isChecked} 请求方式 get
export const changeIsChecked = (skuID, isChecked) => {
return requests({ url: `/cart/checkCart/${skuID}/${isChecked}`, method: 'get' })
}
仓库Vuex:
// 改变商品选中的状态
async changeChecked (context, { skuID, isChecked }) {
let res = await changeIsChecked(skuID, isChecked)
if (res.code === 200) {
return 'ok'
} else {
return Promise.reject(new Error('faile'))
}
},
组件:
<!-- 选择框 -->
<li class="cart-list-con1">
<input type="checkbox" name="chk_list" :checked="good.isChecked == 1"
@change="handleChange(good.skuId, $event)" />
</li>
<script>
// 切换商品选中的状态
async handleChange (skuId, e) {
try {
// 接口中的参数是1或0,不是true 或false
let isChecked = e.target.checked ? 1 : 0
await this.$store.dispatch('shopCart/changeChecked', { skuID: skuId, isChecked: isChecked })
this.getCartData()// 成功重新发请求
} catch (e) {
alert(e.message)
}
}
</script>
(3)、点击全选框,对每个商品的单选框进行操作
由于没有修改多个商品的接口,所以思路是多次调用打勾单个商品的接口,把每个商品的勾选状态改为当前全选框的状态。
此处需要注意在actions
里,如何通过dispatch
调用其他actions
的函数
// 改变商品选中的状态
async changeChecked (context, { skuID, isChecked }) {
let res = await changeIsChecked(skuID, isChecked)
if (res.code === 200) {
return 'ok'
} else {
return Promise.reject(new Error('faile'))
}
},
// 改变所有商品的状态
// 第一个参数是context,包含:state,dispatch,getters登,通过解构将需要的解构出来
async changeAllState ({ dispatch, state }, stateFlag) {
let promiseAll = []
state.cartInfoList.forEach(el => {
if (el.isChecked !== stateFlag) {
// 通过dispatch调用其他的actions函数
let promise = dispatch('changeChecked', { skuID: el.skuId, isChecked: stateFlag })
promiseAll.push(promise)
}
});
return Promise.all(promiseAll)
},
给组件添加的全选点击事件
<div class="select-all">
<!--&& cartInfoList.length > 0 是考虑到处于当商品全部被删除时,全选框不应该处于选中状态-->
<input class="chooseAll" type="checkbox"
:checked="isAllChecked && cartInfoList.length > 0"
@change="handleAllChange" />
<span>全选</span>
</div>
<script>
async handleAllChange (e) {
try {
// e.target.checked获取全选框的值,
let stateFlag = e.target.checked ? 1 : 0
await this.$store.dispatch('shopCart/changeAllState', stateFlag)
// 成功则重新发送请求
this.getCartData()
} catch (error) {
alert(error.message);
}
}
</script>
5. 删除购物车
文档接口:
(1)、删除单个商品
接口:
// 删除购物车商品 url:/api/cart/deleteCart/{skuId} 请求方式:delete
export const reqDeleteGoodById = (skuId) => {
return requests(
{
url: `/cart/deleteCart/${skuId}`,
method: 'delete'
})
}
Vuex (这里也不需要三连环)
// 删除商品
async deleteOneGood (context, skuId) {
let res = await reqDeleteGoodById(skuId)
if (res.code === 200) {
return 'ok'
} else {
return Promise.reject(new Error('failed'))
}
}
组件:
<a class="sindelet" @click="deleteOneGood(good.skuId)">删除</a>
<script>
// 删除一个商品
async deleteOneGood (skuId) {
try {
await this.$store.dispatch('shopCart/deleteOneGood', skuId)
this.getCartData() // 重新发送请求
} catch (error) {
alert(error.message)
}
}
</script>
注意这里有一个bug!
(2)、删除选中的商品
没有删除多个商品的接口,所以思路是多次调用删除单个商品的接口,将选中的商品全都删除。
组件:
// 删除已选中的商品;
async deleteAllChecked () {
try {
await this.$store.dispatch('shopCart/deleteAllSelected')
// 重新发请求获取购物车列表
this.getCartData()
} catch (error) {
alert(error.message)
}
}
重点是Vuex中:
如果有一个删除失败,则这个操作就是失败了。采用Promise.all()
方法给组件反馈成功或失败
actions:{
// 删除全部商品
deleteAllSelected ({ dispatch, state }) {
let promiseAll = []
// 获取购物车的全部商品
state.cartInfoList.forEach((el) => {
// 如果被该商品被勾选
if (el.isChecked === 1) {
let promise = dispatch('deleteOneGood', el.skuId)
// 将每一次返回的Promise添加到数组当中
promiseAll.push(promise)
}
})
//Promise.all([p1,p2,p3]) p1,p2,p3都是Promise对象,其中有一个失败则全失败
return Promise.all(promiseAll)
}
}
6. 处理购物车的产品数量(难)
这里用到的接口之前写过,注意参数的描述!
根据这个文档描述,猜测这个skuNum
的值是商品数量的变化量。
这个接口可以用在两个地方,添加到购物车和对已有物品进行数量改动。在添加到购物车的场景中,skuNum
的值一直都是正的值。
HTML结构:
<!--修改产品数量 -->
<li class="cart-list-con5">
<!-----减号------>
<a class="mins" @click="handler('sub', -1, good)">-</a>
<!-----文本框输入,注意事件是失去焦点事件blur。$event.target.value * 1 是因为 非数值字符串*1 的值为NaN ------>
<input autocomplete="off"
type="text"
:value="good.skuNum"
minnum="1"
class="itxt"
@blur="handler('input', $event.target.value * 1, good)"
/>
<!----加号------>
<a class="plus" @click="handler('add', 1, good)">+</a>
</li>
回调函数:
/*
type:操作的类型 add sub input
disNum: 1,-1,输入框的最终数
good:产品
*/
async handler (type, disNum, good) {
// 判断不同的情况
switch (type) {
case 'add': // 加
disNum = 1;
break;
case 'sub': // 减
if (good.skuNum > 1) {
disNum = -1
} else {
disNum = 0
}
break;
case 'input':
if (isNaN(disNum) || disNum < 0) {
disNum = 0
} else {
// 增量
disNum = parseInt(disNum) - good.skuNum
}
break;
}
// 发送请求
try {
await this.$store.dispatch('detail/addOrUpdateShopCart', { skuId: good.skuId, skuNum: disNum })
this.getCartData() // 请求成功,重新获取数据
} catch (e) {
alert(e.message)
}
}
但是这样做,有个小bug。当用户频繁点击-
是,数量可能会出现负值
这是因为连续快速点击,请求还来不及发送,数据没改,所以每次disnum都是-1。解决办法就是节流,给服务器一些缓冲的时间,防止数据不同步出现上述bug。
// 按需引入
import throttle from 'lodash/throttle'
handler: throttle(async function (type, disNum, good) {
// 判断不同的情况
switch (type) { ... }
try {
await this.$store.dispatch('detail/addOrUpdateShopCart', { skuId: good.skuId, skuNum: disNum })
this.getCartData()
} catch (e) {
alert(e.message)
}
}, 800)
最后分析一下,这个功能为什么要设计的这么麻烦,
(1) 为什么每+
或-
或输入一次都要发送一次请求,
(2) 为什么不能直接把输入框的商品最终数量传给后端,还要计算差值。
答:
(1) 每+
或-
或输入一次都是对数据进行了修改。购物车的商品数量改变后,刷新页面时,数量应该保持修改后的样子。所以每次操作完都要保存一下数据。
(2) 为什么要计算差值?因为这个接口还用来实现将商品添加到购物车。
我用淘宝试了一下,这个业务逻辑是当在页面详情页,多次重复的将商品添加到购物车时。购物车里该商品的数量应该是这几次重复添加的数量的和。而不是最后一次添加时选择的商品数量。
所以如果只将输入框里的最终数量传过去的话,商品数量只会是最后一次将该商品添加到购物车里时的选择的数量。