# 动态菜单 **Repository Path**: sspku_admin/dynamic-menu ## Basic Information - **Project Name**: 动态菜单 - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2023-11-29 - **Last Updated**: 2023-12-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 动态路由与菜单 在使用 Vue 和 SpringBoot 进行前后端开发时,动态路由和菜单机制的引入主要是为了提高应用的灵活性、安全性和可维护性。 1. **用户权限管理**:在多用户、多角色的系统中,不同的用户或角色可能会有不同的访问权限。动态路由可以根据用户的权限动态生成,从而确保用户只能访问他们被授权的路由。 2. **菜单的个性化**:动态菜单可以根据用户的角色或偏好设置来个性化,提供更好的用户体验。例如,管理员可能需要看到系统的全面管理选项,而普通用户则只需要看到与他们相关的功能。 3. **前端和后端分离**:在前后端分离的架构中,前端可以通过调用后端的 API 来获取动态路由和菜单数据,这样前端页面的变更就不需要修改后端代码,提高了开发效率。 4. **应对业务变化**:随着业务的发展和需求的变化,系统的菜单结构和路由可能需要调整。动态路由和菜单机制使得这种调整变得更加灵活和方便。 5. **减少前端的重复编码**:如果路由和菜单是硬编码在前端代码中,每次变更都需要修改代码并重新部署。动态生成可以大幅减少这种重复劳动。 6. **安全性**:通过后端控制路由和菜单的生成,可以更有效地控制用户对应用的访问,避免未授权的访问,增强系统的安全性。 动态路由和菜单机制为应用提供了更大的灵活性和安全性,尤其适用于权限复杂、用户众多、业务经常变更的现代Web应用。 ## 设计思路 ### 前端(Vue2 + ElementUI) 1. **路由设计:** - 使用 Vue Router,创建一个基础路由文件。 - 设计动态加载路由的机制。当用户登录后,根据服务器返回的菜单数据动态添加路由。 2. **菜单组件:** - 使用 ElementUI 提供的导航组件(如 `el-menu`)创建菜单。 - 菜单数据根据后端提供的数据动态生成。 - 菜单项的点击应更新视图,反映相应的路由变化。 3. **用户权限管理:** - 用户登录后,获取用户权限信息。 - 根据用户权限动态展示菜单项。 4. **与后端通信:** - 使用 Axios 或其他 HTTP 客户端与后端 API 交互。 - 在用户登录后,请求动态路由和菜单数据。 ### 后端(SpringBoot + MySQL) 1. **数据库设计:** - 创建表格来存储用户信息、角色、权限和菜单结构。 - 这些表格应该相互关联,如用户与角色、角色与权限、权限与菜单项。 2. **SpringBoot 设置:** - 创建 RESTful API 接口,用于用户登录、获取菜单数据等。 - 使用 Spring Security 或类似框架实现身份验证和授权。 3. **动态菜单生成:** - 当用户登录成功后,根据其角色和权限从数据库中查询相应的菜单数据。 - 将菜单数据格式化为前端可识别的格式(如 JSON),然后发送到前端。 4. **配置文件管理:** - 使用 YAML 格式的配置文件管理应用配置。 - 可以配置数据库连接、安全设置等。 ## 开发步骤 ### 设计数据库 为了实现一个动态路由和菜单系统,数据库设计应该包括用户、角色、权限和菜单项等核心部分。以下是这些表格的基本设计: #### 1. 用户表(users) | 字段名 | 数据类型 | 描述 | |-----------|--------------|----------------| | id | INT | 主键,自增 | | username | VARCHAR(255) | 用户名 | | password | VARCHAR(255) | 密码(加密存储) | | email | VARCHAR(255) | 邮箱 | | status | TINYINT | 账户状态(如激活、禁用) | #### 2. 角色表(roles) | 字段名 | 数据类型 | 描述 | |------|--------------|--------| | id | INT | 主键,自增 | | name | VARCHAR(255) | 角色名称 | #### 3. 用户角色关联表(user_roles) | 字段名 | 数据类型 | 描述 | |---------|------|----------------| | user_id | INT | 用户表的外键 | | role_id | INT | 角色表的外键 | #### 4. 权限表(permissions) | 字段名 | 数据类型 | 描述 | |------|--------------|--------| | id | INT | 主键,自增 | | name | VARCHAR(255) | 权限名称 | #### 5. 角色权限关联表(role_permissions) | 字段名 | 数据类型 | 描述 | |------------|------|----------------| | role_id | INT | 角色表的外键 | | permission_id | INT | 权限表的外键 | #### 6. 菜单表(menus) | 字段名 | 数据类型 | 描述 | |-------------|--------------|--------------------| | id | INT | 主键,自增 | | parent_id | INT | 父菜单ID(如果是顶级菜单则为NULL) | | name | VARCHAR(255) | 菜单名称 | | path | VARCHAR(255) | 路由路径 | | component | VARCHAR(255) | 对应的Vue组件 | | icon | VARCHAR(255) | 图标 | | order | INT | 菜单排序 | #### 7. 权限菜单关联表(permission_menus) | 字段名 | 数据类型 | 描述 | |--------------|------|--------------| | permission_id | INT | 权限表的外键 | | menu_id | INT | 菜单表的外键 | ### 创建数据库表 ```sql CREATE TABLE `users` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(255) NOT NULL, `password` VARCHAR(255) NOT NULL, `email` VARCHAR(255), `status` TINYINT NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `roles` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_roles` ( `user_id` INT NOT NULL, `role_id` INT NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`), FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ); CREATE TABLE `permissions` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role_permissions` ( `role_id` INT NOT NULL, `permission_id` INT NOT NULL, FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`), FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ); CREATE TABLE `menus` ( `id` INT NOT NULL AUTO_INCREMENT, `parent_id` INT, `name` VARCHAR(255) NOT NULL, `path` VARCHAR(255) NOT NULL, `component` VARCHAR(255), `icon` VARCHAR(255), `order` INT, PRIMARY KEY (`id`) ); CREATE TABLE `permission_menus` ( `permission_id` INT NOT NULL, `menu_id` INT NOT NULL, FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`), FOREIGN KEY (`menu_id`) REFERENCES `menus`(`id`) ); INSERT INTO `users` (`username`, `password`, `email`, `status`) VALUES ('user1', 'password1', 'user1@example.com', 1), ('user2', 'password2', 'user2@example.com', 1); INSERT INTO `roles` (`name`) VALUES ('Administrator'), ('User'); INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES (1, 1), (2, 2); INSERT INTO `permissions` (`name`) VALUES ('READ'), ('WRITE'); INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES (1, 1), (1, 2), (2, 1); INSERT INTO `menus` (`parent_id`, `name`, `path`, `component`, `icon`, `order`) VALUES (NULL, 'Home', '/home', 'HomeComponent', 'home-icon', 1), (NULL, 'Settings', '/settings', 'SettingsComponent', 'settings-icon', 2); INSERT INTO `permission_menus` (`permission_id`, `menu_id`) VALUES (1, 1), (2, 2); ``` ### 实现 SpringBoot 后端 使用MyBatis来实现这个功能,你需要定义相应的Mapper接口、XML映射文件以及服务层和控制器代码。以下是一个简化的实现示例: #### 1. Mapper 接口 首先,定义用于查询用户信息、角色、权限和菜单的Mapper接口。 ```java public interface UserMapper { User findUserById(Long userId); } public interface RoleMapper { List findRolesByUserId(Long userId); } public interface PermissionMapper { List findPermissionsByUserId(Long userId); } public interface MenuMapper { List findMenusByUserId(Long userId); } ``` #### 2. MyBatis XML 映射文件 为每个Mapper创建一个XML映射文件。例如,对于`UserMapper`: UserMapper.xml ```xml ``` RoleMapper.xml ```xml ``` PermissionMapper.xml ```xml ``` MenuMapper.xml ```xml ``` 为`RoleMapper`, `PermissionMapper` 和 `MenuMapper` 也需要类似地创建映射文件。 #### 3. 服务层代码 在服务层,聚合数据并返回: ```java @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; @Autowired private MenuMapper menuMapper; public UserMenuDTO getUserMenus(Long userId) { User user = userMapper.findUserById(userId); List roles = roleMapper.findRolesByUserId(userId); List roleNames = roles.stream().map(Role::getName).collect(Collectors.toList()); List permissions = permissionMapper.findPermissionsByUserId(userId); List permissionNames = permissions.stream().map(Permission::getName).collect(Collectors.toList()); List menus = menuMapper.findMenusByUserId(userId); return new UserMenuDTO(user, roleNames, permissionNames, menus); } } ``` #### 4. 控制器代码 创建一个REST控制器来处理HTTP请求: ```java @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("/{userId}/menus") public ResponseEntity getUserMenus(@PathVariable Long userId) { UserMenuDTO userMenuDTO = userService.getUserMenus(userId); return ResponseEntity.ok(userMenuDTO); } } ``` #### 5. DTO定义 定义用于API响应的DTO: ```java public class UserMenuDTO { private Integer code; private UserData data; private String msg; // 构造函数、getters和setters public static class UserData { private User user; private List roles; private List permissions; private List menus; // 构造函数、getters和setters } } ``` #### 6. 测试 http://127.0.0.1:28080/api/user/1/menus ### 直接写一个测试用的后端 #### pom文件 ``` org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime org.projectlombok lombok 1.18.20 provided mysql mysql-connector-java 8.0.27 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.0 ``` #### application.yml ```yml server: port: 28080 spring: datasource: url: jdbc:mysql://localhost:3306/dmenu username: dmenu password: dmenu mybatis: mapper-locations: classpath:mapper/*.xml ``` #### 数据类 ```java import lombok.Data; @Data public class ApiResponse { private int code; private ResData data; private String msg; } ``` ```java import lombok.Data; import java.util.List; @Data public class Menu { private int id; private int parentId; private String name; private String path; private String component; private String componentName; private String icon; private boolean visible; private boolean keepAlive; private boolean alwaysShow; private List children; } ``` ```java import lombok.Data; @Data public class Role { private Long id; private String name; } ``` ```java import lombok.Data; @Data public class User { private int id; private String nickname; } ``` ```java import lombok.Data; import java.util.List; @Data public class ResData { private User user; private List roles; private List permissions; private List menus; } ``` #### Controller ```jva import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/user") public class UserController { @PostMapping("/login") public ResponseEntity> login() { Map response = new HashMap<>(); Map data = new HashMap<>(); data.put("userId", 1); data.put("accessToken", "f9478e1a5b4f42e789ee0c9bbc393389"); data.put("refreshToken", "aa2c6f2deb1e49eb9ea0a295148d3389"); data.put("expiresTime", 1701758599773L); response.put("code", 0); response.put("data", data); response.put("msg", ""); return ResponseEntity.ok(response); } @GetMapping("/getInfo") public ApiResponse getTestData() { ApiResponse response = new ApiResponse(); response.setCode(0); response.setData(createTestData()); response.setMsg(""); return response; } private ResData createTestData() { ResData data = new ResData(); // 创建并设置用户信息 User user = new User(); user.setId(1); user.setNickname("我是程序员"); data.setUser(user); // 设置角色和权限 data.setRoles(Arrays.asList("common", "super_admin")); data.setPermissions(Arrays.asList("", "promotion:seckill-activity:update","crm:receivable-plan:create")); // 设置菜单 List menus = Arrays.asList( createMenu(2397, 0, "客户管理系统", "/crm", null, null, "ep:avatar", true, true, true, Arrays.asList( createMenu(2391, 2397, "客户管理", "customer", "crm/customer/index", "CrmCustomer", "fa:address-book-o", true, true, true, null), createMenu(2404, 2397, "线索管理", "clue", "crm/clue/index", "CrmClue", "fa:pagelines", true, true, true, null), createMenu(2524, 2397, "系统配置", "config", null, null, "ep:connection", true, true, true, Arrays.asList( createMenu(2516, 2524, "客户公海配置", "customer-pool-config", "crm/customerPoolConf/index", "CustomerPoolConf", "ep:data-analysis", true, true, true, null), createMenu(2518, 2524, "客户限制配置", "customer-limit-config", "crm/customerLimitConfig/index", "CrmCustomerLimitConfig", "ep:avatar", true, true, true, null) )) )), createMenu(3300, 0, "会员管理2", "/member", "member/list", "MemberList", "ep:avatar", true, true, true,null) ); data.setMenus(menus); return data; } private Menu createMenu(int id, int parentId, String name, String path, String component, String componentName, String icon, boolean visible, boolean keepAlive, boolean alwaysShow, List children) { Menu menu = new Menu(); menu.setId(id); menu.setParentId(parentId); menu.setName(name); menu.setPath(path); menu.setComponent(component); menu.setComponentName(componentName); menu.setIcon(icon); menu.setVisible(visible); menu.setKeepAlive(keepAlive); menu.setAlwaysShow(alwaysShow); menu.setChildren(children); return menu; } } ``` #### 测试接口 http://127.0.0.1:28080/api/user/getInfo ### 开发 Vue 前端,实现动态路由和菜单 ```bash vue create web2 ``` ```bash yarn add element-ui --save yarn add axios --save ``` 在 `main.js` 中导入 Element-UI 和它的样式: ```javascript import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); ``` #### 使用Vuex `store/index.js`文件 ```java import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // Vuex 提供了一种集中管理应用所有组件的状态的方式 // 并且以一种可预测的方式更新状态 // 这对于大型应用来说是非常有用的 // 创建Vuex对象 export default new Vuex.Store({ state: { // state 是 Vuex store 的基本数据,它是存储的核心。 // 可以通过 this.$store.state 来访问 Vuex store 的 state。 // 例如,this.$store.state.counter 将访问 counter 状态。 counter: 1000, }, getters: { // getters 类似于 Vue 中的计算属性。 // 用于从 Vuex store 的 state 中派生出一些状态。 // 例如,可以有一个 getter 来返回 counter 的平方值。 // 可通过 this.$store.getters.yourGetter 来使用。 squaredCounter: state => { return state.counter * state.counter }, }, mutations: { // mutations 是更改 Vuex store 中的状态的唯一方法 // 要更改状态,需要使用 commit 方法来调用 mutation // 如 this.$store.commit('increment')。 increment(state){ state.counter++ }, decrement(state){ state.counter-- }, }, actions: { // Actions通常用于处理异步操作。然后调用 mutation 来更改状态。 // Action 通过 store.dispatch 方法触发。 // 例如:可以通过 this.$store.dispatch('yourAction') 来使用。 incrementAsync({ commit }) { setTimeout(() => { commit('increment') }, 1000) }, }, modules: { // 在 Vuex 中,modules 是用于将整个 Vuex store 分割成模块。 // 每个模块可以拥有自己的 state、mutations、actions、getters, // 甚至是嵌套子模块。这种结构有利于大型项目的状态管理, // 因为它允许将不同部分的状态管理逻辑分隔开,使得代码更加清晰和易于维护。 } }) ``` ```vue ``` ```vue ``` #### 创建Layout通用布局 - `layout` 目录:用于存放系统的基本布局模板。该目录下的文件通常包括顶部导航栏、左侧菜单栏、底部版权信息等。 **layout/index.vue**: ```vue ``` **router/index.js**: ```javascript /* Layout */ import Layout from '@/layout' const routes = [ { path: '/', component: Layout, // 修改为 Layout 组件 children: [ { path: '', component: () => import( '../views/HomeView.vue'), name: 'home' } ] }, { path: '/rbac', component: Layout, children: [{ path: 'user/list', component:() => import( '../views/User/List.vue') }, { path: 'user/add', component:() => import( '../views/User/Add.vue') } ] }, ] // 防止连续点击多次路由报错 let routerPush = VueRouter.prototype.push; VueRouter.prototype.push = function push(location) { return routerPush.call(this, location).catch(err => err) } ``` ```bash yarn serve ``` #### 实现登录功能 **utils/request.js**: ```javascript import axios from 'axios'; const service = axios.create({ baseURL: '/api', timeout: 5000 }); service.interceptors.request.use( config => { // 在请求发送之前对请求数据进行处理 // ... return config; }, error => { // 对请求错误做些什么 console.log(error); return Promise.reject(error); } ); service.interceptors.response.use( response => { // 对响应数据进行处理 // ... return response.data; }, error => { // 对响应错误做些什么 console.log(error); return Promise.reject(error); } ); export default service; ``` **views/Login.vue**: ```vue ``` **router/index.js**: ```javascript { path: '/login', name: 'login', component: () => import('../views/Login.vue') }, ``` **api/login.js**: ```javascript import request from '@/utils/request' // 用户登录 export function login(username, password) { const data = { username, password } return request({ url: '/user/login', method: 'post', data: data }) } // 退出方法 export function logout() { //可以发送退出请求 } // 获取用户信息 export function getInfo() { return request({ url: '/user/getInfo', method: 'get' }) } ``` **views/login.vue**: ```vue do_login() { this.$refs.form.validate(valid => { if (valid) { this.loading = true this.$store.dispatch("Login", this.loginForm).then(() => { this.$router.push({path: this.redirect || "/"}).catch(() => { }); }).catch(() => { this.loading = false; }); } }) } ``` **utils/auth.js**: ```java const AccessTokenKey = 'ACCESS_TOKEN' const RefreshTokenKey = 'REFRESH_TOKEN' // ========== Token 相关 ========== export function getAccessToken() { return localStorage.getItem(AccessTokenKey) } export function getRefreshToken() { return localStorage.getItem(RefreshTokenKey) } export function setToken(token) { localStorage.setItem(AccessTokenKey, token.accessToken) localStorage.setItem(RefreshTokenKey, token.refreshToken) } export function removeToken() { localStorage.removeItem(AccessTokenKey) localStorage.removeItem(RefreshTokenKey) } ``` **utils/index.js**: ```java // -转驼峰 export function toCamelCase(str, upperCaseFirst) { str = (str || '').toLowerCase().replace(/-(.)/g, function (match, group1) { return group1.toUpperCase(); }); if (upperCaseFirst && str) { str = str.charAt(0).toUpperCase() + str.slice(1); } return str; } ``` ```java actions: { // 登录 Login({ commit }, userInfo) { const username = userInfo.username.trim() const password = userInfo.password // Promise 是 JavaScript 中用于处理异步操作的一个对象。 // Promise 可以处于以下三种状态之一: // Pending(待定):初始状态,既不是成功,也不是失败。 // Fulfilled(已实现):表示操作成功完成。 // Rejected(已拒绝):表示操作失败。 // resolve(): 将状态从 "Pending" 改变为 "Fulfilled" // reject(error): 将状态从 "Pending" 改变为 "Rejected" return new Promise((resolve, reject) => { login(username, password).then(res => { res = res.data; // 设置 token setToken(res) resolve() }).catch(error => { reject(error) }) }) }, }, ``` #### 实现动态路由 ```java import './permission' ``` ```java import router from './router' import store from './store' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import { getAccessToken } from '@/utils/auth' NProgress.configure({ showSpinner: false }) const whiteList = ['/login', '/register'] router.beforeEach((to, from, next) => { console.log("to") console.log(to) NProgress.start() if (getAccessToken()) {// 有token if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { if (store.getters.roles.length === 0) { console.log("permission store.getters.roles.length") // 判断当前用户是否已拉取完 user_info 信息 store.dispatch('GetInfo').then(userInfo => { // 触发 GenerateRoutes 事件时,将 menus 菜单树传递进去 store.dispatch('GenerateRoutes', userInfo.menus).then(accessRoutes => { // 根据 roles 权限生成可访问的路由表 router.addRoutes(accessRoutes) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch(err => { store.dispatch('LogOut').then(() => { next({ path: '/' }) }) }) } else { next() } } } else {// 没有token if (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入 next() } else { const redirect = encodeURIComponent(to.fullPath) // 编码 URI,保证参数跳转回去后,可以继续带上 next(`/login?redirect=${redirect}`) // 否则全部重定向到登录页 NProgress.done() } } }) router.afterEach(() => { NProgress.done() }) ``` ``` import Vue from 'vue' import Vuex from 'vuex' import Layout from '@/layout/index' import ParentView from '@/components/ParentView'; import {constantRoutes} from '@/router' Vue.use(Vuex) // Vuex 提供了一种集中管理应用所有组件的状态的方式 // 并且以一种可预测的方式更新状态 // 这对于大型应用来说是非常有用的 import {login, logout, getInfo} from '@/api/login' import {setToken, removeToken} from '@/utils/auth' import {toCamelCase} from "@/utils"; // 遍历后台传来的路由字符串,转换为组件对象 function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { return asyncRouterMap.filter(route => { // 将 ruoyi 后端原有耦合前端的逻辑,迁移到此处 // 处理 meta 属性 route.meta = { title: route.name, icon: route.icon, noCache: !route.keepAlive, } route.hidden = !route.visible // 处理 name 属性 if (route.componentName && route.componentName.length > 0) { route.name = route.componentName } else { // 路由地址转首字母大写驼峰,作为路由名称,适配 keepAlive route.name = toCamelCase(route.path, true) // 处理三级及以上菜单路由缓存问题,将 path 名字赋值给 name if (route.path.indexOf("/") !== -1) { const pathArr = route.path.split("/"); route.name = toCamelCase(pathArr[pathArr.length - 1], true) } } // 处理 component 属性 if (route.children) { // 父节点 if (route.parentId === 0) { route.component = Layout } else { route.component = ParentView } } else { // 根节点 console.log(route.component) route.component = loadView(route.component) } // filterChildren if (type && route.children) { route.children = filterChildren(route.children) } if (route.children != null && route.children && route.children.length) { route.children = filterAsyncRouter(route.children, route, type) route.alwaysShow = route.alwaysShow !== undefined ? route.alwaysShow : true } else { delete route['children'] delete route['alwaysShow'] // 如果没有子菜单,就不需要考虑 alwaysShow 字段 } return true }) } function filterChildren(childrenMap, lastRouter = false) { let children = []; childrenMap.forEach((el, index) => { if (el.children && el.children.length) { if (!el.component && !lastRouter) { el.children.forEach(c => { c.path = el.path + '/' + c.path if (c.children && c.children.length) { children = children.concat(filterChildren(c.children, c)) return } children.push(c) }) return } } if (lastRouter) { el.path = lastRouter.path + '/' + el.path } children = children.concat(el) }) return children } export const loadView = (view) => { // 路由懒加载 console.log(view) return () => import(`@/views/${view}`) .catch(error => console.error(`Dynamic page loading failed: ${error}`)); // return () => import(`@/views/${view}`); // return () => import(/* webpackChunkName: "view-[request]" */ `@/views/${view}`); // return (resolve) => import([`@/views/${view}`], resolve) } // 创建Vuex对象 export default new Vuex.Store({ state: { // state 是 Vuex store 的基本数据,它是存储的核心。 // 可以通过 this.$store.state 来访问 Vuex store 的 state。 // 例如,this.$store.state.counter 将访问 counter 状态。 counter: 1000, id: 0, // 用户编号 name: '', roles: [], permissions: [], //动态路由及菜单数据 routes: [], addRoutes: [], sidebarRouters: [], // 左侧边菜单的路由,被 Sidebar/index.vue 使用 topbarRouters: [], // 顶部菜单的路由,被 TopNav/index.vue 使用 }, mutations: { // mutations 是更改 Vuex store 中的状态的唯一方法 // 要更改状态,需要使用 commit 方法来调用 mutation // 如 this.$store.commit('increment')。 increment(state){ state.counter++ }, decrement(state){ state.counter-- }, SET_ID: (state, id) => { state.id = id }, SET_NAME: (state, name) => { state.name = name }, SET_NICKNAME: (state, nickname) => { state.nickname = nickname }, SET_AVATAR: (state, avatar) => { state.avatar = avatar }, SET_ROLES: (state, roles) => { state.roles = roles }, SET_PERMISSIONS: (state, permissions) => { state.permissions = permissions }, SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) }, SET_DEFAULT_ROUTES: (state, routes) => { state.defaultRoutes = constantRoutes.concat(routes) }, SET_TOPBAR_ROUTES: (state, routes) => { state.topbarRouters = routes }, SET_SIDEBAR_ROUTERS: (state, routes) => { state.sidebarRouters = routes }, }, actions: { // Actions通常用于处理异步操作。然后调用 mutation 来更改状态。 // Action 通过 store.dispatch 方法触发。 // 例如:可以通过 this.$store.dispatch('yourAction') 来使用。 incrementAsync({ commit }) { setTimeout(() => { commit('increment') }, 1000) }, // 登录 Login({ commit }, userInfo) { const username = userInfo.username.trim() const password = userInfo.password // Promise 是 JavaScript 中用于处理异步操作的一个对象。 // Promise 可以处于以下三种状态之一: // Pending(待定):初始状态,既不是成功,也不是失败。 // Fulfilled(已实现):表示操作成功完成。 // Rejected(已拒绝):表示操作失败。 // resolve(): 将状态从 "Pending" 改变为 "Fulfilled" // reject(error): 将状态从 "Pending" 改变为 "Rejected" return new Promise((resolve, reject) => { login(username, password).then(res => { res = res.data; // 设置 token setToken(res) resolve() }).catch(error => { reject(error) }) }) }, // 获取用户信息 GetInfo({ commit, state }) { return new Promise((resolve, reject) => { getInfo().then(res => { // 没有 data 数据,赋予个默认值 if (!res) { res = { data: { roles: [], user: { id: '', avatar: '', userName: '', nickname: '' } } } } res = res.data; // 读取 data 数据 const user = res.user if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 commit('SET_ROLES', res.roles) commit('SET_PERMISSIONS', res.permissions) } else { commit('SET_ROLES', ['ROLE_DEFAULT']) } commit('SET_ID', user.id) commit('SET_NAME', user.userName) resolve(res) }).catch(error => { reject(error) }) }) }, // 退出系统 LogOut({ commit, state }) { return new Promise((resolve, reject) => { logout(state.token).then(() => { commit('SET_ROLES', []) commit('SET_PERMISSIONS', []) removeToken() resolve() }).catch(error => { reject(error) }) }) }, // 生成路由 GenerateRoutes({commit}, menus) { return new Promise(resolve => { // 将 menus 菜单,转换为 route 路由数组 const sdata = JSON.parse(JSON.stringify(menus)) // 【重要】用于菜单中的数据 const rdata = JSON.parse(JSON.stringify(menus)) // 用于最后添加到 Router 中的数据 const sidebarRoutes = filterAsyncRouter(sdata) const rewriteRoutes = filterAsyncRouter(rdata, false, true) rewriteRoutes.push({path: '*', redirect: '/404', hidden: true}) commit('SET_ROUTES', rewriteRoutes) commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) commit('SET_DEFAULT_ROUTES', sidebarRoutes) commit('SET_TOPBAR_ROUTES', sidebarRoutes) resolve(rewriteRoutes) }) } }, getters: { // getters 类似于 Vue 中的计算属性。 // 用于从 Vuex store 的 state 中派生出一些状态。 // 例如,可以有一个 getter 来返回 counter 的平方值。 // 可通过 this.$store.getters.yourGetter 来使用。 squaredCounter: state => { return state.counter * state.counter }, roles: state => state.roles, sidebarRouters:state => state.sidebarRouters, }, }) ``` #### 实现动态菜单 ```java ``` `/src/components/ParentView/index.vue` ```vue ```