BeWithYou

胡搞的技术博客

  1. 首页
  2. PHP
  3. PHP实现菜单无限分级

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代码

用到的两个函数filterMenumakeMenuTree实现如下:

//递归的判断当前结点是否需要被显示,只有子结点全都不需要显示的时候,当前结点才不需要显示
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里实现这种功能。
最后菜单效果如图:
菜单效果图

回到顶部