node+mysql+layui+ejs实现左侧导航菜单动态显示
-
-
- 实现思路
- 效果图
- 数据库
- 技术栈
- 代码实现
-
- main.html(前端首页页面)
- [查询资源菜单方法 js](#查询资源菜单方法 js)
- app.js配置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);