主题
商品参数选项-关闭选中标签元素与商品参数选项-点击选项和选中(Day21 2/2)
本页对应 Day21_PC项目,共 4 段视频,约 1 小时 6 分。
学习目标
- 能使用事件委托监听选中标签上的关闭按钮,完成删除标签、取消对应选项选中、默认选中同组第一项,并同步维护选中项数组。
- 能封装统一的价格计算函数,在点击参数选项、关闭选中标签等操作后调用该函数并更新页面价格显示。
- 能通过选中项数组及选项上的价格变化自定义属性,完成商品单价的重新计算与展示。
- 能实现商品数量加减按钮和输入框
change事件,完成数量同步、最小数量为 1 及非法输入重置校验。 - 能根据商品单价和数量计算主商品总价,并在数量变化后同步更新价格显示。
- 能实现搭配商品复选框的选择与取消选择,动态加减搭配商品价格、更新搭配数量及最终总价显示。
知识点
先明确:关闭选中标签要同步三件事
为什么需要:每个选中标签右侧都有一个“叉”,用户点它不只是把标签删掉,还要让对应选项取消选中,并让该组第一个选项重新选中。否则会出现“标签没了,但选项还亮着”或“数组里还留着已删选项”的状态错乱。
怎么想:把一次点击拆成三个动作:删除当前选中标签、取消对应 dd 的选中、恢复同组第一个 dd 的选中。三件事必须围绕同一份数据同步,不能只改界面。
怎么做:先通过事件委托命中关闭按钮,拿到它所在的选中标签,再取出索引,找到数组中对应的 dd,完成状态修改。
易错点:event.target 是“叉”,不是整个选中标签;选中标签要用 parentElement 找。删除标签用 selectedBooks.removeChild(tag),不要误删别的节点。
为什么用事件委托,而不是直接给叉绑定
为什么需要:选中标签是用户点击选项后新创建的 span,如果每创建一个就绑一次事件,代码重复且容易漏绑。转写稿里明确说“这个 span 很明显是新创建的,所以还是得委托”。
怎么想:委托目标必须是页面一开始就存在、并且能包住所有选中标签的元素。不能委托给新创建的 selectedTag,应委托给已经获取过的包裹元素 selectedBooks。
怎么做:先写验证逻辑,只打印“删除”,确认点叉能命中:
js
selectedBooks.onclick = function (event) {
if (event.target.className === 'close') {
console.log('删除');
}
};验证通过后再补删除和状态同步。
易错点:转写稿用 className === 'close' 判断;如果以后给叉加多个类名,这种写法会失效,可改成 event.target.classList.contains('close')。委托目标也不能选错。
用 data-index 建立“标签—选项—数组”的对应关系
为什么需要:选中标签和对应的 dd 没有直接 DOM 父子关系,删除标签后还要找到数组里对应的 dd。转写稿里数组 selectedArr 存的就是 dd 元素,所以需要索引桥接。
怎么想:在创建选中标签时,把当前选项索引写到标签的自定义属性上:tag.dataset.index = index。删除时从标签上取索引,就能拿到 selectedArr[index]。
怎么做:删除标签、取消 dd 选中、恢复同组第一个 dd:
js
selectedBooks.onclick = function (event) {
if (event.target.className === 'close') {
const tag = event.target.parentElement;
const index = tag.dataset.index;
const dd = selectedArr[index];
// 对应选项取消选中
dd.classList.remove('active');
// 同组第一个 dd 重新选中:children[0] 是 dt,children[1] 是第一个 dd
dd.parentElement.children[1].classList.add('active');
// 删除当前选中标签
selectedBooks.removeChild(tag);
// 从数组中删除,但保留数组长度
delete selectedArr[index];
// changePrice(); // 转写稿后续在这里补价格重算
}
};易错点:dataset.index 取出来是字符串,作为数组索引通常可用,但不要和数字类型混淆。dd.parentElement.children[1] 依赖结构:第一个子元素是 dt,第二个才是第一个 dd。如果直接 splice,数组长度会变,转写稿强调要用 delete 保留长度,避免已删选项再次出现。
调试过程与完整检查清单
为什么需要:这一步同时改标签、dd、数组三处状态,最容易出现“界面删了但数组没删”的问题。转写稿中先点叉验证命中,再逐步加删除自身、取消 dd、恢复第一个,最后发现不删数组会导致已删选项再次出现。
怎么想:按“命中按钮 → 删除标签 → 取消对应 dd → 恢复同组第一个 → 删除数组项 → 价格重算”的顺序排查。每加一步就刷新页面点一次,观察控制台和界面。
怎么做:完整逻辑就是上面的委托代码。若删除后价格没变,不是关闭逻辑错,而是还没调用后续封装的 changePrice();转写稿在价格变化部分把调用补到了这里。
易错点:event.target 不是标签;parentElement 找错会删错节点;children[1] 写错会选中错误选项;忘记 delete selectedArr[index] 会让已删选项“复活”;className 判断对多类名不稳健。按转写稿的实现,关闭标签的核心就是:用索引同步数组,用数组同步 dd 选中状态,再统一重算价格。
一、先定思路:价格由多个操作共同触发
为什么需要:点击选项、关闭选中标签、修改数量、勾选搭配商品,都会改变价格。若每个事件里各写一套算法,后续很难同步。
怎么想:把“算价格并显示”封装成 changePrice,所有会改价格的地方只负责改数据,然后调用它。转写稿里反复强调“只要一变化就重新算一遍”,这样最稳。
怎么做:先列出入口:选项点击、选中标签关闭、数量加减/输入、搭配商品勾选。每个入口处理完数据后调用 changePrice()。
易错点:漏调用是常见问题,表现为“数据变了但价格没变”。调试时可在 changePrice 里 console.log(price),确认每次操作都进入函数。
二、关闭选中标签:删标签、取消选项、恢复同组第一个
为什么需要:选中标签上的“叉”不仅要删掉标签,还要让对应 DD 取消选中,并让该组第一个选项重新选中。
怎么想:选中标签是动态创建的,不能直接给每个 span 绑事件,应事件委托到包裹元素 selectedBooks。判断点击目标是否为关闭按钮:
js
selectedBooks.onclick = function (event) {
if (event.target.className === 'close') {
// 删除自己
}
};怎么做:创建选中标签时记录索引:selectedTag.dataset.index = index。数组 selectedArr 里存的是对应 DD。删除时:
js
const tag = event.target.parentElement;
const index = tag.dataset.index;
const ddEle = selectedArr[index];
selectedBooks.removeChild(tag);
ddEle.classList.remove('active');
// 同组第一个 DD 重新选中:DL 中第一个子元素是 DT,第二个才是 DD
ddEle.parentElement.children[1].classList.add('active');
// 保持数组长度,不改变后续索引
delete selectedArr[index];
changePrice();易错点:事件委托别绑到动态 span 上;删除标签用 parentElement 找到整个选中标签;数组删除用 delete 而不是 splice,否则索引会错位;删除后必须调用 changePrice()。
三、价格计算:基础价 + 选项价格变化
为什么需要:选项有加价或减价,价格要实时反映。
怎么想:创建 DD 时把该选项的 changePrice 存到自定义属性上:
js
ddEle.dataset.changePrice = ddItem.changePrice;changePrice 里先取基础价,再遍历 selectedArr,把每个选中 DD 的 changePrice 加上:
js
function changePrice() {
let price = goodsDetail.price;
selectedArr.forEach(ddEle => {
price += +ddEle.dataset.changePrice;
});
priceBox.innerHTML = price;
}怎么做:点击选项后、关闭标签后都调用 changePrice()。转写稿中用控制台输出验证过:选中 128G、黑色等,价格会按选项变化;全部叉掉后回到基础价。
易错点:dataset 取出来是字符串,必须用 + 转数字;不要忘记关闭标签也要调用;显示价格元素要提前获取。
四、数量与总价:加减按钮、手动输入
为什么需要:数量影响总价,且加减按钮和输入框都能改数量。
怎么想:定义全局 productNumber 记录数量,所有入口同步它;总价 = 单价 × 数量。
怎么做:
js
plusBtn.onclick = function () {
productNumber++;
nameInput.value = productNumber;
changePrice();
};
minusBtn.onclick = function () {
productNumber--;
if (productNumber < 1) productNumber = 1;
nameInput.value = productNumber;
changePrice();
};
nameInput.onchange = function () {
productNumber = +nameInput.value;
if (isNaN(productNumber) || productNumber < 1) {
productNumber = 1;
nameInput.value = productNumber;
}
changePrice();
};在 changePrice 中计算主商品总价:
js
const masterPrice = price * productNumber;
masterPriceBox.innerHTML = '¥' + masterPrice;
totalPriceBox.innerHTML = '¥' + (masterPrice + collectionPrice);易错点:输入框用 change,不是一输入就触发,而是值改变且失去焦点;空字符串转数字为 0,会被 <1 拦下;不要忘记回写输入框。
五、搭配商品:勾选加价、取消减价
为什么需要:套餐总价要包含搭配商品,勾选和取消要同步数量与总价。
怎么想:定义 collectionPrice 和 collectionNumber。每个复选框监听 change,用 checked 判断选中还是取消。
怎么做:
js
collectionInput.forEach(input => {
input.onchange = function () {
if (input.checked) {
collectionPrice += +input.value;
collectionNumber++;
} else {
collectionPrice -= +input.value;
collectionNumber--;
}
totalNumberBox.innerHTML = collectionNumber;
changePrice();
};
});易错点:input.value 是字符串,必须转数字;总价拼接时曾出现多一个 0,原因是字符串拼接,解决方法是先算数字:(masterPrice + collectionPrice);最后统一调用 changePrice() 显示。
修改商品数量价格变化:从删除标签到总价同步
本节围绕“商品参数选项”继续完善:删除选中标签、价格随选项变化、数量修改、搭配商品选择。转写稿里反复强调的核心是:选中项统一放进数组,所有会改变选择的操作用数组同步,再封装价格计算函数统一刷新显示。本节原始笔记未附图片,以下保留可复现的关键代码。
删除选中标签并同步选项
为什么需要:选中标签上的“叉”不能只删掉标签本身,否则对应的 DD 仍处于选中态,价格和选项状态会错乱。怎么想:关闭按钮是动态创建的,事件要委托给已经存在的包裹元素 selectedBooks;删除时要知道当前标签对应数组里的哪个 DD,所以在创建选中标签时把索引写到 dataset.index。怎么做:
js
selectedBooks.addEventListener('click', function (e) {
if (e.target.classList.contains('close')) {
const tag = e.target.parentElement; // 选中标签
const index = tag.dataset.index; // 对应DD的索引
const dd = selectedArr[index]; // 数组里存的是DD元素
selectedBooks.removeChild(tag); // 删除自己
dd.classList.remove('active'); // 对应DD取消选中
dd.parentElement.children[1].classList.add('active'); // 同组第一个选中
delete selectedArr[index]; // 删除数组元素,但保留长度
changePrice(); // 价格重算
}
});易错点:不能把事件委托给新创建的 span;不要用 splice 把数组长度改掉,转写稿明确用 delete 保留槽位;索引要存在关闭按钮的父元素上。调试时可先 console.log(dd),确认点“128G”拿到的是对应 DD,再继续删除和取消选中。
封装价格计算函数
为什么需要:点击选项、删除标签、修改数量、勾选搭配商品都会影响价格,如果每处都写一遍计算,后期必乱。怎么想:基础价从数据里取,选中项的价格变化从数组里每个 DD 的 data-change-price 累加。怎么做:创建 DD 时把 changePrice 写成自定义属性;计算时注意从 dataset 取出来是字符串,要转数字:
js
function changePrice() {
let unitPrice = goodsDetail.price;
selectedArr.forEach(dd => {
unitPrice += +dd.dataset.changePrice; // + 转数字
});
const masterPrice = unitPrice * productNumber;
masterPriceBox.textContent = masterPrice;
totalPriceBox.textContent = masterPrice + collectionPrice;
}易错点:数组里存的是 DD 元素,不是普通对象,所以取价格要用 dataset;不要忘记正负价格都可能存在;每次选择变化后都要调用 changePrice()。调试时先 console.log(unitPrice),再显示到页面,确认价格随选项增减。
数量加减与手动输入
为什么需要:数量既要点加、点减,也要允许输入框手改,而且价格要跟着变。怎么想:定义一个全局 productNumber 记录数量,三个入口都同步它。怎么做:加按钮累加,减按钮累减且最小为 1;输入框用 change 事件,因为既要值改变又要失去焦点:
js
let productNumber = 1;
plusBtn.addEventListener('click', () => {
productNumber++;
numberInput.value = productNumber;
changePrice();
});
minusBtn.addEventListener('click', () => {
productNumber--;
if (productNumber < 1) productNumber = 1;
numberInput.value = productNumber;
changePrice();
});
numberInput.addEventListener('change', () => {
productNumber = +numberInput.value;
if (isNaN(productNumber) || productNumber < 1) {
productNumber = 1;
}
numberInput.value = productNumber;
changePrice();
});易错点:空字符串转数字是 0,会小于 1,所以要兜底为 1;不校验会出现负数或“hello”;input 事件与 change 事件触发时机不同,这里按转写稿用 change。调试时输入 -45、hello、空值,确认都回到 1。
搭配商品选择与总价
为什么需要:勾选搭配商品后,总价要加上它的价格,并统计搭配数量。怎么想:给每个复选框监听 change,用 checked 判断选中还是取消;选中加 input.value,取消减。怎么做:
js
collectionInputs.forEach(input => {
input.addEventListener('change', () => {
if (input.checked) {
collectionPrice += +input.value;
collectionNumber++;
} else {
collectionPrice -= +input.value;
collectionNumber--;
}
totalPriceBox.textContent = masterPrice + collectionPrice;
});
});易错点:value 是字符串,必须用 + 转数字;总价拼接时注意运算顺序,转写稿出现过多一个 0 的问题,排查后要用括号或先转数字,避免“字符串 + 数字”变成拼接。最终显示要同时刷新主商品价格区域和总价区域,搭配数量也要同步到页面。
课堂画面




代码示例
示例一:商品参数选项 —— 关闭选中标签元素(事件委托 + 数组同步)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>商品参数选项 - 关闭选中标签</title>
<style>
.choose-wrap dl{display:flex;align-items:center;margin:8px 0}
.choose-wrap dt{width:60px;color:#999}
.choose-wrap dd{margin:0 8px 0 0;padding:4px 10px;border:1px solid #ddd;border-radius:4px;cursor:pointer}
.choose-wrap dd.active{border-color:#e4393c;color:#e4393c}
.selected-books{min-height:30px;margin:10px 0}
.selected-tag{display:inline-block;padding:4px 8px;margin-right:8px;border:1px solid #e4393c;color:#e4393c;border-radius:4px}
.selected-tag .close{margin-left:6px;font-style:normal;cursor:pointer}
</style>
</head>
<body>
<div class="choose">
<div class="choose-wrap" id="chooseWrap"></div>
<div class="selected-books" id="selectedBooks"></div>
</div>
<script>
/* 1. 数据:每个选项都带自己的价格变化 changePrice */
var goodsDetail = {
price: 5299,
options: [
{ title: '版本', list: [
{ name: '电信优惠版', changePrice: -300 },
{ name: '移动版', changePrice: -500 },
{ name: '官方标配', changePrice: 0 },
{ name: '公开版', changePrice: 100 }
]},
{ title: '颜色', list: [
{ name: '黑色', changePrice: 200 },
{ name: '金色', changePrice: 0 }
]},
{ title: '容量', list: [
{ name: '128G', changePrice: 0 },
{ name: '256G', changePrice: 800 }
]}
]
};
var chooseWrap = document.getElementById('chooseWrap');
var selectedBooks = document.getElementById('selectedBooks');
/* 核心数据源:数组里存的是被选中的 dd 元素,下标 = 组的索引 */
var selectedArr = [];
/* 2. 创建选中标签 */
function createTag(dd, index) {
var span = document.createElement('span');
span.className = 'selected-tag';
span.dataset.index = index; // 关键:把“组索引”记在标签上
span.innerText = dd.innerText;
var i = document.createElement('i');
i.className = 'close';
i.innerText = '×';
span.appendChild(i);
selectedBooks.appendChild(span);
}
/* 3. 渲染参数选项,每组第一个默认选中 */
goodsDetail.options.forEach(function (item, index) {
var dl = document.createElement('dl');
var dt = document.createElement('dt');
dt.innerText = item.title;
dl.appendChild(dt);
item.list.forEach(function (opt, i) {
var dd = document.createElement('dd');
dd.innerText = opt.name;
dd.dataset.index = index; // 组索引
dd.dataset.changePrice = opt.changePrice; // 价格变化
if (i === 0) {
dd.classList.add('active');
selectedArr[index] = dd;
createTag(dd, index);
}
dl.appendChild(dd);
});
chooseWrap.appendChild(dl);
});
/* 4. 点击选项:同组切换 + 同步数组 + 同步标签 */
chooseWrap.addEventListener('click', function (e) {
var dd = e.target;
if (dd.tagName !== 'DD') return;
var index = +dd.dataset.index;
dd.parentNode.querySelectorAll('dd').forEach(function (el) {
el.classList.remove('active');
});
dd.classList.add('active');
selectedArr[index] = dd;
selectedBooks.querySelectorAll('.selected-tag').forEach(function (tag) {
if (+tag.dataset.index === index) selectedBooks.removeChild(tag);
});
createTag(dd, index);
});
/* 5. 点击叉:关闭选中标签(事件委托给包裹元素) */
selectedBooks.addEventListener('click', function (e) {
if (!e.target.classList.contains('close')) return; // 只认叉
var tag = e.target.parentNode; // 所在的选中标签
var index = +tag.dataset.index; // 拿到组索引
selectedBooks.removeChild(tag); // ① 删掉自己
var dd = selectedArr[index];
dd.classList.remove('active'); // ② 对应的 dd 取消选中
var firstDd = dd.parentNode.children[1]; // ③ 同组第一个(children[0] 是 dt)
firstDd.classList.add('active');
selectedArr[index] = firstDd; // 数组同步,长度保持不变
});
</script>
</body>
</html>关键行与易错点
span.dataset.index = index:这一行是整个联动的枢纽。标签是新建的,只有把「组索引」存到自定义属性上,删除时才能反查回数组里对应的dd。- 事件必须委托给
selectedBooks(包裹元素),因为span/i都是动态创建的,直接绑定绑不到。 e.target.parentNode才是选中标签本身,e.target只是那个叉;用selectedBooks.removeChild(e.target)会报错。dd.parentNode.children[1]:children[0]是dt,同组第一个dd是children[1],别写成children[0]。- 转写稿里用
delete selectedArr[index]保留数组长度,但会留下 empty 空洞,forEach会跳过它,导致该组第一个虽被选中却不在数组里。这里改成selectedArr[index] = firstDd,长度不变且数据一致。
示例二:点击选项和选中标签,价格跟着变(封装 changePrice)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>商品参数选项 - 价格变化</title>
<style>
.choose-wrap dl{display:flex;align-items:center;margin:8px 0}
.choose-wrap dt{width:60px;color:#999}
.choose-wrap dd{margin:0 8px 0 0;padding:4px 10px;border:1px solid #ddd;border-radius:4px;cursor:pointer}
.choose-wrap dd.active{border-color:#e4393c;color:#e4393c}
.selected-books{min-height:30px;margin:10px 0}
.selected-tag{display:inline-block;padding:4px 8px;margin-right:8px;border:1px solid #e4393c;color:#e4393c;border-radius:4px}
.selected-tag .close{margin-left:6px;font-style:normal;cursor:pointer}
.price-box{color:#e4393c;font-size:20px}
</style>
</head>
<body>
<div class="choose">
<div class="choose-wrap" id="chooseWrap"></div>
<div class="selected-books" id="selectedBooks"></div>
<div class="price-box">价格:¥<span id="price">5299</span></div>
</div>
<script>
var goodsDetail = {
price: 5299,
options: [
{ title: '版本', list: [
{ name: '电信优惠版', changePrice: -300 },
{ name: '移动版', changePrice: -500 },
{ name: '官方标配', changePrice: 0 },
{ name: '公开版', changePrice: 100 }
]},
{ title: '颜色', list: [
{ name: '黑色', changePrice: 200 },
{ name: '金色', changePrice: 0 }
]},
{ title: '容量', list: [
{ name: '128G', changePrice: 0 },
{ name: '256G', changePrice: 800 }
]}
]
};
var chooseWrap = document.getElementById('chooseWrap');
var selectedBooks = document.getElementById('selectedBooks');
var priceBox = document.getElementById('price');
var selectedArr = [];
/* 1. 封装价格计算:基础价 + 所有选中项的价格变化 */
function changePrice() {
var price = goodsDetail.price; // 基础价格
selectedArr.forEach(function (dd) {
price += +dd.dataset.changePrice; // 前面的 + 做类型转换,不能省
});
priceBox.innerText = price;
return price;
}
function createTag(dd, index) {
var span = document.createElement('span');
span.className = 'selected-tag';
span.dataset.index = index;
span.innerText = dd.innerText;
var i = document.createElement('i');
i.className = 'close';
i.innerText = '×';
span.appendChild(i);
selectedBooks.appendChild(span);
}
goodsDetail.options.forEach(function (item, index) {
var dl = document.createElement('dl');
var dt = document.createElement('dt');
dt.innerText = item.title;
dl.appendChild(dt);
item.list.forEach(function (opt, i) {
var dd = document.createElement('dd');
dd.innerText = opt.name;
dd.dataset.index = index;
dd.dataset.changePrice = opt.changePrice; // 提前把价格变化存进 DOM
if (i === 0) {
dd.classList.add('active');
selectedArr[index] = dd;
createTag(dd, index);
}
dl.appendChild(dd);
});
chooseWrap.appendChild(dl);
});
/* 2. 点击选项 → 同步标签 → 重新算价 */
chooseWrap.addEventListener('click', function (e) {
var dd = e.target;
if (dd.tagName !== 'DD') return;
var index = +dd.dataset.index;
dd.parentNode.querySelectorAll('dd').forEach(function (el) {
el.classList.remove('active');
});
dd.classList.add('active');
selectedArr[index] = dd;
selectedBooks.querySelectorAll('.selected-tag').forEach(function (tag) {
if (+tag.dataset.index === index) selectedBooks.removeChild(tag);
});
createTag(dd, index);
changePrice(); // 关键调用
});
/* 3. 关闭选中标签 → 也要重新算价 */
selectedBooks.addEventListener('click', function (e) {
if (!e.target.classList.contains('close')) return;
var tag = e.target.parentNode;
var index = +tag.dataset.index;
selectedBooks.removeChild(tag);
var dd = selectedArr[index];
dd.classList.remove('active');
var firstDd = dd.parentNode.children[1];
firstDd.classList.add('active');
selectedArr[index] = firstDd;
changePrice(); // 关键调用
});
/* 4. 初始化一次 */
changePrice();
</script>
</body>
</html>**关键行与
常见错误
现象: 删除选中标签后,再点同一选项,之前删掉的选项又全部“复活”。
原因: 只删除了 DOM,没有同步清理记录选中项的数据集合。
修法: 删除标签时同时移除对应数据项,并用delete保留数组长度,避免索引错位。现象: 价格显示成类似
2264950的异常大数,或出现字符串拼接。
原因: 从自定义属性取出的值是字符串,直接参与加法导致拼接。
修法: 参与计算前统一做数字转换,如+el.dataset.changePrice;涉及多项相加时用括号保证先算数值再加显示单位。现象: 点击动态创建的关闭按钮没有反应。
原因: 事件绑定在动态元素本身,元素创建前绑定或重建后失效。
修法: 使用事件委托,绑定到稳定的父容器上,再判断event.target是否为关闭按钮。现象: 修改数量或删除选项后,价格不同步;搭配商品勾选/取消后总价不对。
原因: 价格计算逻辑分散,某些变更入口漏调用重算函数。
修法: 统一封装价格重算函数,所有会影响价格的入口都调用同一函数。现象: 数量输入框可输入
hello、0、负数,或清空后数量异常。
原因: 未校验输入值,未处理NaN、空字符串转数字为0、小于最小值等情况。
修法: 用change事件在值改变且失焦后校验;非法值统一回退为最小值1,并回写输入框。现象: 搭配商品取消勾选后,数量或价格没有减回去。
原因: 只处理了选中分支,未处理取消选中分支,或加减逻辑不对称。
修法: 用checked判断选中/取消,选中加价、取消减价,并同步搭配商品数量与总价显示。
练习
- 实现“删除选中标签”功能:要求删除标签本身、取消对应选项选中、同组第一个选项恢复选中,并同步清理记录选中项的数据集合。
- 封装
changePrice():根据基础价、选中项价格变化、商品数量、搭配商品价格计算主商品总价和最终总价,并在所有价格变更入口调用。 - 修复数量输入框:输入非数字、空字符串、
0、负数时,统一回退为1;合法数字保留,并同步价格。 - 实现搭配商品勾选联动:勾选时增加搭配商品价格和数量,取消时减回,并同步“搭配商品数量”“最终总价”显示。
- 排查一个 bug:删除某个选中标签后价格未恢复原价。请写出排查顺序,并说明如何定位是数据集合、索引、类型转换还是重算函数调用问题。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 商品参数选项-关闭选中标签元素 | 15:04 |
| 2 | 商品参数选项-点击选项和选中标签价格变化 | 14:08 |
| 3 | 修改商品数量价格变化 | 17:36 |
| 4 | 选择搭配商品 | 19:28 |
小结
- 动态生成的交互元素优先用事件委托,绑定目标要选稳定父容器。
- 删除选中项不是单纯删 DOM,必须同步维护数据集合和同组默认选中状态。
- 价格计算要收敛到一个统一函数,所有变更入口只负责触发重算。
- 自定义属性适合携带索引、价格变化等轻量数据,但取出后必须注意类型转换。
- 数量输入必须做边界校验,重点处理
NaN、空字符串、零和负数。 - 搭配商品的选中与取消必须成对处理,加价和减价逻辑要对称。
- 先保证数据状态正确,再渲染 DOM 和价格;状态同步比局部改界面更可靠。
- 调试价格类问题时,按“数据集合→索引对应→类型转换→重算调用”顺序排查效率最高。