
本文详解如何通过结构化 dom 操作与模板化克隆,为每个选中的商品分类单独创建独立表格区块,彻底解决多分类场景下 ajax 返回数据在页面中重复叠加的问题。
本文详解如何通过结构化 dom 操作与模板化克隆,为每个选中的商品分类单独创建独立表格区块,彻底解决多分类场景下 ajax 返回数据在页面中重复叠加的问题。
在使用 AJAX 动态加载多个商品分类数据时,常见陷阱是:所有分类共用同一套 DOM 克隆逻辑,导致后序请求的结果不断追加到前序已渲染的区块中,造成视觉与逻辑上的重复渲染。如您所见,当选择两个分类(如 category_id = [1, 2])时,第二组数据并非插入新容器,而是反复操作 .inventory_products_lines_values 的最后一个克隆体,并错误地调用 insertAfter($injectAfter) 和 $('.inventory_products_lines_values').closest('div').after(clone) —— 这会将新行插入到全局已有区块内,而非各自独立的分类容器中。
根本原因在于:缺乏隔离的渲染上下文。原始代码中:
- 所有分类共享同一个 .inventory_products_lines_head 查找目标;
- 克隆的 .inventory_products_lines_values 没有绑定到对应分类容器;
- $('#default_inventory_products .inventory_products_block').clone() 被多次执行并统一追加到 #inventory_products 后,但内部产品行未定向注入其所属区块。
✅ 正确解法:为每个分类创建专属容器(block),并基于 HTML 模板按需渲染,杜绝跨容器污染。
✅ 推荐实现方案(结构清晰、可维护性强)
1. 定义语义化模板(推荐使用 <template> 或隐藏 DOM 节点)
<!-- 分类容器模板 -->
<div class="hidden" id="tpl-category-block">
<div class="inventory-products-block">
<h3 class="category-title"></h3>
<table class="category-products-table">
<thead>
<tr>
<th>#</th>
<th>品牌</th>
<th>商品名</th>
<th>库存</th>
<th>状态</th>
</tr>
</thead>
<tbody class="products-body"></tbody>
</table>
</div>
</div>
<!-- 商品行模板 -->
<div class="hidden" id="tpl-product-row">
<tr class="product-row">
<td class="row-number"></td>
<td class="brand"></td>
<td class="name"></td>
<td class="now-quantity"></td>
<td class="status"></td>
</tr>
</div>2. AJAX 请求改造:聚合 + 顺序渲染(避免竞态)
// 收集所有请求 Promise
const ajaxPromises = categories_id.map(category_id =>
$.ajax({
type: 'GET',
url: 'ajax-tab.php',
data: {
ajax: true,
controller: 'AdminInventory',
action: 'searchProductsByCategory',
token: token,
search_category_id: category_id
},
dataType: 'json'
})
);
// 等待全部完成,再统一渲染(保证顺序 & 避免交错)
Promise.all(ajaxPromises).then(responses => {
responses.forEach((data, index) => {
// 创建该分类专属容器
const block = $('#tpl-category-block').clone().removeClass('hidden').attr('id', '');
block.find('.category-title').text(data.category_name);
const tbody = block.find('.products-body');
let rowNumber = 1;
$.each(data.products, function(key, val) {
const row = $('#tpl-product-row').clone().removeClass('hidden').attr('id', '');
row.find('.row-number').text(rowNumber++);
row.find('.brand').html(`<span>${val.manufacturer_name || '-'}</span>`);
row.find('.name').html(`<span>${val.name || '-'}</span>`);
row.find('.now-quantity').html(`<span>${val.quantity || 0}</span>`);
row.find('.status').html(
val.active === 1
? '<i style="color:#72C279" class="icon-check"></i>'
: '<i style="color:#E08F95" class="icon-remove"></i>'
);
// 可选:注入隐藏 input 字段(如 id_product)
row.append(`<input type="hidden" name="id_product[]" value="${val.id_product}">`);
tbody.append(row);
});
// 插入到主容器末尾(确保顺序)
$('#inventory_products').append(block);
});
}).catch(err => console.error('AJAX batch failed:', err));3. 关键注意事项
- 禁用 clone().last() 模式:它依赖 DOM 当前状态,多请求并发时极易错位;
- 避免全局选择器副作用:如 $('.inventory_products_lines_values').closest('div').after(...) 会污染所有已有区块;
- 优先使用 Promise.all():比 forEach + setTimeout 更可靠,确保渲染顺序与请求顺序一致;
- 模板应脱离业务 DOM:<template> 标签或 display:none 的纯模板节点更安全,避免被意外样式/事件影响;
- 为每个 <input> 设置唯一 name[] 且不重复 id:原代码中多次使用 id="inventory_id_product" 会导致 HTML 无效(ID 必须唯一),建议改用 class 或动态生成 ID(如 id="inventory_id_product_${val.id_product}")。
✅ 最终效果
- 每个分类独占一个 <div class="inventory-products-block">;
- 表格结构清晰、语义明确、样式可独立控制;
- 数据无交叉、无重复、可预测、易调试;
- 后续扩展(如分页、筛选、导出)可基于单个区块封装。
此方案不仅修复重复问题,更构建了可伸缩的动态内容架构——让多分类渲染从“脆弱拼接”升级为“模块化组装”。


















