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

.NET 5中Bootstrap表格动态行下拉框批量变更问题排查与解决

问题描述

开发发票页面时,通过下拉框填充Bootstrap表格行数据,已实现行插入功能,但存在异常:选择任意行的下拉框选项时,所有行的对应qty和price值都会被修改,而非仅修改当前选中的行。


前端代码(原代码)

<table id="table1" border="0">
    <tr style="font-size:80%;">
        <th>#</th>
        <th style="width:50%;">Product</th>
        <th style="width:5%;">qty</th>
        <th style="width:5%;">price</th>
        <th class="d-print-none">X</th>
    </tr>
    @{
        int i = 0; 
        foreach (var item in Model.PL)
        {
            <tr class="border-bottom">
                <td class="tbl_id">1</td>
                <td>
                    <select id="Products" asp-for="@Model.PL[@i].ProdId" class="form-control" type="text" name="data[@i][ProdId]" onchange="copyProd(this)" style="width:100%">
                        @foreach (var item in Model.PL)
                        {
                            <option value="@item.ProdId"    
                                    qty="@qty"
                                    price="@Convert.ToInt32(item.price)">
                                @item.name, @Convert.ToInt32(item.price)
                            </option>
                        }
                    </select>

                    <script>
                        function copyProd(el) {
                            let op = el.options[el.selectedIndex];
                            let myvalue = el.value;
                            let qty = op.getAttribute('qty');
                            let price = op.getAttribute('price');

                            $("input[name='qty[@i]']").val(qty);
                            $("input[name='price[@i]']").val(price);
                        }
                    </script>
                </td>
            </tr>
        }
        i++;
    }
</table>

<script>
    //JS class to add/delete rows in html table
    function adRowsTable(id) {
        var table = document.getElementById(id);
        var me = this;
        if (document.getElementById(id)) {
            var row1 = table.rows[1].outerHTML;

            // set index-id in cols with class .tbl_id
            function setIds() {
                var tbl_id = document.querySelectorAll('#' + id + ' .tbl_id');
                for (var i = 0; i < tbl_id.length; i++) tbl_id[i].innerHTML = i + 1;
            }

            // add row after clicked row; receives clicked button in row
            me.addRow = function (btn) {
                btn ? btn.parentNode.parentNode.insertAdjacentHTML('afterend', row1) : table.insertAdjacentHTML('beforeend', row1);
                setIds();
            }

            // delete clicked row; receives clicked button in row
            me.delRow = function (btn) {
                btn.parentNode.parentNode.outerHTML = '';
                setIds();
            }
        }
    }

    //create object of adRowsTable(), pass the table id
    var ob_adRows = new adRowsTable('table1');
</script>

后端代码(原代码)

public async Task<IActionResult> OnGetAsync()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

    try
    {
        PL = await _context.Products.ToListAsync();

        var addrows = new List<InvoiceRows>()
        {
            new Products() { ProdId = 1, qty = 1, price = 0 },
            new Products() { ProdId = 1, qty = 1, price = 0 },
            new Products() { ProdId = 1, qty = 1, price = 0 },
        };

        PL = addrows;

        ViewData["ProdId"] = new SelectList(_context.Products, "ProdId", "Name");
    }
    catch (Exception e) { _ = e.ToString(); }

    return Page();
}

[BindProperty]
public InvoiceRows InvoiceRow { get; set; } = default!;
public IList<Products> PL { get; set; } = default!;

public async Task<IActionResult> OnPostAsync(List<InvoiceRows> addRow)
{
    _context.InvoiceRows.AddRange(addRow);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

问题分析与解决方案

1. 核心问题:循环变量作用域与重复函数定义

  • 问题:在Razor的foreach循环中,每次都会生成一个copyProd函数,且函数内引用的循环变量i会被最后一次循环的值覆盖,导致所有下拉框触发时都会修改同一组输入框。
  • 解决:
    • 将copyProd函数移到循环外部,只定义一次。
    • 给每个下拉框添加自定义属性data-row-index="@i",绑定当前行的索引,在函数中通过该属性获取目标行。

2. 动态行的事件与属性更新

  • 问题:动态插入的行,下拉框的onchange事件无法生效,且输入框的name属性索引固定,无法匹配当前行数。
  • 解决:
    • 使用事件委托监听下拉框变化,确保动态添加的行也能触发事件。
    • 创建行模板,插入时动态替换索引值,并在setIds函数中更新所有行的索引属性与name值。

3. 其他细节问题

  • 下拉框ID重复:将ID改为Products_@i保证唯一性,或直接移除ID(无需通过ID选择时)。
  • 后端类型不匹配:原代码中PL是IList<Products>,却被赋值为List<InvoiceRows>,需修正类型对应关系。

修改后的代码示例

前端Razor视图

<table id="table1" border="0">
    <tr style="font-size:80%;">
        <th>#</th>
        <th style="width:50%;">Product</th>
        <th style="width:5%;">qty</th>
        <th style="width:5%;">price</th>
        <th class="d-print-none">操作</th>
    </tr>
    @{
        int i = 0; 
        foreach (var item in Model.PL)
        {
            <tr class="border-bottom">
                <td class="tbl_id">@(i+1)</td>
                <td>
                    <select id="Products_@i" asp-for="@Model.PL[i].ProdId" class="form-control product-select" 
                            name="data[@i][ProdId]" data-row-index="@i" style="width:100%">
                        @foreach (var prod in Model.ProductsList)
                        {
                            <option value="@prod.ProdId"    
                                    qty="@prod.qty"
                                    price="@Convert.ToInt32(prod.price)">
                                @prod.name, @Convert.ToInt32(prod.price)
                            </option>
                        }
                    </select>
                </td>
                <td>
                    <input type="number" name="qty[@i]" class="form-control qty-input" value="@item.qty" />
                </td>
                <td>
                    <input type="number" name="price[@i]" class="form-control price-input" value="@item.price" />
                </td>
                <td class="d-print-none">
                    <button onclick="ob_adRows.delRow(this)">删除</button>
                </td>
            </tr>
            i++;
        }
    }
</table>

<button onclick="ob_adRows.addRow()">添加行</button>

<script>
    // 全局定义一次函数
    function copyProd(el) {
        let op = el.options[el.selectedIndex];
        let rowIndex = el.getAttribute('data-row-index');
        let qty = op.getAttribute('qty');
        let price = op.getAttribute('price');

        $(`input[name='qty[${rowIndex}]']`).val(qty);
        $(`input[name='price[${rowIndex}]']`).val(price);
    }

    // 事件委托处理动态行的下拉框变化
    $(document).on('change', '.product-select', function() {
        copyProd(this);
    });

    // 行操作类
    function adRowsTable(id) {
        var table = document.getElementById(id);
        var me = this;
        if (document.getElementById(id)) {
            // 行模板,用{index}作为占位符
            var templateRow = `
                <tr class="border-bottom">
                    <td class="tbl_id"></td>
                    <td>
                        <select class="form-control product-select" 
                                name="data[{index}][ProdId]" data-row-index="{index}" style="width:100%">
                            ${document.querySelector('.product-select').innerHTML}
                        </select>
                    </td>
                    <td>
                        <input type="number" name="qty[{index}]" class="form-control qty-input" value="1" />
                    </td>
                    <td>
                        <input type="number" name="price[{index}]" class="form-control price-input" value="0" />
                    </td>
                    <td class="d-print-none">
                        <button onclick="ob_adRows.delRow(this)">删除</button>
                    </td>
                </tr>
            `;

            function setIds() {
                var rows = document.querySelectorAll('#' + id + ' tr.border-bottom');
                for (var i = 0; i < rows.length; i++) {
                    // 更新行号
                    rows[i].querySelector('.tbl_id').innerHTML = i + 1;
                    // 更新索引属性与name值
                    var selectEl = rows[i].querySelector('.product-select');
                    selectEl.setAttribute('data-row-index', i);
                    selectEl.setAttribute('name', `data[${i}][ProdId]`);
                    rows[i].querySelector('.qty-input').setAttribute('name', `qty[${i}]`);
                    rows[i].querySelector('.price-input').setAttribute('name', `price[${i}]`);
                }
            }

            me.addRow = function (btn) {
                var rowCount = document.querySelectorAll('#' + id + ' tr.border-bottom').length;
                var newRow = templateRow.replace(/\{index\}/g, rowCount);
                btn ? btn.parentNode.parentNode.insertAdjacentHTML('afterend', newRow) : table.insertAdjacentHTML('beforeend', newRow);
                setIds();
            }

            me.delRow = function (btn) {
                btn.parentNode.parentNode.outerHTML = '';
                setIds();
            }
        }
    }

    var ob_adRows = new adRowsTable('table1');
</script>

后端代码

public async Task<IActionResult> OnGetAsync()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

    try
    {
        // 获取产品列表用于下拉框选项
        ProductsList = await _context.Products.ToListAsync();

        // 初始化发票行数据
        PL = new List<InvoiceRows>()
        {
            new InvoiceRows() { ProdId = 1, qty = 1, price = 0 },
            new InvoiceRows() { ProdId = 1, qty = 1, price = 0 },
            new InvoiceRows() { ProdId = 1, qty = 1, price = 0 },
        };

        ViewData["ProdId"] = new SelectList(_context.Products, "ProdId", "Name");
    }
    catch (Exception e) { _ = e.ToString(); }

    return Page();
}

[BindProperty]
public InvoiceRows InvoiceRow { get; set; } = default!;
// 发票行列表
public IList<InvoiceRows> PL { get; set; } = default!;
// 产品列表(用于下拉框)
public IList<Products> ProductsList { get; set; } = default!;

public async Task<IActionResult> OnPostAsync(List<InvoiceRows> addRow)
{
    if (addRow != null && addRow.Any())
    {
        _context.InvoiceRows.AddRange(addRow);
        await _context.SaveChangesAsync();
    }
    return RedirectToPage("./Index");
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 01:07:51