如何使用 JavaScript 自动计算账单总价
支付小周
结论
每个金额输入框触发 input 事件时调用 totalIt(),就能同步更新当前行价格和账单总价,无需再设置“Total”按钮。
计算时最好直接遍历表格中的数据行,不要依赖 price1、price2 这类连续编号。删除中间行后,ID 可能出现断号,原来的循环便无法找到对应元素。
推荐实现
为参与计算的三类输入框添加统一的 class,再通过事件委托监听整个表格。初始行和动态新增的行都可以自动参与计算。
HTML
<table id="dataTable">
<thead>
<tr>
<th>Select</th>
<th>Sr. No.</th>
<th>Item Name</th>
<th>Cost</th>
<th>GST Tax</th>
<th>Delivery Charges</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" name="chk[]"></td>
<td class="row-number">1</td>
<td><input type="text" name="item[]"></td>
<td><input type="number" value="0" step="0.001" name="cost[]" class="cost"></td>
<td><input type="number" value="0" step="0.001" name="qty[]" class="tax"></td>
<td><input type="number" value="0" step="0.001" name="del[]" class="delivery"></td>
<td>
<input type="text" value="0.000" name="price[]" class="price" readonly>
</td>
</tr>
</tbody>
</table>
<div class="total">
Total:
<input type="text" id="total" name="total" value="0.000" readonly>
</div>
示例保留了原代码中的 qty[] 字段名,但按照表头含义,这个字段实际表示 GST Tax。如果后端字段可以调整,建议改成含义更清楚的 tax[]。
JavaScript
const table = document.getElementById("dataTable");
const totalField = document.getElementById("total");
function toNumber(value) {
const number = parseFloat(value);
return Number.isFinite(number) ? number : 0;
}
function calculateRow(row) {
const cost = toNumber(row.querySelector(".cost").value);
const tax = toNumber(row.querySelector(".tax").value);
const delivery = toNumber(row.querySelector(".delivery").value);
const price = cost + tax + delivery;
row.querySelector(".price").value = price.toFixed(3);
return price;
}
function totalIt() {
let total = 0;
table.querySelectorAll("tbody tr").forEach(function (row) {
total += calculateRow(row);
});
totalField.value = total.toFixed(3);
}
table.addEventListener("input", function (event) {
if (
event.target.matches(".cost") ||
event.target.matches(".tax") ||
event.target.matches(".delivery")
) {
totalIt();
}
});
totalIt();
这里监听的是 input,而不是 keyup。除了键盘输入,input 还能捕获粘贴、删除和数字输入框箭头调整等操作。
动态添加行
新增行时设置相同的 class 即可。事件已经绑定在 table 上,因此新输入框不需要单独注册 onkeyup。
function addRow() {
const tbody = table.querySelector("tbody");
const row = tbody.insertRow();
row.innerHTML = `
<td><input type="checkbox" name="chk[]"></td>
<td class="row-number"></td>
<td><input type="text" name="item[]"></td>
<td><input type="number" value="0" step="0.001" name="cost[]" class="cost"></td>
<td><input type="number" value="0" step="0.001" name="qty[]" class="tax"></td>
<td><input type="number" value="0" step="0.001" name="del[]" class="delivery"></td>
<td>
<input type="text" value="0.000" name="price[]" class="price" readonly>
</td>
`;
updateRowNumbers();
totalIt();
}
function updateRowNumbers() {
table.querySelectorAll("tbody .row-number").forEach(function (cell, index) {
cell.textContent = index + 1;
});
}
按钮可以这样写:
<button type="button" onclick="addRow()">Add Item</button>
删除行后重新计算
删除选中的行后,需要重新编号并调用 totalIt()。否则,总价可能还包含已删除行的金额。
function deleteSelectedRows() {
table.querySelectorAll('tbody input[name="chk[]"]:checked')
.forEach(function (checkbox) {
checkbox.closest("tr").remove();
});
updateRowNumbers();
totalIt();
}
对应的按钮如下:
<button type="button" onclick="deleteSelectedRows()">Delete Item</button>
如果只想最小幅度修改原代码
可以将各输入框中原来的:
calc(rowCount);
改为:
totalIt();
初始行也改成:
window.onload = function () {
document.getElementsByName("qty[]")[0].oninput = totalIt;
document.getElementsByName("cost[]")[0].oninput = totalIt;
document.getElementsByName("del[]")[0].oninput = totalIt;
totalIt();
};
动态行使用:
element4.oninput = totalIt;
element5.oninput = totalIt;
这种改法仍依赖连续的数字 ID。删除中间行后,例如页面只剩 price1 和 price3,原来的 totalIt() 仍会尝试读取 price2,可能因此报错。实际项目中,更适合采用前面的“按行遍历 + class + 事件委托”方案。
注意事项
readonly的总价输入框不会由用户触发keyup,所以在total字段上设置onkeyup="totalIt()"没有作用。parseFloat("")的结果是NaN,空值或非法值应统一按0处理。- 如果 GST 输入的是税率,例如
18表示18%,就不能直接相加,计算公式应改为:
const price = cost + cost * tax / 100 + delivery;
- 如果 GST 字段输入的是已经计算好的税额,则继续使用:
const price = cost + tax + delivery;
- 金额通常保留两位小数,当前示例按照原需求保留三位。需要两位时,将
toFixed(3)改成toFixed(2)。 - 浏览器端计算只能改善交互体验。表单提交后,服务端仍需根据原始字段重新计算并校验总价,不能直接信任客户端提交的
price[]或total。
备注:内容仅供参考。
