如何在jstree中实现单个节点拥有两个父节点?
嗨,我来帮你解决这个jstree多父节点的问题~
首先得明确:jstree本质是基于DOM的树形控件,而DOM元素不可能同时拥有两个父元素,这就是你遇到报错和节点截断的核心原因——同一个ID的节点被尝试挂载到两个父节点下,DOM结构冲突了。不过既然业务有这个需求,我们可以用两种可行的方案来实现:
问题回顾
你用ASP.NET后端返回JSON渲染jstree,常规树形结构正常,但当一个节点需要属于两个父节点时出现异常。你的代码如下:
前端JS代码
$(function () { $('#jstree').jstree({ 'core': { "animation": 0, "check_callback": true, "themes": { "stripes": true }, 'data': { 'url': '/Products/ProductCategoryStructure/GetData', 'data': function (node) { return { 'id': node.id }; } } }, "types": { "#": { "max_children": 1, "max_depth": 4, "valid_children": ["root"] }, "default": { "valid_children": ["default", "file"] }, "file": { "icon": "glyphicon glyphicon-file", "valid_children": [] } }, plugins: ["contextmenu"], contextmenu: {items: customMenu} }); $('#jstree').on('changed.jstree', function (e, data) { var i, j, r = []; for (i = 0, j = data.selected.length; i < j; i++) { r.push(data.instance.get_node(data.selected[i]).text); } // $('#event_result').html('Selected: ' + r.join(', ')); }).jstree(); });
后端C#代码
[HttpGet] public ActionResult GetData() { var ListProductCategoryStructure = _service.GetAll(); var nodes = new List<JsTreeModel>(); nodes.Add(new JsTreeModel() { id = "1", parent = "#", text = " مادیران مال", opened = true, icon = "/assets/pages/img/mlgo.png" }); foreach (var item in ListProductCategoryStructure) { nodes.Add(new JsTreeModel() { id = item.ProductCategoryId.ToString(), parent = (item.ProductParentCategoryId==65?"#": item.ProductParentCategoryId.ToString()), text = item.ProductParentCategoryTitle, icon="false" }); } return Json(nodes, JsonRequestBehavior.AllowGet); }
方案1:复制节点(独立实例+状态同步)
这是最容易实现且稳定的方案:给同一个业务节点创建多个jstree节点实例(每个实例有唯一ID),同时通过事件监听保持它们的选中、展开等状态同步。
步骤1:修改后端代码,生成唯一节点ID
在生成节点时,用「原业务ID + 父节点ID」拼接成唯一的节点ID,同时把原业务ID存在自定义属性里,方便后续同步:
[HttpGet] public ActionResult GetData() { var ListProductCategoryStructure = _service.GetAll(); var nodes = new List<JsTreeModel>(); nodes.Add(new JsTreeModel() { id = "1", parent = "#", text = " مادیران مال", opened = true, icon = "/assets/pages/img/mlgo.png" }); foreach (var item in ListProductCategoryStructure) { var originalBusinessId = item.ProductCategoryId.ToString(); // 生成唯一节点ID:原业务ID + "_" + 父节点ID,避免冲突 var uniqueNodeId = $"{originalBusinessId}_{item.ProductParentCategoryId}"; nodes.Add(new JsTreeModel() { id = uniqueNodeId, parent = (item.ProductParentCategoryId == 65 ? "#" : item.ProductParentCategoryId.ToString()), text = item.ProductParentCategoryTitle, icon = "false", // 新增data属性存储原业务ID,需要确保JsTreeModel类有这个属性 data = new { OriginalBusinessId = originalBusinessId } }); } return Json(nodes, JsonRequestBehavior.AllowGet); }
注意:需要给你的
JsTreeModel类添加data属性(可以是dynamic或者自定义类),用来存储业务相关的自定义数据。
步骤2:修改前端代码,同步节点状态
添加事件监听,当某个节点的选中、展开状态变化时,自动同步所有同业务ID的节点:
$(function () { const treeInstance = $('#jstree').jstree({ 'core': { "animation": 0, "check_callback": true, "themes": { "stripes": true }, 'data': { 'url': '/Products/ProductCategoryStructure/GetData', 'data': function (node) { return { 'id': node.id }; } } }, "types": { "#": { "max_children": 1, "max_depth": 4, "valid_children": ["root"] }, "default": { "valid_children": ["default", "file"] }, "file": { "icon": "glyphicon glyphicon-file", "valid_children": [] } }, plugins: ["contextmenu"], contextmenu: { items: customMenu } }); // 同步选中状态 $('#jstree').on('changed.jstree', function (e, data) { // 收集当前选中的所有原业务ID const selectedBizIds = new Set(); data.selected.forEach(nodeId => { const node = data.instance.get_node(nodeId); if (node.data?.OriginalBusinessId) { selectedBizIds.add(node.data.OriginalBusinessId); } }); // 遍历所有节点,同步选中状态 const allNodes = data.instance.get_json('#', { flat: true }); allNodes.forEach(node => { if (node.data?.OriginalBusinessId) { const shouldBeSelected = selectedBizIds.has(node.data.OriginalBusinessId); const isSelected = data.selected.includes(node.id); if (shouldBeSelected && !isSelected) { data.instance.select_node(node.id, true); } else if (!shouldBeSelected && isSelected) { data.instance.deselect_node(node.id); } } }); // 保留你原来的业务逻辑 const selectedTexts = data.selected.map(nodeId => data.instance.get_node(nodeId).text); // $('#event_result').html('Selected: ' + selectedTexts.join(', ')); }); // 可选:同步展开/折叠状态 $('#jstree').on('open_node.jstree close_node.jstree', function (e, data) { const targetBizId = data.node.data?.OriginalBusinessId; if (!targetBizId) return; const shouldOpen = e.type === 'open_node'; const allNodes = data.instance.get_json('#', { flat: true }); allNodes.forEach(node => { if (node.data?.OriginalBusinessId === targetBizId && node.id !== data.node.id) { if (shouldOpen) { data.instance.open_node(node.id); } else { data.instance.close_node(node.id); } } }); }); });
方案2:自定义jstree插件(进阶)
如果不想复制节点,你可以深入jstree的API,通过check_callback拦截节点加载逻辑,或者用懒加载动态生成节点实例。不过这种方式复杂度很高,需要对jstree的内部机制有一定了解,适合有前端框架定制经验的开发者。
注意事项
- 复制节点后,每个节点都是独立的DOM元素,所以右键菜单、编辑等操作要基于原业务ID处理,而不是节点的唯一ID。
- 如果后续需要修改节点的文本、图标等属性,要遍历所有同业务ID的节点一起修改,保证一致性。
内容的提问来源于stack exchange,提问作者shaghayegh sheykholeslami
相关产品推荐
相关产品推荐

