目录
一、前言
1.什么是树形菜单
2.树形菜单的使用场景
二、案例实现
1.需求分析
2.前期准备工作
①导入依赖
②工具类
BaseDao(通用增删改查)
BuildTree(完成平级数据到父子级的转换)
ResponseUtil(将数据转换成json格式进行回显)
③编写实体
3.dao层编写
4.servlet层编写
5.jsp页面搭建
6.案例演示
一、前言
在完成案例之前我们先来了解一下什么是树形菜单
1.什么是树形菜单
树形菜单是一种用户界面设计模式,常用于组织和展示大量信息的层次结构。它的名称来自于菜单呈现的结构类似于树的分支和节点。树形菜单通常由主节点和多个子节点组成,每个子节点可以有自己的子节点,形成了父子关系的层级结构。
在树形菜单中,主节点代表顶级选项或类别,而子节点代表与主节点相关联的具体选项或子类别。用户可以通过展开或折叠子节点来浏览更详细的层级。通常,叶子节点是最底层的节点,表示最具体的选项或内容。
树形菜单适用于需要组织和展示复杂数据或多级目录的应用程序和网站。它们使用户可以方便地导航和选择所需的选项,提供了一种简洁而直观的方式来浏览和访问信息。
2.树形菜单的使用场景
树形菜单在各种应用和网站中被广泛使用,以下是一些常见的使用场景:
- 1. 文件资源管理:树形菜单可用于浏览和管理文件资源,特别是当文件被组织成多级目录结构时。用户可以展开和折叠目录以访问所需的文件或文件夹。
- 2. 组织结构和人员管理:企业或组织的组织结构可以通过树形菜单进行可视化展示。每个节点代表一个部门、团队或个人,用户可以通过展开节点查看更详细的层级和信息。
- 3. 导航菜单:网站或应用程序的主要导航菜单常常使用树形结构,以显示不同的页面或功能选项。用户可以根据需要展开或折叠菜单项,快速定位所需的功能或内容。
- 4. 产品分类和目录导航:在线商店或电子商务网站通常使用树形菜单来组织产品分类和目录。用户可以通过展开和折叠不同的类别来浏览和选择产品。
- 5. 内容管理系统:树形菜单在内容管理系统中被广泛使用,用于组织和管理网站的页面、文章、类别和标签等内容。
总而言之,树形菜单适用于任何需要展示和组织层次结构数据的场景,以提供清晰且易于导航的用户界面。
二、案例实现
1.需求分析
我们要完成一个书籍管理系统的左侧列表展示,必不可少的就是表的设计,表设计关乎到我们jsp页面的展示,下面是我的表设计仅供大家参考
表设计有了数据也必不可少,当然表数据也不要随便编写,不然可能会出错哦!!
这里的数据都是有讲究的,我们平常通过dao层拿到的数据都是平级数据,并不符合我们的要求,我们所需要的是有父子级别的数据,那么怎么从平级数据转换成我们的父子级数据呢?? 这时候我们的表设计就起到了至关重要的作用,我们可以看到父级节点的pid都是0,而他们的子节点的pid对应的都是父级节点的id。
得出结论我们只需要两个方法,一个查询全部的方法(拿到平级数据),另一个将平级数据进行两次for遍历加到新集合(父子级容器)的方法,第一次for将pid编号是顶级节点也就是父节点的数据加入容器中,第二次遍历将pid与新集合里面的id做比较,如果相同那就是新集合里的children。
2.前期准备工作
注意:更多细节在我所编写的自定义MVC三部曲中。
①导入依赖
将我们所需的依赖导入到WebContent➡WEB-INF下
创建一个存放js/css的目录将所需文件放入
将多个页面共用的文件封装成公共文件
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html "> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title></title> <!-- 引入layui.css--> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/js/layui/css/layui.css"> <!-- 引入layui.js--> <script type="text/javascript" src="${pageContext.request.contextPath}/static/js/layui/layui.js"></script> </head> </html>
②工具类
这里我们需要用到工具类
BaseDao(通用增删改查)
package com.zking.util; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 所有Dao层的父类 BookDao UserDao OrderDao ... * * @author Administrator * * @param <T> */ public class BaseDao<T> { /** * 适合多表联查的数据返回 * @param sql * @param pageBean * @return * @throws SQLException * @throws InstantiationException * @throws IllegalAccessException */ public List<Map<String, Object>> executeQuery(String sql, PageBean pageBean) throws SQLException, InstantiationException, IllegalAccessException { List<Map<String, Object>> list = new ArrayList<>(); Connection con = DBAccess.getConnection(); PreparedStatement pst = null; ResultSet rs = null; /* * 是否需要分页? 无需分页(项目中的下拉框,查询条件教员下拉框,无须分页) 必须分页(项目中列表类需求、订单列表、商品列表、学生列表...) */ if (pageBean != null && pageBean.isPagination()) { // 必须分页(列表需求) String countSQL = getCountSQL(sql); pst = con.prepareStatement(countSQL); rs = pst.executeQuery(); if (rs.next()) { pageBean.setTotal(String.valueOf(rs.getObject(1))); } // 挪动到下面,是因为最后才处理返回的结果集 // -- sql=SELECT * FROM t_mvc_book WHERE bname like '%圣墟%' // -- pageSql=sql limit (page-1)*rows,rows 对应某一页的数据 // -- countSql=select count(1) from (sql) t 符合条件的总记录数 String pageSQL = getPageSQL(sql, pageBean);// 符合条件的某一页数据 pst = con.prepareStatement(pageSQL); rs = pst.executeQuery(); } else { // 不分页(select需求) pst = con.prepareStatement(sql);// 符合条件的所有数据 rs = pst.executeQuery(); } // 获取源数据 ResultSetMetaData md = rs.getMetaData(); int count = md.getColumnCount(); Map<String, Object> map = null; while (rs.next()) { map = new HashMap<>(); for (int i = 1; i <= count; i++) { // map.put(md.getColumnName(i), rs.getObject(i)); map.put(md.getColumnLabel(i), rs.getObject(i)); } list.add(map); } return list; } /** * * @param sql * @param attrs * map中的key * @param paMap * jsp向后台传递的参数集合 * @return * @throws SQLException * @throws NoSuchFieldException * @throws SecurityException * @throws IllegalArgumentException * @throws IllegalAccessException */ public int executeUpdate(String sql, String[] attrs, Map<String, String[]> paMap) throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { Connection con = DBAccess.getConnection(); PreparedStatement pst = con.prepareStatement(sql); for (int i = 0; i < attrs.length; i++) { pst.setObject(i + 1, JsonUtils.getParamVal(paMap, attrs[i])); } return pst.executeUpdate(); } /** * 批处理 * @param sqlLst * @return */ public static int executeUpdateBatch(String[] sqlLst) { Connection conn = null; PreparedStatement stmt = null; try { conn = DBAccess.getConnection(); // 设置不自动提交 conn.setAutoCommit(false); for (String sql : sqlLst) { stmt = conn.prepareStatement(sql); stmt.executeUpdate(); } conn.commit(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); throw new RuntimeException(e1); } e.printStackTrace(); throw new RuntimeException(e); } finally { DBAccess.close(conn, stmt, null); } return sqlLst.length; } /** * 通用的增删改方法 * * @param book * @throws Exception */ public int executeUpdate(String sql, T t, String[] attrs) throws Exception { // String[] attrs = new String[] {"bid", "bname", "price"}; Connection con = DBAccess.getConnection(); PreparedStatement pst = con.prepareStatement(sql); // pst.setObject(1, book.getBid()); // pst.setObject(2, book.getBname()); // pst.setObject(3, book.getPrice()); /* * 思路: 1.从传进来的t中读取属性值 2.往预定义对象中设置了值 * * t->book f->bid */ for (int i = 0; i < attrs.length; i++) { Field f = t.getClass().getDeclaredField(attrs[i]); f.setAccessible(true); pst.setObject(i + 1, f.get(t)); } return pst.executeUpdate(); } /** * 通用分页查询 * * @param sql * @param clz * @return * @throws Exception */ public List<T> executeQuery(String sql, Class<T> clz, PageBean pageBean) throws Exception { List<T> list = new ArrayList<T>(); Connection con = DBAccess.getConnection(); ; PreparedStatement pst = null; ResultSet rs = null; /* * 是否需要分页? 无需分页(项目中的下拉框,查询条件教员下拉框,无须分页) 必须分页(项目中列表类需求、订单列表、商品列表、学生列表...) */ if (pageBean != null && pageBean.isPagination()) { // 必须分页(列表需求) String countSQL = getCountSQL(sql); pst = con.prepareStatement(countSQL); rs = pst.executeQuery(); if (rs.next()) { pageBean.setTotal(String.valueOf(rs.getObject(1))); } // 挪动到下面,是因为最后才处理返回的结果集 // -- sql=SELECT * FROM t_mvc_book WHERE bname like '%圣墟%' // -- pageSql=sql limit (page-1)*rows,rows 对应某一页的数据 // -- countSql=select count(1) from (sql) t 符合条件的总记录数 String pageSQL = getPageSQL(sql, pageBean);// 符合条件的某一页数据 pst = con.prepareStatement(pageSQL); rs = pst.executeQuery(); } else { // 不分页(select需求) pst = con.prepareStatement(sql);// 符合条件的所有数据 rs = pst.executeQuery(); } while (rs.next()) { T t = clz.newInstance(); Field[] fields = clz.getDeclaredFields(); for (Field f : fields) { f.setAccessible(true); f.set(t, rs.getObject(f.getName())); } list.add(t); } return list; } /** * 将原生SQL转换成符合条件的总记录数countSQL * * @param sql * @return */ private String getCountSQL(String sql) { // -- countSql=select count(1) from (sql) t 符合条件的总记录数 return "select count(1) from (" + sql + ") t"; } /** * 将原生SQL转换成pageSQL * * @param sql * @param pageBean * @return */ private String getPageSQL(String sql, PageBean pageBean) { // (this.page - 1) * this.rows // pageSql=sql limit (page-1)*rows,rows return sql + " limit " + pageBean.getStartIndex() + "," + pageBean.getRows(); } }
BuildTree(完成平级数据到父子级的转换)
package com.zking.util; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class BuildTree { /** * 默认-1为顶级节点 * @param nodes * @param <T> * @return */ public static <T> TreeVo<T> build(List<TreeVo<T>> nodes) { if (nodes == null) { return null; } List<TreeVo<T>> topNodes = new ArrayList<TreeVo<T>>(); for (TreeVo<T> children : nodes) { String pid = children.getParentId(); if (pid == null || "-1".equals(pid)) { topNodes.add(children); continue; } for (TreeVo<T> parent : nodes) { String id = parent.getId(); if (id != null && id.equals(pid)) { parent.getChildren().add(children); children.setHasParent(true); parent.setChildren(true); continue; } } } TreeVo<T> root = new TreeVo<T>(); if (topNodes.size() == 1) { root = topNodes.get(0); } else { root.setId("000"); root.setParentId("-1"); root.setHasParent(false); root.setChildren(true); root.setChecked(true); root.setChildren(topNodes); root.setText("顶级节点"); Map<String, Object> state = new HashMap<>(16); state.put("opened", true); root.setState(state); } return root; } /** * 指定idparam为顶级节点 * @param nodes * @param idParam * @param <T> * @return */ public static <T> List<TreeVo<T>> buildList(List<TreeVo<T>> nodes, String idParam) { if (nodes == null) { return null; } List<TreeVo<T>> topNodes = new ArrayList<TreeVo<T>>(); for (TreeVo<T> children : nodes) { String pid = children.getParentId(); if (pid == null || idParam.equals(pid)) { topNodes.add(children); continue; } for (TreeVo<T> parent : nodes) { String id = parent.getId(); if (id != null && id.equals(pid)) { parent.getChildren().add(children); children.setHasParent(true); parent.setChildren(true); continue; } } } return topNodes; } }
ResponseUtil(将数据转换成json格式进行回显)
package com.zking.util; import java.io.PrintWriter; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; public class ResponseUtil { public static void write(HttpServletResponse response,Object o)throws Exception{ response.setContentType("text/html;charset=utf-8"); PrintWriter out=response.getWriter(); out.println(o.toString()); out.flush(); out.close(); } public static void writeJson(HttpServletResponse response,Object o)throws Exception{ ObjectMapper om = new ObjectMapper(); // om.writeValueAsString(o)代表了json串 write(response, om.writeValueAsString(o)); } }
③编写实体
Permission(数据表实体)
package com.xw.entity; /**书籍管理系统实体 * @author 索隆 * */ public class Permission { private long id; private String name; private String description; private String url; private long pid; private int ismenu; private long displayno; public Permission() { // TODO Auto-generated constructor stub } public Permission(long id, String name, String description, String url, long pid, int ismenu, long displayno) { super(); this.id = id; this.name = name; this.description = description; this.url = url; this.pid = pid; this.ismenu = ismenu; this.displayno = displayno; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public long getPid() { return pid; } public void setPid(long pid) { this.pid = pid; } public int getIsmenu() { return ismenu; } public void setIsmenu(int ismenu) { this.ismenu = ismenu; } public long getDisplayno() { return displayno; } public void setDisplayno(long displayno) { this.displayno = displayno; } @Override public String toString() { return "Permission [id=" + id + ", name=" + name + ", description=" + description + ", url=" + url + ", pid=" + pid + ", ismenu=" + ismenu + ", displayno=" + displayno + "]"; } }
注意:mysql里面的类型bigint在java中是long类型
3.dao层编写
package com.xw.dao; import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.databind.ObjectMapper; import com.xw.entity.Permission; import com.zking.util.BaseDao; import com.zking.util.BuildTree; import com.zking.util.TreeVo; /**书记管理 * @author 索隆 * */ public class PermissionDao extends BaseDao<Permission>{ /**获取书籍管理的所有信息(平级数据) * @return * @throws Exception */ public List<Permission> list() throws Exception{ String sql="select * from t_easyui_permission"; return super.executeQuery(sql, Permission.class, null); } /**将平级数据变成我们需要的父子级数据 * @return * @throws Exception */ public List<TreeVo<Permission>> menu() throws Exception{ //存放父子级的容器 List<TreeVo<Permission>> menu=new ArrayList<TreeVo<Permission>>(); //拿到平级数据 List<Permission> list = this.list(); //遍历平级数据 for (Permission permission : list) { //工具类帮助我们完成父子级关系 TreeVo<Permission> vo=new TreeVo<Permission>(); vo.setId(permission.getId()+""); vo.setParentId(permission.getPid()+""); vo.setText(permission.getName()); menu.add(vo); } //通过工具类筛选父级菜单的儿子,pid为0是父级菜单 return BuildTree.buildList(menu, "0"); } public static void main(String[] args) throws Exception { //测试数据 PermissionDao d=new PermissionDao(); List<TreeVo<Permission>> menu = d.menu(); ObjectMapper om =new ObjectMapper(); System.out.println(om.writeValueAsString(menu)); } }
我们可以进行对比两组数据的不同之处:
- 平级数据
- 父子级数据
4.servlet层编写
package com.xw.web; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.xw.dao.PermissionDao; import com.xw.entity.Permission; import com.zking.framework.ActionSupport; import com.zking.framework.ModelDriver; import com.zking.util.ResponseUtil; import com.zking.util.TreeVo; /**书籍管理的servlet处理 * @author 索隆 * */ public class PermissionAction extends ActionSupport implements ModelDriver<Permission>{ private Permission Permission=new Permission(); private PermissionDao pdao=new PermissionDao(); /**初始化书籍管理系统的动态树 * @param req * @param resp * @throws Exception */ public void listmenu(HttpServletRequest req, HttpServletResponse resp) throws Exception { //查询父子级数据 List<TreeVo<Permission>> menu = pdao.menu(); //将集合转换成json格式进行回显 ResponseUtil.writeJson(resp, menu); } @Override public Permission getModel() { return Permission; } }
编写完servlet别忘记编写配置文件
<?xml version="1.0" encoding="UTF-8"?> <config> <action path="/blog" type="com.zking.web.BlogAction"> <forward name="list" path="/blogList.jsp" redirect="false" /> <forward name="toList" path="/blog.action?methodName=list" redirect="true" /> <forward name="toEdit" path="/blogEdit.jsp" redirect="false" /> </action> <!--用户--> <action path="/user" type="com.xw.web.Useraction"> </action> <!-- 书籍管理——树 --> <action path="/Permission" type="com.xw.web.PermissionAction"> </action> </config>
5.jsp页面搭建
这时候我们就可以去在线Layui网站找一个满意的jsp页面了
Layui 开发使用文档 - 入门指南
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE> <html> <head> <%@ include file="common/static.jsp"%> <script type="text/javascript" src="js/index.js"></script> </head> <body> <div class="layui-layout layui-layout-admin"> <div class="layui-header"> <div class="layui-logo layui-hide-xs layui-bg-black">书籍管理系统</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> <!-- Top导航栏 --> <li class="layui-nav-item layui-hide-xs"><a href="">nav 1</a></li> <li class="layui-nav-item layui-hide-xs"><a href="">nav 2</a></li> <li class="layui-nav-item layui-hide-xs"><a href="">nav 3</a></li> <li class="layui-nav-item"><a href="javascript:;">nav groups</a> <dl class="layui-nav-child"> <dd> <a href="">menu 11</a> </dd> <dd> <a href="">menu 22</a> </dd> <dd> <a href="">menu 33</a> </dd> </dl></li> </ul> <!-- 个人头像及账号操作 --> <ul class="layui-nav layui-layout-right"> <li class="layui-nav-item layui-hide layui-show-md-inline-block"> <a href="javascript:;"> <img src="//tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" class="layui-nav-img"> tester </a> <dl class="layui-nav-child"> <dd> <a href="">Your Profile</a> </dd> <dd> <a href="">Settings</a> </dd> <dd> <a href="login.jsp">Sign out</a> </dd> </dl> </li> <li class="layui-nav-item" lay-header-event="menuRight" lay-unselect> <a href="javascript:;"> <i class="layui-icon layui-icon-more-vertical"></i> </a> </li> </ul> </div> <div class="layui-side layui-bg-black"> <div class="layui-side-scroll"> <!-- 左侧导航区域(可配合layui已有的垂直导航) --> <ul id="menu" class="layui-nav layui-nav-tree" lay-filter="menu"> <!-- <li class="layui-nav-item layui-nav-itemed"> <a class="" href="javascript:;">menu group 1</a> <dl class="layui-nav-child"> <dd><a href="javascript:;">menu 1</a></dd> <dd><a href="javascript:;">menu 2</a></dd> <dd><a href="javascript:;">menu 3</a></dd> <dd><a href="">the links</a></dd> </dl> </li> <li class="layui-nav-item"> <a href="javascript:;">menu group 2</a> <dl class="layui-nav-child"> <dd><a href="javascript:;">list 1</a></dd> <dd><a href="javascript:;">list 2</a></dd> <dd><a href="">超链接</a></dd> </dl> </li> <li class="layui-nav-item"><a href="javascript:;">click menu item</a></li> <li class="layui-nav-item"><a href="">the links</a></li> --> </ul> </div> </div> <div class="layui-body"> <!-- 内容主体区域 --> <div style="padding: 15px;">内容主体区域。记得修改 layui.css 和 js 的路径</div> </div> <div class="layui-footer"> <!-- 底部固定区域 --> 底部固定区域 </div> </div> <script> //JS layui.use(['element', 'layer', 'util'], function(){ var element = layui.element ,layer = layui.layer ,util = layui.util ,$ = layui.$; $.ajax({ url: "${pageContext.request.contextPath}/Permission.action?methodName=listmenu", type: 'post', dataType: 'json', success: function(data) { //定义一个变量将回显的数据进行拼接,最终追加到指定标签上 var str=''; $.each(data,function(i,n){ str+='<li class="layui-nav-item layui-nav-itemed">'; str+=' <a class="" href="javascript:;">'+n.text+'</a>'; //判断有无children节点有就遍历 if(n.hasChildren){ //有children节点拿到children节点 var children=n.children; str+='<dl class="layui-nav-child">'; $.each(children,function(idx,node){ str+='<dd><a href="javascript:;">'+node.text+'</a></dd>'; }) str+='</dl>'; } str+='</li>'; }) //将拼接内容追加到指定ul标签 $("#menu").html(str); element.render('menu'); } }) }); </script> </body> </html>
小贴士:
element.render()的用处:
假设你正在使用某个具体的前端框架或库,其中的`element`是对应的组件或对象,调用`render()`方法。一般而言,`render()`方法用于将组件或元素渲染到网页中的具体位置。具体作用如下:
- 1. 渲染组件:`render()`方法用于将特定的组件渲染到页面上。它会将组件的内容和样式解析生成对应的 HTML 结构,并使其在浏览器中可见。
- 2. 更新页面:如果已经存在该组件或元素并在页面上显示,再次调用`render()`可以触发页面的更新。这在需要根据数据的变化进行页面更新时很有用。
- 3. 绑定事件:在渲染组件时,`render()`方法通常会将组件的事件绑定到相应的 DOM 元素上。这样可以确保用户与组件进行交互时,组件能够响应相应的操作。
- 需要注意的是,`render()`方法的具体实现和用法取决于所使用的前端框架或库,可能会有额外的参数和选项。要确切了解`render()`方法的作用,请参考相应框架或库的文档和使用指南。
6.案例演示
到这里我的分享就结束了,欢迎到评论区探讨交流!!
如果觉得有用的话还请点个赞吧爱心 ♥ ♥