PHP实现菜单无限分级
无限分级,或者说无限分类,就是一种类似于X叉树的结构。比如X国下面有Y省,Y省下面再设立Z市,以此类推。
我的使用场景在于用户菜单的显示。数据在DB中的体现是这样的:每条记录对应一个菜单,并记录其对应的母菜单id。实际上描述整个数据结构,就是以n个母菜单为根节点的树组成的森林。
生成权限的treeview
那么在从DB全量拉取了这些记录以后,如何将其组织成树呢。我们需要用到PHP的引用
,只遍历一次结果集即可。下面是生成角色权限树的代码,checked标志是否拥有这个权限。
function getPermissions($myPerms = array())
{
$arr = Permission::all()->toArray();//从DB中获取所有记录
$data = [];
foreach($arr as $v){
//重新组织数据 拼成一个节点t
$t = [
'text' => $v['display_name'],
'tags' => [$v['name']],
'state' => [
//checked标志当前用户是否有用这个权限
'checked' => in_array($v['id'], $myPerms) ? true : false;
],
'icon' => $v['icon']
];
$data[$v['id']] = $t;
$data[$v['cid']]['nodes'][] = &$data[$v['id']];
}
if(isset($data[0])){
return $data[0]['nodes'];
}
return [];
}
这样只需要遍历一遍记录集合,将其通过引用的方式不断的链接起来,就得到了我们要的森林。之后我们用bootstrap-treeview
插件,将其渲染到页面上。
//第一行是blade模板中将权限树的json赋值给js对象。
var permissions = {!! $permissionAll !!};
var tree = $('#permission_tree').treeview({
showCheckbox: true,
showTags: true,
levels: 5,
data: permissions
}).on('nodeChecked ', function (ev, node) {
for (var i in node.nodes) {
var child = node.nodes[i];
$(this).treeview(true).checkNode(child.nodeId);
}
}).on('nodeUnchecked ', function (ev, node) {
for (var i in node.nodes) {
var child = node.nodes[i];
$(this).treeview(true).uncheckNode(child.nodeId);
}
var t = node;
while (t.parentId !== undefined) {
if (node.parentId !== undefined) {
//静默模式,不让事件继续传播
$(this).treeview("uncheckNode", [t.parentId, {silent: true}]);
}
t = $(this).treeview("getNode", t.parentId);
}
});
我们给treeview绑定了一些事件:在选中某个结点时,让其子结点都被选中;在取消某个结点时,同时取消其所有父系结点和子结点。效果如下:
生成菜单
其实生成菜单与上面的思想基本一致,利用引用
的特性将结果集生成森林。但是这里没有了js插件的支持,需要我们手动编写生成menu的html代码的方法。这里用到深度优先遍历,将森林遍历一遍后即可生成完整的html。
//$myPerms 是角色拥有的权限
$arr = Permission::all()->toArray();
foreach ($arr as $v) {
$v['state'] = in_array($v['id'], $myPerms) ? true : false;
$v['open'] = str_is($v['name'], $routeName) ? true : false;
$data[$v['id']] = $v;
$data[$v['cid']]['nodes'][] = &$data[$v['id']];
}
$roots = $data[0]['nodes'];
filterMenu($roots);
$text = "";
$this->makeMenuTree($roots, $text);
//得到$text即为需要的菜单html代码
用到的两个函数filterMenu
和makeMenuTree
实现如下:
//递归的判断当前结点是否需要被显示,只有子结点全都不需要显示的时候,当前结点才不需要显示
private function filterMenu(&$nodes)
{
$flag = false;
foreach ($nodes as $k => &$node) {
if ($node['state']) {
$flag = true;
//不管如何 都要遍历子结点
if(isset($node['nodes'])){
$this->filterMenu($node['nodes']);
}
} elseif (isset($node['nodes'])) {
//如果子结点也都不需要 就删除该结点
if (!$this->filterMenu($node['nodes'])) {
unset($nodes[$k]);
}
} else {
//连子结点都没有 也删除该节点
unset($nodes[$k]);
}
}
return $flag;
}
//深度优先遍历$root,根据有无子结点,生成相应的html代码
function makeMenuTree($root, &$text)
{
foreach ($root as $node) {
$open = $node['open'];
if (isset($node['nodes'])) {
$text .= '<li class="treeview ' . ($open ? 'active' : '') . '">';
$text .= '<a href="#">';
$text .= '<i class="fa ' . $node['icon'] . '"></i> <span>' . $node['display_name'] . '</span>';
$text .= '<i class="fa fa-angle-left pull-right"></i>';
$text .= '</a>';
$text .= '<ul class="treeview-menu ' . ($open ? 'menu-open' : '') . '" style="display:' . ($open ? 'block;' : 'none;') . '">';
$this->makeMenuTree($node['nodes'], $text);
$text .= '</ul>';
} else {
$url = str_replace(".", "/", $node['name']);
$url = "/".str_replace("/*", "", $url);
$text .= '<li class="' . ($open ? 'active' : '') . '"><a href="'.$url.'"><i class="fa ' . $node['icon'] . '"></i>' . $node['display_name'] . '</a></li>';
}
}
}
之后将$text
吐到页面里即可。本来想把生成html代码的逻辑放到blade模板里去做的,可以更好的做到mvc。但是初学laravel,不知道怎么在blade里实现这种功能。
最后菜单效果如图: