【vue3后台项目】实现Sidebar的动态menu

创建组件,假数据生成临时的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其实分成了三个部分:

  1. el-menu:整个menu菜单;
  2. el-submenu:子集menu菜单;
  3. el-menu-item:具体菜单项;

动态menu菜单处理方案解析

动态menu菜单 主要是和动态路由表 配合去实现用户权限 的;

但是用户权限处理 需要等到后面才做,所以这里就先只处理动态menu菜单这个;

动态menu菜单 是什么呢?

它是根据路由表的配置,自动生成对应的menu菜单,当路由表发生变化时,menu菜单自动发生变化;

这里的动态menu菜单实现方案:

  1. 定义路由表 对应menu菜单规则
  2. 根据规则制定路由表
  3. 根据规则,依据路由表 ,生成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,那么我们需要以下的步骤:

  1. 创建页面组件;
  2. 生成路由表;
  3. 解析路由表;
  4. 生成menu菜单;

创建页面组件

在views文件夹下,创建页面:

  1. 创建文章:article-create
  2. 文章详情:article-detail
  3. 文章排名:article-ranking
  4. 错误页面:error-pate(404/401)
  5. 导入:import
  6. 权限列表:permission-list
  7. 个人中心:profile
  8. 角色列表:role-list
  9. 用户信息:user-info
  10. 用户管理:user-manage

创建结构路由表

我们要实现的路由结构如图:

js 复制代码
const menu = [
  {
    title: '个人中心',
    path: ''
  },
  {
    title: '用户',
    children: [
      {
        title: '员工管理',
        path: ''
      },
      {
        title: '角色列表',
        path: ''
      },
      {
        title: '权限列表',
        path: ''
      }
    ]
  },
  {
    title: '文章',
    children: [
      {
        title: '文章排名',
        path: ''
      },
      {
        title: '创建文章',
        path: ''
      }
    ]
  }
]

我们需要进行用户权限处理,首先得对路由表进行一个划分:

  1. 私有路由表 privateRoutes:权限路由;
  2. 公有路由表 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]
})

解析路由表,获取结构化数据

现在我们要获取到之前的结构化数据,想要获取到路由表数据,有两种方式:

  1. router.options.routes:初始路由列表(有弊端:新增的路由无法获取到);
  2. 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种情况:

  1. 没有children,没有meta,直接return;
  2. 有children,没有meta,迭代generateMenus方法;
  3. 没有children,有meta,res.push;
  4. 有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个组件来进行处理:

  1. SidebarMenu:处理数据,作为最顶层的menu载体;
  2. SidebarItem:根据数据处理当前项为el-submenu || el-menu-item
  3. 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>

菜单现在长成这样:

修复小问题

  1. 样式问题
  2. 路由跳转问题
  3. 默认激活项

样式问题

因为后面我们需要处理主题替换,所以这里我们不能把样式写死;

在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>
相关推荐
DT——5 小时前
Vite项目中eslint的简单配置
前端·javascript·代码规范
学习ing小白7 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
一只小阿乐7 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐7 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
真的很上进7 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er7 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063717 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl7 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码7 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347548 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js