最近在工作中遇到的需求,管理系统中较为常见的场景,角色管理模块。通过可折叠、展开和全选功能的树形菜单来为管理系统中定义的角色分配可操作菜单的权限。
本文提供一种基于最基本的JQuery和json,对后端友好的树形菜单实现方案,主要思路是页面加载完毕时向后端发送ajax请求,后端将具有层级嵌套关系的bean转为json字符串返回给前端。ajax回调函数中先将返回的json字符串转为json对象,再通过递归的方式层层解析树形菜单并渲染页面。本人是Java程序员,并非专业的前端开发,代码难免贻笑大方,对前端要求较高的场景可另寻解决方案。树形菜单效果如图所示:
·加载菜单树所用js:
$(function () {
$.ajax({
url: "getMenuTree.ajax",
type: "POST",
contentType: "application/json",
dataType: "json",
async: false,
data: JSON.stringify({
"id": $("#roleId").val()
}),
success: function (data) {
$("#titleSpan").html($("#title").val() + "--" + data.roleName);
createNode($("#rootUl"), JSON.parse(data.jsonStr).children);
}
});
$(".item-name > i").click(function () {
if ($(this).parent().parent().find("ul:first").css("display") == "none") {
// 如果收起则展开下一层
unfoldRow($(this), 200);
} else {
// 如果展开则所有子节点都收起
$(this).parent().parent().find("ul").each(function () {
$(this).slideUp(200);
$(this).parent().find("a:first").find("i:first").removeClass("unfold");
$(this).parent().find("a:first").find("i:first").removeClass("fold");
});
}
});
$(".checkBox").click(function () {
// 当前CheckBox全选、全不选、中间状态的切换
var currentBox = $(this);
if (currentBox.attr("class").indexOf("part-selected") > 0) {
// 如果当前CheckBox是中间状态,则改为全选
currentBox.removeClass("part-selected");
currentBox.addClass("c-selected");
} else {
// 否则在全选和全不选中切换
currentBox.toggleClass("c-selected");
}
// 递归检查并设置父节点的选中状态
checkFather(currentBox);
// 所有子节点中CheckBox选中状态和当前CheckBox保持一致
// 遍历当前节点所有子节点
currentBox.parent().parent().parent().find("ul:first").find("li").find("a:first").find("span:first").find("div:first").each(function () {
$(this).removeClass("part-selected");
if (currentBox.attr("class").indexOf("c-selected") == -1) {
// 当前节点不选则所有子节点不选
$(this).removeClass("c-selected");
} else {
// 当前节点选中则所有子节点选中
$(this).addClass("c-selected");
}
});
});
// 若叶节点的选中属性为true,则触发点击复选框事件
$(".checkBox").each(function () {
if ($(this).parent().parent().next().length == 0) {
if ($(this).find("input:first").val() == "true") {
$(this).click();
}
}
});
// 展开整棵树
$("#rootUl").find("i").each(function () {
unfoldRow($(this), 150);
});
});
function checkFather(currentBox) {
// 当前CheckBox所有兄弟节点都选中,则父节点也选中,没有都选中则父节点也不选中
var sameChoice = true;
var brothers = currentBox.parent().parent().parent().siblings();
brothers.find("a:first").find("span:first").find("div:first").each(function () {
if (currentBox.attr("class") != $(this).attr("class")) {
sameChoice = false;
return;
}
});
var father = currentBox.parent().parent().parent().parent().prev().find("span:first").find("div:first");
if (sameChoice) {
// 若都选中则父菜单都选中,都没选中则父菜单不选中
father.attr("class", currentBox.attr("class"));
} else {
// 若部分选中,则父菜单设为部分选中的样式
father.attr("class", "checkBox part-selected");
}
if (currentBox.parent().parent().parent().parent().attr("id") == "rootUl") {
// 如果当前节点的父节点是根ul,则不再继续向上递归
return;
}
checkFather(father);
}
function unfoldRow(iNode, speed) {
iNode.parent().parent().find("ul:first").slideDown(speed);
iNode.removeClass("fold");
iNode.addClass("unfold");
}
function createNode(node, arr) {
for (var i = 0; i < arr.length; i++) {
var ele = arr[i];
// 创建li
var row = document.createElement("li");
row.id = ele.id;
row.className = "li-height"
node.append(row);
// 创建a
var name = document.createElement("a");
if (ele.children.length == 0) {
name.className = "item-name item-name-last c-f";
} else {
name.className = "item-name c-f";
}
var nameI = document.createElement("i");
var nameSpan1 = document.createElement("span");
nameSpan1.className = "pull-left";
var nameSpan1Div = document.createElement("div");
nameSpan1Div.className = "checkBox";
var nameSpan1DivIpt = document.createElement("input");
nameSpan1DivIpt.type = "hidden";
nameSpan1DivIpt.value = ele.active;
$(nameSpan1Div).append(nameSpan1DivIpt);
var nameSpan2 = document.createElement("span");
nameSpan1.className = "pull-left";
nameSpan2.innerText = ele.name;
$(row).append(name);
$(name).append(nameI);
$(name).append(nameSpan1);
$(nameSpan1).append(nameSpan1Div);
$(name).append(nameSpan2);
if (ele.children.length > 0) {
// 创建子ul
var child = document.createElement("ul");
child.className = "nav-trunk fold";
$(row).append(child);
createNode($(child), ele.children);
}
}
}
页面菜单树所在div:
<div class="wrapper">
<div class="aiui">
<div class="ai-treenav">
<ul id="rootUl"></ul>
</div>
</div>
</div>
页面所用css样式:
<style type="text/css">
.wrapper {
width: 500px;
height: 1000px;
overflow-x: scroll;
border: 1px solid #ccc;
margin: 0 auto;
}
.aiui {
padding-top: 10px;
padding-bottom: 10px;
}
.fold {
display: none;
}
.aiui .checkBox {
width: 15px;
height: 18px;
border: 1px solid #dadada;
border-radius: 2px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
margin: 2px 5px 0 0;
cursor: pointer;
}
.aiui .c-selected {
border: 1px solid #27b0f2;
position: relative;
}
.aiui .c-selected:after {
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: #27b0f2 url("icon.png") no-repeat -36px -1px;
}
.aiui .c-selected:after {
background-position: -1px -1px;
}
.aiui .part-selected {
border: 1px solid #27b0f2;
position: relative;
}
.aiui .part-selected:after {
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: #27b0f2 url("assets/img/ai_icon.png") no-repeat -0px -0px;
}
.aiui .part-selected:after {
background-position: -1px -12px;
}
.pull-left {
float: left;
}
.c-f:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden
}
ul, li, i {
list-style: none;
padding: 0;
margin: 0;
font-style: normal;
}
.ai-treenav {
width: 100%;
}
.ai-treenav ul {
margin-left: 20px;
}
.ai-treenav i {
width: 14px;
height: 14px;
background: url("icon.png") no-repeat -4px -33px;
margin: 2px 7px 0 0;
float: left
}
.ai-treenav i.unfold, .ai-treenav .item-name-last > i {
width: 14px;
height: 14px;
background: url("icon.png") no-repeat -24px -33px;
}
.ai-treenav a {
display: block;
font-size: 14px;
height: 22px;
padding: 3px 0;
color: #666;
overflow: hidden;
}
.nav-trunk {
margin-left: 20px;
}
.ai-treenav .checkBox {
margin-top: 0;
}
.ai-treenav li.unfold {
min-width: 250px;
}
.ai-treenav li.unfold > ul li .filename {
width: 60%;
}
.li-height {
margin-top: 5px;
}
</style>
页面所用图标:
菜单树加载后,一个分支节点的结构如下图所示:
提交表单方式较为灵活,可以将div放在form内,提交form表单,也可以ajax方式提交,本文不再详述。至于被选中节点id的获取方式可参考如下代码:
for (var i = 0; i < $('#rootUl').find('.c-selected').length; i++) {
var ele = $($('#rootUl').find('.c-selected')[i]);
// 如果是叶节点则选中id
if (ele.parent().parent().next().length == 0) {
console.log("eleId", ele.parent().parent().parent().attr("id"));
}
}