主题
搭建购物车路由与获取购物车数据(Lesson19 2/3)
本页对应 Lesson19,共 5 段视频,约 1 小时 32 分。
学习目标
- 能独立创建购物车页面组件并配置购物车路由,在头部“我的购物车”和商品详情“去购物车结算”两个入口完成跳转验证。
- 能按“浏览器—前端服务—后端接口—数据库”的流程说明购物车数据获取链路,并定位“购物车无数据”的两个原因:未添加商品、未指定用户标识。
- 能配置请求拦截器为购物车相关接口添加临时用户标识,并基于
localStorage与 UUID 实现刷新后标识不变。 - 能在 Vuex 中定义购物车模块的 state、mutation、action,完成购物车列表获取、空数据兼容处理及商品图片、名称、单价、数量、小计等字段渲染。
- 能实现切换商品选中状态方案一:点击复选框调用切换接口,成功后重新获取购物车列表,并验证选中状态与接口返回一致。
- 能对比切换商品选中状态两种方案的实现步骤、优缺点及适用场景,说明方案一对服务器请求频率的影响。
知识点
先建购物车页面组件
为什么需要:路由跳转的目标必须先是组件,否则 router 里没有可注册的页面。怎么想:先把静态购物车页搬进 src/pages/Cart/index.vue,保留结构和样式,再把图片路径改成项目内路径。怎么做:新建 Cart/index.vue,模板如下;三张商品图补上中文 alt。易错点:图片路径别写错,../../assets/images/... 要相对当前文件;组件 name 保持 Cart,后面路由注册和调试都认它。转写里的“客”就是 Cart,“S2C”是 src,都是同音/口误。
vue
<template>
<!-- 购物车列表 -->
<div class="cart">
<h4>全部商品</h4>
<div class="cart-main">
<div class="cart-th">
<div class="cart-th1">全部</div>
<div class="cart-th2">商品</div>
<div class="cart-th4">单价</div>
<div class="cart-th4">数量</div>
<div class="cart-th5">小计(元)</div>
<div class="cart-th6">操作</div>
</div>
<div class="cart-body">
<ul class="cart-list">
<li class="cart-list-con1">
<input type="checkbox" name="chk_list">
</li>
<li class="cart-list-con2">
<img src="../../assets/images/goods1.png" alt="小米智能摄像机">
<div class="item-msg">米家(MIJIA) 小米小白智能摄像机增强版 1080p高清360度全景拍摄AI增强</div>
</li>
<li class="cart-list-con4">
<span class="price">399.00</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">399</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
</li>
</ul>
<ul class="cart-list">
<li class="cart-list-con1">
<input type="checkbox" name="chk_list" id="" value="">
</li>
<li class="cart-list-con2">
<img src="../../assets/images/goods2.png" alt="华为手机">
<div class="item-msg">华为(MIJIA) 华为metaPRO 30 浴霸4摄像 超清晰</div>
</li>
<li class="cart-list-con4">
<span class="price">5622.00</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">5622</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
</li>
</ul>
<ul class="cart-list">
<li class="cart-list-con1">
<input type="checkbox" name="chk_list" id="" value="">
</li>
<li class="cart-list-con2">
<img src="../../assets/images/goods3.png" alt="苹果手机">
<div class="item-msg">iphone 11 max PRO 苹果四摄 超清晰 超费电 超及好用</div>
</li>
<li class="cart-list-con4">
<span class="price">11399.00</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" value="1" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">11399</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
</li>
</ul>
</div>
</div>
<div class="cart-tool">
<div class="select-all">
<input class="chooseAll" type="checkbox">
<span>全选</span>
</div>
<div class="option">
<a href="#none">删除选中的商品</a>
</div>
<div class="money-box">
<div class="chosed">已选择
<span>0</span>件商品</div>
<div class="sumprice">
<em>总价(不含运费) :</em>
<i class="summoney">0</i>
</div>
<div class="sumbtn">
<a class="sum-btn" href="###" target="_blank">结算</a>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Cart"
}
</script>注册 /cart 路由
为什么需要:只有注册路由,router-link 才能把用户带到购物车页。怎么想:在 src/router/index.js 中先引入 Cart,再在路由数组里加 /cart。怎么做:按笔记片段配置,meta.isTypeNav: true 保留,供页面判断是否显示分类导航。易错点:先有组件再注册;component: Cart 不要写成字符串;转写里的“如头”就是 router,“可的”就是 cart。
js
{
path: "/cart",
component: Cart,
meta: {
isTypeNav: true
}
}补两个跳转入口
为什么需要:购物车页不能只能手输地址,用户要从头部和加入成功页进入。怎么想:把原来的 <a> 改成 <router-link>。怎么做:Header 中“我的购物车”指向 /cart;AddCartSuccess 中“去购物车结算”也指向 /cart,同时“查看商品详情”用 @click.prevent="$router.go(-1)" 返回上一页。易错点:to="/cart" 别漏斜杠;原 href 不要保留;转写里的“都田纲林”就是 router-link,“杜”就是 to。
vue
<template>
<div class="typeList">
<a>我的订单</a>
<router-link to="/cart">我的购物车</router-link>
<a>我的尚品汇</a>
<a>尚品汇会员</a>
<a>企业采购</a>
<a>关注尚品汇</a>
<a>合作招商</a>
<a>商家后台</a>
</div>
</template>vue
<template>
<div class="right-gocart">
<a @click.prevent="$router.go(-1)" href="#" class="sui-btn btn-xlarge">查看商品详情</a>
<router-link to="/cart" class="sui-btn btn-xlarge btn-danger ">去购物车结算 > </router-link>
</div>
</template>用转写稿理解数据链路与调试
为什么需要:路由搭好只代表“能进页面”,购物车里的数据仍是假数据。怎么想:转写稿把链路讲成“浏览器请求前端服务器拿页面,页面加载后再请求接口,接口查数据库返回数据”。调试时先直接请求购物车列表,网络里能看到请求,但 data 是空数组。原因有两个:一是还没真正往购物车添加商品;二是请求没有身份标识,服务器不知道“你是谁”。怎么做:在请求拦截器里加临时标识,转写里读作 userTempId,用 uuid 生成并存入 localStorage,保证刷新后不变;再按接口文档调用添加购物车 POST 接口,参数是 skuId、skuNum,成功后重新获取购物车列表。易错点:接口返回 200 但 data 为空,不等于添加成功;不要不加 await 就跳转;接口文档要看全请求方式、地址、参数和请求头;空数组不能直接取 data[0].cartInfoList,先判断长度。转写里的“破损”是 post,“盖”是 get,“school ID”是 skuId,“一思”是 isChecked。
渲染与选中状态方案一
为什么需要:拿到真实列表后要渲染商品、价格、数量、小计和选中状态。怎么想:用 v-for 遍历购物车列表,key 用唯一 id;复选框用 :checked="item.isChecked === 1";图片补中文 alt;小计用单价乘数量,价格走过滤器。怎么做:切换选中状态先按接口文档调“切换商品选中状态”,传 skuId 和取反后的 isChecked,判断 code === 200 后重新 dispatch 获取购物车列表。易错点:isChecked 要取反,不能把原值直接传回;方案一优点是简单,缺点是每次切换都重新请求,频繁操作会给服务器压力。转写里的“雪路/卷落”是选中,“货循环”是 v-for,“马”是 key。
一、先搭购物车路由与入口
为什么需要:购物车页要先能访问,才谈得上获取数据。转写稿先做组件、路由,再补两个入口:头部“我的购物车”和商品详情/结算区的“去购物车结算”。
怎么想:先有页面组件,再在路由表注册,最后把入口链接指向它。入口本质是路由跳转,不是重新写一套页面。
怎么做:按转写稿整理,组件放到购物车目录,路由中增加购物车配置;入口用 router-link 指向购物车路径。示意代码如下,路径名以项目实际为准:
js
// router/index.js
{
path: '/cart',
component: () => import('@/pages/Cart')
}html
<!-- Header 中 -->
<router-link to="/cart">我的购物车</router-link>
<!-- 结算区域 -->
<router-link to="/cart">去购物车结算</router-link>易错点:组件建好但没注册路由;入口写了但 to 路径与路由不一致;把“去购物车结算”仍写成普通按钮或旧链接。转写稿中反复强调“组件—路由—入口”三步,缺一步都跳不过去。
二、理解获取购物车数据的完整链路
为什么需要:购物车数据不在页面里,而在后端数据库。页面只是前端服务器返回的静态资源,真正数据要通过接口拿。
怎么想:浏览器输入网址 → 请求前端服务器拿页面 → 页面渲染后调接口 → 接口访问数据库 → 返回数据给前端 → 前端存入仓库并渲染。转写稿把链路拆成浏览器、前端服务器、后端服务器/数据库三部分,是为了说明“页面能打开”不等于“数据能拿到”。
怎么做:先按接口文档确认请求地址、请求方式、参数,再在 API 层封装,在 store 的 action 中调用,组件中 dispatch 触发。
易错点:只看地址不看请求方式;把“接口返回 200”误认为业务一定成功;数据为空时不查请求头、参数和数据库记录。
三、为什么一开始获取不到数据
为什么需要:转写稿先直接调“获取购物车列表”,结果 data 是空数组。必须定位原因,否则后面渲染无从谈起。
怎么想:转写稿给出两个原因:第一,之前点“加入购物车”只是往本地存了数据,没有真正调用添加接口,数据库里没有商品;第二,调用接口没有身份标识,后端不知道“你是谁”,自然不知道返回谁的购物车。
怎么做:先补“添加购物车”接口,再补身份标识。添加接口按文档使用 POST,参数包含商品 ID 和数量;获取列表按文档使用 GET。转写稿调试时发现:添加接口提示成功,但再获取仍为空;查看请求头后确认缺少 userTempId。
易错点:把本地 localStorage 里的“加入购物车”当成真实入库;接口成功提示不等于数据已写入;缺少标识时接口可能仍返回 200,具有迷惑性。
四、加临时身份标识并持久化
为什么需要:未登录用户也要有临时身份,否则“你加购的商品”和“别人加购的商品”无法区分。
怎么想:在请求拦截器里统一加请求头,避免每个接口手写;临时标识第一次生成后要保存,刷新不能变,否则刷新后购物车又空了。
怎么做:用 UUID 生成临时标识,存 localStorage;请求拦截器统一写入请求头。按转写稿整理:
js
// utils/userTempId.js
import { v4 as uuidv4 } from 'uuid'
export function getUserTempId() {
let userTempId = localStorage.getItem('userTempId')
if (!userTempId) {
userTempId = uuidv4()
localStorage.setItem('userTempId', userTempId)
}
return userTempId
}js
// request.js 请求拦截器
config.headers.userTempId = getUserTempId()易错点:每次请求都重新生成 UUID,导致刷新后标识变化;请求头字段名写错;只生成不保存;把标识写死在代码里。
五、获取列表、存入仓库并渲染
为什么需要:接口数据要进入 Vuex/Pinia 仓库,组件才能响应式渲染。
怎么想:API 层只负责发请求;store 模块维护 cartList;action 调接口后 commit 更新;组件 dispatch 后按数据渲染。
怎么做:转写稿中获取列表后取 res.data[0].cartInfoList,但空数组时直接取 [0] 会报错,因此要先判断。示意:
js
async getCartList({ commit }) {
const res = await getCartListApi()
const list = res.data?.[0]?.cartInfoList || []
commit('SET_CART_LIST', list)
}组件中触发:
js
this.$store.dispatch('cart/getCartList')渲染时用 v-for 绑定商品图片、名称、单价、数量、小计,并用过滤器格式化金额。
易错点:data 为空时直接取 [0];store 模块建了但没注册;action 写了但组件没 dispatch;数量和小计没有按“单价 × 数量”计算。
六、切换商品选中状态:方案一
为什么需要:用户勾选/取消勾选要同步到后端,否则刷新后状态又回到接口返回值。
怎么想:方案一是“调切换接口 → 成功后重新获取购物车列表”。这样远程状态改了,本地仓库也随最新列表更新。
怎么做:API 层封装切换接口,参数为商品 ID 和选中状态;store action 中判断 res.code === 200 后重新 dispatch('cart/getCartList')。组件复选框 change 时传当前项 ID 和取反后的状态。
js
async getCartIsChecked({ dispatch }, { skuId, isChecked }) {
const res = await getCartIsCheckedApi(skuId, isChecked)
if (res.code === 200) {
dispatch('cart/getCartList')
}
}易错点:状态没有取反;传成购物车 ID 而不是商品 ID;接口成功后不更新本地列表;频繁切换会重复请求,转写稿也指出方案一简单直接,但操作频繁会给服务器增加压力。
先把数据链路想清楚
为什么需要:购物车页面不能继续渲染假数据。页面上的图片、名称、单价、数量、合计都应当来自后端购物车列表接口。
怎么想:浏览器请求前端页面,页面加载后再请求接口;接口根据身份标识返回当前用户的购物车数据,前端把数据存入 Vuex 的 cart 模块,模板再通过 $store.state.cart.cartList 渲染。
怎么做:先在 store 的 cart 模块里准备 cartList,通过 action 调“获取购物车列表”接口,再 commit 到 mutation 更新 cartList。组件里用 v-for 遍历。
易错点:接口返回成功但 data 为空,不一定是渲染写错。转写稿里调试时发现:请求没有带临时身份标识,后端不知道“你是谁”,所以拿不到购物车数据;在请求拦截器里增加 userTempId 请求头,并用 UUID 生成、缓存到 localStorage,刷新后保持不变,才能稳定取到同一用户的购物车。另一个易错点是接口返回结构可能是空数组或 data[0].cartInfoList,取值前要判断,避免空数据时报错。
列表渲染与唯一 key
为什么需要:购物车是多条商品记录,必须循环生成每一行。
怎么想:一条记录对应一个 li 集合,外层用 v-for 遍历 cartList,并给每行加唯一 :key,方便 Vue 复用和更新。
怎么做:笔记中直接遍历仓库数据:
vue
<template>
<div class="cart-body">
<ul v-for="item in $store.state.cart.cartList" :key="item.id" class="cart-list">
<li class="cart-list-con1">
<input type="checkbox" :checked="item.isChecked===1" name="chk_list">
</li>
<li class="cart-list-con2">
<img :src="item.imgUrl">
<div class="item-msg">{{item.skuName}}</div>
</li>
<li class="cart-list-con4">
<span class="price">{{item.skuPrice | currency(2,"¥")}}</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" :value="item.skuNum" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">{{(item.skuNum*item.skuPrice) | currency(2,"¥")}}</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
</li>
</ul>
</div>
</template>易错点::key 不要用数组下标,商品行有增删和状态变化时容易错位;v-for 要写在真正重复的元素上;渲染前先确认 cartList 已拿到数据。
图片、标题、价格与数量
为什么需要:购物车每一行要展示商品图、名称、单价、购买数量和小计。
怎么想:字段与接口返回一一对应:imgUrl、skuName、skuPrice、skuNum。价格和小计用过滤器统一格式。
怎么做:图片用 :src="item.imgUrl";名称用 {{item.skuName}};单价用 {{item.skuPrice | currency(2,"¥")}};数量输入框用 :value="item.skuNum";小计用 {{(item.skuNum*item.skuPrice) | currency(2,"¥")}}。
易错点:数量输入框这里不要随手改成 v-model。因为购物车数量修改要调接口,直接双向绑定只改本地显示,和数据库不一致;复选框同理,:checked="item.isChecked===1" 表示状态由接口数据决定,点击后应走“切换选中状态”的接口,再决定重新拉列表还是同步本地状态。图片路径不要写死静态路径,要使用接口字段。
调试顺序与常见坑
为什么需要:渲染问题往往不是模板本身,而是数据没到仓库。
怎么想:按“接口是否发出 → 请求头是否有身份 → 响应 data 是否有值 → mutation 是否写入 cartList → 模板是否遍历”的顺序排查。
怎么做:先在网络面板确认获取购物车列表请求成功;再确认 cartList 在 Vue 开发者工具中有数据;最后检查模板字段名是否和接口一致。
易错点:字段名拼错、过滤器未注册、空数据直接取 data[0]、只改本地状态不调接口,都会造成“页面不动”或“刷新又变回去”。转写稿里还特别提醒:接口文档要看全,请求方式、地址、参数、请求头都要核对,不能只看一个地址就开始写。
方案一要解决的问题
为什么需要:购物车里的复选框点击后,页面看起来变了,但刷新又回到原状态。原因是点击只改了 DOM 的 checked,Vue 数据里的 item.isChecked 并没有真正改变;刷新后重新请求购物车列表,接口返回的仍是旧状态,所以又覆盖回来。
怎么想:既然本地数据没改,就不要只改本地。先调用“切换商品选中状态”接口,把服务器里的状态改掉;改成功后再重新拉一次购物车列表,让本地 cartList 用最新接口数据更新,视图自然跟着变。
怎么做:按“API → store action → 组件事件”三层写。先封装切换接口,再在 store 里编排“改状态 + 重新拉列表”,最后在复选框的 change 事件里 dispatch。
易错点:不要把“购物车行 ID”当成 skuID;skuID 是商品 ID。转写稿里调试时特意确认过:购物车列表里的 item.skuId 才是要传给接口的商品 ID,不是购物车行 ID。
先写 API:切换选中状态
为什么需要:组件和 store 不直接拼请求地址,统一放到 src/api/cart.js,后续复用和改地址都方便。
怎么想:看接口文档:/api/cart/checkCart/{skuID}/{isChecked},请求方式是 get;isChecked 为 0 表示取消选中,为 1 表示选中。
怎么做:
js
// 切换商品选中状态 /api/cart/checkCart/{skuID}/{isChecked} get
// skuID:商品ID
// isChecked:商品选中状态 0代表取消选中 | 1代表选中
export const getCartIsCheckedById = (skuID,isChecked)=>sphRequest.get(`/cart/checkCart/${skuID}/${isChecked}`)易错点:请求方式是 get,参数在路径里,不要写成 post 或 body 参数;isChecked 传的是“目标状态”,不是当前状态。
在 store 中编排:改远端后刷新列表
为什么需要:接口调用成功后,如果只改服务器、不改本地 cartList,页面仍不会按新状态渲染。方案一的核心就是“修改完成后重新调用购物车列表接口”。
怎么想:action 接收 skuID 和 isChecked,先 await 切换接口;判断 res.code === 200 后,dispatch("getCartListAsync") 重新获取购物车列表;失败则提示异常。
怎么做:
js
// 解决方案一
async getCartIsCheckedByIdAsync({dispatch},{skuID,isChecked}){
const res = await getCartIsCheckedById(skuID,isChecked);
if(res.code === 200){
// 重新获取购物车列表。
dispatch("getCartListAsync");
}else{
alert("异常:"+res.message)
}
},易错点:别忘了在 store 中引入 getCartIsCheckedById;getCartListAsync 必须是之前已经写好的“获取购物车列表”action;调试时先看网络面板里 /cart/checkCart/2/0 这类请求是否返回 code: 200,再看刷新列表后的 cartList 是否更新。
在组件中绑定 change 事件
为什么需要:用户勾选/取消勾选时,要把“商品 ID”和“取反后的目标状态”传给 store。
怎么想:当前 item.isChecked === 1 表示选中,点击后应传 0;否则传 1。所以用三元表达式取反。复选框显示状态仍用 :checked="item.isChecked===1"。
怎么做:
vue
<li class="cart-list-con1">
<input @change="$store.dispatch('cart/getCartIsCheckedByIdAsync',{
skuID:item.skuId,
isChecked:item.isChecked===1?0:1
})" type="checkbox" :checked="item.isChecked===1" name="chk_list">
</li>易错点:skuID 用 item.skuId,不要用购物车行 ID;isChecked 要取反,不能把当前值直接传过去;@change 里 dispatch 的模块名要写 cart/。
调试过程与方案边界
调试过程:点击复选框后,在 Network 里确认发出了 get 请求,路径参数正确;接口返回 code: 200 后,再确认 getCartListAsync 被调用,购物车列表接口重新返回数据,item.isChecked 更新,页面勾选状态才真正稳定。若只调切换接口、不重新拉列表,就会出现“接口成功但页面没变”的假象。
优点:简洁,程序少,改完立刻以服务器数据为准。
缺点:每次切换都要重新发请求;如果用户高频勾选,会给服务器造成压力。
易错点:异常分支要提示 res.message;不要把方案一和“本地直接改状态”的方案二混在一起写,否则状态来源会乱。
方案二要解决什么问题
方案一的做法是:点击复选框后调用“切换商品选中状态”接口,接口成功后再重新调用“获取购物车列表”接口,把最新数据拉回来。这样本地状态确实会更新,但每点一次复选框就多发一次购物车列表请求;操作频率一高,会给服务器增加压力。
方案二的目标是:接口仍然要调,但调完接口、确认成功后,不再重新获取购物车列表,而是直接通过 mutation 修改本地仓库中对应商品的 isChecked。 这样减少 ajax 请求次数,但代价是代码量更多、逻辑更细。
为什么需要这样想?因为“切换选中状态”这件事,服务器端必须知道,否则刷新页面后状态会回到接口返回的旧值;但本地界面也没必要为了一个字段重新拉整个列表。于是拆成两步:先让服务器改,成功后再让本地仓库改。
先确认接口参数与状态含义
怎么做:先看接口文档,切换商品选中状态需要两个参数:skuID(商品 ID)和 isChecked(目标选中状态)。0 表示取消选中,1 表示选中。注意这里传的是“改完之后的目标值”,不是当前值。
怎么想:组件里当前 item.isChecked 如果是 1,用户点击后应传 0;如果当前是 0,点击后应传 1。所以组件里不能直接把当前值传过去,而要做取反判断。易错点是把“当前状态”当成“目标状态”传给接口,导致点了没变化。
在 mutation 中改本地仓库
为什么需要 mutation:Vuex 中修改 state 要集中、可追踪,不能在图省事在组件里直接改 cartList。方案二的核心就是新增一个 mutation,根据 skuID 找到购物车项,把它的 isChecked 改成接口成功后的目标值。
怎么做:用 find 按 v.skuId === skuID 查找;找到再赋值,找不到要判空,避免报错。注意字段大小写:state 里是 skuId,接口参数叫 skuID,不要混。
js
const mutations = {
UP_CAART_LIST_CHECKED_BY_ID(state,{skuID,isChecked}){
// 根据商品ID,找到购物车信息。然后将购物车信息中的选中状态更改为isChecked
const info = state.cartList.find(v=>v.skuId===skuID);
if(info) info.isChecked = isChecked;
}
};
const actions = {
async getCartIsCheckedByIdAsync({commit},{skuID,isChecked}){
const res = await getCartIsCheckedById(skuID,isChecked);
if(res.code === 200){
commit("UP_CAART_LIST_CHECKED_BY_ID",{skuID,isChecked})
}else{
alert("异常:"+res.message)
}
}
}易错点:mutation 只负责改本地,不发请求;action 里必须先判断 res.code === 200 再 commit。如果接口失败还改本地,会出现“界面变了但服务器没变”,刷新后状态又回去。find 找不到时不要强行改,先判 if(info)。
组件触发与调试过程
怎么做:在复选框的 change 事件里派发 action,传 skuID: item.skuId,以及取反后的目标状态。示意如下:
js
changeChecked(item){
const isChecked = item.isChecked === 1 ? 0 : 1;
this.$store.dispatch("getCartIsCheckedByIdAsync", {
skuID: item.skuId,
isChecked
});
}调试时先看网络面板:请求地址、请求方式、skuID 和 isChecked 是否传对;再看响应 code 是否为 200。成功后打开 Vue Devtools 看 cartList 中对应项的 isChecked 是否立即变化。若没变,优先查 action 名、mutation 名、skuId 大小写和 find 条件。
方案对比与小结
方案一优点是简单:接口成功后重新拉列表,本地自然最新;缺点是每次切换都多一次列表请求。方案二优点是减少请求;缺点是代码量较多,增加逻辑,要维护“接口成功后再改本地”的顺序。易错点是接口失败仍改本地、取反逻辑写反、字段名不一致。实际开发中,操作频繁、列表较大时更适合方案二;逻辑简单、追求稳妥时可用方案一。
课堂画面





代码示例
代码段1:购物车路由与入口
js
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Cart from '@/pages/Cart'
Vue.use(VueRouter)
const routes = [
// ...已有路由
{
path: "/cart",
component: Cart,
meta: {
isTypeNav: true
}
}
]
export default new VueRouter({
routes
})vue
<!-- src/components/Header/index.vue -->
<template>
<div class="typeList">
<a>我的订单</a>
<router-link to="/cart">我的购物车</router-link>
<a>我的尚品汇</a>
<a>尚品汇会员</a>
<a>企业采购</a>
<a>关注尚品汇</a>
<a>合作招商</a>
<a>商家后台</a>
</div>
</template>vue
<!-- src/pages/AddCartSuccess/index.vue -->
<template>
<div class="right-gocart">
<a @click.prevent="$router.go(-1)" href="#" class="sui-btn btn-xlarge">查看商品详情</a>
<router-link to="/cart" class="sui-btn btn-xlarge btn-danger ">去购物车结算 > </router-link>
</div>
</template>要点:
- 路由对象中保留
meta.isTypeNav,用于控制 TypeNav 是否显示。 - Header 和 AddCartSuccess 都使用
router-link跳/cart,不要写死href。 - 易错点:
router-link的to是路径字符串,不是href。
代码段2:渲染购物车数据 + 切换选中状态(方案二)
vue
<!-- src/pages/Cart/index.vue -->
<template>
<div class="cart">
<h4>全部商品</h4>
<div class="cart-main">
<div class="cart-th">
<div class="cart-th1">全部</div>
<div class="cart-th2">商品</div>
<div class="cart-th4">单价</div>
<div class="cart-th4">数量</div>
<div class="cart-th5">小计(元)</div>
<div class="cart-th6">操作</div>
</div>
<div class="cart-body">
<ul v-for="item in $store.state.cart.cartList" :key="item.id" class="cart-list">
<li class="cart-list-con1">
<input
@change="$store.dispatch('cart/getCartIsCheckedByIdAsync', {
skuID: item.skuId,
isChecked: item.isChecked === 1 ? 0 : 1
})"
type="checkbox"
:checked="item.isChecked === 1"
name="chk_list"
>
</li>
<li class="cart-list-con2">
<img :src="item.imgUrl">
<div class="item-msg">{{ item.skuName }}</div>
</li>
<li class="cart-list-con4">
<span class="price">{{ item.skuPrice | currency(2, "¥") }}</span>
</li>
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins">-</a>
<input autocomplete="off" type="text" :value="item.skuNum" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus">+</a>
</li>
<li class="cart-list-con6">
<span class="sum">{{ (item.skuNum * item.skuPrice) | currency(2, "¥") }}</span>
</li>
<li class="cart-list-con7">
<a href="#none" class="sindelet">删除</a>
<br>
</li>
</ul>
</div>
</div>
<div class="cart-tool">
<div class="select-all">
<input class="chooseAll" type="checkbox">
<span>全选</span>
</div>
<div class="option">
<a href="#none">删除选中的商品</a>
</div>
<div class="money-box">
<div class="chosed">已选择 <span>0</span>件商品</div>
<div class="sumprice">
<em>总价(不含运费) :</em>
<i class="summoney">0</i>
</div>
<div class="sumbtn">
<a class="sum-btn" href="###" target="_blank">结算</a>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Cart"
}
</script>要点:
v-for遍历$store.state.cart.cartList,:key="item.id"用购物车条目 id,不要用索引。- 复选框用
:checked="item.isChecked === 1",@change中取反后传isChecked。 :value="item.skuNum"是单向绑定,避免直接修改仓库数据;小计用item.skuNum * item.skuPrice。- 易错点:
currency过滤器需已注册;item.isChecked是数字 1/0,不是布尔值。
代码段3:Vuex cart 模块(方案二)与 API
js
// src/api/cart.js
import sphRequest from '@/utils/sphRequest'
// 切换商品选中状态 /api/cart/checkCart/{skuID}/{isChecked} get
// skuID:商品ID
// isChecked:商品选中状态 0代表取消选中 | 1代表选中
export const getCartIsCheckedById = (skuID, isChecked) =>
sphRequest.get(`/cart/checkCart/${skuID}/${isChecked}`)js
// src/store/cart.js
import { getCartIsCheckedById } from '@/api/cart'
const state = {
cartList: []
}
const mutations = {
UP_CAART_LIST_CHECKED_BY_ID(state, { skuID, isChecked }) {
const info = state.cartList.find(v => v.skuId === skuID)
if (info) info.isChecked = isChecked
}
}
const actions = {
async getCartIsCheckedByIdAsync({ commit }, { skuID, isChecked }) {
const res = await getCartIsCheckedById(skuID, isChecked)
if (res.code === 200) {
commit("UP_CAART_LIST_CHECKED_BY_ID", { skuID, isChecked })
} else {
alert("异常:" + res.message)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}要点:
- API 地址和参数顺序按笔记:
/cart/checkCart/${skuID}/${isChecked}。 - 方案二在接口成功后只
commit本地 mutation,不重新请求购物车列表。 - mutation 中
find(v => v.skuId === skuID)按商品 ID 找条目,找到后改isChecked。 - 易错点:组件 dispatch 要带模块名
cart/,store 模块要namespaced: true;传参skuID与数据字段skuId不要写混。
常见错误
购物车页面能进但列表一片空白 现象:路由跳转正常,
v-for一个都不渲染,Network 里也没有购物车列表请求。 原因:store/cart.js写好了但没在store/index.js的modules里注册,或组件mounted里没dispatch,或 dispatch 时漏了命名空间前缀cart/。 修法:确认modules: { cart }已挂载;组件里this.$store.dispatch('cart/getCartListAsync'),并先console.log一次返回结果确认链路通。每次刷新购物车就"空了"或换了一份数据 现象:加购后能看到商品,F5 刷新后列表变空,Network 里
userTempId每次都不一样。 原因:临时标识直接用uuid.v4()现算现用,没做持久化,每次刷新都是新身份。 修法:封装getUserTempId(),先localStorage.getItem('userTempId'),有就直接返回,没有才生成并setItem,全局只走这一个方法。接口返回 code 200,但
res.data是空数组 现象:添加购物车提示成功,获取列表也成功,就是没数据;后端不报错,容易卡很久。 原因:请求没带身份标识(请求头缺userTempId),服务端不知道这份购物车属于谁。 修法:在request.js的请求拦截器里统一config.headers.userTempId = getUserTempId(),不要在每个 api 文件里各写各的。res.data[0].cartInfoList报 "Cannot read property of undefined" 现象:购物车清空后或新用户首次进入,页面直接白屏报错。 原因:无数据时data是[],data[0]为undefined,直接点属性就崩。 修法:const list = res.data[0] ? res.data[0].cartInfoList : [](或可选链 + 默认值),mutation 里兜底成空数组。点复选框状态闪一下又弹回去 / 刷新后恢复原样 现象:勾选后界面变了,刷新又变回全选;或者干脆点不动。 原因:只调了接口没同步本地(或只改了本地没调接口);
@change里传的isChecked传成了当前值而不是取反值。 修法:方案一 dispatch 成功后dispatch('cart/getCartListAsync')重拉;方案二 commit mutation 改本地对应项。传参写isChecked: item.isChecked === 1 ? 0 : 1。加入购物车后立刻跳转,购物车却没有这件商品 现象:提示"加入成功",跳到购物车页面数据是旧的。 原因:
dispatch返回 Promise,但没await就$router.push,请求还没落地就跳走了。 修法:async处理函数里await this.$store.dispatch('cart/postAddCartAsync', {...})之后再跳转。商品图片全部 404 现象:列表渲染出来了,图片位置是碎图标。 原因:从静态页复制模板时
src路径没改,./images/xxx.png在 Vue 项目里解析不到。 修法:改成../../assets/images/xxx.png或@/assets/images/xxx.png,用:src绑定时注意不要加引号写死。
练习
给搜索页/详情页的"加入购物车"按钮接上真实接口:接收
skuId与购买数量,await成功后跳转,失败时弹出res.message。跳转后回购物车确认数量是否正确。手写
src/utils/userTempId.js:导出getUserTempId(),要求刷新页面标识不变、清空 localStorage 后能重新生成。写完在请求拦截器里接入,用 Network 面板验证多次刷新userTempId是否恒定。用"方案二"实现复选框切换:调接口成功后通过 mutation 直接改本地对应项的
isChecked,不允许重新拉列表。实现完对比方案一,用 Network 面板数一数点击 10 次产生的请求条数差异。给购物车加空态兜底:当
cartList.length === 0时渲染"购物车还是空的"提示,并保证此时不报任何undefined相关错误。故意清空 localStorage 后刷新验证。在
store/cart.js里加getters:算出"已选商品件数"和"已选商品总价",替换掉模板里写死的0。要求勾选/取消勾选、增减数量后数字实时更新。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 搭建购物车路由 | 07:13 |
| 2 | 获取数据 | 31:38 |
| 3 | 获取数据二 | 25:58 |
| 4 | 渲染购物车数据 | 11:17 |
| 5 | 切换商品选中状态方案一 | 16:19 |
小结
- 路由是三件事的闭环:组件 → 路由表配置 → 入口链接,缺一个都点不过去;入口往往不止一个(Header 和加购成功页)。
- 购物车数据拿不到,90% 不是代码写错,而是"服务端不知道你是谁"——身份标识必须先有,再谈数据。
- 临时标识必须"生成一次、处处复用、刷新不变",所以生成逻辑要收敛到一个方法里,而不是散落在各 api 文件。
- 请求头这类全局共性参数,放在请求拦截器里做,比每个接口手写更不容易漏,也更符合工程习惯。
- 列表渲染前先看数据结构:
data是数组、data[0]是对象、里面才是cartInfoList,空数据时逐层兜底,能避免绝大多数白屏。 - 修改状态的两套方案本质是"重新同步"与"本地同步"的取舍:方案一代码少但请求多,方案二省请求但要维护本地一致性,按操作频率选。
- 异步 action 一定要
await再执行后续动作(跳转、提示),否则会出现"提示成功但页面是旧数据"的假象。 - 调接口排查要当场对齐三件事:请求方式、请求地址、请求参数(含请求头),不要拖成"查了好几天的悬案"。