使用Django、RestFul API和Bootstrap实现可折叠的多级菜单功能

2024-07-06 22:58:14 浏览数 (2)

本文将详细介绍如何使用Django、RestFul API和Bootstrap实现一个可折叠的多级菜单功能,并在菜单末端节点上添加复选框,点击按钮时获取这些节点的ID并查询其内容。这篇教程将涵盖后端的API设计、前端的实现以及如何整合两者,以实现所需的功能。

一、环境准备

在开始之前,请确保已经安装并配置好以下环境:

  • Python 3.x
  • Django
  • Django Rest Framework
  • Bootstrap 4.x

二、后端实现

首先,我们需要在Django中创建一个简单的菜单模型,用于存储菜单的层级结构和内容信息。然后,我们将创建一个API端点来返回菜单数据,并处理根据多个ID查询内容的请求。

1. 创建Django项目和应用

如果还没有创建Django项目,可以使用以下命令创建:

代码语言:bash复制
django-admin startproject myproject
cd myproject
python manage.py startapp myapp

settings.py中,添加新创建的应用到INSTALLED_APPS中:

代码语言:python代码运行次数:0复制
INSTALLED_APPS = [
    ...
    'myapp',
    'rest_framework',
]

2. 创建菜单模型

myapp/models.py中定义菜单模型:

代码语言:python代码运行次数:0复制
from django.db import models

class Menu(models.Model):
    title = models.CharField(max_length=100)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
    content = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.title

这个模型包含了title(菜单项的标题)、parent(父菜单项)和content(菜单项的内容)。

3. 创建序列化器

为了将菜单模型序列化为JSON格式,我们需要创建一个序列化器。在myapp/serializers.py中定义:

代码语言:python代码运行次数:0复制
from rest_framework import serializers
from .models import Menu

class MenuSerializer(serializers.ModelSerializer):
    class Meta:
        model = Menu
        fields = ('id', 'title', 'parent', 'content')

4. 创建视图和路由

myapp/views.py中创建视图,处理菜单列表和根据ID查询内容的请求:

代码语言:python代码运行次数:0复制
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.decorators import api_view
from .models import Menu
from .serializers import MenuSerializer

class MenuListCreate(generics.ListCreateAPIView):
    queryset = Menu.objects.all()
    serializer_class = MenuSerializer

@api_view(['GET'])
def get_contents_by_ids(request):
    ids = request.query_params.get('ids')
    if ids:
        ids_list = ids.split(',')
        menus = Menu.objects.filter(id__in=ids_list)
        serializer = MenuSerializer(menus, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
    return Response({"error": "No IDs provided"}, status=status.HTTP_400_BAD_REQUEST)

myapp/urls.py中定义路由:

代码语言:python代码运行次数:0复制
from django.urls import path
from .views import MenuListCreate, get_contents_by_ids

urlpatterns = [
    path('menus/', MenuListCreate.as_view(), name='menu-list-create'),
    path('menus/contents/', get_contents_by_ids, name='menu-contents-by-ids'),
]

这样,我们就完成了后端的API设计。接下来,我们将实现前端部分。

三、前端实现

前端部分将使用Bootstrap和jQuery来创建可折叠的多级菜单,并在末端节点添加复选框,点击按钮时获取这些节点的ID并查询其内容。

1. 引入必要的CSS和JavaScript文件

在HTML文件中,引入Bootstrap和jQuery:

代码语言:html复制
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>bstreeview</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-Vkoo8x4CGsO3 Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"
          integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <link href="css/bstreeview.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="content">
            <div class="row">
                <div class="col-md-6 pt-5">
                    <div id="tree"></div>
                    <button id="getSelected" class="btn btn-primary mt-3">获取选中的节点ID</button>
                </div>
                <div class="col-md-6 pt-5">
                    <div id="content"></div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"
            integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
            integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
            crossorigin="anonymous"></script>
    <script src="js/bstreeview.js"></script>
</body>
</html>

2. 加载菜单数据并初始化树视图

在页面加载完成后,通过Ajax请求从后端获取菜单数据,并初始化树视图:

代码语言:javascript复制
$(function () {
    function transformMenuData(menuData) {
        let tree = [];
        // 创建所有父节点
        menuData.forEach(item => {
            if (!item.parent) {
                tree.push({
                    id: item.id,
                    text: item.title,
                    icon: "",
                    nodes: []
                });
            }
        });

        // 将子节点添加到父节点中
        menuData.forEach(item => {
            if (item.parent) {
                addNodeToParent(tree, item);
            }
        });

        // 移除叶子节点的空nodes数组
        removeEmptyNodes(tree);

        return tree;
    }

    function addNodeToParent(tree, item) {
        for (let i = 0; i < tree.length; i  ) {
            if (tree[i].id === item.parent) {
                if (!tree[i].nodes) {
                    tree[i].nodes = [];
                }
                tree[i].nodes.push({
                    id: item.id,
                    text: item.title,
                    icon: "",
                    nodes: []
                });
                return;
            }
            if (tree[i].nodes && tree[i].nodes.length > 0) {
                addNodeToParent(tree[i].nodes, item);
            }
        }
    }

    function removeEmptyNodes(tree) {
        tree.forEach(node => {
            if (node.nodes && node.nodes.length === 0) {
                node.icon = "bx bx-book"; // 叶子节点添加图标
                node.text = `<input type="checkbox" class="menu-checkbox" data-id="${node.id}"> ${node.text}`;
                delete node.nodes;
            } else if (node.nodes) {
                removeEmptyNodes(node.nodes);
            }
        });
    }

    $.get('/api/menus/', function(data) {
        let transformedData = transformMenuData(data);
        console.info(transformedData);
        $('#tree').bstreeview({
            data: transformedData,
            expandIcon: 'bx bx-minus',
            collapseIcon: 'bx bx-plus',
            indent: 1.25,
            parentsMarginLeft: '1.25rem',
            openNodeLinkOnNewTab: true
        });
    });

    $('#getSelected').on('click', function() {
        let selectedIds = [];
        $('.menu-checkbox:checked').each(function() {
            selectedIds.push($(this).data('id'));
        });

        if (selectedIds.length > 0) {
            $.get(`/api/menus/contents/?ids=${selectedIds.join(',')}`, function(data) {
                let content = "";
                data.forEach((item, index) => {
                    content  = `問題${index   1}n${item.content}nn`;
                });
                $('#content').text(content.trim());
            });
        } else {
            alert('请选择至少一个节点');
        }
    });
});

3. 添加复选框和按钮功能

在叶子节点的文本中添加复选框,并在按钮点击时获取选中的节点ID,发送请求到后端获取内容数据,并在页面上显示。

前端代码

在前面的代码基础上,我们已经在叶子节点上添加了复选框,同时实现了按钮点击时获取选中的节点ID。下面是完整的前端代码:

代码语言:html复制
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>bstreeview</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-Vkoo8x4CGsO3 Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"
          integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
    <link href="css/bstreeview.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="content">
            <div class="row">
                <div class="col-md-6 pt-5">
                    <div id="tree"></div>
                    <button id="getSelected" class="btn btn-primary mt-3">获取选中的节点ID</button>
                </div>
                <div class="col-md-6 pt-5">
                    <div id="content"></div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"
            integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
            integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
            crossorigin="anonymous"></script>
    <script src="js/bstreeview.js"></script>
    <script>
        $(function () {
            function transformMenuData(menuData) {
                let tree = [];
                // 创建所有父节点
                menuData.forEach(item => {
                    if (!item.parent) {
                        tree.push({
                            id: item.id,
                            text: item.title,
                            icon: "",
                            nodes: []
                        });
                    }
                });

                // 将子节点添加到父节点中
                menuData.forEach(item => {
                    if (item.parent) {
                        addNodeToParent(tree, item);
                    }
                });

                // 移除叶子节点的空nodes数组
                removeEmptyNodes(tree);

                return tree;
            }

            function addNodeToParent(tree, item) {
                for (let i = 0; i < tree.length; i  ) {
                    if (tree[i].id === item.parent) {
                        if (!tree[i].nodes) {
                            tree[i].nodes = [];
                        }
                        tree[i].nodes.push({
                            id: item.id,
                            text: item.title,
                            icon: "",
                            nodes: []
                        });
                        return;
                    }
                    if (tree[i].nodes && tree[i].nodes.length > 0) {
                        addNodeToParent(tree[i].nodes, item);
                    }
                }
            }

            function removeEmptyNodes(tree) {
                tree.forEach(node => {
                    if (node.nodes && node.nodes.length === 0) {
                        node.icon = "bx bx-book"; // 叶子节点添加图标
                        node.text = `<input type="checkbox" class="menu-checkbox" data-id="${node.id}"> ${node.text}`;
                        delete node.nodes;
                    } else if (node.nodes) {
                        removeEmptyNodes(node.nodes);
                    }
                });
            }

            $.get('/api/menus/', function(data) {
                let transformedData = transformMenuData(data);
                console.info(transformedData);
                $('#tree').bstreeview({
                    data: transformedData,
                    expandIcon: 'bx bx-minus',
                    collapseIcon: 'bx bx-plus',
                    indent: 1.25,
                    parentsMarginLeft: '1.25rem',
                    openNodeLinkOnNewTab: true
                });
            });

            $('#getSelected').on('click', function() {
                let selectedIds = [];
                $('.menu-checkbox:checked').each(function() {
                    selectedIds.push($(this).data('id'));
                });

                if (selectedIds.length > 0) {
                    $.get(`/api/menus/contents/?ids=${selectedIds.join(',')}`, function(data) {
                        let content = "";
                        data.forEach((item, index) => {
                            content  = `問題${index   1}n${item.content}nn`;
                        });
                        $('#content').text(content.trim());
                    });
                } else {
                    alert('请选择至少一个节点');
                }
            });
        });
    </script>
</body>
</html>

四、总结

通过本教程,我们实现了一个使用Django、RestFul API和Bootstrap的多级菜单功能,并且在菜单末端节点上添加了复选框,点击按钮时可以获取选中的节点ID,并查询其内容。

关键步骤总结:

  1. 后端实现
    • 创建Django项目和应用。
    • 定义菜单模型,并创建序列化器。
    • 创建视图和路由,处理菜单数据和根据ID查询内容的请求。
  2. 前端实现
    • 引入必要的CSS和JavaScript文件。
    • 通过Ajax请求从后端获取菜单数据,并初始化树视图。
    • 在叶子节点的文本中添加复选框。
    • 实现按钮点击事件,获取选中的节点ID,并查询内容。

后续扩展:

在本教程的基础上,你可以进一步扩展和优化以下功能:

  • 为菜单项添加更多自定义图标和样式。
  • 实现更多复杂的查询条件和过滤功能。
  • 优化前端界面的用户体验。

通过这些扩展,你可以根据具体需求来调整和优化你的项目,构建一个功能更强大、用户体验更佳的多级菜单系统。


我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞