*小tip:每次重新启动前端项目加载困难可以crlt+shift+delete清除浏览器缓存后再重新启动
1.前端改造
首先我们拿到项目,得了解一下目录结构
他是通过src->router->index.js,根据路由来进行页面的跳转
constantRoutes:通用页面路由
asyncRoutes:动态路由
1.1参数讲解
各个参数的意思
有子目录的目录,以最常见的table
举例,table
在这里,ctrl+左键
点进去就行
参数与目录的对照:
1.2动态菜单思路
我们要做到的是,根据后端返回的json对象,动态的显示目录而vue-element-admin,是写死了的菜单,所以我们调用后端接口,实现目录的拼接,最终达到实现动态菜单的目的那么我们就要仿造router目录下index.js文件,动态的生成相似的json对象
1.3前端代码
修改router目录下的index.js中的asyncRoutes方法,使其为空,要我们动态的加入菜单
export const asyncRoutes = []
首先在src->api>user.js,加入一个接口方法
//这里加一个,根据data(token)的不同,后台会返回不同的字符串结果,动态菜单完成export function authMenu(data) { const jsonData = JSON.stringify(data); // 手动转换为 JSON 字符串 console.log("data数据是", jsonData); // 在这里添加打印语句 return request({ url: '/user/selectMenu', method: 'post', data: jsonData, // 使用转换后的 JSON 字符串 headers: { 'Content-Type': 'application/json' } });}
修改store/modules/permission.js文件,在 generateRoutes方法里面调用上面的authMenu接口
import { asyncRoutes, constantRoutes } from '@/router'import { authMenu } from '@/api/user'// 【新加入】引入请求,后面有文件,先不慌import Layout from '@/layout'// 【新加入】引入layout//这里自己写方法,作用就是向 asyncRoutes 插入路由,达到动态路由的效果/** * 【新加入】后台查询的菜单数据拼装成路由格式的数据 * @param routes */export function generaMenu(routes, data) { //data挨个遍历data.forEach(item => { //path不为空的话,就新建一个对象,装数据 if (item.path !== '') { //这个就仿照目录的机构,搭建 const menu = { path: item.path, component: Layout, //这个不用写data里面的内容,引用就行了 redirect: item.redirect, children: [], meta: { // 使用 title 和 icon 创建 meta 对象 title: item.title, icon: item.icon } } //遍历子标签,并加入到主目录的children中去 // 判断是否有子标签 if (item.children && Array.isArray(item.children) && item.children.length > 0) { // 遍历子标签,并加入到主目录的 children 中去 item.children.forEach(childItem => { const menu2 = { path: childItem.path, component: (resolve) => require([`@/views${childItem.component}`], resolve), name: childItem.name, meta: { // 使用 title 和 icon 创建 meta 对象 title: childItem.title, icon: childItem.icon } } // 加入到主目录的 children 中去 menu.children.push(menu2) }) } //追加 routes.push(menu) }})//把404加到最后,因为作者说 // 404 page must be placed at the end !!!const menu3 = { path: '*', redirect: '/404', hidden: true}//追加routes.push(menu3)}const state = { routes: [], addRoutes: []}const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) }}const actions = { generateRoutes({ commit,rootState },) { return new Promise(resolve => { const loadMenuData = [] // 保留加载动态路由的代码 // authMenu 调用可能也需要根据你的需要来决定是否删除 authMenu(rootState.user.token).then(response => { let data = response if (response.code !== 20000) { // 错误处理逻辑 } else { data = response.data Object.assign(loadMenuData, data) const tempAsyncRoutes = Object.assign([], asyncRoutes) generaMenu(tempAsyncRoutes, loadMenuData) let accessedRoutes accessedRoutes = tempAsyncRoutes || [] commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) } }) }).catch(error => { console.log(error) }) }}export default { namespaced: true, state, mutations, actions}
最后,修改views/login下的index.vue,dispatch 一下(在登录成功的前提下)
向handleLogin中添加dispatch( "permission/generateRoutes",userRoles)
handleLogin() { this.$refs.loginForm.validate((valid) => { if (valid) { this.loading = true; // 假设有一种方法用于获取用户角色,例如 getUserRoles() const userRoles = ["admin"]; this.$store .dispatch("user/login", this.loginForm) .then(() => { // 将预定义的角色 "admin" 传递给 generateRoutes action return this.$store.dispatch( "permission/generateRoutes", userRoles ); }) .then((accessedRoutes) => { // 将动态路由添加到路由器 this.$router.addRoutes(accessedRoutes); // 导航到指定路径或默认路径 ('/') this.$router.push({ path: this.redirect || "/", query: this.otherQuery, }); // 重置加载状态 this.loading = false; }) .catch(() => { // 在登录失败的情况下重置加载状态 this.loading = false; }); } else { console.log("提交错误!!"); return false; } }); },
2.后端改造
@Controller层
@Resource private MenuService menuService; /** * 动态菜单获取 */ @PostMapping("/selectMenu") public MenuResponse selectMenu(@RequestBody VoToken voToken) { MenuResponse res = new MenuResponse(); try { // 验证token的合法和有效性 String tokenValue = JwtUtil.verity(voToken.getToken()); if (tokenValue != null && tokenValue.startsWith(JwtUtil.TOKEN_SUCCESS)) { // 从令牌中提取实际的用户名 String username = tokenValue.replaceFirst(JwtUtil.TOKEN_SUCCESS, ""); // 记录调用日志 log.info("从令牌中提取的用户名: {}", username); // 调用 MenuService 获取菜单数据 List<VoMenu> menus = menuService.getAllMenus(); // 记录菜单数量的日志 log.info("获取的菜单数量: {}", menus.size()); // 构建响应对象 res.setCode(Constants.STATUS_OK); res.setMsg(Constants.MESSAGE_OK); res.setData(menus); } else { // 记录token验证失败的日志 log.warn("Token验证失败"); // 验证失败 res.setCode(Constants.STATUS_FAIL); res.setMsg(Constants.MESSAGE_FAIL); } } catch (Exception e) { // 记录处理请求时发生的异常 log.error("处理请求时发生异常", e); // 处理异常 res.setCode(60204); res.setMsg("返回失败"); } return res; }
@Servicer
package com.mv.servicer;import com.mv.entity.VO.VoMenu;import java.awt.*;import java.util.List;public interface MenuService { List<VoMenu> getAllMenus();}
@ServiceImpl
package com.mv.servicer.Impl;import com.mv.entity.VO.VoMenu;import com.mv.mapper.MenuMapper;import com.mv.servicer.MenuService;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.awt.*;import java.util.List;@Servicepublic class MenuServiceImpl implements MenuService { @Resource private MenuMapper menuMapper; @Override public List<VoMenu> getAllMenus() { List<VoMenu> allMenus = menuMapper.getAllMenus(); return VoMenu.buildMenuTree(allMenus); } // 可以根据需要添加其他方法}
@Mapper
package com.mv.mapper;import com.mv.entity.VO.VoMenu;import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapperpublic interface MenuMapper { List<VoMenu> getAllMenus();}
@Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.mv.mapper.MenuMapper"> <select id="getAllMenus" resultType="com.mv.entity.VO.VoMenu"> SELECT * FROM menus; </select></mapper>
对应数据库的实体类@VoMenus
package com.mv.entity.VO;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;@Data@NoArgsConstructor@AllArgsConstructorpublic class VoMenu { private Integer id; // 使用 Integer 类型,与数据库表中的 id 类型匹配 private String path; private String component; private String redirect; private String name; private String title; // 新增字段 title private String icon; // 新增字段 icon private Integer parent_id; // 与数据库表中的 parent_id 类型匹配 private List<VoMenu> children; // Getters and setters // Constructors public Integer getId() { return id; } public Integer getParentId() { return parent_id; } public static List<VoMenu> buildMenuTree(List<VoMenu> menuList) { Map<Integer, VoMenu> menuMap = new HashMap<>(); for (VoMenu menu : menuList) { menuMap.put(menu.getId(), menu); } List<VoMenu> tree = new ArrayList<>(); for (VoMenu menu : menuList) { if (menu.getParentId() != null) { VoMenu parent = menuMap.get(menu.getParentId()); if (parent != null) { if (parent.getChildren() == null) { parent.setChildren(new ArrayList<>()); } parent.getChildren().add(menu); } } else { // 如果没有父菜单,说明是顶级菜单 tree.add(menu); } } return tree; }}
后端单方面测试 我使用的是apifox
返回数据成功。
3.数据库menus以及前端目录结构参考
3.1数据库里的父子级菜单
** 我的动态菜单获取并没有设置权限,因为我的后台只有一个管理员admin
3.2.前端目录结构
位于原项目@/views下的permission文件夹
3.3.前后端互联展示
3.3.1后端返回数据:
{ "code": 20000, "msg": "成功,", "data": [ { "id": 1, "path": "/permission", "component": "Layout", "redirect": "/permission/page", "name": "Permission", "title": "Permission", "icon": "lock", "parent_id": null, "children": [ { "id": 2, "path": "/permission/page", "component": "/permission/page", "redirect": null, "name": "PagePermission", "title": "Page Permission", "icon": null, "parent_id": 1, "children": null, "parentId": 1 }, { "id": 3, "path": "/permission/directive", "component": "/permission/directive", "redirect": null, "name": "DirectivePermission", "title": "Directive Permission", "icon": null, "parent_id": 1, "children": null, "parentId": 1 }, { "id": 4, "path": "/permission/role", "component": "/permission/role", "redirect": null, "name": "RolePermission", "title": "Role Permission", "icon": null, "parent_id": 1, "children": null, "parentId": 1 } ], "parentId": null } ]}
3.3.2前端渲染效果:
前后端互联完成。