You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Django Formset中所有行的价格×数量自动计算

解决Django Formset多行输入实时计算总价的问题

这问题我之前做库存管理系统时也碰到过!你原来的JS只对第一行生效,核心原因是硬编码了第一行的ID(id_form-0-price、id_form-0-quantity),而Django Formset生成的每行ID是按索引递增的(form-0、form-1、form-2...),新增的行根本没被监听。下面给你两种靠谱的解决方案,适配所有现有行和动态新增的行:

方案一:用事件委托(推荐,适配动态新增行)

事件委托的思路是把监听事件绑定到所有行的父容器上,不管是页面加载时的行,还是后来通过Formset新增的行,只要输入框触发input事件,就能自动找到对应的行计算总价。

第一步:调整模板结构(给行和元素加标识)

先给Formset的每行加个容器类(比如form-row),总价显示区域加个类,也可以给price/quantity输入框加类(或者靠Django生成的ID后缀匹配):

<div class="inventory-formset-container">
    {% for form in formset %}
    <div class="form-row">
        {# 可以给输入框加class,或者用Django生成的ID后缀匹配 #}
        {{ form.price }}
        {{ form.quantity }}
        <span class="total-display">0.00</span>
        {# 如果需要提交总价,加个隐藏input #}
        <input type="hidden" name="{{ form.total.name }}" class="total-input">
    </div>
    {% endfor %}
    {{ formset.management_form }}
</div>

第二步:编写通用的JS代码

// 定义单一行的总价计算函数
function updateRowTotal(row) {
    // 用ID后缀匹配找到当前行的price和quantity输入框(不用加类也能生效)
    const priceInput = row.querySelector('[id$="-price"]');
    const quantityInput = row.querySelector('[id$="-quantity"]');
    const totalDisplay = row.querySelector('.total-display');
    const totalInput = row.querySelector('.total-input');

    // 处理非数字输入,避免NaN
    const price = parseFloat(priceInput.value) || 0;
    const quantity = parseInt(quantityInput.value) || 0;
    const total = price * quantity;

    // 更新显示和隐藏输入框(如果需要提交的话)
    totalDisplay.textContent = total.toFixed(2);
    if (totalInput) totalInput.value = total;
}

// 页面加载时初始化所有行的总价
document.addEventListener('DOMContentLoaded', function() {
    const allRows = document.querySelectorAll('.form-row');
    allRows.forEach(row => updateRowTotal(row));

    // 用事件委托监听父容器的input事件
    const formsetContainer = document.querySelector('.inventory-formset-container');
    formsetContainer.addEventListener('input', function(e) {
        // 判断触发事件的元素是price或quantity输入框
        const isPriceOrQuantity = e.target.id.match(/-price$/) || e.target.id.match(/-quantity$/);
        if (isPriceOrQuantity) {
            // 找到当前输入框所在的行
            const currentRow = e.target.closest('.form-row');
            updateRowTotal(currentRow);
        }
    });
});

方案二:遍历所有行绑定事件(仅适配现有行,新增行需额外处理)

如果你的Formset不需要动态新增行,也可以直接遍历所有行,给每个输入框绑定input事件:

document.addEventListener('DOMContentLoaded', function() {
    const allRows = document.querySelectorAll('.form-row');
    allRows.forEach(row => {
        const priceInput = row.querySelector('[id$="-price"]');
        const quantityInput = row.querySelector('[id$="-quantity"]');

        // 给当前行的两个输入框绑定事件
        const handleInput = () => updateRowTotal(row);
        priceInput.addEventListener('input', handleInput);
        quantityInput.addEventListener('input', handleInput);

        // 初始化计算
        updateRowTotal(row);
    });
});

但这种方式有个缺点:如果通过Django Formset的“添加新行”功能新增了行,这些新行的输入框不会自动绑定事件,需要在新增行的回调里重新执行绑定逻辑,所以还是推荐方案一的事件委托更省心。

关键注意点

  1. 处理非数字输入:一定要用|| 0处理parseFloat/parseInt返回的NaN,避免计算结果变成NaN。
  2. 动态行适配:事件委托是最优解,不用关心后续新增多少行,都能自动监听。
  3. 提交总价:如果需要把计算后的总价提交到后端,记得用隐藏的input存储值,而不是只靠页面显示的span。

内容的提问来源于stack exchange,提问作者Vineethvp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:59:15