feat: 动态菜单路由

This commit is contained in:
2025-10-22 13:11:43 +08:00
parent 58f028cad0
commit 2d031fed53
11 changed files with 149 additions and 128 deletions
+46
View File
@@ -0,0 +1,46 @@
export default ({ service, request, serviceForMock, requestForMock, mock, faker, tools }) => ({
/**
* @description 获取菜单与路由
*/
SYS_MENU () {
// 模拟数据
mock
.onAny('/menu')
.reply(() => {
const aside = [
{
title: '百度一下',
icon: 'folder-o',
path: 'https://www.baidu.com'
},
{
title: '单页面',
icon: 'folder-o',
path: '/page1'
},
{
title: '二级目录',
icon: 'folder-o',
children: [
{ path: '/page2', title: '页面 2' },
{ path: '/page3', title: '页面 3' },
{ path: '/page4', title: '页面 4' }
]
}
]
const header = aside
const routes = [
{ path: 'page1', name: 'page1', meta: { title: '单页面', auth: true }, component: 'demo/page1' },
{ path: 'page2', name: 'page2', meta: { title: '页面 2', auth: true }, component: 'demo/page2' },
{ path: 'page3', name: 'page3', meta: { title: '页面 3', auth: true }, component: 'demo/page3' },
{ path: 'page4', name: 'page4', meta: { title: '页面 4', auth: true }, component: 'demo/page4' }
]
return tools.responseSuccess({ header, aside, routes })
})
// 接口请求
return requestForMock({
url: '/menu',
method: 'get'
})
}
})
+4 -2
View File
@@ -4,5 +4,7 @@ import d2Container from './d2-container'
// 注意 有些组件使用异步加载会有影响
Vue.component('d2-container', d2Container)
Vue.component('d2-icon', () => import('./d2-icon'))
Vue.component('d2-icon-svg', () => import('./d2-icon-svg/index.vue'))
import d2Icon from './d2-icon'
import d2IconSvg from './d2-icon-svg/index.vue'
Vue.component('d2-icon', d2Icon)
Vue.component('d2-icon-svg', d2IconSvg)
+3 -7
View File
@@ -9,9 +9,11 @@ import store from '@/store/index'
// 菜单和路由设置
import router from './router'
import { menuHeader, menuAside } from '@/menu'
import { frameInRoutes } from '@/router/routes'
// 全局提前加载字体图标样式,避免首次渲染闪烁
import '@/components/d2-icon/font-awesome-4.7.0/css/font-awesome.min.css'
// 核心插件
Vue.use(d2Admin)
@@ -23,12 +25,6 @@ new Vue({
created () {
// 处理路由 得到每一级的路由设置
this.$store.commit('d2admin/page/init', frameInRoutes)
// 设置顶栏菜单
this.$store.commit('d2admin/menu/headerSet', menuHeader)
// 设置侧边栏菜单
this.$store.commit('d2admin/menu/asideSet', menuAside)
// 初始化菜单搜索功能
this.$store.commit('d2admin/search/init', menuHeader)
},
mounted () {
// 展示系统信息
+3 -30
View File
@@ -1,10 +1,5 @@
import { uniqueId } from 'lodash'
/**
* @description 给菜单数据补充上 path 字段
* @description https://github.com/d2-projects/d2-admin/issues/209
* @param {Array} menu 原始的菜单数据
*/
function supplementPath (menu) {
return menu.map(e => ({
...e,
@@ -15,28 +10,6 @@ function supplementPath (menu) {
}))
}
export const menuHeader = supplementPath([
{ path: '/index', title: '首页', icon: 'home' },
{
title: '页面',
icon: 'folder-o',
children: [
{ path: '/page1', title: '页面 1' },
{ path: '/page2', title: '页面 2' },
{ path: '/page3', title: '页面 3' }
]
}
])
export const menuAside = supplementPath([
{ path: '/index', title: '首页', icon: 'home' },
{
title: '页面',
icon: 'folder-o',
children: [
{ path: '/page1', title: '页面 1' },
{ path: '/page2', title: '页面 2' },
{ path: '/page3', title: '页面 3' }
]
}
])
// 启动时不再使用本地菜单,改为登录后从服务端获取
export const menuHeader = supplementPath([])
export const menuAside = supplementPath([])
+43 -7
View File
@@ -10,6 +10,7 @@ import util from '@/libs/util.js'
// 路由数据
import routes from './routes'
import api from '@/api'
// fix vue-router NavigationDuplicated
const VueRouterPush = VueRouter.prototype.push
@@ -23,6 +24,8 @@ VueRouter.prototype.replace = function replace (location) {
Vue.use(VueRouter)
let dynamicAdded = false
// 导出路由 在 main.js 里使用
const router = new VueRouter({
routes
@@ -41,27 +44,60 @@ router.beforeEach(async (to, from, next) => {
NProgress.start()
// 关闭搜索面板
store.commit('d2admin/search/set', false)
// 如果已登录且尚未注入动态菜单与路由,先从服务端获取并注入,避免刷新后菜单丢失
const token = util.cookies.get('token')
if (token && token !== 'undefined' && !dynamicAdded) {
try {
// 重要:避免当前是 /login 时注入导致无限跳转
if (to.name === 'login') {
next()
return
}
const menuRes = await api.SYS_MENU()
const { header = [], aside = [], routes = [] } = menuRes
store.commit('d2admin/menu/headerSet', header)
store.commit('d2admin/menu/asideSet', aside)
store.commit('d2admin/search/init', header)
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)
const layoutHeaderAside = require('@/layout/header-aside').default
const dynamicChildren = routes.map(r => ({
path: r.path,
name: r.name,
meta: r.meta || {},
component: _import(r.component)
}))
if (dynamicChildren.length) {
router.addRoutes([{ path: '/', component: layoutHeaderAside, children: dynamicChildren }])
const { frameInRoutes } = require('@/router/routes')
const dynamicFrameIn = JSON.parse(JSON.stringify(frameInRoutes))
if (dynamicFrameIn && dynamicFrameIn[0] && Array.isArray(dynamicFrameIn[0].children)) {
dynamicFrameIn[0].children = dynamicFrameIn[0].children.concat(dynamicChildren)
}
store.commit('d2admin/page/init', dynamicFrameIn)
dynamicAdded = true
// 关键:重新进入当前路由,确保新注册的路由生效,避免刷新后 404
next({ path: to.fullPath, replace: true })
return
}
dynamicAdded = true
} catch (e) {
// 注入失败直接继续,避免阻塞
}
}
// 验证当前路由所有的匹配中是否需要有登录验证的
if (to.matched.some(r => r.meta.auth)) {
// 这里暂时将cookie里是否存有token作为验证是否登录的条件
// 请根据自身业务需要修改
const token = util.cookies.get('token')
if (token && token !== 'undefined') {
next()
} else {
// 没有登录的时候跳转到登录界面
// 携带上登陆成功之后需要跳转的页面完整路径
next({
name: 'login',
query: {
redirect: to.fullPath
}
})
// https://github.com/d2-projects/d2-admin/issues/138
NProgress.done()
}
} else {
// 不需要身份校验 直接通过
next()
}
})
-28
View File
@@ -21,34 +21,6 @@ const frameIn = [
},
component: _import('system/index')
},
// 演示页面
{
path: 'page1',
name: 'page1',
meta: {
title: '页面 1',
auth: true
},
component: _import('demo/page1')
},
{
path: 'page2',
name: 'page2',
meta: {
title: '页面 2',
auth: true
},
component: _import('demo/page2')
},
{
path: 'page3',
name: 'page3',
meta: {
title: '页面 3',
auth: true
},
component: _import('demo/page3')
},
// 系统 前端日志
{
path: 'log',
+26 -1
View File
@@ -13,7 +13,7 @@ export default {
* @param {Object} payload password {String} 密码
* @param {Object} payload route {Object} 登录成功后定向的路由对象 任何 vue-router 支持的格式
*/
async login ({ dispatch }, {
async login ({ dispatch, commit }, {
username = '',
password = ''
} = {}) {
@@ -29,6 +29,31 @@ export default {
await dispatch('d2admin/user/set', { name: res.name }, { root: true })
// 用户登录后从持久化数据加载一系列的设置
await dispatch('load')
// 登录成功后,从服务端获取菜单与路由并设置
const menuRes = await api.SYS_MENU()
const { header = [], aside = [], routes = [] } = menuRes
// 设置菜单与搜索池
await dispatch('d2admin/menu/asideCollapseSet', false, { root: true })
commit('d2admin/menu/headerSet', header, { root: true })
commit('d2admin/menu/asideSet', aside, { root: true })
commit('d2admin/search/init', header, { root: true })
// 动态注册路由(仅注册框架内的子路由)
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)
const layoutHeaderAside = require('@/layout/header-aside').default
const dynamicChildren = routes.map(r => ({
path: r.path,
name: r.name,
meta: r.meta || {},
component: _import(r.component)
}))
router.addRoutes([{ path: '/', component: layoutHeaderAside, children: dynamicChildren }])
// 重新初始化 page 池,确保标签页与新路由一致
const { frameInRoutes } = require('@/router/routes')
const dynamicFrameIn = JSON.parse(JSON.stringify(frameInRoutes))
if (dynamicFrameIn && dynamicFrameIn[0] && Array.isArray(dynamicFrameIn[0].children)) {
dynamicFrameIn[0].children = dynamicFrameIn[0].children.concat(dynamicChildren)
}
commit('d2admin/page/init', dynamicFrameIn, { root: true })
},
/**
* @description 注销用户并返回登录页面
+12
View File
@@ -0,0 +1,12 @@
<template>
<d2-container>
<template slot="header">Page 4 header</template>
Hello World
</d2-container>
</template>
<script>
export default {
name: 'page4'
}
</script>