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