重难点说明
用户临时ID的处理
购物车数据的管理(复杂)
不使用v-model监控用户输入
async / await / Promise.all() 的使用
思路:
加入购物车按钮
路由跳转之前发请求
成功路由跳转与参数传递
失败提示失败信息
addCartSuce
购物车
修改购物车产品的数量(需要发请求:参数理解)
删除某一产品
修改产品状态
删除全部选中的商品
全选操作
加入购物车按钮
路由跳转之前发请求
成功路由跳转与参数传递
失败提示失败信息
修改代码:
src/pages/Detail/index.vue
加入购物车
methods: {
//加入购物车
async addOrUpdateShopCar() {
//1:在点击加入购物车这个按钮的时候,做的第一件事情,将参数带给服务器(发请求),通知服务器加入购车的产品是谁
//this.$store.dispatch('addOrUpdateShopCart'),说白了,它是在调用vuex仓库中的这个addOrUpdateShopCart函数。
//2:你需要知道这次请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示
try {
//成功
await this.$store.dispatch("addOrUpdateShopCar", {skuId: this.$route.params.skuId, skuNum: this.skuNum});
//3:进行路由跳转
//4:在路由跳转的时候还需要将产品的信息带给下一级的路由组件
//一些简单的数据skuNum,通过query形式给路由组件传递过去
//产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据在消失)
//本地存储|会话存储,一般存储的是字符串
sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo))
this.$router.push({name: "addCartSuccess", query:{skuNum: this.skuNum}})
} catch (error) {
//失败
alert(error.message);
}
}
}
src/api/index.js
//将产品添加到购物车中(获取更新某一个产品的个数)
// /api/cart/addToCart/{ skuId }/{ skuNum } POST
export const addOrUpdateShopCar = (skuId, skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`, method:"post"});
src/store/detail/index.js
import {getGoodsInfo, addOrUpdateShopCar} from "@/api";
const actions = {
//加入购物车的||修改某一个产品的个数,注意一定要用解构方法解构形参对象,不能写成接收两个形参形式,那样会报错接口调不通
async addOrUpdateShopCar(context, {skuId, skuNum}) {
//发请求:前端带一些参数给服务器【需要存储这些数据】,存储成功了,没有给返回数据
//不需要在三连环(仓库存储数据了)
//注意:async函数执行返回的结果一定是一个promise【要么成功,要么失败】
let response = await addOrUpdateShopCar(skuId, skuNum);
if (response.code == 200) {
//返回的是成功的标记
return "OK";
} else {
//返回的是失败的标记
return Promise.reject(new Error("fail"))
}
},
}
注意点1:
问题:加入购物车路由跳转跟普通的路由跳转不一样,不一样在哪里?
答案:因为加入购物车功能得先调接口保存加入购物车的数据,然后再跳转路由。而普通的路由跳转直接跳转并传参就行。
注意点2:
问题:点击购物车函数派发定义在详情组件中,但是加入购物车接口调用在仓库中,如何获取调用函数的结果呢?
答案:第一种方案在vuex的state中保存结果,第二种方案在详情组件中采用async+await修饰派发方法获取调用成功失败结果。
注意点3:
问题:如下代码vuex中执行ajax调用接口返回失败,为啥?
src/api/index.js
//将产品添加到购物车中(获取更新某一个产品的个数)
// /api/cart/addToCart/{ skuId }/{ skuNum } POST
export const addOrUpdateShopCar = (skuId, skuNum)=>requests({url:`/cart/addToCart/${skuId}/${skuNum}`, method:"post"});
src/store/detail/index.js
async addOrUpdateShopCar(context, skuId, skuNum) {
let response = await addOrUpdateShopCar(skuId, skuNum);
console.log("******response:", response)
}
报错如下:
答案:vuex中action定义的函数接收形参书写个数不对。
如果是一个参数可以写成:addOrUpdateShopCar(context, skuId)
但如果是传入多个参数则不能这样写:这是错误写法:addOrUpdateShopCar(context, skuId, skuNum)
正确写法是派发时传入一个对象通过解构方式获取属性值,正确写法:addOrUpdateShopCar(context, {skuId, skuNum})
注意点4:
问题:理解异步函数async和await的用法?
答案:
等着Promise对象resolve,然后得到resolve的值,
作为await表达式的运算结果。async
和await
基于promise的。使用async的函数将会始终返回一个 promise 对象。这一点很重要,要记住,可能是你遇到容易犯错的地方。await
的时候我们只是暂停了函数,而非整段代码。非阻塞的
Promise
,例如Promise.all()
.await
必须写在async
函数中, 但async函数中可以没有awaittry…catch
来捕获处理重难点说明:区别使用sessionStorage与localStorage
addCartSuce
效果如图
修改代码:
拷贝src/pages/AddCartSuccess文件夹
src/router/routes.js
import AddCartSuccess from '@/pages/AddCartSuccess'
import ShopCart from '@/pages/ShopCart'
{
name: 'addCartSuccess',
path: '/addCartSuccess',
component: AddCartSuccess,
meta:{"isShow": true} //自定义元数据属性,判断Footer组件在底部是否显示
},
{
name: 'shopCart',
path: '/shopCart',
component: ShopCart,
meta:{"isShow": true} //自定义元数据属性,判断Footer组件在底部是否显示
},
src/pages/AddCartSuccess/index.vue
{{skuInfo.skuName}}
{{skuInfo.skuDesc}} 数量:{{$route.query.skuNum}}
查看商品详情
去购物车结算
computed: {
skuInfo() {
return JSON.parse(sessionStorage.getItem("SKUINFO"))
}
}
注意点1:
问题:点击加入购物车按钮后跳转路由需要传参购物详情参数,采用哪种方式?query?params?
答案:其实都可以,但是最优雅的还是采用query方式,具体效果看如下:
注意点2:
问题:参数传递采用本地存储好还是会话存储好?
答案:推荐采用会话存储,localStorage本地存储会在硬盘保留一份需手动清除,而sessionStorage会话存储随着浏览器窗口关闭就清楚了,使用方便。
注意点3:
问题:无论是本地存储还是会话存储,可以存对象吗?
答案:不能,只能存json字符串,直接存对象会无法解析。
购物车
修改代码:
src/api/index.js
//获取购物车列表数据接口
//URL:/api/cart/cartList method:get
export const reqCartList = ()=>requests({url:'/cart/cartList ',method:'get'});
src/store/index.js
import shopCar from "@/store/shopCar"
modules:{
...
shopCar
}
src/store/shopCar/index.js
import {reqCartList} from "@/api";
//shopCar模块的小仓库
//actions代表一系列动作,可以书写自己的业务逻辑,也可以处理异步
const actions = {
async reqCartList({commit}) {
let response = await reqCartList();
if (response.code == 200) {
commit("REQ_CART_LIST", response.data)
}
},
}
//mutations代表维护,操作维护的是state中的数据,且state中数据只能在mutations中处理
const mutations = {
REQ_CART_LIST(state, carList) {
state.carList = carList
},
}
//state代表仓库中的数据
const state = {
//购物车列表
carList: [],
}
//计算属性
//项目当中getters主要的作用是:简化仓库中的数据(简化数据而生)
//可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
const getters = {
carList(state) {
return state.carList[0] || {};
}
}
//创建并暴露store
export default {
actions,
mutations,
state,
getters
}
src/pages/ShopCart/index.vue
methods: {
getData() {
this.$store.dispatch('reqCartList')
},
},
mounted() {
this.getData()
}
注意点1:如图,这一列多余,删除第三项并调整css让各个项目对齐,比例为:15 35 10 17 10 13
注意点2:
问题:向服务器发送查询购物车ajax请求,获取购物车数据,为啥返回数据是空呢?
答案:因为你得带UUID的东西,后端不知道你谁,也就不知道把那条数据返回给你了。
安装命令:cnpm install --save uuid
修改代码:
src/utils/uuid_token.js
import { v4 as uuidv4 } from 'uuid';
//要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID = ()=>{
//先从本地存储获取uuid(看一下本地存储里面是否有)
let uuid_token = localStorage.getItem('UUIDTOKEN');
//如果没有
if(!uuid_token){
//我生成游客临时身份
uuid_token = uuidv4();
//本地存储存储一次
localStorage.setItem('UUIDTOKEN',uuid_token);
}
//切记有返回值,没有返回值undefined
return uuid_token;
}
src/api/axios.js
//在当前模块中引入store
import store from '@/store';
if(store.state.detail.uuid_token){
//请求头添加一个字段(userTempId):和后台老师商量好了
config.headers.userTempId = store.state.detail.uuid_token;
}
src/store/detail/index.js
const state = {
//游客临时身份
uuid_token: getUUID()
}
注意点1:
问题:假设现在我有UUID了,那如何传过去呢?接口已经定义好了只有2个参数,UUID咋传过去?
答案:可以放在请求头header中传递过去。
注意点2:
问题:我如何使用UUID第三方插件呢?
答案:登录npm官网https://www.npmjs.com/package/uuid搜索uuid,下方就是使用方式。
注意点3:切记UUID不能在vuex中生成,因为vuex是每次触发就会随机生成,会变动这是不行的,正常应该是一次生成有效期内不改变。所以这个uuidv4()不能放在vuex中,最好放在src目录下新建一个文件夹utils,这个文件夹里面经常放一些常用的功能模块,比如正则、临时身份UUID等等。
注意点4:切记src/utils/uuid_token.js下封装的uuid方法一定要有返回值,否则返回值是underfine无效的。
注意点5:
问题:在src/api/axios.js中现在需要给请求头header添加uuid,但是uuid在vuex仓库中,如何获取呢?
答案:在src/api/axios.js中引入import store from ‘@/store’;,通过store 对象可以获取仓库中的state属性。
修改代码:
src/pages/ShopCart/index.vue
{{car.skuName}}
{{car.skuPrice}}.00
{{car.skuNum * car.skuPrice}}
{{carTotalPrice}}
import {mapGetters} from "vuex";
data() {
return {
totalPrice: 0
}
},
computed: {
...mapGetters(["carList"]),
//购物车数据
cartInfoList() {
return this.carList.cartInfoList || []
},
//计算购买产品的总价
carTotalPrice() {
this.cartInfoList.forEach(item => {
this.totalPrice += item.skuNum * item.skuPrice;
})
return this.totalPrice;
},
//判断底部复选框是否勾选【全部产品都选中,采勾选】
isAllChecked() {
//遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true
//只要有一个不是1======>假false
return this.cartInfoList.every(item=> item.isChecked == 1)
}
}
注意点1:
问题:查询购物车信息的接口方法放在哪里?是放在“去购物车结算”按钮还是放在“购物车”页面加载完毕中定义?
答案:放在“购物车”页面加载完毕中定义,只要购物车页面一初始化,甭管哪个入口跳转进来的都能进行批量查询购物车数据并动态展示。
注意点2:购物车左下角的全选按钮判断是否勾选,推荐适用every而不是foreach方法
isAllChecked() {
//遍历数组里面原理,只要全部元素isChecked属性都为1===>真 true
//只要有一个不是1======>假false
return this.cartInfoList.every(item=> item.isChecked == 1)
}
修改代码:
src/pages/ShopCart/index.vue
-
+
//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";
methods: {
//修改某一个产品的个数[节流]
handle: throttle(async function(flag, disNum, car) {
//type:为了区分这三个元素
//disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品【身上有id】
//向服务器发请求,修改数量
switch (flag) {
case "add":
//加号
disNum = 1;
break;
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = car.skuNum > 1 ? -1 : 0;
break;
case "change":
/*用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
* 问题:为啥是disNum < 1,而不是disNum <= 1
* 答案:哪怕输入1也会走else里面的代码,不影响功能,所以无需加“=”等于号
*/
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - car.skuNum;
}
break;
}
try {
//派发action
await this.$store.dispatch('addOrUpdateShopCar', {skuId: car.skuId, skuNum: disNum})
//再一次获取服务器最新的数据进行展示
this.getData();
} catch (error) {
//失败
alert(error.message);
}
}, 500),
}
注意点1:如图,购物车中的【+、-、输入值】都应该触发修改购物车接口,且三个函数是同一个函数
注意点2:
问题:如何区分是点击了加号、减号、还是输入值呢?
答案:通过传参字符串flag标识名称进行区分。
注意点3:
问题:实际触发修改购物车接口的传值要注意。
答案:点击“+”号应传值(1),点击“-”号应传值(-1),点击输入值应传值(输入值-原始值)。
注意点4:输入框中事件一定要写成@change,而不是@click,否则效果不对。
注意点5:
问题:对于“minus”情况,为啥是disNum < 1,而不是disNum <= 1
答案:哪怕输入1也会走else里面的代码,不影响功能,所以无需加“=”等于号
注意点6:点击减号“-”要加判断,不能让值小于1。
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = car.skuNum > 1 ? -1 : 0;
break;
注意点7:对于“change”情况,也要加判断
case "change":
/*用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
* 问题:为啥是disNum < 1,而不是disNum <= 1
* 答案:哪怕输入1也会走else里面的代码,不影响功能,所以无需加“=”等于号
*/
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - car.skuNum;
}
break;
注意点8:
问题:连续点击,居然能出现负数,而慢慢点击就不会出问题,为什么?
答案:因为没有加“节流”效果,由于点击过快就会出现这种错误现象。
旧代码:
//修改某一个产品的个数[节流]
async handle(flag, disNum, car) {
//type:为了区分这三个元素
//disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品【身上有id】
//向服务器发请求,修改数量
switch (flag) {
case "add":
//加号
disNum = 1;
break;
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = car.skuNum > 1 ? -1 : 0;
break;
case "change":
/*用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
* 问题:为啥是disNum < 1,而不是disNum <= 1
* 答案:哪怕输入1也会走else里面的代码,不影响功能,所以无需加“=”等于号
*/
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - car.skuNum;
}
break;
}
try {
//派发action
await this.$store.dispatch('addOrUpdateShopCar', {skuId: car.skuId, skuNum: disNum})
//再一次获取服务器最新的数据进行展示
this.getData();
} catch (error) {
//失败
alert(error.message);
}
},
修改后代码
//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";
//修改某一个产品的个数[节流]
handle: throttle(async function(flag, disNum, car) {
//type:为了区分这三个元素
//disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品【身上有id】
//向服务器发请求,修改数量
switch (flag) {
case "add":
//加号
disNum = 1;
break;
case "minus":
//判断产品的个数大于1,才可以传递给服务器-1
//如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
disNum = car.skuNum > 1 ? -1 : 0;
break;
case "change":
/*用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
* 问题:为啥是disNum < 1,而不是disNum <= 1
* 答案:哪怕输入1也会走else里面的代码,不影响功能,所以无需加“=”等于号
*/
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
//属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
disNum = parseInt(disNum) - car.skuNum;
}
break;
}
try {
//派发action
await this.$store.dispatch('addOrUpdateShopCar', {skuId: car.skuId, skuNum: disNum})
//再一次获取服务器最新的数据进行展示
this.getData();
} catch (error) {
//失败
alert(error.message);
}
}, 500),
修改代码:
app/src/api/index.js
//删除购物产品的接口
//URL:/api/cart/deleteCart/{skuId} method:DELETE
export const reqDeleteCartById = (skuId)=>requests({url:`/cart/deleteCart/${skuId}`,method:'delete'});
src/store/shopCar/index.js
import {reqDeleteCartById} from "@/api";
const actions = {
//删除购物车某一个产品
async reqDeleteCartById({commit}, skuId) {
let response = await reqDeleteCartById(skuId);
if (response.code == 200) {
//返回的是成功的标记
return "OK";
} else {
//返回的是失败的标记
return Promise.reject(new Error("fail"))
}
}
}
src/pages/ShopCart/index.vue
删除
//删除某一个产品的操作
async reqDeleteCartById(skuId) {
try {
//派发action
await this.$store.dispatch('reqDeleteCartById', skuId)
this.getData();
} catch (error) {
alert(error.message)
}
}
修改代码:
app/src/api/index.js
//修改商品的选中状态
//URL:/api/cart/checkCart/{skuId}/{isChecked} method:get
export const reqUpdateCheckedByid = (skuId,isChecked)=>requests({url:`/cart/checkCart/${skuId}/${isChecked}`,method:'get'});
src/store/shopCar/index.js
import {reqUpdateCheckedByid} from "@/api";
const actions = {
//修改购物车某一个产品的选中状态
async reqUpdateCheckedByid({commit}, {skuId,isChecked}) {
let response = await reqUpdateCheckedByid(skuId,isChecked);
if (response.code == 200) {
//返回的是成功的标记
return "OK";
} else {
//返回的是失败的标记
return Promise.reject(new Error("fail"))
}
},
}
src/pages/ShopCart/index.vue
//修改某个产品的勾选状态
async reqUpdateCheckedByid(skuId, event) {
try {
//如果修改数据成功,再次获取服务器数据(购物车)
let isChecked = event.target.checked ? "1" : "0";
//派发action
await this.$store.dispatch('reqUpdateCheckedByid', {skuId: skuId, isChecked})
this.getData();
} catch (error) {
alert(error.message)
}
},
修改代码:
src/pages/ShopCart/index.vue
删除选中的商品
//删除全部选中的产品
//这个回调函数咱门没办法手机到一些有用数据
async deleteAllCheckedCar() {
try {
//成功
//派发一个action
await this.$store.dispatch('deleteAllCheckedCar')
//再发请求获取购物车列表
this.getData()
} catch (error) {
//失败
alert(error.message)
}
}
src/store/shopCar/index.js
//删除全部勾选的产品
async deleteAllCheckedCar({dispatch, getters}) {
//context:小仓库,commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】
//获取购物车中全部的产品(是一个数组)
let primoseAll = [];
getters.carList.cartInfoList.forEach(item => {
let promise = item.isChecked == 1 ? dispatch('reqDeleteCartById', item.skuId) : '';
//将每一次返回的Promise添加到数组当中
primoseAll.push(promise);
})
//只要全部的p1|p2....都成功,返回结果即为成功
//如果有一个失败,返回即为失败结果
return Promise.all(primoseAll);
}
注意点1:接口API未提供选中删除的功能,只提供了单独删除的,所以可以采用批量循环删除的方式。
注意点2:dispatch派发不仅可以存在于vue中使用,也可以在vuex仓库中使用
注意点3:vuex仓库中的context包含很多东西,包括【commit、dispatch、getters、state…】,具体如图
注意点4:Promise.all用法,all里面的是个数组,如果其中的子项promise有一个失败,返回即为失败结果。具体用法如下:
let primoseAll = [];
let promise = item.isChecked == 1 ? dispatch('reqDeleteCartById', item.skuId) : '';
//将每一次返回的Promise添加到数组当中
primoseAll.push(promise);
//只要全部的p1|p2....都成功,返回结果即为成功
//如果有一个失败,返回即为失败结果
return Promise.all(primoseAll);
修改代码:
src/pages/ShopCart/index.vue
//修改全部产品的选中状态
async updateAllCheckedCar(checked) {
try {
//成功
//派发一个action
let isChecked = checked ? "1" : "0";
await this.$store.dispatch('updateAllCheckedCar', isChecked)
//再发请求获取购物车列表
this.getData()
} catch (error) {
//失败
alert(error.message)
}
},
src/store/shopCar/index.js
//修改全部产品的状态
async updateAllCheckedCar({dispatch, getters}, isChecked) {
//context:小仓库,commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】
//获取购物车中全部的产品(是一个数组)
let primoseAll = [];
getters.carList.cartInfoList.forEach(item => {
let promise = dispatch('reqUpdateCheckedByid', {skuId:item.skuId, isChecked});
//将每一次返回的Promise添加到数组当中
primoseAll.push(promise);
})
//只要全部的p1|p2....都成功,返回结果即为成功
//如果有一个失败,返回即为失败结果
return Promise.all(primoseAll);
}
注意点1:全选或全不选后的正确效果如图
注意点2:
问题:全部删除后,左下角的勾选按钮居然还是选中状态,需要优化
答案:在后面多补充个cartInfoList.length > 0判断条件即可
:checked="isAllChecked && cartInfoList.length > 0
1.vue尚品汇商城项目-day04【24.点击搜索按钮跳转后的页面商品列表、平台售卖属性动态展示(开发Search组件)】
2.vue尚品汇商城项目-day04【25.面包屑处理关键字】
3.vue尚品汇商城项目-day04【26.排序操作(难点)】
4.vue尚品汇商城项目-day04【27.分页器静态组件(难点)】
5.vue尚品汇商城项目-day04【28.详情页面Detail】
6.vue尚品汇商城项目-day04【29.加入购物车操作(难点)】