【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>
相关推荐
gnip1 小时前
链式调用和延迟执行
前端·javascript
SoaringHeart1 小时前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.1 小时前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu1 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss1 小时前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师2 小时前
React面试题
前端·javascript·react.js
木兮xg2 小时前
react基础篇
前端·react.js·前端框架
ssshooter2 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘3 小时前
HTML--最简的二级菜单页面
前端·html
yume_sibai3 小时前
HTML HTML基础(4)
前端·html