← 返回目录

6. 购物车批量操作

把这页想成"超市购物车":你往车里放商品、调数量、勾选不要的、最后一键结算。购物车本质上就是一个数组 cartData,所有操作(加、减、勾选、删除)都是先改这个数组,再重新渲染一遍 renderCart。记住这个口诀:改数据 → 重渲染,页面永远是数据的"镜子"。

6.1 先跑起来:完整购物车

商品 单价 数量 小计 操作
已选 0 合计:¥0.00

试试:加数量到"库存上限"会提示;数量为 1 时再点减号会问你是否删除;批量删除前会先校验有没有勾选。

注意初始默认勾选的是 iPhone 和 MacBook(AirPods 没勾),所以默认合计是 ¥20998.00——你可以验证一下:5999 + 14999 = 20998。把 AirPods 勾上,合计会实时变化。

6.2 核心数据结构

let cartData = [                     // 购物车就是一个数组,每一项是一个商品
  { id: 1, name: "iPhone 15", price: 5999, count: 1, stock: 30, checked: false },  // id 编号、name 名字、price 单价、count 要买几个、stock 库存上限、checked 勾没勾
  { id: 2, name: "AirPods Pro", price: 1899, count: 2, stock: 30, checked: false },
  { id: 3, name: "MacBook Pro", price: 14999, count: 1, stock: 30, checked: false }
];
// count 是"我要几个",stock 是"最多只能买几个",别搞混

每个字段干嘛的,先搞清楚:price 单价、count 你想买的数量、stock 库存上限、checked 当前有没有被勾选。特别注意 count 和 stock 别搞混:count 是"我要几个",stock 是"最多只能买几个"。所有操作都是改这个数组,然后重新渲染。

6.3 渲染 + 统计(renderCart 是总开关)

function renderCart() {             // 总开关:数据一改,就调它把整辆车重画一遍
  let html = "";                      // 先准备空字符串攒表格行
  let totalPrice = 0,                 // 合计总价,先清零
    totalCount = 0,                   // 合计件数,先清零
    allChecked = true; // ★ 统计都在这里算 → 先默认全选,下面遇到没勾的再改成 false

  cartData.forEach((item, index) => {  // 把购物车里每个商品走一遍
    if (item.checked) {               // 如果这件被勾了
      totalPrice += item.price * item.count; // ★ 小计 = 单价 × 数量(不是单价!)→ 勾着的才计入总价
      totalCount += item.count;     // 勾着的件数也累加
    } else {
      allChecked = false; // 只要有一个没勾,全选框就不算全选
    }
    html += `                        // 拼这一行的 HTML
   <tr data-index="${index}">       // 把数组下标写进行上,方便点按钮时反查第几件
    <td><input type="checkbox" class="item-cb" ${item.checked ? "checked" : ""}></td>  // 复选框,勾着的就打上勾
    <td>${item.name}</td>            // 商品名
    <td>¥${item.price}</td>          // 单价
    <td><button class="minus">-</button><span class="qty-num">${item.count}</span><button class="plus">+</button></td>  // 数量加减按钮
    <td>¥${(item.price * item.count).toFixed(2)}</td>  // 这一件的小计:单价×数量,保留两位小数
    <td><button class="del">删除</button></td>        // 删除按钮
   </tr>`;
  });

  document.getElementById("cartBody").innerHTML = html;  // 把拼好的所有行塞进表格
  document.getElementById("totalPrice").textContent = totalPrice.toFixed(2);  // 显示合计总价
  document.getElementById("totalCount").textContent = totalCount;              // 显示合计件数
  // 全选框状态 = 所有项都选中 且 购物车不是空的 → 有一件没勾或车是空的,全选框就别打勾
  document.getElementById("allCheckbox").checked = allChecked && cartData.length > 0;
}

两个易错点:
累加的时候要做数值运算(price × count),toFixed(2) 只放在最后展示。如果你在累加过程中就 toFixed,数字会变成字符串,"+" 就变成了"拼接",5999 + 1899 变成 "59991899"。
统计只在 renderCart 里做一遍,别"改一次数据就单独算一次总价",那样东算一次西算一次,很容易漏同步。页面上所有数字,都以这个函数的输出为准。

6.4 全选 / 单选联动

// 全选:把同一个状态同步给每一项 → 点全选,所有商品一起勾或一起不勾
$("#allCheckbox").on("change", function () {
  const checked = $(this).prop("checked");  // 读全选框现在是勾还是没勾
  cartData.forEach(item => item.checked = checked);  // 把这个状态塞给每一件
  renderCart; // 重渲染,总价自然跟着变 → 改完数据必须重画
});

// 单选:从行上取出 index,只改那一项 → 点某一行的小勾,只改这一件
$(document).on("change", ".item-cb", function () {
  const index = $(this).closest("tr").data("index"); // ★ 关键:从 DOM 反推出数组下标 → 点的这行是第几件
  cartData[index].checked = $(this).prop("checked");  // 把这一件的勾选状态改掉
  renderCart; // 重算后,只要有未勾选的,全选框自动取消 → 重画时全选状态会自动同步
});

难点在于:你点的是页面上某个复选框,但真正要改的是数组里那一项。怎么知道点的是第几项?靠tr 上的 data-index——渲染时把数组下标写进 DOM,点击时再读出来。这就是"用 DOM 反查数组"的常用套路。

6.5 数量修改(库存上限 + 最小为 1)

// 加数量:不能超过库存 → 点 + 号,数量加一,但别超过库存
$(".plus").on("click", function () {
  const index = $(this).closest("tr").data("index");  // 这行是第几件
  if (cartData[index].count < cartData[index].stock) {  // 还没到库存上限
    cartData[index].count++;            // 数量 +1
    renderCart;                          // 重画
  } else {
    alert("库存不足"); // 到顶了就提示 → 已经买满,不能再加
  }
});

// 减数量:最小是 1;等于 1 时再减 = 问你要不要删除 → 点 - 号,数量减一,减到 1 再减就变删除
$(".minus").on("click", function () {
  const index = $(this).closest("tr").data("index");  // 这行是第几件
  if (cartData[index].count > 1) {      // 数量还大于 1,正常减
    cartData[index].count--;            // 数量 -1
    renderCart;                          // 重画
  } else if (confirm("数量为 1,确定删除该商品吗?")) {  // 已经是 1 了,再减等于删,先问一句
    cartData.splice(index, 1); // 数量减到 0 就直接从数组删掉 → 把这一件整个从购物车移除
    renderCart;                          // 重画
  }
});

这一段全是边界判断:加的时候别超过 stock,减的时候别小于 1。当数量已经是 1 还要减,说明用户其实是想"删掉这个商品",所以弹个 confirm 问一下,确认就用 splice(index, 1) 从数组里删掉。

6.6 批量删除(filter 是最安全的写法)

$("#batchDeleteBtn").on("click", function() {  // 点"批量删除选中"
 // 前置校验:有没有勾选的?一个都没勾就别删 → 先看看到底勾了几件
 const checkedItems = cartData.filter(item => item.checked);  // 把勾着的挑出来
 if (checkedItems.length === 0) {       // 一件都没勾
  alert("请先选择要删除的商品");          // 提示先勾选
  return;                                // 直接结束,别往下走
 }
 if (confirm("确定删除选中的 " + checkedItems.length + " 件商品吗?")) {  // 弹个确认,问一下
  // 留下"没勾选"的 → 等于把勾选的删掉了 → filter 出新数组,只要没勾的
  cartData = cartData.filter(item => !item.checked);
  renderCart;                            // 重画
 }
});
	

为什么用 filter 留下没勾选的,而不是边遍历边 splice?因为一边遍历一边删除会"跳项"——删掉一个元素后,后面的元素全往前挪了一位,但循环计数器还在往前走,就会跳过一个。用 filter 生成一个新数组"只要没勾的",干净又安全。

6.7 批量加购 / 清空 + 持久化

// 批量加购:每个勾选的商品数量都 +1 → 点一下,所有勾着的都再买一份
$("#batchAddBtn").on("click", function () {
  let hasChecked = false;               // 先记一下到底有没有勾着的
  cartData.forEach(item => {            // 每件走一遍
    if (item.checked) {                 // 勾着的才管
      hasChecked = true;                // 标记:至少勾了一件
      if (item.count < item.stock) item.count++;  // 没到库存就 +1
      else alert(item.name + " 库存不足");          // 到顶了就提示这件加不动
    }
  });
  if (!hasChecked) { alert("请先选择商品"); return; }  // 一件都没勾,提示先勾选
  renderCart;                            // 重画
});

// 持久化:任何修改后存进 localStorage,刷新后还在 → 把购物车存到浏览器本地
function saveCart() {
  localStorage.setItem("cartData", JSON.stringify(cartData));  // 数组只能存字符串,先转成 JSON 字符串再存
}

function loadCart() {
  cartData = JSON.parse(localStorage.getItem("cartData")) || [];  // 读出来时再把 JSON 字符串还原成数组;没存过就用空数组
}

localStorage 只能存字符串,所以要用 JSON.stringify 把数组变成字符串存进去,读的时候再 JSON.parse 还原回来。这样购物车刷新页面也不会丢。

6.8 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:电商购物车、批量操作后台(批量删除/批量改状态)、投票勾选、待办事项的批量完成——凡是"勾一堆、一起操作"的界面都是这套。

② 常见的问题:合计金额算成字符串拼接(累加时用了 toFixed);全选框和单选状态不同步(改完没 renderCart 重渲染);批量删除后商品错位(边遍历边 splice 导致跳项);数量加超库存或减到 0 还在减(边界判断漏了);不知道点的是第几行(没写 data-index)。

③ 解决思路:金额不对就在 renderCart 里 console.log 一下 totalPrice 看它是数字还是字符串;状态不同步就检查每次改数据后有没有调 renderCart;删除错位就换成 filter 生成新数组;加购/减购越界就核对 count < stockcount > 1 这两个判断。

一句话:购物车 = 数组 cartData + 一个 renderCart 重渲染;批量删改用 filter,数量要卡库存和下限,总价记得 price × count 再 toFixed。