前言

这是一个在实习中的小项目,其中需要使用LayUI进行构造前端,JSON控制菜单的控制面板。


简介

LayUI

LayUI是一种基于JavaScript的前端框架,让Web开发变得更加容易。而JSON(JavaScript Object Notation)则是一种用于数据交换的轻量级格式,被广泛用于Web应用程序中。

LayUI的特点:

  • 简单易用:LayUI的API设计非常简单,易于学习和使用。
  • 功能丰富:LayUI提供了丰富的UI组件,包括按钮、表单、弹窗、表格等。
  • 灵活性高:LayUI提供了丰富的配置项,可以灵活地满足不同的需求。
  • 高性能:LayUI的渲染速度非常快,可以提高用户的使用体验。

LayUI的使用非常广泛,特别是在后台管理系统、企业管理系统、电商平台等领域。如果想要快速构建一个Web应用程序,LayUI是一个不错的选择。

这里就拿LayUI当做练手。LayUI文档:http://layui.org.cn

JSON

JSON(JavaScript Object Notation)是一种用于数据交换的轻量级格式,它易于阅读和编写,同时也易于解析和生成。JSON格式由键值对组成,键和值之间使用冒号分隔,键值对之间使用逗号分隔,最外层使用花括号或方括号括起来。


实践

先放一个实践图:

Header

在head部分,需要添加LayUI的CSS:

<link rel="stylesheet" href="layui/css/layui.css">

我的布局包含了:头部、菜单列表和选项卡容器。头部包含了一个网站的标志和用户信息,菜单列表包含了网站的主要导航菜单,选项卡容器包含了用户在网站中打开的各个选项卡。

Layout

布局代码如下:

<!-- LayUI layout -->
    <div class="layui-layout layui-layout-admin">
        <!-- Header section -->
        <div class="layui-header">
            <div class="layui-logo">Hoyue Panel</div>
            <ul class="layui-nav layui-layout-right">
                <li class="layui-nav-item">
                    Welcome, Hoyue
                </li>
                <li class="layui-nav-item"><a href="#" onclick="out_msg()">Log Out</a></li>
            </ul>
        </div>
        <!-- Menu list -->
        <div class="layui-side layui-bg-black">
            <div class="layui-side-scroll">
                <ul id="menu" class="layui-nav layui-nav-tree" lay-filter="meun">
                </ul>
            </div>
        </div>
        <!-- Tab container -->
        <div class="layui-tab" lay-filter="demo" lay-allowclose="true" style="margin-left: 200px;">
            <ul class="layui-tab-title"></ul>
            <div class="layui-tab-content"></div>
        </div>
    </div>

其中,菜单栏的id为menu,便于我之后通过JSON导入。

Logic

首先就是在尾部载入LayUI的JS:

<script src="layui/layui.js"></script>

在加载菜单之前,我们先来看看JSON文件的格式。以被要求的格式为例:

[
    {
        "menuid":"00",
        "menu":"Home",
        "parentid": "",
        "operationid": "",
        "levelno": "1",
        "notes": "",
        "url": "home.html",
        "active": "1"
    },
    {
        "menuid":"01",
        "menu":"Website",
        "parentid": "",
        "operationid": "",
        "levelno": "1",
        "notes": "",
        "url": "",
        "active": "1"
    },
    {
        "menuid":"0101",
        "menu":"Files",
        "parentid": "01",
        "operationid": "",
        "levelno": "2",
        "notes": "",
        "url": "file.html",
        "active": "1"
    },
    {
        "menuid":"0102",
        "menu":"Logs",
        "parentid": "01",
        "operationid": "",
        "levelno": "2",
        "notes": "",
        "url": "",
        "active": "1"
    },
    {
        "menuid":"010201",
        "menu":"Logs 1",
        "parentid": "0102",
        "operationid": "",
        "levelno": "3",
        "notes": "",
        "url": "log1.html",
        "active": "1"
    },
    {
        "menuid":"010202",
        "menu":"Logs 2",
        "parentid": "0102",
        "operationid": "",
        "levelno": "3",
        "notes": "",
        "url": "log2.html",
        "active": "1"
    },
    {
        "menuid":"02",
        "menu":"Database",
        "parentid": "",
        "operationid": "",
        "levelno": "1",
        "notes": "",
        "url": "",
        "active": "1"
    },
    {
        "menuid":"0201",
        "menu":"Database Lists",
        "parentid": "02",
        "operationid": "",
        "levelno": "2",
        "notes": "",
        "url": "db.html",
        "active": "1"
    },
    {
        "menuid":"0202",
        "menu":"Admin",
        "parentid": "02",
        "operationid": "",
        "levelno": "2",
        "notes": "",
        "url": "",
        "active": "0"
    },
    {
        "menuid":"03",
        "menu":"Panel Setting",
        "parentid": "",
        "operationid": "",
        "levelno": "1",
        "notes": "",
        "url": "setting.html",
        "active": "1"
    },
    {
        "menuid":"04",
        "menu":"Log Out",
        "parentid": "",
        "operationid": "",
        "levelno": "1",
        "notes": "",
        "url": "",
        "active": "1"
    }
]

其中:

  • menuid:菜单项的ID,用于唯一标识该菜单项。
  • menu:菜单项的标题,用于在菜单列表中显示。
  • parentid:该菜单项的父菜单项ID,用于构建菜单项的层级结构。
  • operationid:该菜单项对应的操作ID,用于记录用户操作日志等。这个在这个例子中没有读取。
  • levelno:该菜单项在菜单项层级结构中的层级编号,从1开始递增。
  • notes:该菜单项的备注信息。
  • url:该菜单项对应的页面URL,用于在选项卡组件中打开对应的页面。
  • active:该菜单项是否处于激活状态,1表示激活,0表示不激活。

加载菜单

首先是加载菜单数据,它是从一个JSON文件中动态获取的:

//Load menu from JSON
    function loadMenu() {
        fetch('ComMenu.json')    //JSON文件URL
            .then(response => response.json())
            .then(data => {
                const menuElement = document.getElementById('menu');
                for (let i = 0; i < data.length; i++) {
                    const item = data[i];
                    if(item.active == 0)
                        continue;
                    else {
                        const menuItem = createMenuItem(item);
                        menuElement.appendChild(menuItem);
                    }
                }
            });
    }

这部分代码使用Fetch API发起请求,获取菜单的JSON数据。然后使用Promise处理响应,将JSON解析为JavaScript对象。最后遍历对象中的菜单数据,动态插入到页面中。

这样实现的好处是前后端分离,菜单数据可配置化,只需要修改JSON文件就可以改变菜单内容,无需修改代码。并且利用Promise可以非常优雅地处理异步请求。

在读取数组数据的时候,判断active位,如果为0就跳过。菜单在createMenuItem()方法中创建,使用appendChild()方法累加。

处理主菜单

该函数接收一个菜单项的JSON数据,并根据数据创建对应的菜单项DOM节点。

//Create Main menu
    function createMenuItem(item) {
        // Handle submenu
        if(item.parentid){
            return subChild(item);
        }
        
        // create li and a
        const li = document.createElement('li');
        li.classList.add('layui-nav-item');
        const a = document.createElement('a');
        a.href = 'javascript:;';
        a.textContent = item.menu;

        // Construct li 
        li.appendChild(a);  

        // Set ID and data attributes
        a.setAttribute("id",item.menuid); 
        a.setAttribute('data-id', item.menuid);
        a.setAttribute('data-title', item.menu);
        a.setAttribute('data-url', item.url);
        a.setAttribute('data-type', 'tabAdd');

        // Add styles 
        a.classList.add('site-demo-active');

        // Special handle for logout link
        if(a.textContent == "Log Out")
        {
            a.setAttribute("onclick","out_msg()");
            a.setAttribute("href","#");
            a.classList.remove('site-demo-active');
        }

        return li;
    }

对于创建菜单,我分为创建一级菜单和多级子菜单,因为它们在静态上不同。首先判断菜单项的parentid字段是否存在,如果存在则调用subChild()函数创建子菜单项。如果不存在,则创建一个简单的菜单项。

创建菜单项所需的DOM节点包括一个<li>标签和一个<a>标签,其中<li>标签用于表示菜单项,<a>标签用于显示菜单项的标题和处理点击事件。

在创建<a>标签时,需要设置其href属性为javascript:;,以便在点击时不会跳转到其他页面。同时,还需要设置一些自定义的属性,包括菜单项的ID、标题、URL和类型等,以便在用户点击菜单项时打开对应的选项卡。

在设置完这些属性之后,还需要为菜单项添加一些样式,以便在菜单列表中显示为激活状态。特别地,对于“Log Out”菜单项需要进行特殊处理,即添加onclick事件处理函数和删除一些样式。

最后,函数返回创建好的菜单项DOM节点。使用这个函数可以方便地创建菜单项,并在用户点击菜单项时处理对应的事件。

处理子菜单

与主菜单类似的,但子菜单需要多做一些特判。

// Handle submenu
    function subChild(item){
        // Get parent a and remove parent's click style
        const parentA = document.getElementById(item.parentid);
        parentA.classList.remove('site-demo-active');

        /* Level 2 submenu, get parent li
           Level 3 submenu, get parent dl */
        if(item.levelno == 2)
            var parentLi = parentA.parentElement;
        else 
            var parentLi = parentA.closest('dd');

        // Query and Add drop down icon
        var iElement = parentA.querySelector('i.layui-icon.layui-icon-down.layui-nav-more');
        if (!iElement)
        { 
            parentA.insertAdjacentHTML('beforeend', '');
            // Click handler to expand/collapse
            parentA.addEventListener('click', (e) => {
            if (parentLi.classList.contains('layui-nav-itemed')) 
            {
                parentLi.classList.remove('layui-nav-itemed');
            } else {
                parentLi.classList.add('layui-nav-itemed'); 
            }
            })
        };

        // Create dl and dd
        const dl = document.createElement('dl');
        dl.classList.add('layui-nav-child');
        parentLi.appendChild(dl);
            
        const dd = document.createElement('dd');
        dl.appendChild(dd);

        // Create submenu a
        const sub_a = document.createElement('a');
        sub_a.href = 'javascript:;';
        sub_a.textContent = item.menu;
        dd.appendChild(sub_a);

        // Set attributes
        sub_a.setAttribute('id', item.menuid);
        sub_a.setAttribute('data-id', item.menuid);
        sub_a.setAttribute('data-title', item.menu);
        sub_a.setAttribute('data-url', item.url);
        sub_a.setAttribute('data-type', 'tabAdd');
        sub_a.classList.add('site-demo-active');

        // Retrun li
        if(item.levelno == "3")  // level 3 may a dl
            return parentLi.closest('li');
        return parentLi;
    }

在函数中,首先获取该子菜单项的父菜单项<a>标签,并移除其激活样式。然后根据子菜单项的层级编号判断其父菜单项的类型,如果是二级菜单,则获取父菜单项的<li>标签,否则获取父菜单项的最近的<dd>标签。

接下来,需要为父菜单项添加一个下拉箭头图标,以便在用户点击时展开或收起子菜单项。如果已经存在下拉箭头图标,则不需要再次添加。同时,还需要为父菜单项的<a>标签添加一个点击事件处理函数,用于展开或收起子菜单项。

在添加完下拉箭头图标和点击事件处理函数之后,需要创建子菜单项的DOM节点。该节点包括一个<dl>标签和一个<dd>标签,用于表示子菜单项的层级结构。然后再创建一个<a>标签,用于显示子菜单项的标题和处理点击事件。

在创建<a>标签时,需要设置其href属性为javascript:;,以便在点击时不会跳转到其他页面。同时,还需要设置一些自定义的属性,包括菜单项的ID、标题、URL和类型等,以便在用户点击菜单项时打开对应的选项卡。

最后,函数返回创建好的子菜单项DOM节点。如果子菜单项是三级菜单,则需要返回其父菜单项的<li>标签。使用这个函数可以方便地创建子菜单项,并在用户点击菜单项时处理对应的事件。

Tab处理

点击菜单后,会打开一个新的Tab标签页。首先需要检查该Tab是否已存在:

// 点击菜单处理
$('#menu').on('click','a', function(){

  // 判断Tab是否存在
  if($(".layui-tab-title li[lay-id]").length == 0) {
    // 不存在则新增
  } else {
    var isData = false;

    // 循环检查已有的Tab
    $.each($(".layui-tab-title li[lay-id]"), function(){
      if($(this).attr("lay-id") == dataid.attr("data-id")){
        isData = true; // 存在
      }
    });

    if(isData == false) {
      // 不存在则新增
    } 
  }

  // 切换 Tab
  active.tabChange(id);

});

遍历已打开的Tab,检查其id是否与当前点击菜单对应的id相同。如果没有匹配的则表示该Tab未打开,需要新增。

新增和切换Tab的方法封装成了active对象:

// Tab handling object
        var active = {

            tabAdd: function (url, id, name) {
                element.tabAdd('demo', {
                    title: name,
                    content: '<iframe data-frameid="' + id + '" scrolling="auto" frameborder="0" src="' + url + '" style="width:100%;height:99%;"></iframe>',
                    id: id
                })
                FrameWH();
            },
            tabChange: function (id) {

                element.tabChange('demo', id);
            },
            tabDelete: function (id) {
                element.tabDelete("demo", id);
            }
        };

        // Set iframe height
        function FrameWH() {
            var h = $(window).height();
            $("iframe").css("height",h+"px");
        }
    });

这样可以方便统一调用,添加新功能也比较容易通过新增方法实现。


后记

至此,整个小项目的重要逻辑已经写完了,附上一个整篇代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Hoyue Panel</title>
    <link rel="stylesheet" href="layui/css/layui.css">
</head>
<body>
    <!-- LayUI layout -->
    <div class="layui-layout layui-layout-admin">
        <!-- Header section -->
        <div class="layui-header">
            <div class="layui-logo">Hoyue Panel</div>
            <ul class="layui-nav layui-layout-right">
                <li class="layui-nav-item">
                    Welcome, Hoyue
                </li>
                <li class="layui-nav-item"><a href="#" onclick="out_msg()">Log Out</a></li>
            </ul>
        </div>

        <!-- Menu list -->
        <div class="layui-side layui-bg-black">
            <div class="layui-side-scroll">

                <ul id="menu" class="layui-nav layui-nav-tree" lay-filter="meun">
                </ul>
            </div>
        </div>

        <!-- Tab container -->
        <div class="layui-tab" lay-filter="demo" lay-allowclose="true" style="margin-left: 200px;">
            <ul class="layui-tab-title"></ul>
            <div class="layui-tab-content"></div>

        </div>

    </div>

<script src="layui/layui.js"></script>
<script>
//Load menu from JSON
    function loadMenu() {
        fetch('ComMenu.json')
            .then(response => response.json())
            .then(data => {
                const menuElement = document.getElementById('menu');
                for (let i = 0; i < data.length; i++) {
                    const item = data[i];
                    if(item.active == 0)
                        continue;
                    else {
                        const menuItem = createMenuItem(item);
                        menuElement.appendChild(menuItem);
                    }
                }
            });
    }

    //Create Main menu
    function createMenuItem(item) {
        // Handle submenu
        if(item.parentid){
            return subChild(item);
        }
        
        // create li and a
        const li = document.createElement('li');
        li.classList.add('layui-nav-item');
        const a = document.createElement('a');
        a.href = 'javascript:;';
        a.textContent = item.menu;

        // Construct li 
        li.appendChild(a);  

        // Set ID and data attributes
        a.setAttribute("id",item.menuid); 
        a.setAttribute('data-id', item.menuid);
        a.setAttribute('data-title', item.menu);
        a.setAttribute('data-url', item.url);
        a.setAttribute('data-type', 'tabAdd');

        // Add styles 
        a.classList.add('site-demo-active');

        // Special handle for logout link
        if(a.textContent == "Log Out")
        {
            a.setAttribute("onclick","out_msg()");
            a.setAttribute("href","#");
            a.classList.remove('site-demo-active');
        }

        return li;
    }

    
    // Handle submenu
    function subChild(item){
        // Get parent a and remove parent's click style
        const parentA = document.getElementById(item.parentid);
        parentA.classList.remove('site-demo-active');

        /* Level 2 submenu, get parent li
           Level 3 submenu, get parent dl */
        if(item.levelno == 2)
            var parentLi = parentA.parentElement;
        else 
            var parentLi = parentA.closest('dl');

        // Query and Add drop down icon
        var iElement = parentA.querySelector('i.layui-icon.layui-icon-down.layui-nav-more');
        if (!iElement)
        { 
            parentA.insertAdjacentHTML('beforeend', '');
            // Click handler to expand/collapse
            parentA.addEventListener('click', (e) => {
            if (parentLi.classList.contains('layui-nav-itemed')) 
            {
                parentLi.classList.remove('layui-nav-itemed');
            } else {
                parentLi.classList.add('layui-nav-itemed'); 
            }
            })
        };

        // Create dl and dd
        const dl = document.createElement('dl');
        dl.classList.add('layui-nav-child');
        parentLi.appendChild(dl);
            
        const dd = document.createElement('dd');
        dl.appendChild(dd);

        // Create submenu a
        const sub_a = document.createElement('a');
        sub_a.href = 'javascript:;';
        sub_a.textContent = item.menu;
        dd.appendChild(sub_a);

        // Set attributes
        sub_a.setAttribute('id', item.menuid);
        sub_a.setAttribute('data-id', item.menuid);
        sub_a.setAttribute('data-title', item.menu);
        sub_a.setAttribute('data-url', item.url);
        sub_a.setAttribute('data-type', 'tabAdd');
        sub_a.classList.add('site-demo-active');

        // Retrun li
        if(item.levelno == "3")  // level 3 may a dl
            return parentLi.closest('li');
        return parentLi;
    }
    </script>
<script>
    loadMenu();

    // Initialize LayUI tab
    layui.use(['element'], function(){
        var element = layui.element;
        element.tabAdd('demo', {
        title: 'home',
        content: '<iframe data-frameid="0" scrolling="auto" frameborder="0" src="home.html" style="width:100%;height:99%;"&rt;</iframe&rt;',
        id: 0
        });
        element.tabChange('demo', 0);
    });

    // Tab handling logic
    layui.use(['element', 'layer', 'jquery'], function () {
        var element = layui.element;
        var $ = layui.$;

        // Use event delegation for menu click handlers
        $('#menu').on('click', '.site-demo-active', function () {
            var dataid = $(this);

            // Check if tab already exists
            if ($(".layui-tab-title li[lay-id]").length < 0) {
                // If not, add new tab
                active.tabAdd(dataid.attr("data-url"), dataid.attr("data-id"), dataid.attr("data-title"));
            } else {
                var isData = false;

                // Loop through existing tabs
                $.each($(".layui-tab-title li[lay-id]"), function () {

                    if ($(this).attr("lay-id") == dataid.attr("data-id")) {
                        isData = true;
                    }
                })

                // If tab doesn't exist, add it
                if (isData == false) {

                    active.tabAdd(dataid.attr("data-url"), dataid.attr("data-id"), dataid.attr("data-title"));
                }
            }

            // Switch to new tab 
            active.tabChange(dataid.attr("data-id"));
            
            // Remove current selected menu style
            $('#menu .layui-this').removeClass('layui-this'); 

            // Add selected style to clicked menu item
            $(this).addClass('layui-this');
        });

        // Tab handling object
        var active = {

            tabAdd: function (url, id, name) {
                element.tabAdd('demo', {
                    title: name,
                    content: '<iframe data-frameid="' + id + '" scrolling="auto" frameborder="0" src="' + url + '" style="width:100%;height:99%;"&rt;</iframe&rt;',
                    id: id
                })
                FrameWH();
            },
            tabChange: function (id) {

                element.tabChange('demo', id);
            },
            tabDelete: function (id) {
                element.tabDelete("demo", id);
            }
        };

        // Set iframe height
        function FrameWH() {
            var h = $(window).height();
            $("iframe").css("height",h+"px");
        }
    });

    // Logout message  
    function out_msg()
    {
        layer.msg('You have been logged out.', {icon: 6});
    }

</script>
</body>
</html>