P17 | 管理台动态路由:后端返回菜单树,前端运行时注入

P17 | 管理台动态路由:后端返回菜单树,前端运行时注入

💰 付费文章 | 第三阶段:Web 管理台


动态路由原理

复制代码
传统方式:前端写死所有路由 → 用户登录后显示/隐藏菜单
问题:前端代码暴露所有路由地址,不安全的路由也能直接访问

本项目方式:前端不写任何路由 → 登录后从后端拉取菜单树 → 运行时 addRoute()
优势:未授权的路由根本不存在,更安全

后端菜单树接口

复制代码
@PostMapping("/menu/tree")
fun getMenuTree(): Result<*> {
    val operator = AuthInterceptor.currentUser.get() ?: return Result.unauthorized()
    
    // 1. 查询运营员的角色
    val roleIds = operatorService.getRoleIds(operator.userId)
    
    // 2. 根据角色查询有权限的菜单
    val menuList = if (roleIds.contains("SUPER_ADMIN")) {
        // 超级管理员看到所有菜单
        menuMapper.selectList(LambdaQueryWrapper<PlatMenuEntity>().eq(PlatMenuEntity::flag, 1))
    } else {
        // 普通运营员只看到角色关联的菜单
        roleMenuMapper.getMenusByRoleIds(roleIds)
    }
    
    // 3. 构建树形结构
    val tree = buildTree(menuList)
    return Result.ok(tree)
}

private fun buildTree(menus: List<PlatMenuEntity>): List<Map<String, Any?>> {
    val menuMap = menus.associateBy { it.commId }
    val rootMenus = menus.filter { it.parentId.isNullOrBlank() || it.parentId == "0" }
    
    return rootMenus.map { menu ->
        buildMenuNode(menu, menuMap)
    }
}

private fun buildMenuNode(menu: PlatMenuEntity, menuMap: Map<String, PlatMenuEntity>): Map<String, Any?> {
    val children = menuMapper.selectList(
        LambdaQueryWrapper<PlatMenuEntity>()
            .eq(PlatMenuEntity::parentId, menu.commId)
            .eq(PlatMenuEntity::flag, 1)
            .orderByAsc(PlatMenuEntity::sort)
    )
    
    return mapOf(
        "menuId" to menu.commId,
        "menuName" to menu.menuName,
        "menuCode" to menu.menuCode,
        "path" to menu.path,
        "component" to menu.component,
        "icon" to menu.icon,
        "sort" to menu.sort,
        "children" to children.map { buildMenuNode(it, menuMap) }
    )
}

前端路由动态注入

复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { post } from '@/api/pikachuNetwork'

// 静态路由(不需要权限的)
const staticRoutes = [
  { path: '/login', component: () => import('@/views/Login.vue') },
  { path: '/', redirect: '/dashboard' }
]

// 主布局
const layoutRoute = {
  path: '/main',
  component: () => import('@/views/Layout.vue'),
  children: [] // 动态填充
}

const router = createRouter({
  history: createWebHistory(),
  routes: staticRoutes
})

// 加载动态路由
export async function loadDynamicRoutes() {
  const menuTree = await post('/plat/menu/tree', {})
  
  const dynamicRoutes = []
  const views = import.meta.glob('../views/**/*.vue')
  
  function processMenu(menus) {
    menus.forEach(menu => {
      if (menu.component && views[`../views/${menu.component}.vue`]) {
        dynamicRoutes.push({
          path: menu.path,
          name: menu.menuCode,
          component: views[`../views/${menu.component}.vue`],
          meta: { title: menu.menuName, icon: menu.icon }
        })
      }
      if (menu.children?.length) {
        processMenu(menu.children)
      }
    })
  }
  
  processMenu(menuTree)
  
  // 注册所有动态路由
  dynamicRoutes.forEach(route => {
    router.addRoute('main', route)
  })
  
  return menuTree  // 返回菜单树,用于渲染侧边栏
}

export default router

侧边栏菜单渲染

复制代码
<!-- Layout.vue -->
<template>
  <el-container>
    <el-aside width="220px">
      <Sidebar :menuTree="menuTree" />
    </el-aside>
    <el-main>
      <router-view />
    </el-main>
  </el-container>
</template>

<script>
export default {
  data() {
    return { menuTree: [] }
  },
  async created() {
    this.menuTree = await loadDynamicRoutes()
  }
}
</script>

按钮权限指令

复制代码
// permission.js
const permissionStore = usePermissionStore()

app.directive('permission', {
  mounted(el, binding) {
    const requiredPermission = binding.value
    if (!permissionStore.hasButton(requiredPermission)) {
      el.parentNode?.removeChild(el) // 彻底移除,不只是隐藏
    }
  }
})

// 使用
<el-button v-permission="'attraction:add'" type="primary">新增景点</el-button>
<el-button v-permission="'attraction:delete'" type="danger">删除</el-button>

下一篇

P18 → Element Plus 管理台:通用 CRUD 页面模板

相关推荐
倾颜1 天前
从 textarea 到 AI 输入框:用 Tiptap 实现 / 命令、@ 引用和结构化请求
前端·langchain·next.js
kyriewen1 天前
程序员连夜带团队跑路,省了23万:这AI太贵,真的用不起了
前端·javascript·openai
kyriewen1 天前
你写的代码没有测试,就像出门不锁门——Jest + Testing Library 从入门到不慌
前端·单元测试·jest
yuzhiboyouye1 天前
web前端英语面试
前端·面试·状态模式
canonical_entropy1 天前
下一代低代码渲染框架 nop-chaos-flux 的设计原则
前端·低代码·前端框架
东方小月1 天前
5分钟搞懂Harness Engineering(驾驭工程):从提示词到AI Agent的进化之路
前端·后端·架构
我叫黑大帅1 天前
为什么需要 @types/react?解决“无法找到模块 react 的声明文件”报错
前端·javascript·面试
之歆1 天前
DAY_21JavaScript 深度解析:数组(Array)与函数(Function)(一)
前端·javascript
XinZong1 天前
【AI社交】基于OpenClaw自研轻量化AI社交平台实战
前端
Le_ee1 天前
ctfweb:php/php短标签/.haccess+图片马/XXE
开发语言·前端·php