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

如何用Django Model Form实现含400+项的嵌套多选树?

嘿,我来帮你搞定这个支持400+数据的嵌套多选树形控件需求!结合你的CNPQ模型,我会一步步教你用Django + jQuery实现这个效果,兼顾性能和交互体验。

整体思路

核心是先在Django后端把自关联的CNPQ数据转换成嵌套的树形结构,再用jQuery前端渲染成带复选框的树形控件,最后实现父子复选框的联动逻辑。


第一步:Django后端处理树形数据

首先我们需要把扁平的数据库数据转换成前端好处理的嵌套JSON结构。可以写个递归函数来生成树形数据:

# 在你的views.py里添加这两个函数
import json
from .models import CNPQ

def build_cnpq_children(parent_node):
    """递归生成子节点列表"""
    children = CNPQ.objects.filter(parent=parent_node).order_by('title')
    child_list = []
    for child in children:
        child_list.append({
            'id': child.id,
            'title': child.title,
            'level': int(child.level),  # 假设level是数字字符串,转成整数方便前端计算缩进
            'children': build_cnpq_children(child)
        })
    return child_list

def get_cnpq_tree():
    """获取完整的树形结构(根节点是parent为null的记录)"""
    root_nodes = CNPQ.objects.filter(parent__isnull=True).order_by('title')
    tree_data = []
    for root in root_nodes:
        tree_data.append({
            'id': root.id,
            'title': root.title,
            'level': int(root.level),
            'children': build_cnpq_children(root)
        })
    return json.dumps(tree_data)

然后在你的视图里把树形数据传递给模板:

def your_form_view(request):
    cnpq_tree_json = get_cnpq_tree()
    
    if request.method == 'POST':
        # 处理表单提交,获取选中的CNPQ ID列表
        selected_cnpq_ids = request.POST.getlist('cnpq_selected')
        # 这里根据你的业务逻辑保存,比如关联到主模型的多对多字段
        # example: your_instance.cnpq.set(selected_cnpq_ids)
    
    # 初始化你的ModelForm
    form = YourModelForm()
    return render(request, 'your_template.html', {
        'form': form,
        'cnpq_tree_json': cnpq_tree_json
    })

第二步:前端用jQuery渲染树形控件

在模板里,我们用递归方式渲染树形结构,同时实现复选框的联动逻辑,并且针对400+数据做性能优化(减少DOM操作次数)。

<!-- 你的模板文件 your_template.html -->
<div class="cnpq-tree-wrapper">
    <h3>选择知识领域</h3>
    <ul id="cnpq-tree-container"></ul>
    <!-- 如果用ModelForm的多对多字段,可以隐藏默认控件,用我们的树形控件替代 -->
    {{ form.cnpq.as_hidden }}
</div>

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
    // 解析后端传递的树形JSON
    const cnpqTree = JSON.parse('{{ cnpq_tree_json|safe }}');

    // 递归生成树形HTML字符串(比多次append更高效,适合大量数据)
    function generateTreeHtml(nodes) {
        let html = '';
        nodes.forEach(node => {
            // 根据level计算缩进,每级缩进20px
            const indent = (node.level - 1) * 20;
            html += `<li style="margin-left: ${indent}px;">`;
            html += `<input type="checkbox" name="cnpq_selected" value="${node.id}">`;
            html += `<label>${node.title}</label>`;
            
            if (node.children.length > 0) {
                html += `<ul class="child-nodes">${generateTreeHtml(node.children)}</ul>`;
            }
            html += '</li>';
        });
        return html;
    }

    // 一次性渲染整个树形结构
    $('#cnpq-tree-container').html(generateTreeHtml(cnpqTree));

    // 实现复选框联动逻辑
    $('.cnpq-tree-wrapper').on('change', 'input[type="checkbox"]', function() {
        const $checkbox = $(this);
        const $currentLi = $checkbox.closest('li');

        // 子节点联动:选中父节点时,所有子节点同步选中/取消
        $currentLi.find('.child-nodes input[type="checkbox"]').prop('checked', $checkbox.prop('checked'));

        // 父节点联动:子节点全部选中时,父节点自动选中;部分选中时父节点半选(可选)
        const $parentLi = $currentLi.closest('.child-nodes').closest('li');
        if ($parentLi.length > 0) {
            const $parentCheckbox = $parentLi.find('input[type="checkbox"]');
            const allChildren = $parentLi.find('.child-nodes input[type="checkbox"]');
            const checkedChildren = $parentLi.find('.child-nodes input[type="checkbox"]:checked');
            $parentCheckbox.prop('checked', allChildren.length === checkedChildren.length);
            // 可选:添加半选样式
            $parentCheckbox.prop('indeterminate', checkedChildren.length > 0 && checkedChildren.length < allChildren.length);
        }
    });
});
</script>

<style>
.cnpq-tree-wrapper ul {
    list-style: none;
    padding-left: 0;
}
.cnpq-tree-wrapper li {
    margin: 6px 0;
    cursor: pointer;
}
.cnpq-tree-wrapper input[type="checkbox"] {
    margin-right: 8px;
}
</style>

第三步:表单提交与验证

提交表单时,request.POST.getlist('cnpq_selected')会返回所有选中的CNPQID列表,你可以直接用这个列表关联到你的主模型(比如多对多字段)。如果需要验证,可以在视图里加一步:

from django.core.exceptions import ValidationError

selected_cnpq_ids = request.POST.getlist('cnpq_selected')
# 验证所有ID都是存在的CNPQ记录
valid_ids = CNPQ.objects.filter(id__in=selected_cnpq_ids).values_list('id', flat=True)
if len(selected_cnpq_ids) != len(valid_ids):
    raise ValidationError("存在无效的知识领域选择")

这个方案完全适配你的CNPQ模型,支持任意层级的嵌套,400+数据的渲染和交互也不会有明显卡顿(因为用了批量HTML生成减少DOM操作)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:29:22