zoukankan      html  css  js  c++  java
  • sqlalchemy tree 树形分类 无限极分类的管理。预排序树,左右值树。sqlalchemy-mptt

    简介:

    无限极分类是一种比较常见的数据格式,生成组织结构,生成商品分类信息,权限管理当中的细节权限设置,都离不开无限极分类的管理。

    常见的有链表式,即有一个Pid指向上级的ID,以此来设置结构。写的时候简单,用的时候效果一班,比如说,同一级没有办法手动重新排序,查询所有子孙的时候不方便。

    所以有了预排序树,即左右值树形管理。

    优点还是挺多的。

    可以快速确定关系,最短路径,同级排序,查找所有子孙(最好的地方)

    一:主要包

    1. sqlalchemy_mptt

      1. https://github.com/uralbash/sqlalchemy_mptt 
      2. pip install sqlalchemy_mptt 
    2. sqlalchemy-orm-tree

      1. https://github.com/monetizeio/sqlalchemy-orm-tree/
      2. pip install sqlalchemy-orm-tree
      3. 最后更新2015年8月27日,还没有完整的示例代码,仅有的几行,还………………
      4. 放弃
    3. ztree

      1. http://www.treejs.cn/
      2. 这个是前台JS显示用的,希望我能整合到flask-admin中去。

    二:sqlalchemy_mptt

    1.快速指南

    源码:

    # !/usr/bin/python3
    # -*- coding: utf-8 -*-
    # @Time    : 2018-09-12 16:32
    # @Author  : Jackadam
    # @Email   :jackadam@sina.com
    # @File    : mptt.py
    # @Software: PyCharm
    from sqlalchemy import create_engine
    from sqlalchemy.orm import scoped_session, sessionmaker
    from sqlalchemy_mptt import mptt_sessionmaker
    from sqlalchemy import Column, Integer, String
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy_mptt.mixins import BaseNestedSets
    
    Base = declarative_base()
    
    
    class Tree(Base, BaseNestedSets):
        __tablename__ = "tree"
        id = Column(Integer, primary_key=True)
        name = Column(String(8))
    
        def __repr__(self):
            return "<Node (%s)>" % self.id
    
    
    engine = create_engine('sqlite:///mptt.db', echo=False)
    mptt_ession = mptt_sessionmaker(sessionmaker(bind=engine))
    db_session = scoped_session(sessionmaker(autocommit=False,
                                             autoflush=False,
                                             bind=engine))
    
    
    
    def print_tree(group_name, tab=1):
        """
        :param str group_name:要查找的树的根的名称
        :param int tab: 格式化用的-数量
        """
        group = db_session.query(Tree).filter_by(name=group_name).one_or_none()  # type: TreeGroup
        if not group:
            return
        # group found - print name and find children
        print('- ' * tab + group.name)
        for child_group in group.children:  # type: TreeGroup
            # new tabulation value for child record
            print_tree(child_group.name, tab * 2)
    
    if __name__ == '__main__':
        # Base.metadata.create_all(bind=engine)
        # nodes=[]
        # node=Tree(name='中国')
        # nodes.append(node)
        # db_session.add_all(nodes)
        # db_session.commit()
        # nodes = []
        # ref_id=db_session.query(Tree.id).filter_by(name='中国').first()[0]
        # print(ref_id)
        # new_name=['河南','河北','山东','山西','陕西']
        # for i in new_name:
        #     print(i)
        #     node=Tree(name=i,parent_id=ref_id)
        #     nodes.append(node)
        # db_session.add_all(nodes)
        # db_session.commit()
        # nodes = []
        # ref_id = db_session.query(Tree.id).filter_by(name='河南').first()[0]
        # print(ref_id)
        # new_name = ['郑州', '洛阳', '开封', '新乡', '新郑']
        # for i in new_name:
        #     print(i)
        #     node = Tree(name=i, parent_id=ref_id)
        #     nodes.append(node)
        # db_session.add_all(nodes)
        # db_session.commit()
        print_tree('中国')
        print_tree('河南')

    2.结果样式:

    print_tree('中国')

    - 中国
    - - 河南
    - - - - 郑州
    - - - - 洛阳
    - - - - 开封
    - - - - 新乡
    - - - - 新郑
    - - 河北
    - - 山东
    - - 山西
    - - 陕西

    print_tree('河南')


    - 河南
    - - 郑州
    - - 洛阳
    - - 开封
    - - 新乡
    - - 新郑

    3.优势

    我加入的顺序可不是这个顺序,但是数据结构,和sqlalchemy_mptt帮我们处理了层级数据结构,就显示为树形结构了。

    再试试下面的代码,可以列出所有的数。

    def print_all_tree(tab=1):
        """
        :param int tab: 格式化用的-数量
        """
        group = db_session.query(Tree).filter_by(parent_id=None).all()  # type: TreeGroup
        print(group)
        if not group:
            return
        # group found - print name and find children
        for i in group:
            print('- ' * tab + i.name)
            for child_group in i.children:  # type: TreeGroup
                # new tabulation value for child record
                print_tree(child_group.name, tab * 2)    
        # nodes=[]
        # new_name = ['美国', '英国', '法国', '英国', '德国']
        # for i in new_name:
        #     print(i)
        #     node = Tree(name=i)
        #     nodes.append(node)
        # db_session.add_all(nodes)
        # db_session.commit()
        # ref_id = db_session.query(Tree.id).filter_by(name='美国').first()[0]
        # nodes = []
        # new_name = ['亚拉巴马', '阿拉斯加', '亚利桑那', '阿肯色', '加利福尼亚']
        # for i in new_name:
        #     print(i)
        #     node = Tree(name=i,parent_id=ref_id)
        #     nodes.append(node)
        # db_session.add_all(nodes)
        # db_session.commit()
        print_all_tree(1)

    三:数据结构分析

    数据库结构

    数据库里面的内容为

    映射定义结构

    分析:

    我们只定义了主键id,name名字。但是数据库里面多了几列:lft,rgt,level,tree_id,parent_id

    这些结构就是左右值树用的东西,参照最上面的图,每个名字都有左值和右值,可以很方便的查到结构。

    比如说:

    找到所有的子孙:查找Food的子孙,Food作为参考点,左值为1,右值为18,所有的子孙,就是数据库中左值大于1,小于18的。

            查找Fruit的子孙,Fruit作为参考点,左值为2,右值为11,所有的子孙,就是数据库中左值大于2,小于11的。

    找到所有的子节点(不包括孙节点):查找Food的子节点,Food作为参考点,level=1,tree_id=1。

            那么所有的子节点为  tree_id=1,level=1+1   层级为2的。

    查找最短路径:一般用在导航中,也有用在组合显示上,因为需要知道上一级,上N级的路径结构:

            查找Banana的上级路径,Banana作为参考点,左值为8,右值为9,那么路径就是数据库中左值小于8,并且右值大于9的。排序按左值或右值的升序降序,就随便你了。

            结果是    Food--Fruit--Yellow--Banana


    四:算法

    1.简介:

    关于预排序树的算法,有很多,我也写不好,所以我用了sqlalchemy_mptt,还是大概介绍一下吧。

    2.增加

    按sqlalchemy_mptt的用法,

    如果没有parent_id,那么就创建为一个新树的根节点,parent_id是空的,level是1,tree_id根据数据库的情况顺序向上加。

    如果有提供parent_id,那么久创建为parent的子节点,parent_id是提供的,level是parent的level+1,tree_id和parent一致。

    同时要更新受影响的其他节点。

    左值处理一遍。

    大于parent_id右值的所有左值,+2

    右值处理一遍。

    大于等于parent_id右值的所有右值,+2

    3.删除

    和增加差不多,删除一个节点以后,也要更新受影响的左值和右值。

    4.移动

    这个其实就是删除一个老节点,再创建一个新节点。

    5.完全不用担心

    因为这些功能,sqlalchem_mptt全都实现了,根本不需要你操心。具体怎么用,我还在学习中。

    下面的东西,随着我的学习,逐步更新。

    五:ztree

    1.ztree基础显示(带编辑功能)

    下面的代码,保存为html,直接就可以了。

    <!DOCTYPE html>
    <HTML>
    <HEAD>
        <TITLE> ZTREE DEMO - beforeEditName / beforeRemove / onRemove / beforeRename / onRename</TITLE>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" href="../../../css/demo.css" type="text/css">
        <link rel="stylesheet" href="../../../css/zTreeStyle/zTreeStyle.css" type="text/css">
        <script type="text/javascript" src="../../../js/jquery-1.4.4.min.js"></script>
        <script type="text/javascript" src="../../../js/jquery.ztree.core.js"></script>
        <script type="text/javascript" src="../../../js/jquery.ztree.excheck.js"></script>
        <script type="text/javascript" src="../../../js/jquery.ztree.exedit.js"></script>
        <SCRIPT type="text/javascript">
            <!--
            var setting = {
                view: {
                    addHoverDom: addHoverDom,
                    removeHoverDom: removeHoverDom,
                    selectedMulti: false
                },
                edit: {
                    enable: true,
                    editNameSelectAll: true,
                    showRemoveBtn: showRemoveBtn,
                    showRenameBtn: showRenameBtn
                },
                data: {
                    simpleData: {
                        enable: true
                    }
                },
                callback: {
                    beforeDrag: beforeDrag,
                    beforeEditName: beforeEditName,
                    beforeRemove: beforeRemove,
                    beforeRename: beforeRename,
                    onRemove: onRemove,
                    onRename: onRename
                }
            };
    
            var zNodes = [
                {id: 1, pId: 0, name: "父节点 1", open: true},
                {id: 11, pId: 1, name: "叶子节点 1-1"},
                {id: 12, pId: 1, name: "叶子节点 1-2"},
                {id: 13, pId: 1, name: "叶子节点 1-3"},
                {id: 2, pId: 0, name: "父节点 2", open: true},
                {id: 21, pId: 2, name: "叶子节点 2-1"},
                {id: 22, pId: 2, name: "叶子节点 2-2"},
                {id: 23, pId: 2, name: "叶子节点 2-3"},
                {id: 3, pId: 0, name: "父节点 3", open: true},
                {id: 31, pId: 3, name: "叶子节点 3-1"},
                {id: 32, pId: 3, name: "叶子节点 3-2"},
                {id: 33, pId: 3, name: "叶子节点 3-3"}
            ];
            var log, className = "dark";
    
            function beforeDrag(treeId, treeNodes) {
                return false;
            }
    
            function beforeEditName(treeId, treeNode) {
                className = (className === "dark" ? "" : "dark");
                showLog("[ " + getTime() + " beforeEditName ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.selectNode(treeNode);
                setTimeout(function () {
                    if (confirm("进入节点 -- " + treeNode.name + " 的编辑状态吗?")) {
                        setTimeout(function () {
                            zTree.editName(treeNode);
                        }, 0);
                    }
                }, 0);
                return false;
            }
    
            function beforeRemove(treeId, treeNode) {
                className = (className === "dark" ? "" : "dark");
                showLog("[ " + getTime() + " beforeRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.selectNode(treeNode);
                return confirm("确认删除 节点 -- " + treeNode.name + " 吗?");
            }
    
            function onRemove(e, treeId, treeNode) {
                showLog("[ " + getTime() + " onRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            }
    
            function beforeRename(treeId, treeNode, newName, isCancel) {
                className = (className === "dark" ? "" : "dark");
                showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
                if (newName.length == 0) {
                    setTimeout(function () {
                        var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                        zTree.cancelEditName();
                        alert("节点名称不能为空.");
                    }, 0);
                    return false;
                }
                return true;
            }
    
            function onRename(e, treeId, treeNode, isCancel) {
                showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
            }
    
            function showRemoveBtn(treeId, treeNode) {
                return !treeNode.isFirstNode;
            }
    
            function showRenameBtn(treeId, treeNode) {
                return !treeNode.isLastNode;
            }
    
            function showLog(str) {
                if (!log) log = $("#log");
                log.append("<li class='" + className + "'>" + str + "</li>");
                if (log.children("li").length > 8) {
                    log.get(0).removeChild(log.children("li")[0]);
                }
            }
    
            function getTime() {
                var now = new Date(),
                    h = now.getHours(),
                    m = now.getMinutes(),
                    s = now.getSeconds(),
                    ms = now.getMilliseconds();
                return (h + ":" + m + ":" + s + " " + ms);
            }
    
            var newCount = 1;
    
            function addHoverDom(treeId, treeNode) {
                var sObj = $("#" + treeNode.tId + "_span");
                if (treeNode.editNameFlag || $("#addBtn_" + treeNode.tId).length > 0) return;
                var addStr = "<span class='button add' id='addBtn_" + treeNode.tId
                    + "' title='add node' onfocus='this.blur();'></span>";
                sObj.after(addStr);
                var btn = $("#addBtn_" + treeNode.tId);
                if (btn) btn.bind("click", function () {
                    var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                    zTree.addNodes(treeNode, {id: (100 + newCount), pId: treeNode.id, name: "new node" + (newCount++)});
                    return false;
                });
            };
    
            function removeHoverDom(treeId, treeNode) {
                $("#addBtn_" + treeNode.tId).unbind().remove();
            };
    
            function selectAll() {
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked");
            }
    
            $(document).ready(function () {
                $.fn.zTree.init($("#treeDemo"), setting, zNodes);
                $("#selectAll").bind("click", selectAll);
            });
            //-->
        </SCRIPT>
        <style type="text/css">
            .ztree li span.button.add {
                margin-left: 2px;
                margin-right: -1px;
                background-position: -144px 0;
                vertical-align: top;
                *vertical-align: middle
            }
        </style>
    </HEAD>
    
    <BODY>
    
    <ul id="treeDemo" class="ztree"></ul>
    
    </BODY>
    </HTML>
    View Code

    其中head引入了jquery,ztree的一些JS文件。

    然后var setting,设置了ztree的一些参数

    var zNodes,设置了一个简单结构的数结构数据。

    一些function,定义了鼠标移上去,移出去,点击……的一些事件。

    <style type="text/css">
    .ztree li span.button.add {


    这个样式,定义了添加按钮的图标。

    在body中

    直接  <ul id="treeDemo" class="ztree"></ul>就可以显示了

    2.在flask中显示

    把整页作为模板,动态传入zNodes,就可以直接显示了。

    3.在flask-admin中显示

    因为我这个懒蛋,准备把flask-admin当前台用。数据展示也挺方便。

    先写一个页面,单独只显示ztree

    视图函数:

    用extra_css引入自定义css(ztree的)

    用extra_js引入自定义js(ztree的)

    用@expose('/')定义路由URL(flask-admin的)

    tree_info,是读取数据库拿到的树的基本信息。

    class User_Groups_ModelView(ModelView):
        extra_css=[
            "../static/zTree_v3/css/demo.css",
            "../static/zTree_v3/css/zTreeStyle/zTreeStyle.css"
        ]
        extra_js = [
            "../static/zTree_v3/js/jquery.ztree.all.js",
            "../static/zTree_v3/js/jquery.ztree.core.js",
            "../static/zTree_v3/js/jquery.ztree.excheck.js",
            "../static/zTree_v3/js/jquery.ztree.exedit.js",
            "../static/zTree_v3/js/jquery.ztree.exhide.js",
            # "../static/zTree_v3/js/jquery-1.4.4.min.js",
        ]
        page_size = 20  # the number of entries to display on the list view
        can_create = False
        # can_edit = True
        # can_delete = False
        # can_view_details = True
        @expose('/')
        def index(self):
            tree_info=get_simple_json()
            return self.render('users/list.html',tree_info=tree_info)
    View Code

    get_simple_json就是按ztree的简单结构,生成树的基本数据。带到模板去渲染

    def get_simple_json():
        groups = db_session.query(User_Groups).all()
        # print(len(groups))
        result = []
        for i in groups:
            if i.parent_id == None:
                i.parent_id = 0
            _dict = {
                'id': i.id,
                'pId': i.parent_id,
                'name': i.groups_name,
            }
            result.append(_dict)
        # print(result)
        return result
    View Code

    模板文件

    前面引入了flask-admin的基本模板

    block body(显示主体)

    block model_menu_bar(菜单导航,就是新建--删除--导出那一行)

    <script></script>中间,写了ztree的一些方法,用来动态的显示出添加,编辑,删除的小按钮。

    <ul id="treeDemo" class="ztree"></ul>

    这个就是显示了。

    block tail

    ztree的渲染,我把它写在了尾部

    {% extends 'admin/master.html' %}
    {% import 'admin/lib.html' as lib with context %}
    {% import 'admin/actions.html' as actionslib with context %}
    
    {% block body %}
        {% block model_menu_bar %}
            <ul class="nav nav-tabs actions-nav">
                <li class="active">
                    <a href="javascript:void(0)">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
                </li>
    
                {% if admin_view.can_create %}
                    <li>
                        {%- if admin_view.create_modal -%}
                            {{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), title=_gettext('Create New Record'), content=_gettext('Create')) }}
                        {% else %}
                            <a href="{{ get_url('.create_view', url=return_url) }}"
                               title="{{ _gettext('Create New Record') }}">{{ _gettext('Create') }}</a>
                        {%- endif -%}
                    </li>
                {% endif %}
    
                {% if admin_view.can_export %}
                    {{ model_layout.export_options() }}
                {% endif %}
    
                {% block model_menu_bar_before_filters %}{% endblock %}
    
                {% if filters %}
                    <li class="dropdown">
                        {{ model_layout.filter_options() }}
                    </li>
                {% endif %}
    
                {% if can_set_page_size %}
                    <li class="dropdown">
                        {{ model_layout.page_size_form(page_size_url) }}
                    </li>
                {% endif %}
    
                {% if actions %}
                    <li class="dropdown">
                        {{ actionlib.dropdown(actions) }}
                    </li>
                {% endif %}
    
                {% if search_supported %}
                    <li>
                        {{ model_layout.search_form() }}
                    </li>
                {% endif %}
                {% block model_menu_bar_after_filters %}{% endblock %}
            </ul>
        {% endblock %}
        <SCRIPT type="text/javascript">
            <!--
    
            var setting = {
                view: {
                    addHoverDom: addHoverDom,
                    removeHoverDom: removeHoverDom,
                    selectedMulti: false
                },
                edit: {
                    enable: true,
                    editNameSelectAll: true,
                    showRemoveBtn: showRemoveBtn,
                    showRenameBtn: showRenameBtn
                },
                data: {
                    simpleData: {
                        enable: true
                    }
                },
                callback: {
                    beforeDrag: beforeDrag,
                    beforeEditName: beforeEditName,
                    beforeRemove: beforeRemove,
                    beforeRename: beforeRename,
                    onRemove: onRemove,
                    onRename: onRename
                }
            };
    
            var zNodes = {{ tree_info|safe }};
            var log, className = "dark";
    
            function beforeDrag(treeId, treeNodes) {
                alert('这里是增加?')
                return false;
            }
    
            function beforeEditName(treeId, treeNode) {
                className = (className === "dark" ? "" : "dark");
                showLog("[ " + getTime() + " beforeEditName ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.selectNode(treeNode);
                setTimeout(function () {
                    if (confirm("进入节点 -- " + treeNode.name + " 的编辑状态吗?")) {
                        setTimeout(function () {
                            zTree.editName(treeNode);
                        }, 0);
                    }
                }, 0);
                return false;
            }
    
            function beforeRemove(treeId, treeNode) {
                className = (className === "dark" ? "" : "dark");
                showLog("[ " + getTime() + " beforeRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.selectNode(treeNode);
                return confirm("确认删除 节点 -- " + treeNode.name + " 吗?");
            }
    
            function onRemove(e, treeId, treeNode) {
                showLog("[ " + getTime() + " onRemove ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name);
            }
    
            function beforeRename(treeId, treeNode, newName, isCancel) {
                className = (className === "dark" ? "" : "dark");
                showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
                if (newName.length == 0) {
                    setTimeout(function () {
                        var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                        zTree.cancelEditName();
                        alert("节点名称不能为空.");
                    }, 0);
                    return false;
                }
                return true;
            }
    
            function onRename(e, treeId, treeNode, isCancel) {
                showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ]&nbsp;&nbsp;&nbsp;&nbsp; " + treeNode.name + (isCancel ? "</span>" : ""));
            }
    
            function showRemoveBtn(treeId, treeNode) {
                return !treeNode.isFirstNode;
            }
    
            function showRenameBtn(treeId, treeNode) {
                return !treeNode.isLastNode;
            }
    
            function showLog(str) {
                if (!log) log = $("#log");
                log.append("<li class='" + className + "'>" + str + "</li>");
                if (log.children("li").length > 8) {
                    log.get(0).removeChild(log.children("li")[0]);
                }
            }
    
            function getTime() {
                var now = new Date(),
                    h = now.getHours(),
                    m = now.getMinutes(),
                    s = now.getSeconds(),
                    ms = now.getMilliseconds();
                return (h + ":" + m + ":" + s + " " + ms);
            }
    
            var newCount = 1;
    
            function addHoverDom(treeId, treeNode) {
                var sObj = $("#" + treeNode.tId + "_span");
                if (treeNode.editNameFlag || $("#addBtn_"+treeNode.tId).length>0) return;
                var addStr = "<span class='button add' id='addBtn_" + treeNode.tId
                    + "' title='增加节点' onfocus='this.blur();'></span>";
                sObj.after(addStr);
                var btn = $("#addBtn_"+treeNode.tId);
                if (btn) btn.bind("click", function(){
                    var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                    zTree.addNodes(treeNode, {id:(100 + newCount), pId:treeNode.id, name:"新节点" + (newCount++)});
    
                    return false;
                });
            };
            function removeHoverDom(treeId, treeNode) {
                $("#addBtn_"+treeNode.tId).unbind().remove();
            };
    
            function selectAll() {
                var zTree = $.fn.zTree.getZTreeObj("treeDemo");
                zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked");
            }
    
            //-->
        </SCRIPT>
    
        <ul id="treeDemo" class="ztree"></ul>
    
    {% endblock %}
    {% block tail %}
        <SCRIPT>
            $(document).ready(function () {
                $.fn.zTree.init($("#treeDemo"), setting, zNodes);
                $("#selectAll").bind("click", selectAll);
            });
        </SCRIPT>
    {% endblock %}
    View Code

    有一个小问题

    ztree的编辑功能当中没有默认增加。所以增加这个功能写在了,默认样式表中也没有这个样式。

    基础示例当中有定义这个CSS。

    .ztree li span.button.add {
        margin-left: 2px;
        margin-right: -1px;
        background-position: -144px 0;
        vertical-align: top;
        *vertical-align: middle
    }

    我就随便写进了demo.css,这样显示的就正常了。

    4.定制flask-admin的显示

    待编辑功能写完,再来研究这个问题。

    六:ztree的编辑功能

    1.简介

    默认的flask-admin功能是不能管理这个树形结构的。它管理的是简单结构,就是 id pid name 这种格式。

    或许可以尝试由简单格式,自动创建为左右值树格式

    2.

    3.


    七:


    八:


    九:


    十:

  • 相关阅读:
    vs2015 停 在 update kb2999226 一直不动
    修复vs2012出现 “无法找到包源”的错误
    forward 和redirect的区别
    软件测试分类
    centos7安装HTTPS协议
    php抓取网页特定div区块及图片,从简单入手
    nginx报 File not found 错误
    关于端口
    调试技巧:让断点停在for循环中的 i 为某个值得时候
    an AC a day keeps the WA away ~
  • 原文地址:https://www.cnblogs.com/jackadam/p/9620144.html
Copyright © 2011-2022 走看看