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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:35:36