尚筹网-7.Menu树形结构的维护

1 建模

1.1 创建数据库

CREATE TABLE t_menu (
    id INT (11) NOT NULL auto_increment,
    pid INT (11),
    `name` VARCHAR (200),
    url VARCHAR (200),
    icon VARCHAR (200),
    PRIMARY KEY (id)
);
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('1',NULL,'系统权限菜单','glyphicon glyphicon-th-list',NULL);
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('2','1','控制面板','glyphicon glyphicon-dashboard','main.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('3','1','权限管理','glyphicon glyphicon glyphicon-tasks',NULL);
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('4','3','用户维护','glyphicon glyphicon-user','user/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('5','3','角色维护','glyphicon glyphicon-king','role/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('6','3','许可维护','glyphicon glyphicon-lock','permission/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('7','1','业务审核','glyphicon glyphicon-ok',NULL);
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('8','7','实名认证审核','glyphicon glyphicon-check','auth_cert/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('9','7','广告审核','glyphicon glyphicon-check','auth_adv/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('10','7','项目审核','glyphicon glyphicon-check','auth_project/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('11','1','业务管理','glyphicon glyphicon-th-large',NULL);
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('12','11','资质维护','glyphicon glyphicon-picture','cert/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('13','11','分类管理','glyphicon glyphicon-equalizer','certtype/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('14','11','流程管理','glyphicon glyphicon-random','process/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('15','11','广告管理','glyphicon glyphicon-hdd','advert/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('16','11','消息模板','glyphicon glyphicon-comment','message/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('17','11','项目分类','glyphicon glyphicon-list','projectType/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('18','11','项目标签','glyphicon glyphicon-tags','tag/index.htm');
insert into `t_menu` (`id`, `pid`, `name`, `icon`, `url`) values('19','1','参数管理','glyphicon glyphicon-list-alt','param/index.htm');

1.2 形成树形结构的关键依据

在数据库表中通过pid字段,让当前记录指向它的父节点的记录,从而形成树形结构。

一条记录关联了同一个表中的其他记录,称之为自关联。

1.3 逆向工程

<table tableName="t_menu" domainObjectName="Menu" />

生成的Menu实体类需要做一些修改

// 对应数据库表主键
private Integer id;
// 对应父节点id(如果pid为null,则说明当前节点是根节点)
private Integer pid;
// 节点名称
private String name;
// 节点对应的URL地址
private String url;
// 节点的图标
private String icon;
// 当前节点的子节点集合,设置默认值是为了避免组装节点时空指针异常
private List<Menu> children = new ArrayList<>();
// 控制节点展开还是折叠,设置为true是让整个树形菜单默认展开
private Boolean open = true;

2 在页面显示树形结构

2.1 后端操作

把树形结构组装好,具体来说是:给前端返回根节点对象。在根节点中包含子节点,子节点中再包含下一级的子节点。

handler方法中的具体操作:

@RequestMapping("/menu/get/whole/tree")
public ResultEntity<Menu> getWholeTree() {  
    // 1.查询所有的树形节点用于组装
    List<Menu> menuList = menuService.getAll(); 
    // 2.将List<Menu>转换为Map<Menu的id,Menu>
    Map<Integer,Menu> menuMap = new HashMap<>();    
    for (Menu menu : menuList) {
        Integer id = menu.getId();
        menuMap.put(id, menu);
    }   
    // 3.声明变量用于存储根节点对象
    Menu rootNode = null;   
    // 4.遍历List<Menu>
    for (Menu menu : menuList) {        
        // 5.获取当前Menu对象的pid属性
        Integer pid = menu.getPid();        
        // 6.判断pid是否为null
        if(pid == null) {           
            // 7.如果pid为null,说明当前节点是根节点,所以赋值
            rootNode = menu;            
            // 8.根节点没有父节点,所以不必找父节点组装,本次for循环停止执行,继续执行下一次循环
            continue ;
        }       
        // 9.既然pid不为null,那么我们根据这个pid查找当前节点的父节点。
        Menu father = menuMap.get(pid); 
        // 10.组装:将menu添加到maybeFather的子节点集合中
        father.getChildren().add(menu);
    }   
    return ResultEntity.successWithData(rootNode);
}

service方法:

@Override
public List<Menu> getAll() {
    return menuMapper.selectByExample(new MenuExample());
}

2.2 前端操作

2.2.1 创建menu-page.jsp

主体内容如下:

<div class="panel panel-default">
    <div class="panel-heading">
        <i class="glyphicon glyphicon-th-list"></i> 权限菜单列表
        <div style="float: right; cursor: pointer;" data-toggle="modal"
             data-target="#myModal">
            <i class="glyphicon glyphicon-question-sign"></i>
        </div>
    </div>
    <div class="panel-body">
        <ul id="treeDemo" class="ztree"></ul>
    </div>
</div>

配置view-controller跳转到menp-page.jsp

<mvc:view-controller path="/menu/to/page.html" view-name="menu-page"/>

sidebar中的超链接也做相应调整

<li style="height: 30px;"><a href="menu/to/page.html"><i class="glyphicon glyphicon-lock"></i> 菜单维护</a></li>

2.2.2 加入zTree环境

<script type="text/javascript" src="ztree/jquery.ztree.all-3.5.min.js"></script>
<script type="text/javascript" src="script/my-menu.js"></script>

2.2.3 显示树形结构

$(function(){
    // setting对象中包含zTree的设置属性
    var setting = {
        "view": {
            "addDiyDom": showMyIcon
        },
        "data": {
            "key": {
                "url": "notExistsProperty" // 阻止点击节点后跳转
            }
        }
    };  
    // 发送Ajax请求获取zNodes数据
    $.ajax({
        "url":"menu/get/whole/tree.json",
        "type":"post",
        "dataType":"json",
        "success":function(response){           
            var result = response.result;           
            if(result == "SUCCESS") {
                // 用于显示树形结构的节点数据
                var zNodes = response.data;                     
                // 执行树形结构的初始操作
                $.fn.zTree.init($("#treeDemo"), setting, zNodes);
            }           
            if(result == "FAILED") {
                layer.msg("加载菜单数据失败!原因是:"+response.message);
            }
        },
        "error":function(response){
            layer.msg("加载菜单数据失败!原因是:"+response.message);
        }
    });
});

2.2.4 声明函数将图标修改为自定义图标

// 由setting.view.addDiyDom属性引用,负责显示自定义图标
// treeId是<ul id="treeDemo" class="ztree"></ul>的id属性值
// treeNode对应每一个树形节点
function showMyIcon(treeId, treeNode) { 
    // 获取当前节点的id
    var currentNodeId = treeNode.tId;   
    // 获取新的class值用于替换
    var newClass = treeNode.icon;   
    // 在当前节点id之后附加“_ico”得到目标span的id
    var targetSpanId = currentNodeId + "_ico";  
    // 将目标span的旧class移除,添加新class
    $("#"+targetSpanId)
        .removeClass()
        .addClass(newClass)
        .css("background","");
}

附:节点图标修改前后HTML代码变化

<span 
    id="treeDemo_4_ico" 
    title="" 
    treenode_ico="" 
    class="button ico_docu" 
    style="background:url(glyphicon glyphicon-user) 0 0 no-repeat;">
</span>

<span 
    id="treeDemo_2_ico" 
    title="" 
    treenode_ico="" 
    class="glyphicon glyphicon-dashboard" 
    style="background:url(glyphicon glyphicon-dashboard) 0 0 no-repeat;">
</span>

2.2.5 节点结构

<li 节点>
    <span 展开折叠部分图标></span>
    <a 节点内容>
        <span 节点的具体图标></span>
        <span>节点文本内容</span>
    </a>
    <ul 子节点></ul>
</li>

id结构:treeDemo_节点序号_节点具体部件

id="treeDemo_3_ul" id="treeDemo_3_ico"

2.2.6 阻止点击节点后跳转

使用了setting.data.key.url属性。

2.3 封装函数实现生成树形结构操作

function initWholeTree() {
    // setting对象中包含zTree的设置属性
    var setting = {
        "view": {
            "addDiyDom": showMyIcon
        },
        "data": {
            "key": {
                "url": "notExistsProperty" // 阻止点击节点后跳转
            }
        }
    };
    // 发送Ajax请求获取zNodes数据
    $.ajax({
        "url":"menu/get/whole/tree.json",
        "type":"post",
        "dataType":"json",
        "success":function(response){           
            var result = response.result;           
            if(result == "SUCCESS") {
                // 用于显示树形结构的节点数据
                var zNodes = response.data;                     
                // 执行树形结构的初始操作
                $.fn.zTree.init($("#treeDemo"), setting, zNodes);
            }           
            if(result == "FAILED") {
                layer.msg("加载菜单数据失败!原因是:"+response.message);
            }
        },
        "error":function(response){
            layer.msg("加载菜单数据失败!原因是:"+response.message);
        }
    });
}

3 生成按钮组

3.1 生成按钮组规则

3.1.1 根节点(level 0)

  • 增加子节点

3.1.2 level为1的有子节点的节点

  • 增加子节点
  • 修改

3.1.3 level为1的无子节点的节点

  • 增加子节点
  • 删除
  • 修改

3.1.4 level为2的节点

  • 删除
  • 修改

3.2 设计按钮组所在span的id

  • 规则:treeDemo_序号_btnGrp
  • 举例:treeDemo_20_btnGrp

3.3 各个按钮HTML标签

<span id="treeDemo_20_btnGrp">
    <a onclick="showAddModal(this)" 
       id="19" 
       class="btn btn-info dropdown-toggle btn-xs" 
       style="margin-left:10px;padding-top:0px;" 
       title="添加子节点">
        &nbsp;&nbsp;<i class="fa fa-fw fa-plus rbg"></i>
    </a>
    <a onclick="showEditModal(this)" 
       id="19" 
       class="btn btn-info dropdown-toggle btn-xs" 
       style="margin-left:10px;padding-top:0px;" 
       title="编辑节点">
        &nbsp;&nbsp;<i class="fa fa-fw fa-edit rbg"></i>
    </a>
    <a onclick="showConfirmModal(this)" 
       id="19" 
       class="btn btn-info 
              dropdown-toggle btn-xs" 
       style="margin-left:10px;padding-top:0px;" 
       title="删除节点">
        &nbsp;&nbsp;<i class="fa fa-fw fa-times rbg"></i>
    </a>
</span>

3.4 声明函数生成按钮组

// 专门生成按钮组的函数
function generateBtnGrp(treeNode) { 
    // 获取当前节点的id(HTML中li标签的id)
    var treeNodeId = treeNode.tId;  
    // 获取当前节点在数据库中的id值
    // Menu实体类中的属性,都可以通过treeNode以“.属性名”的方式直接访问
    var menuId = treeNode.id;   
    // 组装按钮组所在的span的id
    var btnGrpSpanId = treeNodeId + "_btnGrp";  
    // 生成span标签对应的jQuery对象
    var $span = $("<span id='"+btnGrpSpanId+"'></span>");   
    // 获取当前节点的level值
    var level = treeNode.level; 
    // 声明三种按钮
    var addBtn = "<a onclick='showAddModal(this)' id='"+menuId+"' class='btn btn-info dropdown-toggle btn-xs' style='margin-left:10px;padding-top:0px;' title='添加子节点'>&nbsp;&nbsp;<i class='fa fa-fw fa-plus rbg'></i></a>";
    var editBtn = "<a onclick='showEditModal(this)' id='"+menuId+"' class='btn btn-info dropdown-toggle btn-xs' style='margin-left:10px;padding-top:0px;' title='编辑节点'>&nbsp;&nbsp;<i class='fa fa-fw fa-edit rbg'></i></a>";
    var removeBtn = "<a onclick='showConfirmModal(this)' id='"+menuId+"' class='btn btn-info dropdown-toggle btn-xs' style='margin-left:10px;padding-top:0px;' title='删除节点'>&nbsp;&nbsp;<i class='fa fa-fw fa-times rbg'></i></a>"; 
    // 根据level进行判断
    if(level == 0) {
        $span.append(addBtn);
    }   
    if(level == 1) {        
        if(treeNode.children.length > 0) {          
            $span.append(addBtn+" "+editBtn);           
        } else {            
            $span.append(addBtn+" "+editBtn+" "+removeBtn);         
        }       
    }   
    if(level == 2) {
        $span.append(editBtn+" "+removeBtn);        
    }
    return $span;
}

3.5 addHoverDom(treeId, treeNode)函数

// 在鼠标移入节点范围时添加自定义控件
function addHoverDom(treeId, treeNode) {
    // 在执行添加前,先进行判断,如果已经添加过,就停止执行
    // 组装按钮组所在的span标签的id
    var btnGrpSpanId = treeNode.tId + "_btnGrp";
    var btnGrpSpanLength = $("#"+btnGrpSpanId).length;
    if(btnGrpSpanLength > 0) {
        return ;
    }
    // 由于按钮组是放在当前节点中的超链接(a标签)后面,所以先定位到a标签
    // 按id生成规则组装a标签的id
    var anchorId = treeNode.tId + "_a";
    // 调用已封装函数生成按钮组
    var $btnGrpSpan = generateBtnGrp(treeNode);
    // 在a标签后面追加按钮组
    $("#"+anchorId).after($btnGrpSpan);
}

3.6 removeHoverDom(treeId, treeNode)函数

// 在鼠标移出节点范围时删除自定义控件
function removeHoverDom(treeId, treeNode) {
    // 组装按钮组所在的span标签的id
    var btnGrpSpanId = treeNode.tId + "_btnGrp";
    // 删除对应的元素
    $("#"+btnGrpSpanId).remove();
}

3.7 加入add和remove的setting

var setting = {
    "view": {
        "addDiyDom": showMyIcon,
        "addHoverDom": addHoverDom,
        "removeHoverDom": removeHoverDom
    },
    "data": {
        "key": {
            "url": "notExistsProperty" // 阻止点击节点后跳转
        }
    }
};

4 添加子节点

4.1 准备模态框

模态框的HTML代码,参考下面文件的代码:

include-modal-menu-add.jsp

<%@ include file="/WEB-INF/include-modal-menu-add.jsp" %>

4.2 showAddModal()函数

// 在点击添加按钮时执行这个函数,打开模态框
function showAddModal(currentBtn) {
    // 打开模态框
    $("#menuAddModal").modal("show");   
    // 将当前节点的id存入全局变量
    window.menuId = currentBtn.id;
}

4.3 给保存按钮绑定单击响应函数

$("#menuAddBtn").click(function(){  
    // 收集表单填写的数据
    var name = $.trim($("#menuAddModal [name='name']").val());
    var url = $.trim($("#menuAddModal [name='url']").val());
    var icon = $("#menuAddModal [name='icon']:checked").val();  
    if(name == null || name == "") {
        layer.msg("请填写菜单项名称!");
        return ;
    }   
    if(url == null || url == "") {
        layer.msg("请填写菜单项对应的访问地址!");
        return ;
    }   
    // 发送Ajax请求
    $.ajax({
        "url":"menu/save.json",
        "type":"post",
        "dataType":"json",
        "data":{
            "name":name,
            "url":url,
            "pid":window.menuId,    // 当前操作的节点是新节点的父节点
            "icon":icon
        },
        "success":function(response){           
            var result = response.result;           
            if(result == "SUCCESS") {
                layer.msg("操作成功!");             
                initWholeTree();
            }           
            if(result == "FAILED") {
                layer.msg(response.message);
            }           
        },
        "error":function(response){
            layer.msg(response.message);
        }
    }); 
    $("#menuAddModal").modal("hide");
});

4.4 后端代码

@RequestMapping("/menu/save")
public ResultEntity<String> saveMenu(Menu menu) {
    menuService.saveMenu(menu); 
    return ResultEntity.successWithoutData();
}

5 更新节点

5.1 准备模态框

include-modal-menu-edit.jsp

<%@ include file="/WEB-INF/include-modal-menu-edit.jsp" %>

5.2 showEditModal()函数

// 在点击编辑按钮时执行这个函数,打开模态框
function showEditModal(currentBtn) {
    // 打开模态框
    $("#menuEditModal").modal("show");
    // 获取当前节点的id存入全局变量
    window.menuId = currentBtn.id;
    // 发送请求查询Menu对象
    $.ajax({
        "url":"menu/get/"+window.menuId+".json",
        "type":"get",
        "dataType":"json",
        "success":function(response) {
            var result = response.result;
            if(result == "SUCCESS") {
                // 从响应数据中获取Menu对象
                var menu = response.data;
                // 获取各个属性值
                var name = menu.name;
                var url = menu.url;
                var icon = menu.icon;
                // 设置各个对应的表单项
                $("#menuEditModal [name='name']").val(name);
                $("#menuEditModal [name='url']").val(url);
                // radio需要让value值和后端查询到的icon一致的设置为被选中
                $("#menuEditModal [name='icon'][value='"+icon+"']").attr("checked",true);
            }
            if(result == "FAILED") {
                layer.msg(response.message);
            }
        },
        "error":function(response) {
            layer.msg(response.message);
        }
    });
}

handler

@RequestMapping("/menu/get/{menuId}")
public ResultEntity<Menu> getMenuById(@PathVariable("menuId") Integer menuId){
    Menu menu = menuService.getMenuById(menuId);
    return ResultEntity.successWithData(menu);
}

service

@Override
public Menu getMenuById(Integer menuId) {
    return menuMapper.selectByPrimaryKey(menuId);
}

5.3 给更新按钮绑定单击响应函数

$("#menuEditBtn").click(function(){
    // 收集表单填写的数据
    var name = $.trim($("#menuEditModal [name='name']").val());
    var url = $.trim($("#menuEditModal [name='url']").val());
    var icon = $("#menuEditModal [name='icon']:checked").val();
    if(name == null || name == "") {
        layer.msg("请填写菜单项名称!");
        return ;
    }
    if(url == null || url == "") {
        layer.msg("请填写菜单项对应的访问地址!");
        return ;
    }
    // 发送Ajax请求
    $.ajax({
        "url":"menu/update.json",
        "type":"post",
        "dataType":"json",
        "data":{
            "id":window.menuId,
            "name":name,
            "url":url,
            "icon":icon
        },
        "success":function(response){
            var result = response.result;
            if(result == "SUCCESS") {
                layer.msg("操作成功!");
                initWholeTree();
            }
            if(result == "FAILED") {
                layer.msg(response.message);
            }
        },
        "error":function(response){
            layer.msg(response.message);
        }
    });
    $("#menuEditModal").modal("hide");
});

5.4 执行更新的后端代码

handler

@RequestMapping("/menu/update")
public ResultEntity<String> updateMenu(Menu menu){
    menuService.updateMenu(menu);
    return ResultEntity.successWithoutData();
}

service

@Override
public void updateMenu(Menu menu) {
    menuMapper.updateByPrimaryKey(menu);
}

MenuMapper.xml

<update id="updateByPrimaryKey" parameterType="com.rgh.crowd.funding.entity.Menu" >
    update t_menu
    set
    name = #{name,jdbcType=VARCHAR},
    url = #{url,jdbcType=VARCHAR},
    icon = #{icon,jdbcType=VARCHAR}
    where id = #{id,jdbcType=INTEGER}
</update>

注意:把SQL中的pid修改去掉,不改变节点的父节点。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  •   DOM(文档对象模型)是针对 HTML 和 XML 文档的一个 API(应用程序编程接口)。   DOM 描绘...
    霜天晓阅读 9,130评论 0 7
  • 1. 权限验证 2. 给Admin分配Role 2.1 创建中间表 ※说明:不做逆向工程,直接使用SQL操作。 2...
    弹钢琴的崽崽阅读 2,593评论 0 5
  • 写在前面的话 代码中的# > 表示的是输出结果 输入 使用input()函数 用法 注意input函数输出的均是字...
    FlyingLittlePG阅读 8,236评论 0 9
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 9,506评论 0 5
  • 程序设计中常使用树型结构来表征某些数据的关联关系,如上下级、栏目结构、商品分类、菜单、回复等。 分类的层级关系可以...
    JunChow520阅读 9,543评论 4 3

友情链接更多精彩内容