node+mysql+layui+ejs实现左侧导航栏菜单动态显示

node+mysql+layui+ejs实现左侧导航菜单动态显示

node入门到入土项目实战开始,前端篇项目适合node小白入门,因为我也是小白来学习node前端的,代码不是很简洁,优雅,各位读者多多包涵一下。

实现思路

账户表中编写一个字段,role_id(字段)用来存储该账户所拥有的相关角色权限,然后创建资源表用来存储相关项目的菜单资源,创建角色权限表用来存储相关的角色权限,创建角色权限资源中间表用来存储每个角色 拥有哪些资源。
账户在登陆界面输入账户相关信息进行登录时查询该数据库中的相关账户是否存在如果存在且登录成功则将该账户的角色id值提取出来进行菜单资源查询,查询成功以后跳转到系统首页,如果该账户角色id为空或者该角色下没有任何资源菜单时跳转至账户授权提示页面。

效果图


数据库

这里用到四个表进行导航资源的动态显示,资源表(tb_resource),角色表(tb_role)

角色资源中间表(tb_rolr_resource),账户表(tb_account)

技术栈

node.js
layui
layui(消息插件notify)
mysql2
ejs
Express

代码实现

main.html(前端首页页面)
html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>暖意书栈-首页</title>
  <!-- 设置系统图标 -->
  <link rel="shortcut icon" href="../icon/main.ico" type="image/x-icon" />
  <meta name="renderer" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="../layui/css/layui.css" rel="stylesheet">
  <link href="../css/main.css" rel="stylesheet">
 
</head>
<body>
<div class="layui-layout layui-layout-admin">
  <div class="layui-header">
    <div class="layui-logo layui-hide-xs layui-bg-black">
      <i class="layui-icon layui-icon-read" style="color: #cff60cd3;font-size: 22px;"></i>
      <strong style="font-family: 华文行楷;font-size: 25px; color: #a6b5afd3;">暖意书栈</strong> 
    </div>
    <!-- 头部区域(可配合layui 已有的水平导航) -->
    <ul class="layui-nav layui-layout-left">
      <!-- 移动端显示 -->
      <li class="layui-nav-item layui-show-xs-inline-block layui-hide-sm" lay-header-event="menuLeft">
        <i class="layui-icon layui-icon-spread-left"></i>
      </li>
      <li class="layui-nav-item layui-hide-xs"><a href="javascript:;">书籍借阅</a></li>
      <li class="layui-nav-item layui-hide-xs"><a href="javascript:;">座位预约</a></li>
      <li class="layui-nav-item layui-hide-xs"><a href="javascript:;">贴吧</a></li>
      <li class="layui-nav-item">
        <a href="javascript:;">更多</a>
        <dl class="layui-nav-child">
          <dd><a href="javascript:;">意见反馈</a></dd>
          <dd><a href="javascript:;">违规处理</a></dd>
          <dd><a href="javascript:;">联系我们</a></dd>
        </dl>
      </li>
    </ul>
    <ul class="layui-nav layui-layout-right">
      <li class="layui-nav-item layui-hide layui-show-sm-inline-block">
        <a href="javascript:;">
          <img src="../image/admin.jpeg" class="layui-nav-img">
          我的
        </a>
        <dl class="layui-nav-child">
          <dd><a href="javascript:;">我的资料</a></dd>
          <dd><a href="javascript:;">我的借阅</a></dd>
          <dd><a href="javascript:;">我的预约</a></dd>
          <dd><a href="javascript:;">安全管理</a></dd>
          <dd><a href="javascript:;">退出登录</a></dd>
        </dl>
      </li>
      <li class="layui-nav-item" lay-header-event="menuRight" lay-unselect>
        <a href="javascript:;">
          <i class="layui-icon layui-icon-notice"></i> 通知/公告
        </a>
      </li>
    </ul>
  </div>
  <div class="layui-side layui-bg-black">
    <div class="layui-side-scroll">
      <ul class="layui-nav layui-nav-tree" lay-filter="test">
        <% Object.keys(navItems).forEach(parentId => { %>
          <% if (parentId === "0") { %>
            <% navItems[parentId].forEach(item => { %>
              <li class="layui-nav-item">
                <a data-id="<%= item.re_id %>"><i class="layui-icon <%= item.re_icon %>"></i><span> <%= item.re_title %></span></a>
                <% if (navItems[item.re_id] && navItems[item.re_id].length > 0) { %>
                  <dl class="layui-nav-child">
                    <% navItems[item.re_id].forEach(subItem => { %>
                      <dd>
                        <a data-id="<%= subItem.re_id %>" data-url="<%= subItem.re_url %>"  href="javascript:void(0);"><i class="layui-icon <%= subItem.re_icon %>"></i> <%= subItem.re_title %></a>
                      </dd>
                    <% }) %>
                  </dl>
                <% } %>
              </li>
            <% }) %>
          <% } %>
        <% }) %>
      </ul>
    </div>
  </div>

  <div class="layui-body layui-form">
      <div class="layui-tab marg0 layui-tab-brief" lay-filter="bodyTab" id="top_tabs_box" lay-allowclose="true">
          <ul class="layui-tab-title top_tab" id="top_tabs">
              <li class="layui-this" lay-allowclose="false"><i class="layui-icon layui-icon-home"></i><cite>首页</cite></li>
          </ul>
            <!-- 当前页面操作 -->
        <ul class="layui-nav closeBox">
          <li class="layui-nav-item">
              <a href="javascript:;">页面操作</a>
              <dl class="layui-nav-child">
                  <dd>
                      <a href="javascript:;" class="refresh refreshThis"><i class="layui-icon layui-icon-refresh-3"></i> 刷新当前</a>
                  </dd>
                  <dd>
                      <a href="javascript:;" class="closePageOther"><i class="layui-icon layui-icon-clear"></i> 关闭其他</a>
                  </dd>
                  <dd>
                      <a href="javascript:;" class="closePageAll"><strong><i class="layui-icon layui-icon-close"></i></strong> 关闭全部</a>
                  </dd>
              </dl>
          </li>
      </ul>
          <div class="layui-tab-content clildFrame">
              <div class="layui-tab-item layui-show">
                  <iframe src="/index"></iframe>
              </div>
          </div>
      </div>
  </div>
  <div class="layui-footer">
    <!-- 底部固定区域 -->
    底部固定区域
  </div>
</div>
<script src="../jquery/jquery-3.7.1.min.js"></script>
<script src="../layui/layui.js"></script>
<script src="../notify/notify.js"></script>
<script>
//JS 
layui.use(['element', 'layer', 'util','notify'], function(){
  var element = layui.element;
  var layer = layui.layer;
  var util = layui.util;
  var $ = layui.$;
  var notify = layui.notify;

  // 监听左侧导航的二级菜单点击事件
  $('.layui-nav .layui-nav-child').on('click', 'a[data-url]', function(e) {
    e.preventDefault(); // 阻止默认行为,避免页面跳转

    var $this = $(this),// 获取当前点击的a元素
        tabTitle = $this.text().trim(),// 获取当前点击的a元素的文本内容
        tabId = $this.data('id'),// 获取当前点击的a元素的data-id属性
        tabUrl = $this.data('url');// 获取当前点击的a元素的data-url属性
        
    // 检查是否已经有此tab
    var hasTab = $('#top_tabs li').filter(function() {
      return $(this).find('cite').text() === tabTitle;// 使用filter方法筛选出匹配的元素
    });

    if (!hasTab.length) {
      // 如果没有,则添加新的tab
      element.tabAdd('bodyTab', {// 调用element.tabAdd方法添加新的tab
        title: '<cite>' + tabTitle + '</cite>',// 设置tab的标题
        content: '<iframe src="' + tabUrl + '" frameborder="0" scrolling="auto"></iframe>',// 设置tab的内容
        id: tabId // 设置tab的id
      });
    }
    // 切换到该tab
    element.tabChange('bodyTab', tabId);// 调用element.tabChange方法切换到该tab
  });

  //点击刷新当前
  $(".refresh").on("click",function(){  //
      if($(this).hasClass("refreshThis")){// 判断是否是点击刷新当前
          $(this).removeClass("refreshThis");// 移除refreshThis类
          // 获取当前页面的iframe元素,并调用其contentWindow属性的location属性的reload方法刷新页面
          $(".clildFrame .layui-tab-item.layui-show").find("iframe")[0].contentWindow.location.reload(true);
          setTimeout(function(){
              $(".refresh").addClass("refreshThis");// 添加refreshThis类
          },2000)
      }else{
        notify.info({msg:'您点击的速度超过了服务器的响应速度,还是等两秒再刷新吧!',position:'vcenter',shadow:true, closable:false,duration:1500});
      }
  });
  
 
  // 当点击 ".closePageOther" 元素时触发此事件处理程序,关闭其他 就是把除了当前窗口意外的其他窗口关闭 首页除外
  $(".closePageOther").on("click", function () {
    // 获取当前激活的标签页(即被选中的标签页)
    var $currentTab = $("#top_tabs li.layui-this"),
        // 从当前激活的标签页中获取标题文本
        currentTitle = $currentTab.find("cite").text(),
        // 从 sessionStorage 中获取名为 "menu" 的数据,并将其解析为 JavaScript 对象
        // 如果 sessionStorage 中没有 "menu" 数据,则使用空数组
        menu = JSON.parse(window.sessionStorage.getItem("menu")) || [],
        // 计算非首页的标签页数量
        nonHomeTabsCount = $("#top_tabs li:not(.layui-this)").not("[cite='首页']").length;

    // 如果当前标签页是 "首页" 并且存在其他非首页标签页
    if (currentTitle === "首页" && nonHomeTabsCount > 0) {
        // 关闭所有其他非首页标签页,并清空 sessionStorage
        $("#top_tabs li[lay-id]").not(".layui-this").each(function() {
            // 获取当前标签页的 "lay-id" 属性值
            var layId = $(this).attr("lay-id");
            // 使用 "element.tabDelete" 方法删除当前标签页,并调用 "init" 方法刷新界面
            element.tabDelete("bodyTab", layId).init();
        });
        // 清除 sessionStorage 中的所有数据
        sessionStorage.clear();
    } else if (currentTitle !== "首页" && nonHomeTabsCount > 1) { // 如果当前不是首页并且存在其他非首页标签页
        // 关闭所有其他非当前标签页
        $("#top_tabs li[lay-id]").not(".layui-this").each(function() {
            // 获取当前标签页的 "lay-id" 属性值
            var layId = $(this).attr("lay-id");
            // 使用 "element.tabDelete" 方法删除当前标签页,并调用 "init" 方法刷新界面
            element.tabDelete("bodyTab", layId).init();
        });

        // 更新 sessionStorage 中的 "menu" 数组,只包含当前标签页的信息
        // 使用 Array.prototype.filter 方法过滤数组,只保留当前标签页的项
        menu = menu.filter(item => item.title === currentTitle);
        // 将更新后的 "menu" 数组保存回 sessionStorage
        sessionStorage.setItem("menu", JSON.stringify(menu));
    } else {
        // 如果只剩下首页和当前页面时,显示提示信息
        notify.info({
            msg: '没有可以关闭的窗口了哦!',
            position: 'vcenter', // 提示信息的位置设置为中心
            shadow: true, // 是否启用阴影效果
            closable: false, // 是否允许手动关闭提示信息
            duration: 1000 // 提示信息显示的持续时间(毫秒)
        });
    }

    // 调用 "tab.tabMove" 方法重新渲染顶部的标签页
    tab.tabMove();
  });
  //关闭全部窗口 只留下 首页
  $(".closePageAll").on("click",function(){
      if($("#top_tabs li").length > 1){
          $("#top_tabs li").each(function(){
              if($(this).attr("lay-id") != ''){
                  element.tabDelete("bodyTab",$(this).attr("lay-id")).init();
                  window.sessionStorage.removeItem("menu");
                  menu = [];
                  window.sessionStorage.removeItem("curmenu");
              }
          })
      }else{
        notify.info({msg:'没有可以关闭的窗口了!',position:'vcenter',shadow:true, closable:false,duration:1000});
      }
      //渲染顶部窗口
      tab.tabMove();
  })
});
</script>
</body>
</html>
查询资源菜单方法 js
javascript 复制代码
// 创建一个对象来保存 roleId
const roleManager = {
    // 初始化时可以设定一个默认值
    _roleId: null,
  
    // 方法用于设置 roleId
    setRoleId: function (roleId1) {
      this._roleId = roleId1;
    },
  
    // 方法用于获取 roleId
    getRoleId: function () {
      return this._roleId;
    }
};




router.get('/main', (req, res) => {
    // 获取用户角色 ID
    const roleId = roleManager.getRoleId();

    // 使用数据库连接池执行 SQL 查询,获取与角色 ID 关联的所有资源 ID
    pool.query(userSQL.queryAllResource, [roleId], (err, resourceIds) => {
      if (err) throw err; // 如果发生错误,则抛出异常

      // 如果没有找到任何资源 ID,则重定向到指定页面
      if (resourceIds.length === 0) {
        return res.redirect('/forbidden'); // 假设这是跳转到的页面
      }

      // 将查询结果中的所有资源 ID 提取到一个数组中
      const resourceIdsList = resourceIds.map(id => id.resource_id);

      // 构建 SQL 查询字符串,用于查询具体的资源详情
      const query = `SELECT * FROM tb_resource WHERE re_id IN (${resourceIdsList.join(',')})`;

      // 使用数据库连接池执行 SQL 查询,获取具体的资源详情
      pool.query(query, (err, resources) => {
        if (err) throw err; // 如果发生错误,则抛出异常

        // 如果查询到的资源为空,则重定向到指定页面
        if (resources.length === 0) {
          return res.redirect('/forbidden'); // 假设这是跳转到的页面
        }

        // 对查询到的资源进行分组处理,按父资源 ID 分组
        const groupedResources = groupResourcesByParentId(resources);

        // 渲染 'main' 视图,并传递分组后的资源作为数据
        res.render('main', { navItems: groupedResources });
      });
    });
});
/**
 * 将资源按照其父ID分组。
 * 
 * @param {Array} resources - 包含资源信息的数组,其中每个资源对象都应包含 re_parentId 属性。
 * @returns {Object} - 返回一个对象,其中键是父ID,值是一个包含具有相同父ID的资源的数组。
 */
function groupResourcesByParentId(resources) {
    // 使用 reduce 函数对资源数组进行处理。
    // reduce 接收一个回调函数作为参数,该回调函数定义了如何累积结果。
    // 第一个参数是累加器(accumulator),初始值为空对象 {}。
    // 第二个参数是当前元素(current)。
    return resources.reduce((acc, curr) => {
      // 检查累加器对象中是否存在当前元素的 re_parentId 键。
      // 如果不存在,则在累加器对象上创建一个新的空数组。
      if (!acc[curr.re_parentId]) {
        acc[curr.re_parentId] = [];
      }

      // 将当前元素推入与它的 re_parentId 相关联的数组中。
      acc[curr.re_parentId].push(curr);

      // 返回累加器对象,以便 reduce 函数继续处理下一个元素。
      return acc;
    }, {});
}

数据库查询语句

javascript 复制代码
 //查询每个角色拥有的所有资源
    queryAllResource: 'SELECT resource_id FROM tb_role_resource WHERE role_id = ?',
app.js配置ejs模板
javascript 复制代码
//安装ejs模板
npm install ejs
javascript 复制代码
// 设置模板引擎
app.set('view engine', 'html');
app.set('views',path.join(__dirname, 'views/login'));
// 设置后缀名的文件使用什么模板引擎
app.engine('html', require('ejs').renderFile);
相关推荐
初晴~11 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813617 分钟前
InnoDB 的页分裂和页合并
数据库·后端
滚雪球~28 分钟前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语30 分钟前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
m0_748234522 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
YashanDB2 小时前
【YashanDB知识库】XMLAGG方法的兼容
数据库·yashandb·崖山数据库
独行soc2 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍11基于XML的SQL注入(XML-Based SQL Injection)
数据库·安全·web安全·漏洞挖掘·sql注入·hw·xml注入
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
风间琉璃""3 小时前
bugkctf 渗透测试1超详细版
数据库·web安全·网络安全·渗透测试·内网·安全工具
drebander3 小时前
SQL 实战-巧用 CASE WHEN 实现条件分组与统计
大数据·数据库·sql