← 返回目录

6. 购物车批量操作

购物车批量操作:全选/反选、数量加减、小计计算、总价计算。

核心四步:
全选反选——表头 checkbox 控制所有行
数量加减——+1/-1 改数量,同步小计
小计——单价 × 数量
总价——所有勾选商品小计之和

坑:数量不能小于 1、删行要重算总价、全选框要同步(手动取消一个要取消全选)

6.1 先跑起来:完整购物车

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

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

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

6.2 核心数据结构

// 购物车就是一个数组,每一项是一个商品
let cartData = [
  // id 编号、name 名字、price 单价、count 要
  { id: 1, name: "iPhone 15", price: 5999, count: 1, stock: 30, checked: false },
  { 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;

  cartData.forEach(function (item, index) {
    if (item.checked) {
      totalPrice += item.price * item.count;
      totalCount += item.count;
    } else {
      allChecked = false;
    }
    let checkedAttr = item.checked ? "checked" : "";
    html += '<tr data-index="' + index + '">';
    html += '<td><input type="checkbox" class="item-cb" ' + checkedAttr + '></td></td>';
    html += '<td>' + item.name + '</td>';
    html += '<td>¥' + item.price + '</td>';
    html += '<td><button class="minus">-</button><span class="qty-num">' + item.count + '</span><button class="plus">+</button></td>';
    html += '<td>¥' + (item.price * item.count).toFixed(2) + '</td>';
    html += '<td><button class="del">删除</button></td>';
    html += '</tr>';
  });

  $("#cartBody").html(html);
  $("#totalPrice").text(totalPrice.toFixed(2));
  $("#totalCount").text(totalCount);
  $("#allCheckbox").prop("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");
  // 把这个状态塞给每一件(普通 function)
  cartData.forEach(function (item) { item.checked = checked; });
  // 重渲染,总价自然跟着变 → 改完数据必须重画
  renderCart();
});

// 单选:从行上取出 index,只改那一项 → 点某一行的小勾,只改这一件
$(document).on("change", ".item-cb", function () {
  // ★ 关键:从 DOM 反推出数组下标 → 点的这行是第几件
  const index = $(this).closest("tr").data("index");
  // 把这一件的勾选状态改掉
  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) {
    // 数量 +1
    cartData[index].count++;
    // 重画
    renderCart();
  } else {
    // 到顶了就提示 → 已经买满,不能再加
    alert("库存不足");
  }
});

// 减数量:最小是 1;等于 1 再减 = 问要不要删除
$(".minus").on("click", function () {
  // 这行是第几件
  const index = $(this).closest("tr").data("index");
  // 数量还大于 1,正常减
  if (cartData[index].count > 1) {
    // 数量 -1
    cartData[index].count--;
    // 重画
    renderCart();
  } else if (confirm("数量为 1,确定删除该商品吗?")) {
    // 已经是 1 了,再减等于删
    cartData.splice(index, 1);
    // 从购物车移除
    renderCart();
  }
});

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

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

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

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

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

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

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

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

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。

本页重点

API作用参数怎么传返回/结果代码示例
cartData.forEach遍历每件商品画表格(商品函数)cartData.forEach(function(item){ ... });
filter批量删:只留下没勾的(条件函数)新数组cartData = cartData.filter(function(it) { return !it.checked; });
splice点删除:拿掉那一件(下标, 1)删完的数组cartData.splice(index, 1);
toFixed(2)金额保留两位小数(2)"5999.00" 字符串price.toFixed(2);
closest("tr")反查点的是哪一行("tr")那一行 trlet tr = btn.closest("tr");