主题
删除购物车商品与删除选中购物车商品(Lesson19 3/3)
本页对应 Lesson19,共 5 段视频,约 50 分钟。
学习目标
- 能够说明切换商品选中状态两种方案的差异,并实现方案二:调用接口后通过 mutation 修改本地仓库状态,减少重复请求。
- 能够根据接口文档新增删除购物车商品 API,完成单个商品删除,并在购物车为空时展示空状态提示。
- 能够获取选中商品 ID 列表,完成批量删除选中购物车商品,并处理请求方式、请求体格式、确认提示及删除后列表更新。
- 能够使用 Vuex getter 或计算属性统计选中商品数量和总价,并根据统计结果控制删除选中商品、结算等区域的显示或可用状态。
- 能够实现购物车商品数量加减,调用接口同步服务端数据,并使用防抖控制频繁请求。
- 能够实现搜索页面点击“加入购物车”完成商品加入购物车,并跳转或更新到购物车页面。
知识点
先封装删除购物车商品接口
为什么需要:删除购物车不是只改页面显示,还要通知服务端把这条购物车记录删掉。因此先把接口统一放到 src/api/cart.js,组件和 store 不直接拼请求地址,后续维护更方便。
怎么想:看接口文档:删除单个购物车商品的请求方式是 delete,地址是 /cart/deleteCart/{skuId},路径参数就是商品 skuId。所以封装时只接收一个 skuId,用模板字符串拼到地址里。
怎么做:
js
// 删除购物车商品: /api/cart/deleteCart/{skuId} delete
export const deleteCartListById = (skuId)=>sphRequest.delete(`/cart/deleteCart/${skuId}`)易错点:转写稿里提醒,调不通时先检查三处:请求方式、请求地址、传参。删除接口是 delete,不要写成 get 或 post;路径参数是 skuId,不要传成购物车行对象;如果接口需要请求头,也要检查请求头是否带上、名字是否写对。
在 Vuex 中删除并更新购物车列表
为什么需要:页面只负责触发删除,真正的“调接口 + 更新仓库里的 cartList”放到 store 的 action 中,组件逻辑更薄,购物车数据也保持单一来源。
怎么想:删除成功后有两种更新思路。方案一是重新调用 getCartListAsync 拉取最新购物车列表;方案二是本地用 filter 过滤掉被删除的 skuId。转写稿里两种都演示了:方案一请求次数多但逻辑简单;方案二减少一次请求、降低服务器压力,但本地逻辑更多。
怎么做:
js
const mutations = {
// 根据商品ID删除购物车中的商品
DELETE_CART_LIST_BY_ID(state,skuId){
state.cartList = state.cartList.filter(v=>v.skuId !== skuId);
}
};
const actions = {
async deleteCartListByIdAsync({dispatch,commit},skuId){
// 根据商品ID删除购物车信息
await deleteCartListById(skuId);
// 方案一:更新购物车列表
// await dispatch("getCartListAsync");
// 方案二:
commit("DELETE_CART_LIST_BY_ID",skuId);
}
}易错点:filter 里要保留“不等于当前 skuId”的商品,写反了会把要保留的删掉;action 必须 async/await 等接口成功后再更新本地,否则接口失败也可能先删页面数据;commit 调用 mutation,dispatch 调用 action,不要混用;如果 store 开启了命名空间,页面调用时要带 cart/。
页面删除按钮绑定 action
为什么需要:用户点击“删除”时要触发上面的 action,并把当前行的 item.skuId 传进去。否则 store 不知道删哪一条。
怎么想:在 src/pages/Cart/index.vue 的删除按钮上绑定点击事件,使用 @click.prevent 阻止 href="#none" 的默认跳转,再 dispatch('cart/deleteCartListByIdAsync', item.skuId)。
怎么做:
vue
<template>
<li class="cart-list-con7">
<a @click.prevent="$store.dispatch('cart/deleteCartListByIdAsync',item.skuId)" href="#none" class="sindelet">删除</a>
<br>
</li>
</template>易错点:传参要传 item.skuId,不要传整个 item;不加 .prevent 可能触发链接默认行为;action 名要和 store 中定义一致;转写稿中点击删除后看到列表少一项,刷新后也正常,说明接口删除和本地更新都生效。若点击没反应,先看控制台报错和 Network 里的请求方式、地址、参数。
方案选择与调试小结
为什么需要:同样能删除,方案一和方案二在请求次数、代码复杂度上不同,要按场景取舍。
怎么想:方案一“删除后重新拉列表”最稳,适合列表数据复杂、怕本地算漏的场景;方案二“本地过滤”少发一次请求,适合删除单条且本地数据结构清楚的情况。
怎么做:本节笔记保留两种写法,实际练习可先跑方案一确认接口通,再切方案二观察页面是否立即少一条;刷新页面仍没有该商品,才说明服务端也删成功。
易错点:不要把“页面少了”误当成“接口成功”,要结合 Network 和刷新验证;不要漏写 await;不要把 skuId 和商品 id 混淆;删除后若列表为空,后续可再补空购物车提示,这属于后续功能,不要在本节强行编造。
功能目标:一次删除多个勾选商品
购物车中用户可能勾选多条商品,如果逐条调用删除接口,会产生多次请求。本节要实现的是“删除选中的商品”:把当前 cartList 中 isChecked === 1 的商品筛出来,取出它们的 skuId,组成数组,一次性交给批量删除接口。
这里要先明确方案。转写稿里提到两种思路:方案一是删除成功后重新调用购物车列表接口,用服务器最新数据替换仓库状态;方案二是在接口成功后直接修改本地仓库数据,减少一次请求。本节笔记采用的是方案一:删除后 dispatch("getCartListAsync") 重新拉取列表。这样逻辑简单,数据以服务端为准;缺点是会多一次请求。方案二代码量更多、逻辑更绕,但能减少 ajax 请求次数。初学阶段先按笔记实现方案一。
补 API:地址、方法、参数位置都要对
为什么需要单独封装 API?因为页面和 store 不应该到处写请求地址,统一放到 src/api/cart.js 便于维护。怎么想?对照接口文档确认三件事:请求地址是 /cart/batchDeleteCart,请求方式是 post,参数是请求体中的 skuIdList 数组。怎么做?在 cart.js 中增加:
js
// 批量删除购物车 /api/cart/batchDeleteCart post
// 参数:skuIdList 数组 代表修改的商品id列表 请求体参数
export const deleteCartListBatch = (data)=>sphRequest.post(`/cart/batchDeleteCart`,data)易错点:转写稿调试时反复强调“请求方式、请求地址、参数”三处要检查。批量删除不是普通 delete,而是 post;数组要放在请求体 data 里,不是拼在 URL 上。转写里的“合载/荷载”指请求体 payload,“school ID、扣 ID、死 coI”都应纠正为 skuId。如果请求头、请求方式或参数名写错,就会出现删除无效、504 或请求失败。
写 store action:先过滤、再映射、后刷新
为什么在 store 写?因为删除需要读取 state.cartList,删除后还要刷新列表,放在 cart 模块的 action 中最合适。怎么想?先让用户确认,避免误删;确认后从 cartList 中过滤出勾选项,映射成 skuId 数组;调用 API;成功后重新获取购物车列表。怎么做?
js
async deleteCartListBatchAsync({dispatch,state}){
if(window.confirm("您确定要删除选中的数据吗")){
// console.log(state.cartList.filter(v=>v.isChecked===1).map(item=>item.skuId));
await deleteCartListBatch(state.cartList.filter(v=>v.isChecked===1).map(item=>item.skuId));
await dispatch("getCartListAsync");
}
},调试时可以先取消注释那行 console.log,在控制台确认输出的是类似 [2, 20] 这样的 skuId 数组,而不是对象数组或字符串。易错点:过滤条件写错会删错商品;map 后必须取 skuId;删除后不刷新,页面可能还显示旧数据;window.confirm 返回 false 时不能继续执行。转写稿还提到,若改成方案二,可在删除成功后用 commit 修改本地 cartList,但本节笔记不展开。
页面绑定:点击“删除选中的商品”触发 action
为什么要在页面绑定?用户操作入口在购物车页的“删除选中的商品”链接上。怎么想?点击时直接派发 cart 模块的 action,把删除逻辑留在 store。怎么做?在 src/pages/Cart/index.vue 的 .option 中写:
vue
<div class="option">
<a href="#none" @click="$store.dispatch('cart/deleteCartListBatchAsync')">删除选中的商品</a>
</div>易错点:模块名要写对,这里是 cart/deleteCartListBatchAsync;href="#none" 防止跳转;不要在模板里直接写过滤和请求逻辑,否则页面会臃肿。转写稿调试时先打印 cartList,确认勾选数据存在,再点击删除,通过浏览器网络面板查看请求方法、地址和请求体,最终确认删除成功后列表减少。若接口临时 504,要先判断是服务端问题还是参数问题,再对照文档修正请求方式和 data。
为什么需要统计选中商品数量和总价
购物车底部需要明确告诉用户:当前选中了几件商品、这些商品合计多少钱。这个结果不能靠模板里临时拼字符串,也不建议在组件里反复遍历购物车列表。更合适的做法是把“统计选中商品”的逻辑放到 Vuex 的 getters 中:state 负责存购物车列表,getters 负责根据列表派生出统计结果。这样模板只负责展示,逻辑集中、可复用,也方便后续控制“结算”按钮是否可用。
转写稿里反复强调的思路是:计算属性可以解决统计问题,而 Vuex 中对应的就是 getters。最终得到的是一个对象,里面同时包含选中数量和选中总价。
怎么想:遍历一次,同时累加数量和金额
统计规则很直接:遍历 cartList,只处理 isChecked === 1 的商品;每遇到一个选中商品,数量加一;金额累加 skuPrice * skuNum。注意这里判断的是数字 1,不是布尔值 true,这是购物车字段定义决定的,写错会导致统计恒为 0。
转写稿中的调试过程也说明了这一点:一开始在 getters 里输出 cartList,发现是空数组,统计结果自然是 0。原因不是 getters 写错,而是获取购物车列表的异步 action 还没执行完,数据尚未进入 state。后来在获取列表的 action 中补上 await,等接口返回并提交 mutation 后再计算,结果才正常。也就是说,统计逻辑依赖“数据已经就绪”,调试时要先看 cartList 有没有值,再看 isChecked 是否匹配。
怎么做:在 cart.js 中增加 getCountResult
在 src/store/cart.js 的 getters 中增加统计方法,保留本节笔记代码如下:
js
const getters = {
getCountResult({cartList}){
let checkedNum = 0;
let checkedPrice = 0;
console.log("cartList",cartList)
cartList.forEach(item=>{
if(item.isChecked===1){
checkedNum++;
checkedPrice+=item.skuPrice*item.skuNum
}
})
console.log(checkedNum,checkedPrice);
return {
checkedNum,
checkedPrice
}
}
}这段代码的关键点有三个:第一,参数解构出 cartList,说明 getters 依赖 state 中的购物车列表;第二,checkedNum++ 和 checkedPrice += ... 在同一次遍历中完成,避免遍历两遍;第三,返回对象,模板通过 getCountResult.checkedNum、getCountResult.checkedPrice 读取。
在购物车页面展示统计结果
在 src/pages/Cart/index.vue 中,底部统计区域直接使用 getters 的返回值。保留本节笔记模板代码如下:
vue
<template>
<div class="money-box">
<div class="chosed">已选择
<span>{{getCountResult.checkedNum}}</span>件商品</div>
<div class="sumprice">
<em>总价(不含运费) :</em>
<i class="summoney">{{getCountResult.checkedPrice | currency(2,"¥")}}</i>
</div>
<div class="sumbtn">
<a class="sum-btn" href="###" target="_blank">结算</a>
</div>
</div>
</template>这里 getCountResult 作为 getters 暴露到组件后,可以像计算属性一样在模板中使用。总价通过 currency(2,"¥") 过滤器格式化为两位小数并带人民币符号。转写稿中还提到可以用 mapGetters 简写,例如把 getCountResult 映射到组件中;无论哪种写法,模板读取的字段名都不能漏掉 .checkedNum 和 .checkedPrice。
易错点与调试建议
第一,isChecked === 1 不要写成 true 或字符串 "1"。第二,金额是 skuPrice * skuNum,不要只加单价。第三,getters 只做派生计算,不要在 getters 里修改 cartList。第四,如果页面显示 0,先按转写稿的方式打印 cartList,确认异步数据是否已经回来;若为空,检查获取购物车列表的 action 是否 await 了接口调用。第五,模板中如果写成 getCountResult 而忘了取属性,展示的会是对象而不是数字。统计结果还可以继续用于交互控制:转写稿里提到当选中数量为 0 时,可以让“删除选中商品”或“结算”区域不显示或不可点击,这属于同一份统计结果的延伸用法。
作业目标:数量加减不是只改页面数字
为什么需要:购物车里的数量加减是高频操作。页面上的“3”变成“4”只是视觉结果,服务端购物车数据也必须同步;否则刷新页面、结算时数量会不一致。转写稿里明确提到:点“加”让 3 变成 4 后,还要调接口让服务端数据发生改变。
怎么想:先改本地显示,让用户即时看到变化;再用防抖把连续点击合并,停止操作后再请求接口。接口选择上,转写稿说可以调“添加到购物车”接口,传入商品 ID 和最终数量,例如最终数量是 5 就传 5。
怎么做:在数量组件上绑定加、减、输入修改事件;每次触发先更新当前项数量;清掉上一次定时器,重新计时;定时器结束后读取最新数量,调用“添加到购物车”接口。字段名以项目接口文档为准,转写稿口述的“s ID、s name”需要和文档核对,不要凭记忆写。
易错点:每次点击都发请求;防抖回调里读到旧数量;数量减到 0 或负数;输入框为空或非数字;接口失败后页面数字没有回滚。
用防抖合并连续点击
为什么需要:转写稿在作业部分特别提到“希望用一下防抖”。因为用户连点“加”会连续触发请求,既浪费请求,也给服务器增加压力。前面讲选中状态方案时也提到减少 axios 请求次数、降低服务器压力,这个思路同样适用于数量加减。
怎么想:防抖应该包住“发请求”这一动作,而不是包住“改本地数字”。本地数字要立即变,用户才觉得跟手;请求要等用户停手后再发。定时器要保存最后一次数量,不能在回调里读闭包旧值。
怎么做:按转写稿口述可整理成如下示意,具体函数名和字段以项目接口文档为准:
js
// 示意:字段名以接口文档为准
let timer = null
function onNumChange(item, num) {
item.num = num // 先更新本地显示
clearTimeout(timer)
timer = setTimeout(() => {
// 调“添加到购物车”接口,传商品ID和最终数量
addToCart({ id: item.id, num })
}, 300)
}易错点:把 clearTimeout 写在更新数字之前导致逻辑混乱;防抖时间过长;组件卸载时没有清理定时器;输入框和加减按钮各写一套逻辑,最后数量不一致。
调接口后要处理成功与失败
为什么需要:数量加减不是纯前端交互。转写稿说“同时服务端里面数据也得发生改变,得调接口”。如果只改本地,刷新后数量会回到旧值;如果接口失败却不提示,用户会以为已保存。
怎么想:请求成功后可以重新获取购物车列表,或通过 commit 更新仓库状态;请求失败要提示并回滚数量。转写稿在删除功能里调试过请求方式、地址、参数位置,数量接口同样要核对这三处,尤其注意 delete 和 post 的请求体位置不同:转写稿提到 delete 时第二个参数不一定是请求体,data 才是;后来接口用 post 才成功。这个调试经验可迁移到数量接口排查。
怎么做:封装数量更新方法;在防抖回调里调用;try/catch 或 .catch 处理失败;成功后触发列表更新或本地仓库更新;失败时把数量改回旧值并提示。
易错点:只看页面变化不看 Network;请求方式写错;参数放在 URL 或请求体位置错误;失败后不提示;成功后不更新仓库导致总价不刷新。
与选中统计、删除功能的联动
为什么需要:转写稿后面实现了选中商品数量和总价统计。数量一变,总价就要变;如果数量只改 DOM 不改状态,计算属性拿不到新值。
怎么想:数量应保存在购物车列表项或仓库状态中,统计逻辑从状态里读,而不是从页面文本读。转写稿中用计算属性遍历 cartList,按选中状态累加数量和单价,这说明数量字段必须可响应。
怎么做:数量更新后确保列表项或仓库中的数量同步;统计逻辑依赖该字段;删除、批量删除不是本节作业核心,但调试时注意接口请求方式与请求体,避免把删除问题误判成数量问题。
易错点:直接改 DOM 文本;数量字段和统计字段不同步;把删除接口的调试结论硬套到数量接口。
自测清单
加入购物车后进入购物车,连续点“加”多次,观察 Network 是否只在停手后发一次请求;刷新页面看数量是否保持;修改数量后看选中数量和总价是否更新;测试数量为 1 时继续减、输入空值、输入非数字;模拟接口失败看是否回滚并提示。转写稿最后布置的作业就是完成购物车中的数量加减,并要求使用防抖;按“先本地、再防抖、后接口、失败回滚”的顺序做,调试时优先核对请求方式、地址、参数位置和字段名。
一、先把本节目标说清楚
这一节的核心任务有两个:一是把购物车里的删除、批量删除、选中商品统计做完整;二是完成作业——在搜索页点击“加入购物车”按钮,把商品加入购物车,并跳转到购物车页面。转写里反复出现的 school ID 实际是 skuId,co list、ca list 都是 cartList,get called list 是 getCartList,阅读时先统一这些叫法。
二、选中状态为什么有两种改法
为什么需要:点击商品前的勾选框时,要同时改本地显示和服务端状态。如果只改本地,刷新后状态会丢;如果只改服务端,页面不会立即变化。
怎么想:方案一是“调接口改服务端 → 再调购物车列表接口 → 用新数据替换仓库”;方案二是“调接口改服务端 → 直接 commit 修改仓库里的选中状态”。
怎么做:方案二在 action 里先调接口,成功后提交 mutation,只改对应商品的 isChecked。这样不用重新拉列表。
易错点:方案二代码量多、逻辑多,要保证接口成功后再改本地;方案一请求次数多,但数据最稳。转写里明确说方案二“减少请求次数、降低服务器压力”,缺点就是“代码量较多、增加逻辑”。
三、删除购物车商品:API → store → 组件
为什么需要:购物车列表里的删除按钮必须真正删掉服务端数据,否则刷新又回来。
怎么想:按“接口文档 → API 文件 → store action → 组件事件”的顺序改,三个地方都要动。
怎么做:先在 API 文件里加删除接口,请求方式按文档是 delete,参数只传商品 skuId;再在 store 里写 action:先调删除接口,再 commit 更新本地 cartList,或者重新调用 getCartList 刷新列表;最后在购物车组件里给删除按钮加 @click,把 item.skuId 传过去。
js
// store 中示意
async deleteCartById({ commit }, skuId) {
await deleteCart(skuId)
commit('DELETE_CART', skuId)
}js
// mutation 中示意
DELETE_CART(state, skuId) {
state.cartList = state.cartList.filter(item => item.skuId !== skuId)
}易错点:转写里提醒得很具体——调不通时先检查“请求方式、请求地址、参数”这三处;获取不到购物车商品,要检查请求头有没有带、请求头名字有没有写对。删除按钮的 @click 不要漏写参数。
四、批量删除:先按文档写,再用调试纠正
为什么需要:用户勾选多个商品后,要一次删掉,不能一个个点。
怎么想:批量删除接口接收的是商品 ID 数组,不是单个 ID。组件里先找出“被选中”的商品,映射成 skuId 数组,再传给接口。
怎么做:转写里先用 filter 找 isChecked 为真的项,再用 map 取出 skuId;API 请求方式一开始按 delete 写,调试发现服务端实际用 post,参数放在请求体里,最终把数组直接作为 body 传过去。
js
const skuIdList = cartList
.filter(item => item.isChecked)
.map(item => item.skuId)易错点:转写里连续出现 504,最后对比可用的示例才发现请求方式应是 post,并且 post 的第二个参数才是请求体;delete 和 post 的参数位置不一样,照抄容易错。批量删除前最好加确认提示,例如“确定要删除选中的商品吗”。
五、选中商品数量与总价:用 getters 统计
为什么需要:页面要显示“已选多少件、合计多少钱”,这个结果由 cartList 派生,不应该再存一份状态。
怎么想:用计算属性/getters 遍历 cartList,只统计 isChecked 为真的商品。
怎么做:在 getters 里定义 getCartResult,返回 { totalNum, totalPrice };组件里通过 mapGetters 取用,模板中显示 getCartResult.totalNum 和 getCartResult.totalPrice。
js
getCartResult(state) {
let totalNum = 0
let totalPrice = 0
state.cartList.forEach(item => {
if (item.isChecked) {
totalNum += item.skuNum
totalPrice += item.cartPrice * item.skuNum
}
})
return { totalNum, totalPrice }
}易错点:转写里一开始统计全是 0,原因是数据还没执行完就去取值;另外要确认 cartList 确实有数据、字段名是 skuNum 和 cartPrice,不要把单价和总价混在一起。没有选中商品时,可以把“删除选中商品”和结算按钮隐藏或禁用。
六、作业2:搜索页“加入购物车”
为什么需要:搜索页只展示商品,用户点“加入购物车”后要真正写入购物车,并让用户看到结果。
怎么想:点击按钮 → 调加入购物车接口 → 成功后跳转到购物车页面。转写里还特别提到:点完加入购物车后按钮应失效,防止连续点击发多次请求。
怎么做:给按钮绑定点击事件,传当前商品 skuId 和数量;请求期间用 loading 或 disabled 锁住按钮,成功后再跳转。
js
async addToCart() {
if (this.loading) return
this.loading = true
try {
await addCart({ skuId: this.skuId, skuNum: 1 })
this.$router.push('/cart')
} finally {
this.loading = false
}
}易错点:不要忘记 finally 里解锁,否则请求失败后按钮一直不能点;跳转前要确认接口成功;搜索页传的是商品 skuId,不是购物车条目 ID。转写里还提到数量加减作业可以配合防抖,避免连续点“+”时频繁发请求。
课堂画面





代码示例
示例一:删除购物车中的单个商品(api + store + 组件)
js
/* ---------------- src/api/cart.js ---------------- */
// 删除购物车商品:DELETE /api/cart/deleteCart/{skuId}
export const deleteCartListById = (skuId) =>
sphRequest.delete(`/cart/deleteCart/${skuId}`)
/* ---------------- src/store/modules/cart.js ---------------- */
import { deleteCartListById } from '@/api/cart'
const state = {
cartList: []
}
const mutations = {
// 根据商品ID删除购物车中的商品
DELETE_CART_LIST_BY_ID(state, skuId) {
state.cartList = state.cartList.filter(v => v.skuId !== skuId)
}
}
const actions = {
async deleteCartListByIdAsync({ dispatch, commit }, skuId) {
// 1. 先调用接口,把数据库中的这条记录删掉
await deleteCartListById(skuId)
// 方案一:更新购物车列表(重新请求,用接口数据整体替换仓库)
// await dispatch('getCartListAsync')
// 方案二:本地删除(少发一次请求,但逻辑多一段)
commit('DELETE_CART_LIST_BY_ID', skuId)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}vue
<!-- src/pages/Cart/index.vue -->
<template>
<li class="cart-list-con7">
<a href="#none"
class="sindelet"
@click.prevent="$store.dispatch('cart/deleteCartListByIdAsync', item.skuId)">删除</a>
</li>
</template>要点解释:
- 接口是
delete方式,skuId直接拼在路径上,不需要请求体。 @click.prevent必须加,否则<a href="#none">会跳转,页面一刷新就看不到删除效果。dispatch的第一个参数要带模块名cart/,因为 store 模块开了namespaced: true。- 方案一(
dispatch('getCartListAsync'))和方案二(commit本地 filter)的区别:方案一多发一次请求但数据最准,方案二减少请求但要多写逻辑;两种都可以,转写稿里演示的是方案二。 - 必须先
await接口成功,再commit改本地数据,否则接口失败而本地已删,页面和服务器不一致。 filter返回的是新数组,必须重新赋值给state.cartList,不能直接splice/push原数组。
示例二:批量删除选中的购物车商品
js
/* ---------------- src/api/cart.js ---------------- */
// 批量删除购物车:POST /api/cart/batchDeleteCart
// 参数:skuIdList 数组,代表要删除的商品id列表(请求体参数)
export const deleteCartListBatch = (data) =>
sphRequest.post('/cart/batchDeleteCart', data)
/* ---------------- src/store/modules/cart.js ---------------- */
async deleteCartListBatchAsync({ dispatch, state }) {
if (window.confirm('您确定要删除选中的数据吗')) {
// 先过滤出选中的商品,再映射出 skuId 数组
const skuIdList = state.cartList
.filter(v => v.isChecked === 1)
.map(item => item.skuId)
// 请求体直接就是这个数组
await deleteCartListBatch(skuIdList)
// 删除后重新拉取购物车列表
await dispatch('getCartListAsync')
}
}vue
<!-- src/pages/Cart/index.vue -->
<template>
<div class="option">
<a href="#none"
@click="$store.dispatch('cart/deleteCartListBatchAsync')">删除选中的商品</a>
</div>
</template>要点解释:
- 接口文档写的是
skuIdList数组,实际发送时请求体直接传数组(转写稿里最终就是sphRequest.post(url, data),data就是那个数组),不要再包一层{ skuIdList: [...] }。 - 请求方式要看清:这里是
post,第二个参数就是请求体;如果接口是delete,第二个参数是配置对象,要写成sphRequest.delete(url, { data: skuIdList }),转写稿里 504/删不掉就是踩了这个坑。 filter(v => v.isChecked === 1).map(item => item.skuId)两步顺序不能反:先筛出选中项,再取skuId。isChecked === 1用严格相等,避免字符串'1'判断出错。- 删除完成后
dispatch('getCartListAsync')刷新列表;如果不想再发请求,也可以像示例一那样commit一个本地删除的 mutation(转写稿里留给课后自己做)。 - 选中数量为 0 时最好不要发请求,可以配合示例三的
getCountResult.checkedNum > 0做v-if控制按钮显示。
示例三:统计选中商品数量与总价(getters + 页面渲染)
js
/* ---------------- src/store/modules/cart.js ---------------- */
const getters = {
getCountResult({ cartList }) {
let checkedNum = 0
let checkedPrice = 0
cartList.forEach(item => {
if (item.isChecked === 1) {
checkedNum++
checkedPrice += item.skuPrice * item.skuNum
}
})
return {
checkedNum,
checkedPrice
}
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}vue
<!-- src/pages/Cart/index.vue -->
<template>
<div class="money-box">
<div class="chosed">已选择
<span>{{ getCountResult.checkedNum }}</span>件商品</div>
<div class="sumprice">
<em>总价(不含运费) :</em>
<i class="summoney">{{ getCountResult.checkedPrice | currency(2, "¥") }}</i>
</div>
<div class="sumbtn" v-if="getCountResult.checkedNum > 0">
<a class="sum-btn" href="###" target="_blank">结算</a>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'CartMoneyBox',
computed: {
...mapGetters('cart', ['getCountResult'])
}
}
</script>要点解释:
getters的第一个参数就是state,直接{ cartList }解构出来用。- 数量用
checkedNum++,总价用checkedPrice += item.skuPrice * item.skuNum,注意是单价 × 数量,不是只加单价。 getters里只读不写,不要在 getters 中修改state。- 页面里用
getCountResult(计算属性缓存),不要在模板里写方法做循环统计,那样每次渲染都会重新遍历。 mapGetters('cart', [...])的命名空间要和 store 模块名一致;不写'cart'会取不到值。- 转写稿中第一次显示 0 的原因:
getCartListAsync是异步的,列表还没回来就计算了;getters 依赖state.cartList,数据回来后会自动重新计算,不需要手动处理。 v-if="getCountResult.checkedNum > 0"用来控制结算按钮/删除按钮的显示,没选中商品时不显示。| currency(2, "¥")是项目里的过滤器,用于保留两位小数并加货币符号。
常见错误
1. 点删除后控制台报 405 / 404,商品没被删
- 现象:Network 里请求发出去了,但状态码是 404 或 405,或请求地址少了一段。
- 原因:接口三要素没对齐——
deleteCartListById写成post,或路径漏了/cart/deleteCart/,或把skuId拼进了错误位置。 - 修法:按文档逐项核对 method、url 模板、路径参数;删除单个是
delete+ 路径参数,批量删除是文档指定的 method + 请求体。核对完再刷新页面重试,不要靠"改代码猜"。
2. 批量删除接口返回成功,但一条都没删掉
- 现象:响应 200,刷新后数据还在;Network → Payload 里请求体是空的。
- 原因:把数组直接当第二个参数传给
delete。delete(url, config)的第二个参数是配置对象而不是请求体,数组被当成 config 丢掉了;如果接口是post,则第二个参数才是请求体本身。 - 修法:
delete传请求体要写成sphRequest.delete(url, { data: skuIdList });post直接sphRequest.post(url, skuIdList)。改完在 Network 里确认 Payload 里确实有数组再收工。
3. 页面白屏或点击报 xxx is not a function / undefined
- 现象:
state.cartList.filter is not a function、commit is not defined、Cannot read property 'map' of undefined。 - 原因:手写时拼错关键字(
fifi、cot、ca list、commit漏写),或解构 action 参数时漏了commit,或cartList初始值不是数组。 - 修法:先读控制台堆栈定位到行;action 里固定写成
async fn({ dispatch, commit, state }, payload),只解构真正用到的;store 里给cartList一个[]初值。
4. 删除成功后,底部"已选 N 件 / 总价"不刷新或一直是 0
- 现象:列表少了一条,但合计数字没变;或首次进页面恒为 0。
- 原因:① getter 依赖
state.cartList,但删除走的是"重新拉列表"分支且没await,读到旧数据;② 模板里写成getCountResult.checkedNum但 getter 被定义成方法却没调用;③isChecked后端返回字符串'1',用=== 1判断恒为 false。 - 修法:action 内所有异步调用前加
await,commit后再让视图渲染;getter 返回对象就用xxx.yyy取值;判断选中统一写Number(item.isChecked) === 1。
5. 删光所有商品后页面一片空白/结构错乱
- 现象:购物车清空后列表区域只剩边框,或"结算"按钮还在。
- 原因:模板没做空态分支,
v-for遍历空数组时外层容器仍然渲染。 - 修法:列表区
v-if="cartList.length",空态用v-else放提示图和文字;批量删除、结算按钮加v-if="getCountResult.checkedNum > 0",无选中时自然隐藏。
6. 连点"加入购物车"导致同一商品数量翻倍
- 现象:手速快时点两下,购物车出现两条或数量 +2。
- 原因:按钮没有请求期禁用,每次点击都发一次请求。
- 修法:用一个
loading标志位(或pending变量)在请求前置 true、finally置 false,按钮绑:disabled="loading";数量加减再叠加防抖。
练习
数量加减:在购物车列表的
+ / -按钮上实现数量修改,本地立即改state.cartList中对应项的skuNum,同时调"加入购物车"接口把最终数量同步到服务端;连点+要防抖,避免每点一次发一次请求。搜索页加入购物车:在搜索页/商品详情页的"加入购物车"按钮上接入接口,请求期间禁用按钮防重复提交;成功后给出提示,并验证购物车角标或列表数据已更新。
批量删除体验增强:给列表加全选/反选;无选中商品时隐藏"删除选中的商品"与"结算"按钮(或置灰);删除前保留
window.confirm二次确认,确认后调用批量删除接口并更新列表。两种同步方案对比:把单个删除分别用"方案一:删完重新
dispatch('getCartListAsync')"和"方案二:commit('DELETE_CART_LIST_BY_ID', skuId)"各实现一遍,用 Network 面板数出请求次数差异,并写出各自在"代码量 / 服务器压力 / 数据一致性"上的取舍结论。合计 getter 加固:重写
getCountResult,要求只统计isChecked为真的商品,价格按skuPrice * skuNum累加并保留两位小数(避免浮点误差);用 3 组手造数据(全选 / 部分选 / 全不选)在控制台验证返回值正确。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 切换商品选中状态方案二 | 01:59 |
| 2 | 删除购物车商品 | 08:59 |
| 3 | 删除选中购物车商品 | 23:26 |
| 4 | 完成选中商品以及价格的统计 | 14:12 |
| 5 | 作业 | 02:09 |
小结
- 接一个新接口,先对齐 method、url 模板、参数位置三件事,删不动/改不动时优先打开 Network 看 Payload,而不是改业务代码。
delete的第二个参数是 config,请求体要包成{ data };post的第二个参数才是请求体。这一个差别能吃掉半天时间。- 接口调用后同步视图有两条路:重新拉列表(稳、多一次请求)或本地
commit改 state(快、逻辑多)。选哪条取决于数据是否会被别人改动。 - mutation 只做纯同步的状态变更,异步与副作用一律放 action,这是 Vuex 里最不该破的边界。
- 派生数据(选中件数、总价)交给 getter,组件里只负责展示,避免多个组件各算一遍导致数字打架。
- 异步顺序是隐形坑:没
await就去读 getter,拿到的永远是上一轮的值,调试时先看时序再看逻辑。 - 展示层的健壮性同样算功能:空态、按钮禁用、二次确认、防重复提交,缺一个都会被真实用户点出来。
- 命名与拼写保持一致(
cartList/skuId/commit)是第一排查入口,报错信息基本都能指向具体某一行。