
本文介绍如何在动态创建多个 元素后,实时监听其值变化,通过 ajax 获取对应单价,并自动汇总所有已选项目的总价,全程使用原生 javaScript 实现,无需 jquery 依赖。
本文介绍如何在动态创建多个 `
在构建表单类应用(如故障报修、服务选配等)时,常需支持用户动态添加多个选择项(如“故障类型”),并为每个选项异步加载对应价格,最后实时汇总总金额。原问题中使用 jQuery 混合原生 js,且事件绑定存在重复注册与作用域混乱问题,导致聚合失效。以下是结构清晰、可维护性强的纯原生 javascript 解决方案。
✅ 核心设计原则
- 委托监听:对容器 #newselectContainer 使用 change 事件委托,避免为每个动态 select 单独绑定事件;
- 语义化标记:为价格展示 添加统一 class(如 pricePlaceHolder),便于精准定位;
- 聚合解耦:将求和逻辑抽离为独立函数 countAll(),在每次选择变更后调用;
- 健壮性处理:空值/错误响应时降级显示,不中断流程。
? 完整实现代码
<!-- HTML 结构示例(确保存在以下元素) --> <button id="createNewSelect">新增故障项</button> <div id="newSelectContainer"></div> <div>总计:<span id="total">0.00$</span></div>
const selectContainer = document.getElementById("newSelectContainer"); const totalElement = document.getElementById("total"); // 【聚合函数】遍历所有 select,累加对应价格(需先获取价格并缓存,见下文) const countAll = () => { const prices = Array.from(selectContainer.querySelectorAll("select")) .map(select => { const priceSpan = select.closest(".form-group")?.querySelector(".pricePlaceHolder"); const text = priceSpan?.textContent || ""; // 提取数字:如 "(12.50 $)" → 12.50;或 "12.50$" → 12.50 const match = text.match(/([d.]+)s*$/); return match ? parseFloat(match[1]) : 0; }) .filter(price => !isNaN(price)); const total = prices.reduce((sum, price) => sum + price, 0); totalElement.textContent = `${total.toFixed(2)}$`; }; // 【统一事件委托】监听所有动态 select 的 change 事件 selectContainer.addEventListener("change", function (e) { const select = e.target; if (!select.matches("select")) return; const priceSpan = select.closest(".form-group")?.querySelector(".pricePlaceHolder"); if (!priceSpan) return; const selectedId = select.value; if (!selectedId) { priceSpan.textContent = ""; countAll(); return; } // 异步获取单价 fetch(`/Home/GetPrice?selectedFaultId=${selectedId}`) .then(res => { if (!res.ok) throw new Error("Network response was not ok"); return res.json(); }) .then(data => { if (data.UnitPrice === undefined) { throw new Error("Invalid price data"); } priceSpan.textContent = `${parseFloat(data.UnitPrice).toFixed(2)}$`; countAll(); // 更新总计 }) .catch(err => { console.error("价格获取失败:", err); priceSpan.textContent = "—"; countAll(); }); }); // 【新增下拉项】点击按钮时创建新 select document.getElementById("createNewSelect").addEventListener("click", function () { const count = selectContainer.querySelectorAll(".form-group").length + 1; // 创建结构 const group = document.createElement("div"); group.className = "form-group"; const label = document.createElement("label"); label.className = "control-label col-md-3 col-sm-3 col-xs-12"; label.textContent = `Arıza ${count}`; const colDiv = document.createElement("div"); colDiv.className = "col-md-6 col-sm-6 col-xs-12"; const select = document.createElement("select"); select.className = "form-control"; select.name = `fault${count}`; select.id = `fault${count}`; const priceSpan = document.createElement("span"); priceSpan.className = "pricePlaceHolder"; // 关键:统一 class 用于查找 // 插入 DOM group.appendChild(label); group.appendChild(colDiv); colDiv.appendChild(select); colDiv.appendChild(priceSpan); selectContainer.appendChild(group); // 加载选项数据 fetch("/Home/GetFaultOption") .then(res => res.json()) .then(data => { // 清空并填充选项 select.innerHTML = '<option value="">Lütfen Arıza Seçiniz</option>'; data.forEach(option => { const opt = document.createElement("option"); opt.value = option.Id; opt.textContent = option.Option; select.appendChild(opt); }); }) .catch(err => console.error("选项加载失败:", err)); });
⚠️ 注意事项与最佳实践
- 不要重复绑定事件:原代码中在 $(function(){…}) 内循环绑定 change,会导致同一 select 被多次监听,引发重复请求与计算错误。本文采用事件委托,一劳永逸。
- 价格缓存建议(进阶):若后端允许,可在首次 /GetFaultOption 响应中直接返回 UnitPrice 字段,避免后续 N 次 /GetPrice 请求,大幅提升性能。
- 空值与校验:
- CSS 类命名一致性:确保 .form-group、.pricePlaceHolder 等类名与实际 HTML 结构严格匹配,否则 closest() 和 querySelector() 将失效。
- 浮点精度处理:使用 toFixed(2) 格式化显示,但内部计算仍用 parseFloat() 保持精度,避免 0.1 + 0.2 !== 0.3 问题。
✅ 总结
本方案以「事件委托 + 函数式聚合」为核心,彻底解决动态表单项的价格汇总难题。代码无外部依赖、逻辑分层明确、错误处理完善,可直接集成至现有项目。后续如需扩展(如删除某一项、支持数量输入、多币种切换),均可基于此结构平滑演进。