当前位置: 首页 > news >正文

【Vue2手录09】购物车实战

购物车案例图示-vue实现

(完整示例代码见底部)

一、购物车实战基础(需求与结构)

1.1 核心需求拆解

购物车是 Vue 数据驱动思想的典型应用,需实现以下核心功能:

  • 商品列表展示(名称、单价、数量、选中状态)
  • 商品数量 加减操作(数量 ≥ 1)
  • 单个商品 选中/取消 切换
  • 全选/取消全选 批量操作
  • 已选商品 总价计算数量统计
  • 结算按钮 禁用状态控制(无选中商品时禁用)
  • 商品 删除功能(删除后同步更新统计数据)

1.2 核心数据结构

购物车数据分为「商品列表数据」和「汇总统计数据」,需设计为 响应式数据(确保数据变化自动更新视图)。

1.2.1 商品列表数据(products

数组类型,每个元素为商品对象,包含以下属性:

属性名类型含义默认值响应式说明
selectBoolean商品选中状态false点击切换时取反
nameString商品名称-静态数据(从后端获取)
priceNumber商品单价(元)-静态数据(从后端获取)
countNumber商品数量1加减操作时修改

示例代码

data() {return {products: [{ select: false, name: "iPhone14", price: 10000, count: 1 },{ select: false, name: "键盘", price: 400, count: 1 },{ select: false, name: "粉底液", price: 300, count: 1 },{ select: false, name: "PS5", price: 5000, count: 1 }]};
}
1.2.2 汇总统计数据(total 相关)

无需单独定义 total 对象,可通过 计算属性 动态生成(避免冗余数据),核心统计项:

  • 已选商品数量:selectedCount(选中商品数组的长度)
  • 已选商品总价:totalPrice(选中商品 count × price 累加)
  • 全选状态:isAllSelected(所有商品 selecttrue 时为 true
  • 结算按钮禁用状态:isBtnDisabled(无选中商品时为 true

1.3 页面结构实现(表格布局仅作为示例)

购物车需展示结构化数据,表格布局div+CSS 更高效(自带行列对齐,减少样式代码),核心结构分为「商品列表区」和「底部汇总区」。

1.3.1 商品列表渲染(v-for 核心应用)

通过 v-for 循环生成商品行,动态绑定选中图标、商品信息与操作按钮:

<table class="cart-table"><!-- 表头 --><thead><tr><th>选择</th><th>商品名称</th><th>单价(元)</th><th>数量</th><th>操作</th></tr></thead><!-- 商品列表(v-for 循环行) --><tbody><tr v-for="(item, index) in products" :key="index"><!-- 1. 选中状态图标(动态切换) --><td><img @click="toggleSelect(index)" :src="item.select ? './img/selected.png' : './img/unselect.png'" alt="选中状态"></td><!-- 2. 商品信息(直接绑定数据) --><td>{{ item.name }}</td><td>{{ item.price }}</td><!-- 3. 数量加减操作 --><td><button @click="changeCount(false, index)">-</button><span>{{ item.count }}</span><button @click="changeCount(true, index)">+</button></td><!-- 4. 删除按钮 --><td><img @click="deleteProduct(index)" src="./img/delete.png" alt="删除"></td></tr></tbody>
</table>
1.3.2 底部汇总区域

展示全选、已选数量、总价与结算按钮,所有数据通过 计算属性动态绑定

<div class="cart-footer"><!-- 全选图标 --><img @click="toggleAllSelect" :src="isAllSelected ? './img/selected.png' : './img/unselect.png'" alt="全选"><!-- 已选数量与总价 --><span>已选({{ selectedCount }})</span><span>总价:¥{{ totalPrice }}</span><!-- 结算按钮(禁用状态动态控制) --><button :disabled="isBtnDisabled">结算</button>
</div>
1.3.3 基础样式(确保布局美观)
.cart-table {width: 800px;margin: 20px auto;border-collapse: collapse; /* 合并边框(避免双线) */
}
.cart-table th, .cart-table td {border: 1px solid #ddd;padding: 10px;text-align: center;
}
.cart-footer {width: 800px;margin: 0 auto;display: flex;justify-content: space-between;align-items: center;
}
.cart-footer button:disabled {background: #ccc;cursor: not-allowed;
}

二、核心功能实现(数据驱动视图)

所有功能遵循 「修改数据即更新视图」 原则,无需手动操作 DOM(Vue 响应式机制自动处理视图更新)。

2.1 商品数量加减(changeCount 方法)

2.1.1 核心逻辑
  • 通过 一个方法+参数 合并「加」和「减」操作(避免重复代码)
  • 参数 flagtrue 表示加,false 表示减
  • 参数 index:定位当前操作的商品(通过 v-forindex 传递)
  • 边界处理:减操作时确保 count ≥ 1(避免负数)
2.1.2 代码实现
methods: {changeCount(flag, index) {const currentProduct = this.products[index];if (flag) {// 加操作:直接修改 count(响应式)currentProduct.count++;} else {// 减操作:仅当 count > 1 时允许if (currentProduct.count > 1) {currentProduct.count--;}}}
}
2.1.3 响应式原理

Vue 对 对象属性修改(如 currentProduct.count++)是响应式的——修改后会触发依赖追踪,自动更新视图中使用 count 的位置(如数量显示、总价计算)。

2.2 单个商品选中切换(toggleSelect 方法)

2.2.1 核心逻辑
  • 商品选中状态由 item.select 控制(true 选中,false 未选中)
  • 点击图标时 取反 select 属性!item.select
  • 通过 index 定位当前操作的商品
2.2.2 代码实现
methods: {toggleSelect(index) {// 取反选中状态(响应式更新)this.products[index].select = !this.products[index].select;}
}
2.2.3 常见错误
  • 忘记传递 index:无法定位商品,导致所有商品选中状态同步变化
  • 直接赋值 truethis.products[index].select = true,无法取消选中

2.3 已选商品统计与总价计算(计算属性)

2.3.1 核心思想

总价和已选数量 依赖商品的 selectcount,适合用 计算属性(computed 实现——计算属性会缓存结果,仅当依赖项变化时重新计算(性能优于方法 methods)。

2.3.2 代码实现
computed: {// 1. 已选商品数组(筛选 select: true 的商品)selectedProducts() {return this.products.filter(item => item.select);},// 2. 已选商品数量(已选数组的长度)selectedCount() {return this.selectedProducts.length;},// 3. 已选商品总价(已选商品 count × price 累加)totalPrice() {// 初始总价为 0,遍历已选商品累加return this.selectedProducts.reduce((total, item) => {return total + item.count * item.price;}, 0);},// 4. 结算按钮禁用状态(无选中商品时禁用)isBtnDisabled() {return this.selectedProducts.length === 0;},// 5. 全选状态(所有商品选中则为 true)isAllSelected() {// every:所有元素满足条件才返回 true(空数组时返回 true,需特殊处理)return this.products.length > 0 && this.products.every(item => item.select);}
}
2.3.3 计算属性 vs 方法
对比维度计算属性(computed方法(methods
核心目的获取一个依赖其他数据的 结果值执行一个 操作过程(如修改数据)
缓存机制有缓存(依赖不变则复用结果)无缓存(每次调用都重新执行)
调用方式像变量一样使用({{ totalPrice }}需主动调用({{ getTotalPrice() }}
适用场景总价计算、状态判断(如全选)事件处理、数据修改(如加减数量)

2.4 全选/取消全选(toggleAllSelect 方法)

2.4.1 核心逻辑
  • 全选状态由 isAllSelected(计算属性)判断:
    • isAllSelectedtrue(已全选),点击后 取消所有选中select: false
    • isAllSelectedfalse(未全选),点击后 选中所有商品select: true
  • 通过 forEach 批量修改所有商品的 select 属性
2.4.2 代码实现
methods: {toggleAllSelect() {// 1. 确定目标状态:已全选则改为 false,否则改为 trueconst targetStatus = !this.isAllSelected;// 2. 批量修改所有商品的 select 属性this.products.forEach(item => {item.select = targetStatus;});}
}
2.4.3 优化点

避免在 if/else 中重复写循环逻辑(如原笔记中的冗余代码),通过 targetStatus 统一赋值,代码更简洁、可维护。

2.5 商品删除功能(deleteProduct 方法)

2.5.1 核心逻辑
  • products 数组中删除指定索引的商品
  • 使用 Vue 响应式数组方法 splice(index, 1)(直接修改数组索引无法触发响应式)
  • 删除后自动更新已选数量、总价、全选状态(依赖计算属性)
2.5.2 代码实现
methods: {deleteProduct(index) {// splice(起始索引, 删除个数):响应式删除数组元素this.products.splice(index, 1);}
}
2.5.3 响应式数组方法说明

Vue 对数组的 直接索引修改(如 this.products[index] = null)不触发响应式,需使用以下内置方法:

方法名作用示例
splice删除/插入/替换元素this.products.splice(0, 1)
push尾部添加元素this.products.push(newItem)
pop尾部删除元素this.products.pop()
shift头部删除元素this.products.shift()
unshift头部添加元素this.products.unshift(newItem)

三、代码优化(优雅与性能)

3.1 事件合并:减少重复代码

「代码越少越好」,将相似功能合并为一个方法(如数量加减),避免分别定义 plusCountminusCount 两个方法,减少冗余。

3.2 计算属性替代手动状态更新

3.2.1 问题场景

原笔记初期手动定义 total.select 存储全选状态,但需在 toggleSelecttoggleAllSelectdeleteProduct 等多个方法中手动更新,易遗漏导致状态同步错误。

3.2.2 优化方案

total.select 改为计算属性 isAllSelected(见 2.3.2 节),自动响应所有商品的 select 变化,无需手动维护状态,减少错误。

3.3 逻辑简化:使用布尔值与数组方法

3.3.1 全选逻辑优化

通过 filter 筛选已选商品后比较长度,优化后使用 every 方法直接判断所有商品是否选中,代码更简洁:

// 优化前(冗余)
const selectedArr = this.products.filter(item => item.select);
const isAll = selectedArr.length === this.products.length;// 优化后(简洁)
const isAll = this.products.every(item => item.select);
3.3.2 总价计算优化

使用 reduce 方法替代 forEach 累加,代码更紧凑:

// 优化前(forEach)
let total = 0;
this.selectedProducts.forEach(item => {total += item.count * item.price;
});
return total;// 优化后(reduce)
return this.selectedProducts.reduce((total, item) => total + item.count * item.price, 0);

3.4 命名规范:避免关键字冲突

  • 事件名/方法名避免使用 JavaScript 关键字(如 delete 是删除对象属性的关键字,方法名改用 deleteProductremoveProduct
  • 禁用状态变量避免使用 disabled(HTML 关键字),改用 isBtnDisabled 等语义化命名

四、Vue vs jQuery 开发思想差异(核心对比)

购物车实战是体现 Vue 数据驱动思想的典型案例,与 jQuery 的 DOM 操作思想有本质区别:

对比维度Vue(数据驱动)jQuery(DOM 操作)
核心思路先定义数据,视图绑定数据,修改数据即更新视图直接操作 DOM 元素(如 $('.count').text(5)
代码关注点业务逻辑(数据修改)视图细节(DOM 查找、样式修改)
响应式实现自动(Vue 响应式机制)手动(修改 DOM 后需重新计算总价)
代码量少(无需重复操作 DOM)多(需手动更新所有关联视图)
可维护性高(数据与视图分离)低(DOM 操作散落在代码中)

示例对比:商品数量修改

  • jQuery 写法(需手动更新数量显示和总价):
// 1. 查找 DOM 元素修改数量
$('.count').eq(index).text(count);
// 2. 重新计算总价并更新 DOM
let total = 0;
$('.product:checked').each(() => {total += parseInt($(this).find('.price').text()) * parseInt($(this).find('.count').text());
});
$('.total-price').text(total);
  • Vue 写法(仅修改数据,视图自动更新):
this.products[index].count++; // 仅修改数据,总价和数量显示自动更新

五、开发调试技巧(实战必备)

  1. 保持控制台开启:控制台是发现错误的第一道防线,Vue 会在控制台打印响应式错误(如未定义变量、Props 类型错误)。
  2. 分步验证功能:开发时先验证基础逻辑(如 console.log(index) 确认商品定位正确),再实现完整功能,避免一次性写大量代码导致调试困难。
  3. 数据变更优先:若视图未更新,先检查数据是否正确修改(如 console.log(this.products[index].select)),再排查视图绑定问题。
  4. 边界测试:验证极端场景(如删除最后一个商品、全选后取消一个商品、数量减到 1 时再点击减按钮),确保功能健壮。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>购物车实战</title><script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {background-color: #f5f5f5;}li {list-style: none;}.wrapper {width: 640px;margin: 0 auto;}.title {border-bottom: 1px solid #e1e1e1;padding: 10px;}.product {display: flex;align-items: center;border-bottom: 1px solid #e1e1e1;padding: 8px;position: relative;}.btn {width: 20px;height: 20px;cursor: pointer;}.select {margin: 0 10px;}.del {position: absolute;right: 10px;top: 10px;}.operations {position: absolute;display: flex;width: 112px;height: 36px;right: 10px;bottom: 4px;}.operation {width: 36px;height: 27px;border: 1px solid #dbdbdb;text-align: center;background: none;color: #7a7979;}.operations>input {border-left: 0px solid #dbdbdb;border-right: 0px solid #dbdbdb;background-color: #fff;color: #000;pointer-events: none;}.operations>button {cursor: pointer;}.sales {width: 72px;height: 72px;margin-right: 10px;}.pro-desc {height: 72px;}.price {margin-top: 20px;color: #e46b7e;}.footer {display: flex;align-items: center;justify-content: space-between;position: absolute;bottom: 0px;background-color: #fff;height: 50px;width: 640px;}.footer-content {display: flex;align-items: center;flex-grow: 1;justify-content: space-between;padding: 0 10px;}.all-selected {color: #797979;font-size: 18px;}.total-price {font-size: 20px;color: #f41638;font-weight: bolder;}.submit {background: #ed535f;height: 50px;width: 100px;border: 0px;color: #fff;font-size: 18px;}.submit-forbidden {background: grey;color: #e1e1e1;}</style>
</head>
<body><div id="app"><div class="wrapper"><p class="title">我的购物车</p><ul><li v-for="(item, index) in products" :key="item.id"class="product"><img v-show="!item.isSelected" src="./images/unselected.png" class="btn select" @click="toggleSelect(index)"/><img v-show="item.isSelected" src="./images/selected.png" class="btn select" @click="toggleSelect(index)"/><img src="./images/sales.png" class="sales"/><div class="pro-desc"><p>{{ item.name }}</p><p class="price">{{ `¥ ${item.price}` }}</p></div><img src="./images/del.png" class="btn del" @click="deleteProduct(index)"/><div class="operations"><button class="operation" @click="changeCount(false, index)">-</button><input type="text" :value="item.count" class="operation"/><button class="operation" @click="changeCount(true, index)">+</button></div></li></ul><div class="footer"><img v-show="!isAllSelected" src="./images/unselected.png" class="btn select" @click="toggleSelectAll()"/><img v-show="isAllSelected" src="./images/selected.png" class="btn select" @click="toggleSelectAll()"/><div class="footer-content"><p class="all-selected">{{ footerText }}</p><p class="total-price">{{ `¥ ${totalPrice}` }}</p></div><button class="submit" :class="isSubmitDisabled?'submit-forbidden':''">结算</button></div></div></div>
<script>/*购物车需求分析1. 商品列表展示(商品名称、单价、数量、选中状态)2. 商品数量 +、- 操作3. 单个商品 选中、取消状态切换4. 全选、取消全选 批量操作5. 已选商品 总价计算 与 数量统计6. 结算按钮 禁用状态控制(无选中商品时禁用)7. 商品删除功能(删除商品后同步更新统计数据)*/new Vue({el: '#app',data: {// 商品列表信息products: [{ id: 1, name: 'iPhone14', price: 10000, count: 1, isSelected: false },{ id: 2, name: '高斯键盘3模款', price: 488, count: 1, isSelected: false },{ id: 3, name: 'DW粉底液', price: 800, count: 1, isSelected: false },{ id: 4, name: 'PS5', price: 5000, count: 1, isSelected: false },{ id: 5, name: '《javaScript 高级开发教程》', price: 98, count: 1, isSelected: false },]},computed: {// 已选商品数组selectedProducts(){return this.products.filter(item => item.isSelected)},// 已选商品数量selectCount(){return this.selectedProducts.length},// 已选商品总价totalPrice(){let sum = this.selectedProducts.reduce((acc, item) => {return acc+item.price*item.count},0)return sum},// 全选状态isAllSelected(){if(this.selectedProducts.length == this.products.length){return true}else{return false}},// 结算按钮禁用状态isSubmitDisabled(){if(this.selectedProducts.length == 0){// 当前没有选择商品,则禁用return true}else{// 当前选择了商品,则启用return false}},// 底部选择文本footerText(){if(this.selectedProducts.length == 0){return '未选择商品'}else{return `已选(${this.selectedProducts.length}`}}},methods: {// 商品数量加减changeCount(flag, index){if(flag){// +this.products[index].count++}else{// -if(this.products[index].count == 0){return}this.products[index].count--}},// 切换选中状态-单个商品toggleSelect(index){this.products[index].isSelected = !this.products[index].isSelected},// 切换选中状态-全选toggleSelectAll(){if(this.isAllSelected){this.products.map(item => item.isSelected = false)}else{this.products.map(item => item.isSelected = true)}},// 删除商品deleteProduct(index){this.products.splice(index,1)}}})
</script>
</body>
</html>

效果图:

文件夹结构:


文章转载自:

http://p538MQa7.jtqxs.cn
http://88ZnGdHx.jtqxs.cn
http://BRODo9Xk.jtqxs.cn
http://loHQgl7L.jtqxs.cn
http://jg5aYGF8.jtqxs.cn
http://NdLHXK3Z.jtqxs.cn
http://E2vFAGte.jtqxs.cn
http://4KOKcEXY.jtqxs.cn
http://1Iv7AFbH.jtqxs.cn
http://sNnNqwdA.jtqxs.cn
http://hA0YWYAz.jtqxs.cn
http://m3nn4X8E.jtqxs.cn
http://ChrfUd0U.jtqxs.cn
http://e91GuAGJ.jtqxs.cn
http://9Es8bXIJ.jtqxs.cn
http://PFInFBkC.jtqxs.cn
http://lrkR8g5G.jtqxs.cn
http://DwW0Oiq8.jtqxs.cn
http://5jeLya8s.jtqxs.cn
http://Qze72hlD.jtqxs.cn
http://TPgBeFzm.jtqxs.cn
http://PHmFFc5X.jtqxs.cn
http://zfEtma5L.jtqxs.cn
http://lcD1ZGL1.jtqxs.cn
http://3iXQPRrz.jtqxs.cn
http://JckKm2st.jtqxs.cn
http://2AURBBcU.jtqxs.cn
http://le4cCyfB.jtqxs.cn
http://KsWTWPhI.jtqxs.cn
http://py327yUD.jtqxs.cn
http://www.dtcms.com/a/379998.html

相关文章:

  • 【论文阅读】Uncertainty Modeling for Out-of-Distribution Generalization (ICLR 2022)
  • PAT乙级_1111 对称日_Python_AC解法_无疑难点
  • Kafka面试精讲 Day 16:生产者性能优化策略
  • vue 批量自动引入并注册组件或路由
  • Kubernetes(K8s)详解
  • 趣味学solana(介绍)
  • Apache Thrift:跨语言服务开发的高性能RPC框架指南
  • Flutter 应用国际化 (i18n) 与本地化 (l10n) 完整指南
  • 第 5 篇:深入浅出学 Java 语言(JDK8 版)—— 精通类与对象进阶,掌握 Java 面向对象核心能力
  • Gin-Vue-Admin学习笔记
  • Golang關於信件的
  • The 2024 ICPC Asia East Continent Online Contest (I)
  • 【数所有因子和快速新解/范围亲密数/分解因式怎么去掉重复项】2022-10-31
  • SQL语句执行时间太慢,有什么优化措施?以及衍生的相关问题
  • 【论文阅读】Language-Guided Image Tokenization for Generation
  • PHP:从入门到实战的全方位指南
  • 经典动态规划题解
  • 商城购物系统自动化测试报告
  • [工作表控件20] 拼音排序功能:中文数据高效检索实战指南
  • 9120 部 TMDb 高分电影数据集 | 7 列全维度指标 (评分 / 热度 / 剧情)+API 权威源 | 电影趋势分析 / 推荐系统 / NLP 建模用
  • 【Java】多态
  • LeetCode热题 438.找到字符中所有字母异位词 (滑动窗口)
  • 解决 N1 ARMBIAN Prometheus 服务启动失败问题
  • Linux 正则表达式详解(基础 + 扩展 + 实操)
  • 01.【Linux系统编程】Linux初识(Linux内核版本、基础指令、理论知识、shell命令及运行原理)
  • MATLAB 的无人机 PID 控制及智能 PID 控制器设计的仿真
  • D007 django+neo4j三维知识图谱医疗问答系统|3D+2D双知识图谱可视化+问答+寻医问药系统
  • 5G单兵图传 5G单兵 单兵图传 无线图传 无线图传方案 无人机图传解决方案 指挥中心大屏一目了然
  • npm / yarn / pnpm 包管理器对比与最佳实践(含国内镜像源配置与缓存优化)
  • 运维安全06 - 服务安全