Skip to content

封装菜单组件

1、概述

在项目之中,一般我们的后台管理正常结构部分都是包括菜单部分,左侧菜单是必不可少的,菜单部分一般由以下几个部分组成

  • 菜单标题
  • 菜单图标
  • 菜单子项
  • 菜单折叠

我们可以简单的预览一下正常一个完整的后台管理的大致页面应该是什么样子:

image.png

2、结构搭建

接下来我们就在 vue3 之中搭建一个左侧的菜单组件,然后进行封装,在我们的项目之中我们选择的是国内目前最为流行的 ui 框架,element-plus,接下来我们就围绕他的左侧折叠面板和菜单进行开发和封装我们的菜单组件。

可以看出,菜单栏大致分为两类,一类是有子菜单的,一类是无子菜单的。我们可以对这两类进行分类,然后根据路由列表,动态渲染菜单栏。

image.png

之前我们搭建好了一个基础的 vue3 项目,并且引入了 element-plus,接下来我们就开始引入我们的菜单组件代码,这部分直接使用官方的就行。

vue
<template>
  <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
    <el-radio-button :value="false">expand</el-radio-button>
    <el-radio-button :value="true">collapse</el-radio-button>
  </el-radio-group>
  <el-menu
    default-active="2"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
    @open="handleOpen"
    @close="handleClose"
  >
    <el-sub-menu index="1">
      <template #title>
        <el-icon><location /></el-icon>
        <span>Navigator One</span>
      </template>
      <el-menu-item-group>
        <template #title><span>Group One</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-menu-item-group>
      <el-menu-item-group title="Group Two">
        <el-menu-item index="1-3">item three</el-menu-item>
      </el-menu-item-group>
      <el-sub-menu index="1-4">
        <template #title><span>item four</span></template>
        <el-menu-item index="1-4-1">item one</el-menu-item>
      </el-sub-menu>
    </el-sub-menu>
    <el-menu-item index="2">
      <el-icon><icon-menu /></el-icon>
      <template #title>Navigator Two</template>
    </el-menu-item>
    <el-menu-item index="3" disabled>
      <el-icon><document /></el-icon>
      <template #title>Navigator Three</template>
    </el-menu-item>
    <el-menu-item index="4">
      <el-icon><setting /></el-icon>
      <template #title>Navigator Four</template>
    </el-menu-item>
  </el-menu>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import {
  Document,
  Menu as IconMenu,
  Location,
  Setting,
} from "@element-plus/icons-vue";

const isCollapse = ref(true);
const handleOpen = (key: string, keyPath: string[]) => {
  console.log(key, keyPath);
};
const handleClose = (key: string, keyPath: string[]) => {
  console.log(key, keyPath);
};
</script>

<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 400px;
}
</style>

然后我们看看引入官方默认提供给我们菜单的效果

image.png

3、菜单渲染

接下来我们就把我们后台管理部分的所有菜单全部导入进来,然后对菜单实施一个渲染,然后我们就可以看到完整的菜单结构了。

☞ 在 router=> index 之中先把我们的 admin(后台管理部分)的菜单隔离出来

这里隔离出来也是为了方便我自己的操作

js
// 后台部分相关路由
export const adminRouter= [
    {
      path: '/user', // 用户页面
      name: 'user',
      component: () => import('@/views/admin/user/index.vue'),
    },
];

//使用amdin部分路由的时候
// 默认路由 home页面
 {
    path: '/admin',
    name: 'admin',
    component: () => import('@/views/admin/index.vue'),
    children:[...adminRouter],
},

☞ 然后在我们的后台菜单里面去拿这部分菜单的值,在组件 Menu=> leftMenu 部分封装我们这部分 admin 的值

  • leftMenu 里面输出查看
js
import { adminRouter } from "@/router";
console.log(adminRouter, "adminRouter");

☞ 查看输出的值,可以看到,这个时候我们已经拿到了我们需要的菜单,接下来就可以完善菜单了

js
[
  {
    path: "/user",
    name: "user",
  },
];

☞ 渲染第一层菜单

菜单首先需要一个标题,接下来我们完善菜单,然后渲染第一层的菜单

js
// 后台部分相关路由
export const adminRouter = [
  {
    path: '/home', // 后台主页
    name: 'home',
    component: () => import('@/views/admin/adminhome/index.vue'),
    meta: { title: '后台主页' },
  },
  {
    path: '/system', // 系统管理
    name: 'system',
    component: () => import('@/views/admin/user/index.vue'),
    meta: { title: '系统管理' },
    children: [
      {
        path: '/role', // 角色管理
        name: 'role',
        component: () => import('@/views/admin/role/index.vue'),
      },
      {
        path: '/user', // 用户管理
        name: 'user',
        component: () => import('@/views/admin/user/index.vue'),
      },
    ],
  },
];

在菜单使用部分进行渲染

js
<el-menu
  default-active="1"
  class="el-menu-vertical-demo"
  :collapse="isCollapse"
  @open="handleOpen"
  @close="handleClose"
>
  <el-menu-item :index="index" v-for="(val,index) in adminRouter" :key="index">
    <el-icon><icon-menu /></el-icon>
    <template #title>{{val.meta?.title}}</template>
  </el-menu-item>
</el-menu>

查看效果,这个时候我们的第一层菜单已经封装好了

image.png

4、子菜单封装

我们封装好了第一层的菜单以后还不够,因为可能菜单的层级会有无数个,这个时候我们就需要把菜单写成嵌套组件

☞ src=> layout => leftAside => SideItem.vue

这里我们直接将我们之前的部分挪移过来进行一些简单的封装

js
<template>
	<el-menu-item :index="sideItemdata.path"  v-if="!sideItemdata.children || sideItemdata.children.length === 0">
      <el-icon><Menu /></el-icon>
      <template #title>{{ sideItemdata.meta.title?sideItemdata.meta.title:'--' }}</template>
    </el-menu-item>
 	<el-sub-menu :index="sideItemdata.path" v-else>
 		<template #title>
            <el-icon><Operation /></el-icon>
            <span>{{ sideItemdata.meta.title?sideItemdata.meta.title:'--' }}</span>
        </template>
        <SideItem v-for="(item,index) in sideItemdata.children" :key="item.path + index" :sideItemdata="item"/>
    </el-sub-menu> 
</template>
<script setup>
import SideItem from './SideItem.vue'
const props = defineProps({
    sideItemdata: {
        type: Object,
        required: true
    },
    isNest: {
        type: Boolean,
        default: false
    },
    basePath: {
        type: String,
        default: ''
    }
})
</script>

☞ src=> layout => leftAside => index.vue

更换一下我们之前的组件主要的部分,父组件引入使用子组件

JS
<el-menu background-color="#FFF" text-color="#030303" active-text-color="#1890FF" default-active="1" class="el-menu-vertical-demo" :collapse="isCollapse" @open="handleOpen" @close="handleClose">
        <SideItem v-for="(item,index) in sideRouters" :key="item.path + index" :sideItemdata="item"/>
</el-menu>

import SideItem from './SideItem.vue'

查看我们的效果

image.png

5、菜单事件完善

接下来我们完善一下菜单子组件的点击事件

js
这里我是直接添加到el-menu-item 上的 

@click="topageUrl(sideItemdata)"

//引入路由
import {useRouter} from 'vue-router' 

//使用 
const router=useRouter();

//跳转
const topageUrl=(row)=>{
	  router.push(row);
}

6、菜单样式属性

el-menu上的属性显示

JS
background-color="#FFF"  // 设置默认背景色
text-color="#030303" // 设置默认文字颜色
active-text-color="#1890FF" // 设置点击后文字颜色
default-active="1" // 设置默认选中
class="el-menu-vertical-demo" // 设置样式
:collapse="isCollapse" // 设置折叠
@open="handleOpen" // 设置展开
@close="handleClose" // 设置关闭

7、菜单过滤

在我们正常的菜单显示过程中,很多时候我们的菜单并不需要展示出来,这个时候我们菜单之中有个属性hidden来进行控制显示隐藏

  • 路由信息配置
JS
{
    path: '/userinfo', // 用户页面
    name: 'userinfo',
    meta: { title: '用户信息', icon: '<User/>' },
    children: [],
    hidden:false, // 隐藏
    component: () => import('@/pages/userInfo.vue'),
},
  • 在菜单之中我们过滤一下
JS
onMounted(() => {
  console.log(router.options.routes, '路由--all');
  router.options.routes.forEach((item) => {
      // console.log(item);
      if (item.name == '/') {
          sideRouters.value = item.children.filter((row)=>!row.hidden);
      }
  })
})

8、菜单设置

👉只保持一个子菜单的展开

JS
el-menu 上面增加属性 
:unique-opened="true"

👉设置折叠动画

感觉差距不大,效果不是很明显

JS
:collapse-transition="true" // 设置折叠动画

👉 设置默认选中

接下来我们设置一下默认选中的菜单

JS
:default-active="activeMenu" // 设置默认选中

const activeMenu = ref('/admin'); // 设置默认选中

这样我们默认选中菜单就设置好了,但是刷新以后无法选中我们的菜单,这个时候我们需要监听路由变化,然后根据地址设置选中的菜单

👉 src=> layout => leftAside => index.vue

更改一下activeMenu的值

JS
const activeMenu = computed(() => {
  const { meta, path } = route;
  console.log(meta, path,'meta, path');
  if (meta?.activeMenu) {
    return meta.activeMenu;
  }
  return path;
})

路由之中也设置对应的部分,设置meta的activeMenu。

当我们设置一部分菜单并不展示到页面的时候,就可以直接显示高亮某一个菜单部分

JS
{
    path: '/system/dictdata',
    meta: { title: '字典数据', icon: '<User/>', },
    hidden: true,
    children: [
    {
        path: 'index/:dictId(\\d+)',
        component: () => import('@/views/system/dict/dictdata.vue'),
        name: 'Data',
        hidden: true,
        meta: { title: '字典数据', activeMenu: '/system/dictdata' }
    }]
},

9、菜单的展开和折叠

👉使用pinia持久化状态管理左侧菜单

接下来我们就用pinia持久化状态来管理左侧的菜单是否展开

存储一些关于客户在网站上面设置的主题数据

之前我们已经安装pinia,接下来我们直接来实现这一部分

👉添加store状态

store仓库状态管理目录下新建menuStore.js,负责管理菜单的选中状态

JS
// menuStore.js 存储菜单展开还是收起
import { defineStore } from 'pinia';
import Cookies from 'js-cookie';

const useMenuStore = defineStore('menu', {
  state: () => ({
    // 确保 isCollapse 为布尔值,读取 Cookies 时处理好类型转换
    isCollapse: Cookies.get('sidebarStatus') == 'true' || false,
  }),
  actions: {
    // 设置折叠状态
    setCollapseStatus(status) {
      // 保存折叠状态,并更新 Cookies
      this.isCollapse = status;
      Cookies.set('sidebarStatus', this.isCollapse.toString());
    },
  },
});

export default useMenuStore;

👉添加点击事件

去我们的任意组件之中添加折叠与展开事件,注意引入store

JS
@click="handleExpand"
@click="handleFold"

import useMenuStore from '@/store/modules/menuStore'
const menuStore = useMenuStore()

// 设置新的折叠状态
const handleExpand=()=>{
    console.log('handleExpand');
    menuStore.setCollapseStatus(true); // 或者 false
    console.log(menuStore.isCollapse);
}
const handleFold=()=>{
    console.log('handleFold');
    menuStore.setCollapseStatus(false); // 或者 false
    console.log(menuStore.isCollapse);
}

👉管理展开状态

JS
import useMenuStore from '@/store/modules/menuStore'

const menuStore = useMenuStore()
const isCollapse = computed(() => {
  return JSON.parse(menuStore.isCollapse); // 使用正确的 store 实例
});

这个时候我们已经可以控制菜单的展开和收起了,再进行抽离组件,就可以在我们的菜单组件中使用了

👉侧边栏宽度

调整好我们菜单的展开收起以后,接下来我们就可以根据我们的菜单状态来调整我们的侧边栏的宽度了

这里我们直接拿类控制即可

JS
:class="isCollapse?'asidefold pageadmin backbaige':'asideexpend pageadmin backbaige'"

import useMenuStore from '@/store/modules/menuStore'
const menuStore = useMenuStore()

Released under the MIT License.