创建组件,假数据生成临时的menu菜单
这里我们先不去直接处理动态菜单,我们先用假数据生成一个临时的menu菜单; 创建layout/components/Sidebar/SidebarMenu.vue
文件;
SidebarMenu.vue
<template>
<el-menu
:unique-opened="true"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<!--子集menu-->
<el-submenu index="1">
<template #title>
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-submenu>
<!--具体的菜单-->
<el-menu-item index="2-2"
><i class="el-icon-location"></i> <span>导航4</span></el-menu-item
>
</el-menu>
</template>
从假数据生成的临时menu可以看出,el-menu其实分成了三个部分:
- el-menu:整个menu菜单;
- el-submenu:子集menu菜单;
- el-menu-item:具体菜单项;
动态menu菜单处理方案解析
动态menu菜单 主要是和动态路由表 配合去实现用户权限 的;
但是用户权限处理 需要等到后面才做,所以这里就先只处理动态menu菜单这个;
动态menu菜单 是什么呢?
它是根据路由表的配置,自动生成对应的menu菜单,当路由表发生变化时,menu菜单自动发生变化;
这里的动态menu菜单实现方案:
- 定义路由表 对应menu菜单规则;
- 根据规则制定路由表;
- 根据规则,依据路由表 ,生成menu菜单;
实现动态menu菜单最核心的关键点在于步骤一,也就是定义路由表
对应menu菜单规则
;
这个规则如何制定呢?
js
1 对于单个路由规则而言(循环)
1 如果meta && meta.title && meta.icon:则显示在menu菜单中,其中title为显示的内容,icon为显示的图标;
1 如果存在children:则以el-sub-menu(子菜单)展示;
2 否则:则以el-menu-item(菜单项)展示;
2 否则:不显示在menu菜单中;
业务落地:生成项目页面组件
想要完成动态的menu,那么我们需要以下的步骤:
- 创建页面组件;
- 生成路由表;
- 解析路由表;
- 生成menu菜单;
创建页面组件
在views文件夹下,创建页面:
- 创建文章:article-create
- 文章详情:article-detail
- 文章排名:article-ranking
- 错误页面:error-pate(404/401)
- 导入:import
- 权限列表:permission-list
- 个人中心:profile
- 角色列表:role-list
- 用户信息:user-info
- 用户管理:user-manage
创建结构路由表
我们要实现的路由结构如图:
js
const menu = [
{
title: '个人中心',
path: ''
},
{
title: '用户',
children: [
{
title: '员工管理',
path: ''
},
{
title: '角色列表',
path: ''
},
{
title: '权限列表',
path: ''
}
]
},
{
title: '文章',
children: [
{
title: '文章排名',
path: ''
},
{
title: '创建文章',
path: ''
}
]
}
]
我们需要进行用户权限处理,首先得对路由表进行一个划分:
- 私有路由表 privateRoutes:权限路由;
- 公有路由表 publicRoutes:无权限路由; 可以生成以下路由表结构:
在router/index.vue
中写入:
router/index.vue
/**
* 私有路由表
*/
const privateRoutes = [
{
path: '/user',
component: layout,
meta: {
title: 'user',
icon: 'personnel'
},
children: [
{
path: '/user/manage',
name: 'user-manage',
component: () => import('@/views/user-manage/index'),
meta: {
title: 'user-manage',
icon: 'personnel-manage'
}
},
{
path: '/user/role',
name: 'user-role',
component: () => import('@/views/role-list/index'),
meta: {
title: 'user-role',
icon: 'role'
}
},
{
path: '/user/permission',
name: 'user-permission',
component: () => import('@/views/permission-list/index'),
meta: {
title: 'user-permission',
icon: 'permission'
}
},
{
path: '/user/info/:id',
name: 'user-info',
component: () => import('@/views/user-info/index'),
meta: {
title: 'user-info'
}
},
{
path: '/user/import',
name: 'user-import',
component: () => import('@/views/user-import/index'),
meta: {
title: 'user-import'
}
}
]
},
{
path: '/article',
component: layout,
redirect: '/article/ranking',
name: 'articl-ranking',
meta: { title: 'article', icon: 'article' },
children: [
{
path: '/article/create',
component: () =>
import(
/* webpackChunkName: "article-ranking" */ '@/views/article-create/index'
),
meta: {
title: 'article-create',
icon: 'article-create'
}
},
{
path: '/article/ranking',
component: () =>
import(
/* webpackChunkName: "article-ranking" */ '@/views/article-ranking/index'
),
meta: {
title: 'article-ranking',
icon: 'article-ranking'
}
},
{
path: '/article/:id',
component: () =>
import(
/* webpackChunkName: "article-ranking" */ '@/views/article-detail/index'
),
meta: {
title: 'article-detail'
}
}
]
}
]
/**
* 公开路由表
*/
const publicRoutes = [
{
path: '/login',
component: () => import('@/views/login/index')
},
{
path: '/',
redirect: '/profile',
component: layout,
children: [
// 个人中心
{
path: '/profile',
name: 'profile',
component: () => import('@/views/profile/index'),
meta: {
title: 'profile',
icon: 'el-icon-user'
}
},
// 404
{
path: '/404',
name: '404',
component: () => import('@/views/error-page/index')
},
// 401
{
path: '/401',
name: '401',
component: () => import('@/views/error-page/index')
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes: [...publicRoutes, ...privateRoutes]
})
解析路由表,获取结构化数据
现在我们要获取到之前的结构化数据,想要获取到路由表数据,有两种方式:
- router.options.routes:初始路由列表(有弊端:新增的路由无法获取到);
- router.getRouters():获取所有的路由记录完整列表;
此时我们选择使用router.getRouters;
在layout/components/Sidebar/SidebarMenu
写入:
SidebarMenu.vue
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { filterRoutes, generateMenus } from '@/utils/route'
import SidebarItem from './SidebarItem.vue'
const router = useRouter()
const routers = computed(() => {
const filterRoute = filterRoutes(router.getRoutes())
return generateMenus(filterRoute)
})
</script>
这是我们未处理的routers结构:
router.getRouters返回的路由结构不是我们想要的,所以我们需要进行转化;
首先创建utils/route文件,创建filterRouters方法;
utils/route.js
/**
* 返回所有子路由
*/
const getChildrenRoutes = routes => {
const result = []
routes.forEach(route => {
if (route.children && route.children.length > 0) {
result.push(...route.children)
}
})
return result
}
/**
* 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级
* @param {*} routes router.getRoutes()
*/
export const filterRouters = routes => {
const childrenRoutes = getChildrenRoutes(routes)
return routes.filter(route => {
return !childrenRoutes.find(childrenRoute => {
return childrenRoute.path === route.path
})
})
}
执行完filterRouters方法得到routes长这样:
接着还需要增加一个generateMenus方法,这个方法有点复杂: 主要分为4种情况:
- 没有children,没有meta,直接return;
- 有children,没有meta,迭代generateMenus方法;
- 没有children,有meta,res.push;
- 有children,有mate;
utils/route.js
/**
* 判断是不是空值
*/
function isNull(data) {
if (!data) return true
if (JSON.stringify(data) === '[]') return true
if (JSON.stringify(data) === '{}') return true
}
/**
* 根据routes数据,返回对应的menu规则数据
*/
export const generateMenus = (routes, basePath = '') => {
const res = []
// 去除不满足该条件`meta && meta.title && mate.icon`的数据
routes.forEach((item) => {
// 如果没有children和meta,直接return
if (isNull(item.children) && isNull(item.meta)) return
// 存在children,不存在meta,迭代generateMenus
if (!isNull(item.children) && isNull(item.meta)) {
const resArr = generateMenus(item.children)
res.push(...resArr)
return
}
// 不存在children,存在meta
// 因为最终的menu需要进行跳转,此时需要合并path
const routePath = path.resolve(basePath, item.path)
// 路由分离之后,可能存在同名父路由的情况
let route = res.find((item) => item.path === routePath)
// 当前路由尚未加入到result
if (!route) {
route = {
...item,
path: routePath,
children: []
}
// icon && title
if (route.meta.icon && route.meta.title) {
res.push(route)
}
}
// 存在children,存在meta
if (!isNull(item.children)) {
route.children.push(...generateMenus(item.children, route.path))
}
})
return res
}
调用完generateMenus方法,routes长成这样:
我们最终生成的routes结构:
js
[{
"path": "/profile",
"name": "profile",
"meta": {
"title": "profile",
"icon": "el-icon-user"
},
"children": []
}, {
"path": "/user",
"meta": {
"title": "user",
"icon": "personnel"
},
"props": {
"default": false
},
"children": [{
"path": "/user/manage",
"name": "user-manage",
"meta": {
"title": "user-manage",
"icon": "personnel-manage"
},
"children": []
}, {
"path": "/user/role",
"name": "user-role",
"meta": {
"title": "user-role",
"icon": "role"
},
"children": []
}, {
"path": "/user/permission",
"name": "user-permission",
"meta": {
"title": "user-permission",
"icon": "permission"
},
"children": []
}],
}, {
"path": "/article",
"redirect": "/article/ranking",
"name": "articl-ranking",
"meta": {
"title": "article",
"icon": "article"
},
"props": {
"default": false
},
"children": [{
"path": "/article/create",
"meta": {
"title": "article-create",
"icon": "article-create"
},
"children": []
}, {
"path": "/article/ranking",
"meta": {
"title": "article-ranking",
"icon": "article-ranking"
},
"children": []
}],
}]
生成动态menu菜单
整个menu菜单,我们将它分成3个组件来进行处理:
- SidebarMenu:处理数据,作为最顶层的menu载体;
- SidebarItem:根据数据处理当前项为el-submenu || el-menu-item
- MenuItem:处理el-menu-item样式;
首先是SidebarMenu:
SidebarMenu.vue
<template>
<el-menu
:unique-opened="true"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<SidebarItem
v-for="item in routers"
:key="item.path"
:route="item"
></SidebarItem>
</el-menu>
</template>
SidebarItem.vue
<template>
<el-submenu v-if="route.children.length > 0" :index="route.path">
<template #title>
<menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
</template>
<!--循环渲染children-->
<sidebar-item
v-for="item in route.children"
:key="item.path"
:route="item"
></sidebar-item>
</el-submenu>
<el-menu-item v-else :index="route.path">
<menu-item :title="route.meta.title" :icon="route.meta.icon"></menu-item>
</el-menu-item>
</template>
<script setup>
import { defineProps } from 'vue'
import MenuItem from './MenuItem.vue'
defineProps({
route: {
type: Object,
required: true
}
})
</script>
<style></style>
MenuItem.vue
<template>
<!--element icon-->
<i v-if="icon.includes('el-icon')" class="sub-el-icon" :class="icon"></i>
<!--非element icon-->
<svg-icon v-else :icon="icon"></svg-icon>
<!--文本-->
{{ title }}
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
title: {
type: String,
required: true
},
icon: {
type: String,
required: true
}
})
</script>
<style></style>
菜单现在长成这样:
修复小问题
- 样式问题
- 路由跳转问题
- 默认激活项
样式问题
因为后面我们需要处理主题替换,所以这里我们不能把样式写死;
在store/gettters中创建一个新的快捷访问;
getters.js
import variables from '@/styles/variables.module.scss'
// 快捷访问
const getters = {
...
cssVar: (state) => variables
}
export default getters
在SidebarMenu中写入样式:
SidebarMenu.vue
<el-menu
:unique-opened="true"
background-color="$store.getters.cssVar.menuBg"
text-color="$store.getters.cssVar.menuText"
active-text-color="$store.getters.cssVar.menuActiveText"
>
路由跳转问题
这个很简单只要加上router
的属性;
SidebarMenu.vue
<el-menu
:unique-opened="true"
:background-color="$store.getters.cssVar.menuBg"
:text-color="$store.getters.cssVar.menuText"
:active-text-color="$store.getters.cssVar.menuActiveText"
router
>
默认激活项
根据当前url进行判断即可;
SidebarMenu.vue
<template>
<el-menu
:default-active="activeMenu"
>
</el-menu>
</template>
<script setup>
const route = useRoute()
// 计算高亮menu的方法
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
})
</script>