Vue2电商前台项目——完成加入购物车功能和购物车页面
一、加入购物车
1、路由跳转前先发请求把商品数据给服务器
(1)观察接口文档
这里其实只需要把已有物品的id和数量传给后台,后台不需要返回数据
(2)写接口
又到了咱滚瓜烂熟的写接口环节:
src/api/index.js
// 将产品添加到购物车中
export const reqAddShopCart = (skuId, skuNum) => {
return requests({
url: `/cart/addToCart/{skuId}/{skuNum}`,
method: "post",
});
(3)dispatch传数据
1、给加入购物车按钮添加点击事件
2、派送actions,然后把数据以对象的形式传过去,第一个键值对是商品id(当从Search到Detail路由跳转时就传过来的params参数),第二个键值对是购物车数量(我们之前已经存到了Detail组件的data里了)
// 加入购物车的回调函数
addShopcar() {
// 派发actions
// 1、发请求——将产品加入到数据库(通知服务、传递参数器)
this.$store.dispatch("getShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
// 2、服务器存储成功——进行路由跳转
// 3、失败——给用户进行提示
},
3、actions这边通过解构赋值,调用接口并把数据传给服务器(后端数据库)
const actions = {
......
async getShopCart({ commit }, { skuId, skuNum }) {
let result = await reqAddShopCart(skuId, skuNum);
console.log("添加到购物车的信息" + result);
},
};
(4)判断服务器是否已经收到商品数据
这里要判断请求是否已经成功,也就是服务器是否收到了要加入购物车商品的数据,若成功了就要进行路由跳转并传参,若失败了就要给用户提示。
dispatch
就会调用这个actions
里的函数,调用这个async函数返回一个promise
对象,这个Promise对象的状态和结果值取决于这个async函数的返回值。
1、如果返回一个非Promise对象,那么就是成功,值就是返回值;
2、如果返回Promise对象,那么状态和结果值取决于该Promise
3、如果不写返回值,且await后是失败的Promise,那么就会抛出异常,既然抛出异常,那么async函数返回的就是一个失败的Promise。
4、如果不写返回值,且await后是成功的Promise,那么就会返回undefined,async函数返回的就是一个成功的Promise,值是undefined。
const actions = {
......
//加入购物车
//异步请求,把商品id和数量发送给服务器
async getShopCart({ commit }, { skuId, skuNum }) {
//其实这里不用try-catch,因为那边已经try了,不写return默认返回undefined(成功的Primise)
//如果await后成功则返回undefined(没写return,返回成功的Promise)
//如果失败则抛出异常(返回失败的Promise)
let result = await reqAddShopCart(skuId, skuNum);
console.log("添加到购物车的信息" + result);
//这里只是把购物车数据给服务器,但是服务器不需要返回什么东西。所以这里我们不用再三连环了,通过dispatch把数据给服务器就已经好了
// if (result.code == 200) {
// commit("GETSHOPCART", result.data);
// }
},
};
methods: {
......
// 加入购物车的回调函数
async addShopcar() {
// 派发actions
// 1、发请求——将产品加入到数据库(通知服务、传递参数器)
try {
// 2、服务器存储成功——进行路由跳转
this.$store.dispatch("getShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
} catch (error) {
// 3、失败——给用户进行提示
console.log("请求失败", error.message);
}
},
},
2、请求成功后进行路由跳转
路由组件通常存放在
pages
文件夹,一般组件通常存放在components
文件夹。
(1)创建路由并配置路由规则
1、所以我们先创建路由
2、配置路由规则src/router/routes.js
{
name: "addcat",
path: "/addcartsuccess",
component: AddCartSuccess,
meta: {
showFooter: true,
},
(2)路由跳转并传参(练习本地存储)
3、写路由跳转和传参的代码
这里要传两个值:skuInfo对象和商品数量shopCarNum(其实skuInfo根本不用传,直接去仓库读就行了)。传参的时候最好别用params和query,因为带过去的需要是skuInfo这个对象,那么对象传过去的话,地址栏可能会是乱码。这里采用的方案是:会话存储——点此复习
1、使用query传简单的商品数量shopCarNum
,因为数字不会乱码
2、使用会话存储(本地存储也行)带skuInfo
过去(其实直接从仓库读就行了;或者你用query也行,无非就是地址栏乱码)
async addShopcar() {
// 派发actions
// 1、发请求——将产品加入到数据库(通知服务、传递参数器)
try {
// 2、服务器存储成功——进行路由跳转
await this.$store.dispatch("getShopCart", {
skuId: this.$route.params.skuid,
skuNum: this.skuNum,
});
// 进行路由跳转并传参
// 一些简单的数据可以通过query形式传参
// 此处的产品信息比较复杂,是一个对象,所以我们可以采用会话存储sessionStorage
// 本地存储和会话存储一般存的是字符串,所以我们把对象转化成JSON字符串进行存储
sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo));
this.$router.push({ name: "addcat", query: { skuNum: this.skuNum } });
} catch (error) {
// 3、失败——给用户进行提示
console.log("请求失败", error.message);
}
},
注意:
- 本地存储 里面只能存储字符串格式 ,因此需要把对象转换为字符串
JSON.stringify()
- 获取本地存储数据,需要把里面的字符串转换为对象格式
JSON.parse()
我们才能使用里面的数据。
然后把相应的数据放到页面上
二、完成购物车页面的业务
点击查看商品详情就直接跳回去就行了,数据仓库本来就有不用重新发请求
<router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">
查看商品详情
</router-link>
接下来是点击去购物车结算,跳到购物车结算页面
把购物车部分的静态搞过来,然后注册一下路由,并写个路由跳转。
<router-link to="/shopcart">去购物车结算 > </router-link>
1、生成游客id
这里后端应该是写了个逻辑,用一个叫userTempId
的请求头字段来判断你是谁,然后返回给你相应的数据。
一般来说正常的逻辑应该是每个用户有自己的token,然后点击加入购物车之后,往用户-商品这个表里添加一行数据;读购物车取数据的时候呢,应该是传用户token参数获取相应的商品列表。而这里为了模拟,后端写好了useTempId字段,刷新时我们就给他个游客id(唯一id),它就拿着这个字段直接作为本地浏览器游客,所以请求购物车数据也不用传参。
这里随机生成游客id的方法有很多,可以使用nanoid、uuid、时间戳,这里我使用的是uuid。并且需要持久存储我们可以使用localStorage。
1、进入页面的时候先随机生成一个时间戳,作为用户的id。
2、请求数据时,在请求拦截器中将该id作为请求中userTempId
的值。也就是把这个id放在请求头里传给服务器。
3、这样就完成了,只要本地存储中这个id没有被手动清除,那么每次都可以获取该id的购物车数据。
2、获取相应的购物车数据
写接口
// 获取购物车列表数据
export const reqCartList = () => {
return requests({
url: "/cart/cartList",
method: "get",
});
};
发请求,三连环。
最后,把数据展示在页面上
3、计算打勾商品总价
这个好算,利用isChecked属性,只计算选中(isChecked=1)的价格,forEach循环一下就行了。
computed: {
......
// 计算购买商品的总价
totalPrice() {
let totalPrice = 0;
this.cartInfoList.forEach((element) => {
if (element.isChecked == 1) {
totalPrice += element.skuNum * element.skuPrice;
}
});
return totalPrice;
},
4、全选和商品的打勾联动
(1)全选按钮是否选中
全选按钮是否选中,取决于每个复选框是否都选中,也就是判断每个元素的isChecked是不是都为1
// 判断全选框是否勾选(若每个产品都勾选了,也就是isCheck都等于一,则勾选)
isAllChecked() {
// every:遍历每个元素
// 只要有一个不等于1就返回false
return this.cartInfoList.every((item) => item.isChecked == 1);
},
(2)修改单个产品的选中状态
修改单个产品状态需要去发送请求修改isChecked
字段,这是因为总价那里用到了这个字段去计算,我们要实现勾选的计算总价,取消勾选就不计算。
修改产品勾选状态的接口,需要传两个参数:skuId(产品id)、isChecked(产品的选中状态)
下面依旧是咱们熟悉的步骤。
写接口:
// 修改购物车产品选中状态
export const reqUpdateCheck = (skuId, isChecked) => {
return requests({
url: `/cart/checkCart/${skuId}/${isChecked}`,
method: "get",
});
};
三连环vuex调用接口:
const actions = {
// 修改购物车某个产品的选中状态
async updateChecked({ commit }, { skuId, isChecked }) {
let result = await reqUpdateCheck(skuId, isChecked);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("fail"));
}
},
};
每个购物车商品按钮配置一个点击事件(或者切换事件)如果当前勾选状态为1(勾选),那么改成0(取消勾选),反之也一样。
在html添加事件:
// 修改某个产品的勾选状态
async updateChecked(cart, $event) {
// 带过去的isChecked原本是布尔值,但是我们需要的应该是0或者1
// console.log(event.target.checked);
try {
// 如果修改成功,再次获取服务器数据
let checked = event.target.checked ? "1" : "0";
await this.$store.dispatch("updateChecked", {
skuId: cart.skuId,
isChecked,
});
this.getData();
} catch (error) {
alert("修改失败" + error);
}
},
(3)点击全选时所有商品状态跟着切换
给全选的勾选框添加点击事件。总体来说主要思路就是点击全选时派发请求,这个请求需要把每个商品的勾选状态改成当前全选框的状态。
所以需要在actions中遍历购物车数据并派发请求,(当然其实在组件中写也一样,就是这样规范点),用try-catch捕获,如果都请求成功,那么就使用Promise.all获取成功的标志
// 修改购物车某个产品的选中状态
async updateChecked({ commit }, { skuId, isChecked }) {
let result = await reqUpdateCheck(skuId, isChecked);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("fail"));
}
},
// 点击全选按钮修改所有商品的状态
changeAllChecked({ dispatch, state }, isChecked) {
let promiseAll = [];
state.cartList[0].cartInfoList.forEach((el) => {
try {
let promise = dispatch("updateChecked", {
skuId: el.skuId,
isChecked,
});
promiseAll.push(promise);
} catch (error) {
console.log(`${el.skuNum}修改失败`, err);
}
});
return Promise.all(promiseAll);
},
然后去组件中给全选添加点击事件就行
// 修改全部产品的选中状态
async changeAllChecked() {
try {
let isChecked = event.target.checked ? "1" : "0";
await this.$store.dispatch("changeAllChecked", isChecked);
this.getData();
} catch (error) {
console.log("全选修改失败", error);
}
},
5、删除购物车数据
(1)删除单个商品
这个就比较简单了,不多说了,就是写接口——三连环——派发action
- 写接口
//删除购物车商品的接口
// /api/cart/deleteCart/{skuId}
export const reqDeleteGoodById = (skuId) => {
return requests({
url: `/cart/deleteCart/${skuId}`,
method: 'delete',
})
}
写接口要注意,一般带参的url要用反引号(一般处于电脑键盘Tab上面那个)而不是引号。
- 三连环
// 删除购物车产品
async deleteCartGood({ commit }, skuId) {
let result = await reqDeleteCart(skuId);
console.log("被删除的产品信息" + result);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("fail"));
}
},
- 添加点击事件并派发action重新请求并展示
先在对应位置添加点击事件:
// 删除某个购物车产品
async deleteCartById(cart) {
try {
// 如果删除成功,再次发请求获取数据进行展示
await this.$store.dispatch("deleteCartGood", cart.skuId);
this.getData();
} catch (error) {
console.log("删除失败", error);
}
},
(2)删除选中的所有商品
这个逻辑和点击全选修改每个商品的勾选状态有点像。
- vuex中利用遍历购物车数据,查出来哪个是选中的,然后依次发请求删除 删除成功与否的结果利用Promise.all传给组件
// 删除某个购物车产品
async deleteCartGood({ commit }, skuId) {
let result = await reqDeleteCart(skuId);
console.log("被删除的产品信息" + result);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
// 删除购物车所有被选中的产品
deleteAllCartGood(context) {
// context身上有dispatch、commit、getters、state等数据
// 获取购物车中全部的产品
// console.log(context.getters.cartList.cartInfoList);
let PromiseArr = [];
context.getters.cartList.cartInfoList.forEach((el) => {
let promise =
el.isChecked==1 ? context.dispatch("deleteCartGood", el.skuId): "";
// 将每一次返回的promise添加到数组中
PromiseArr.push(promise);
});
// 只要有一个失败都返回失败的结果
return Promise.all(PromiseArr);
},
- 点击删除选中商品按钮生效,方法中派发请求
// 删除被选中的产品
// 这个回调函数收集不到cart.skuId,因为它不在v-for内
async deleteAllCheckedCart() {
try {
// 派发一个action
await this.$store.dispatch("deleteAllCartGood");
this.getData();
} catch (error) {
alert(error.message);
}
},
6、购物车商品数量加减改(难点)
(1)分析一下
这个修改商品数量主要有三个地方,加号、减号、用户输入,那么每改一次实际上都要重新发送请求。这个修改商品数量的接口和加入购物车的接口是一样的(带上要修改的产品id和数量)。
注意这里的skuNum参数类型,它并不是说直接把修改后的数量直接传到服务器,而是传与原来数量的差值。参数的规则就是传正数代表增加,传负数代表减少。
(2)配置请求的方法
首先找到修改数量的位置:
<li class="cart-list-con5">
<a
href="javascript:void(0)"
class="mins"
@click="handler('mins', -1, cart)" //减一
>-</a
>
<input
autocomplete="off"
type="text"
minnum="1"
class="itxt"
:value="cart.skuNum"
@change="handler('change', $event.target.value * 1, cart)" //直接编辑修改
/>
<a
href="javascript:void(0)"
class="plus"
@click="handler('plus', 1, cart)" //加一
>+</a
>
</li>
仔细看注释:
methods: {
// 获取个人购物车
getData() {
this.$store.dispatch("getCartList");
},
// 修改某个产品数量
//有个bug,如果连续点减号,那么可能会变成负数
//这是因为连续快速点击,请求还来不及发送,数据没改,所以每次disnum都是-1
//解决办法就是节流,给服务器一些缓冲的时间,防止数据不同步出现上述bug
handler: throttle(async function (type, disNum, cart) {
// type:为了区分三个元素
// 目前disNum形参:+变化量(1) -变化量(-1)
// cart:哪一个产品
// console.log("派发action,通知服务器修改个数" + type);
// 向服务器发请求,修改数量
switch (type) {
// 加
case "plus":
// 带给服务器变化的量
disNum = 1;
break;
case "mins":
// 判断产品是否大于1,大于1才能减
if (cart.skuNum > 1) {
disNum = -1;
} else {
disNum = 0;
}
// 上面这判断可以用三元表达式disNum=cart.skuNum>1?-1:0
break;
case "change":
// 如果用户输入的是非数字字符,则保留原数量不变,也就是disNum=0
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
// 正常情况(避免用户输入小数,转化为整数)
disNum = parseInt(disNum) - cart.skuNum; //需要传的是它们的差值(接口规定)
}
break;
}
// 派发action
try {
await this.$store.dispatch("getShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
//如果成功,就再次请求数据刷新页面
this.getData();
} catch {
alert("修改数量失败", err);
}
}, 800),
},
至此,购物车模块的笔记也基本整理完了。下一篇笔记是登录注册模块。