购物车批量操作:全选/反选、数量加减、小计计算、总价计算。
核心四步:
① 全选反选——表头 checkbox 控制所有行
② 数量加减——+1/-1 改数量,同步小计
③ 小计——单价 × 数量
④ 总价——所有勾选商品小计之和
坑:数量不能小于 1、删行要重算总价、全选框要同步(手动取消一个要取消全选)。
| 商品 | 单价 | 数量 | 小计 | 操作 |
|---|
试试:加数量到"库存上限"会提示;数量为 1 时再点减号会问你是否删除;批量删除前会先校验有没有勾选。
注意初始默认勾选的是 iPhone 和 MacBook(AirPods 没勾),所以默认合计是 ¥20998.00——你可以验证一下:5999 + 14999 = 20998。把 AirPods 勾上,合计会实时变化。
// 购物车就是一个数组,每一项是一个商品
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 是"最多只能买几个"。所有操作都是改这个数组,然后重新渲染。
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 里做一遍,别"改一次数据就单独算一次总价",那样东算一次西算一次,很容易漏同步。页面上所有数字,都以这个函数的输出为准。
// 全选:把同一个状态同步给每一项 → 点全选,所有商品一起勾或一起不勾
$("#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 反查数组"的常用套路。
// 加数量:不能超过库存
$(".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) 从数组里删掉。
// 点"批量删除选中"
$("#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 生成一个新数组"只要没勾的",干净又安全。
// 批量加购:每个勾选的商品数量都 +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 还原回来。这样购物车刷新页面也不会丢。
① 可能在什么地方用:电商购物车、批量操作后台(批量删除/批量改状态)、投票勾选、待办事项的批量完成——凡是"勾一堆、一起操作"的界面都是这套。
② 常见的问题:合计金额算成字符串拼接(累加时用了 toFixed);全选框和单选状态不同步(改完没 renderCart 重渲染);批量删除后商品错位(边遍历边 splice 导致跳项);数量加超库存或减到 0 还在减(边界判断漏了);不知道点的是第几行(没写 data-index)。
③ 解决思路:金额不对就在 renderCart 里 console.log 一下 totalPrice 看它是数字还是字符串;状态不同步就检查每次改数据后有没有调 renderCart;删除错位就换成 filter 生成新数组;加购/减购越界就核对 count < stock 和 count > 1 这两个判断。
| 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") | 那一行 tr | let tr = btn.closest("tr"); |