动态获取菜单

本文介绍了一个基于Java的菜单系统的实现过程,包括菜单类的设计、数据库表结构的创建、菜单数据的查询及递归处理,最后通过JSON格式展示菜单结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文章来源于:https://blog.youkuaiyun.com/frankcheng5143/article/details/52958486,如有不当之处,请告知删除!

首先看一下菜单的样子

这里写图片描述

根据这个样子我们定义菜单类

public class Menu {
    // 菜单id
    private String id;
    // 菜单名称
    private String name;
    // 父菜单id
    private String parentId;
    // 菜单url
    private String url;
    // 菜单图标
    private String icon;
    // 菜单顺序
    private int order;
    // 子菜单
    private List<Menu> childMenus;
    // ... 省去getter和setter方法以及toString方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们根据这个类定义数据库,并插入菜单数据

DROP TABLE IF EXISTS `jrbac_menu`;
CREATE TABLE `jrbac_menu` (
  `id` varchar(32) NOT NULL COMMENT '主键id,uuid32位',
  `name` varchar(64) NOT NULL COMMENT '登录用户名',
  `parent_id` varchar(32) DEFAULT NULL COMMENT '父菜单id',
  `url` varchar(64) DEFAULT NULL COMMENT '访问地址',
  `icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
  `order` tinyint(4) DEFAULT '0' COMMENT '菜单顺序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜单表';

-- ----------------------------
-- Records of jrbac_menu
-- ----------------------------
INSERT INTO `jrbac_menu` VALUES ('1', 'Forms', null, 'forms.html', 'fa fa-edit', '0');
INSERT INTO `jrbac_menu` VALUES ('2', 'UI Elements', null, '', 'fa fa-wrench', '1');
INSERT INTO `jrbac_menu` VALUES ('3', 'Buttons', '2', 'buttons.html', '', '0');
INSERT INTO `jrbac_menu` VALUES ('4', 'Icons', '2', 'icons.html', null, '1');
INSERT INTO `jrbac_menu` VALUES ('5', 'Multi-Level Dropdown', '', '', 'fa fa-sitemap', '2');
INSERT INTO `jrbac_menu` VALUES ('6', 'Second Level Item', '5', 'second.html', null, '0');
INSERT INTO `jrbac_menu` VALUES ('7', 'Third Level', '5', null, '', '1');
INSERT INTO `jrbac_menu` VALUES ('8', 'Third Level Item', '7', 'third.html', null, '0');
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

为了演示,我们把可展开的没有做完,仅仅插入几条数据能出效果就可以了。

测试方法与递归方法

private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
@Test
public void testQueryMenuList() {
    // 原始的数据
    List<Menu> rootMenu = menuDao.queryMenuList(null);

    // 查看结果
    for (Menu menu : rootMenu) {
        System.out.println(menu);
    }
    // 最后的结果
    List<Menu> menuList = new ArrayList<Menu>();
    // 先找到所有的一级菜单
    for (int i = 0; i < rootMenu.size(); i++) {
        // 一级菜单没有parentId
        if (StringUtils.isBlank(rootMenu.get(i).getParentId())) {
            menuList.add(rootMenu.get(i));
        }
    }
    // 为一级菜单设置子菜单,getChild是递归调用的
    for (Menu menu : menuList) {
        menu.setChildMenus(getChild(menu.getId(), rootMenu));
    }
    Map<String,Object> jsonMap = new HashMap<>();
    jsonMap.put("menu", menuList);
    System.out.println(gson.toJson(jsonMap));

}

/**
 * 递归查找子菜单
 * 
 * @param id
 *            当前菜单id
 * @param rootMenu
 *            要查找的列表
 * @return
 */
private List<Menu> getChild(String id, List<Menu> rootMenu) {
    // 子菜单
    List<Menu> childList = new ArrayList<>();
    for (Menu menu : rootMenu) {
        // 遍历所有节点,将父菜单id与传过来的id比较
        if (StringUtils.isNotBlank(menu.getParentId())) {
            if (menu.getParentId().equals(id)) {
                childList.add(menu);
            }
        }
    }
    // 把子菜单的子菜单再循环一遍
    for (Menu menu : childList) {// 没有url子菜单还有子菜单
        if (StringUtils.isBlank(menu.getUrl())) {
            // 递归
            menu.setChildMenus(getChild(menu.getId(), rootMenu));
        }
    } // 递归退出条件
    if (childList.size() == 0) {
        return null;
    }
    return childList;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

menuDao.queryMenuList(null);查找的结果是一条一条的数据

这里写图片描述

meuDao

package com.jrbac.dao;

import java.util.List;

import com.jrbac.entity.LoginUser;
import com.jrbac.entity.Menu;

public interface MenuDao {

    /**
     * 查找用户的菜单
     * @param loginUser
     * @return
     */
    public List<Menu> queryMenuList(LoginUser loginUser);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

mybatis

<?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.jrbac.dao.MenuDao">
    <select id="queryMenuList" resultType="Menu">
        SELECT 
            id,`name`,parent_id,url,icon,`order`
        FROM 
            jrbac_menu ORDER BY `order` ASC
    </select>
</mapper>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

测试程序的运行结果,对输出的json进行个格式化后的对比

这里写图片描述

最终效果

这里写图片描述

如果你也使用sbadmin后台模版的话,它只做到了二级菜单,三级的没有做展开控制。

要做到三级将sb-admin-2.js的最后一个替换成下面的。

if (element.is('li')) {
    element.addClass('active');
    element.parent().addClass('in');
}
  • 1
  • 2
  • 3
  • 4

再奉上前端jsp页面输出菜单的代码

<c:forEach items="${userMenuList }" var="menu" varStatus="status">
                    <!-- 一级子菜单没有parentId,有url -->
                    <c:if test="${empty menu.parentId and not empty menu.url}">
        <li>
            <a href="<c:url value='${menu.url }'/>">
                <i class="${menu.icon } fa-fw"></i> ${menu.name }
            </a>
        </li>
    </c:if>
    <!-- 可展开的一级菜单,没有parentId,有url -->
    <c:if test="${empty menu.parentId and empty menu.url}">
        <li>
            <a href="#">
                <i class="${menu.icon } fa-fw"></i> ${menu.name }<span class="fa arrow"></span>
            </a>
            <ul class="nav nav-second-level">
                <!-- 没有url的是三级菜单,有url的直接输出到li中 -->
                <c:forEach items="${menu.childMenus}" var="secondChild" varStatus="status">
                    <c:if test="${not empty secondChild.url }">
                        <li>
                                 <a href="<c:url value='${secondChild.url }'/>">${secondChild.name }</a>
                              </li>
                    </c:if>
                    <!-- 二级菜单url为空,表示还有三级菜单 -->
                    <c:if test="${empty secondChild.url }">
                        <li>
                                 <a href="#">${secondChild.name }<span class="fa arrow"></span></a>
                                 <ul class="nav nav-third-level">
                                        <c:forEach items="${secondChild.childMenus}" var="thirdChild" varStatus="status">
                                        <li>
                                           <a href="<c:url value='${thirdChild.url }'/>">${thirdChild.name }</a>
                                       </li>
                                      </c:forEach>
                                 </ul>
                              </li>
                    </c:if>
                </c:forEach>
            </ul>
        </li>
    </c:if>
</c:forEach>
<think>我们有一个需求:在Vue3与TypeScript环境下,用户登录后动态获取菜单数据并展示菜单。 根据引用内容,我们可以总结出以下步骤: 1. 用户登录,发送账号密码,获取token并存储(如localStorage),并在后续请求中携带(如通过axios拦截器)。 2. 登录成功后,跳转到首页,在首页初始化时(或路由守卫中)获取用户信息和菜单数据。 3. 根据菜单数据动态生成路由(使用router.addRoute)和渲染菜单(使用v-for循环菜单数据)。 具体实现步骤: 一、登录获取token 参考引用[3]和[4],我们创建一个登录请求,并将返回的token存储起来,同时设置axios的请求拦截器,在每次请求时携带token。 二、获取用户信息和菜单数据 登录成功后,我们可以获取用户信息,然后根据用户信息(如角色id)获取菜单数据。 三、动态路由添加 根据菜单数据(通常是一个树形结构,包含路由信息)动态添加路由。注意,我们需要在路由守卫中处理,确保在路由跳转前已经添加了动态路由。 四、菜单渲染 将获取菜单数据存储在状态管理(如Pinia)中,然后在菜单组件(如侧边栏)中通过v-for循环渲染。 五、权限控制与缓存 为了避免每次切换路由都重新获取菜单,我们可以将菜单数据缓存到Pinia中,并设置一个标志位表示已经获取过。 下面我们逐步实现: 1. 登录功能 创建登录请求,存储token。 2. 设置axios拦截器 在axios的请求拦截器中,从本地存储获取token并添加到请求头。 3. 路由守卫中获取菜单并添加动态路由 在全局前置守卫(router.beforeEach)中,判断是否已经登录且没有菜单数据,则获取菜单数据并添加路由。 4. 动态添加路由 使用router.addRoute()方法添加路由。注意:动态路由需要在一开始定义好基本路由(如登录页、404等),然后动态添加权限路由。 5. 菜单组件 在菜单组件中,从状态管理中获取菜单数据,进行渲染。 6. 状态管理(Pinia) 使用Pinia存储菜单数据、用户信息等。 具体代码示例: 步骤1:登录请求(参考引用[3]) 在service/login/login.ts中: ```typescript import hyRequest from '..' // 假设这是封装好的axios实例 import type { IAccount } from '@/types' export function accountLoginRequest(account: IAccount) { return hyRequest.post({ url: '/login', data: account }) } ``` 步骤2:登录后存储token,并跳转 在登录组件中: ```typescript import { accountLoginRequest } from '@/service/login/login' const handleLogin = async () => { const res = await accountLoginRequest(account) // 存储token localStorage.setItem('token', res.data.token) // 跳转到首页 router.push('/') } ``` 步骤3:设置axios拦截器(在封装的axios中) ```typescript // 例如在hyRequest的封装中 import axios from 'axios' const instance = axios.create({ baseURL: 'http://xxx' }) instance.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) ``` 步骤4:创建Pinia store存储用户信息和菜单 创建store/user.ts: ```typescript import { defineStore } from 'pinia' interface IUserState { menus: any[] // 菜单数据,根据实际接口定义 userInfo: any hasFetchedMenu: boolean // 是否已获取菜单 } export const useUserStore = defineStore('user', { state: (): IUserState => ({ menus: [], userInfo: {}, hasFetchedMenu: false }), actions: { setMenus(menus: any[]) { this.menus = menus this.hasFetchedMenu = true }, setUserInfo(userInfo: any) { this.userInfo = userInfo }, // 获取用户信息和菜单 async fetchUserInfoAndMenus() { // 如果已经获取过,则跳过 if (this.hasFetchedMenu) return // 获取用户信息(假设需要先获取用户信息,然后根据用户信息中的角色id获取菜单) const userRes = await getUserInfo() // 假设的获取用户信息接口 this.setUserInfo(userRes.data) // 获取菜单 const menuRes = await getMenuByRoleId(userRes.data.roleId) // 根据角色id获取菜单 this.setMenus(menuRes.data) // 动态添加路由 // 这里需要将菜单数据转换成路由配置,然后逐个添加 const routes = convertMenuToRoutes(menuRes.data) // 转换函数需要自己实现 routes.forEach(route => { router.addRoute(route) // 注意:这里添加到根路由,或者根据你的路由结构添加 }) } } }) ``` 步骤5:在路由守卫中调用 在router/index.ts中: ```typescript import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/store/user' const router = createRouter({ history: createWebHistory(), routes: [ // 静态路由,如登录页、首页框架等 { path: '/login', component: () => import('@/views/login.vue') }, { path: '/', component: () => import('@/layouts/MainLayout.vue'), children: [ // 这里可以放一些公共路由,比如首页、404等 { path: '', name: 'home', component: () => import('@/views/home.vue') }, // 动态路由会添加到这个children中吗?注意:我们通常将动态路由添加到某个父路由的children中 ] } ] }) router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const token = localStorage.getItem('token') // 如果用户未登录且访问的不是登录页,则跳转到登录页 if (!token && to.path !== '/login') { next('/login') } // 如果用户已登录且访问的是登录页,则跳转到首页 else if (token && to.path === '/login') { next('/') } // 如果用户已登录且不是登录页 else if (token) { // 如果还没有获取菜单,则获取菜单并添加路由 if (!userStore.hasFetchedMenu) { try { await userStore.fetchUserInfoAndMenus() // 添加路由后,需要重新跳转到目标路由,因为可能添加的路由包含了当前要访问的路由 // 使用next({ ...to, replace: true })避免重复跳转 next({ ...to, replace: true }) } catch (error) { // 获取菜单失败,跳转到登录页或者错误页 next('/login') } } else { next() } } else { next() } }) ``` 步骤6:菜单转换函数 将菜单数据转换为路由配置,需要根据菜单数据中的路径、组件路径等信息。假设菜单数据中每个菜单项包含: - path: 路由路径 - name: 路由名称 - component: 组件路径(字符串,对应views下的文件) 转换函数示例: ```typescript function convertMenuToRoutes(menus: any[]): RouteRecordRaw[] { const routes: RouteRecordRaw[] = [] menus.forEach(menu => { const route: RouteRecordRaw = { path: menu.path, name: menu.name, component: () => import(`@/views/${menu.component}.vue`) // 注意:这里需要保证路径正确,且支持动态导入 } if (menu.children) { route.children = convertMenuToRoutes(menu.children) } routes.push(route) }) return routes } ``` 步骤7:菜单组件 在侧边栏组件中,使用v-for循环菜单数据: ```vue <template> <div> <div v-for="menu in userStore.menus" :key="menu.id"> <!-- 如果有子菜单,可以使用递归组件 --> <router-link v-if="!menu.children" :to="menu.path">{{ menu.name }}</router-link> <el-submenu v-else :index="menu.path"> <template #title>{{ menu.name }}</template> <menu-item :menus="menu.children"></menu-item> </el-submenu> </div> </div> </template> <script setup lang="ts"> import { useUserStore } from '@/store/user' const userStore = useUserStore() </script> ``` 注意:以上代码示例可能需要根据实际项目结构调整,尤其是动态导入组件部分,需要确保路径正确。 总结:通过以上步骤,我们实现了登录后动态获取菜单并展示,同时动态添加路由的功能。关键点在于路由守卫中获取菜单并添加路由,以及菜单数据的转换和渲染。 §§相关问题§§ 1. 如何优化动态路由的添加,避免重复请求菜单数据? 2. 如何实现菜单的权限控制,只显示用户有权限的菜单? 3. 动态添加路由后,如何处理刷新页面时路由丢失的问题?</think>### 在 Vue3 + TypeScript 中实现动态菜单功能 在 Vue3 和 TypeScript 环境下实现用户登录后动态获取并展示菜单的功能,主要涉及以下核心步骤: #### 1. 登录获取 Token ```typescript // service/login/login.ts import request from '@/utils/request' import type { LoginParams } from '@/types' export const login = (data: LoginParams) => { return request.post({ url: '/api/login', data }).then(res => { // 存储token到本地 localStorage.setItem('token', res.data.token) return res }) } ``` #### 2. 获取菜单数据 ```typescript // service/menu.ts import request from '@/utils/request' export const getMenuData = () => { return request.get({ url: '/api/user/menus', headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }) } ``` #### 3. 动态路由处理 ```typescript // router/index.ts import { createRouter, createWebHistory } from 'vue-router' import { getMenuData } from '@/service/menu' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', redirect: '/home' } ] }) // 动态添加路由 export const addDynamicRoutes = async () => { try { const { data } = await getMenuData() const routes = transformMenuToRoutes(data) // 转换菜单为路由格式 routes.forEach(route => { router.addRoute('MainLayout', route) // 添加到主布局下 }) } catch (error) { console.error('加载菜单失败', error) } } // 菜单转路由的转换函数 const transformMenuToRoutes = (menus: MenuItem[]): RouteRecordRaw[] => { return menus.map(menu => ({ path: menu.path, name: menu.name, component: () => import(`@/views/${menu.component}.vue`), meta: { title: menu.title, icon: menu.icon }, children: menu.children ? transformMenuToRoutes(menu.children) : [] })) } ``` #### 4. 路由守卫处理 ```typescript // router/index.ts import { useUserStore } from '@/stores/user' router.beforeEach(async (to, from) => { const userStore = useUserStore() // 已登录且需要动态路由 if (userStore.isLoggedIn && !userStore.hasRoutes) { await addDynamicRoutes() userStore.setHasRoutes(true) return to.fullPath // 重新导航到目标路由 } // 未登录访问受保护路由 if (to.meta.requiresAuth && !userStore.isLoggedIn) { return '/login' } }) ``` #### 5. Pinia 状态管理 ```typescript // stores/user.ts import { defineStore } from 'pinia' interface UserState { menus: MenuItem[] hasRoutes: boolean isLoggedIn: boolean } export const useUserStore = defineStore('user', { state: (): UserState => ({ menus: [], hasRoutes: false, isLoggedIn: !!localStorage.getItem('token') }), actions: { setMenus(menus: MenuItem[]) { this.menus = menus }, setHasRoutes(hasRoutes: boolean) { this.hasRoutes = hasRoutes } } }) ``` #### 6. 菜单组件实现 ```vue <!-- components/Layout/Sidebar.vue --> <script setup lang="ts"> import { useUserStore } from '@/stores/user' const userStore = useUserStore() // 递归渲染菜单项 const renderMenu = (menus: MenuItem[]) => { return menus.map(menu => ( <el-sub-menu index={menu.path} v-slots={{ title: () => menu.title }}> {menu.children && renderMenu(menu.children)} </el-sub-menu> )) } </script> <template> <el-menu> {renderMenu(userStore.menus)} </el-menu> </template> ``` #### 7. 类型定义 ```typescript // types/menu.d.ts interface MenuItem { id: number path: string name: string title: string icon?: string component: string children?: MenuItem[] } interface RouteRecordRaw { path: string name: string component: Component meta?: { title: string; icon?: string } children?: RouteRecordRaw[] } ``` ### 关键实现要点 1. **权限控制流程**: - 用户登录获取 token - 使用 token 请求菜单接口 - 转换菜单数据为路由配置 - 通过 `router.addRoute()` 动态添加路由[^4][^5] 2. **路由守卫优化**: - 使用标志位 `hasRoutes` 避免重复加载 - 首次加载后重定向到目标路由 - 未登录用户自动跳转登录页 3. **菜单数据转换**: ```typescript // 示例菜单数据结构 const menuData = [ { path: '/dashboard', name: 'Dashboard', component: 'Dashboard', children: [ { path: 'analysis', name: 'Analysis', component: 'Analysis' } ] } ] ``` 4. **刷新页面处理**: ```typescript // App.vue onMounted(async () => { if (userStore.isLoggedIn && !userStore.hasRoutes) { await addDynamicRoutes() } }) ``` ### 注意事项 1. 组件路径需要与后端返回的 `component` 字段匹配 2. 动态导入组件使用 `() => import('@/views/...')` 语法 3. 路由层级需要与菜单数据结构一致 4. 使用 `router.removeRoute()` 清理旧路由[^5] 5. 敏感路由(如管理页面)需要后端额外校验权限 此方案实现了完整的动态菜单加载流程,通过路由守卫、Pinia 状态管理和动态路由添加,确保不同权限用户看到不同的菜单结构[^4][^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值