赞
踩
目录
本篇文章旨在从零搭建一个动态路由动态菜单的后台管理系统初始环境,如果您只有个别地方没有实现,那么可以根据目录选择性的阅读您所需的内容
本文使用技术:vue3,pinia状态管理,element-plus,axios
注意在本文章中,有些方法并没有粘出来,比如一些发送请求的方法,因为没有必要,需要根据你们自己的情况进行修改,如果你看到一些方法并没有写出来,那多半就是发送请求的方法。
效果预览


本文需要使用axios,路由,pinia,安装element-plus,并且本文vue3是基于js而非ts的,这些环境如何搭建不做描述,需要读者自己完成。下面开始
在这之前我们需要一个布局

假设我们的home组件,是如下
- <template>
- <div class="common-layout">
- <el-container>
- <el-aside width="200px">
- <div>
- <!-- 菜单侧栏导航栏 -->
- <menus></menus>
- </div>
- </el-aside>
- <el-container>
- <el-header>
- <div class="head_class">
- <!-- 头部内容 可以忽略-->
- <Crumbs></Crumbs>
- <div>
- <el-button type="primary" text @click="logOut">注销</el-button>
- </div>
- </div>
- </el-header>
- <el-main>
- <!-- 主要内容 -->
- <div>
- <router-view></router-view>
- </div>
- </el-main>
- </el-container>
- </el-container>
- </div>
- </template>

话不多说,下面正片开始
功能:根据不同的角色显示不同的菜单,菜单数据由后端提供,前端负责渲染即可
实现步骤:获取菜单--->缓存--->渲染菜单
我们使用elemen-plus的menu菜单来渲染

首先我们需要先获取到菜单,一般是在登录的时候获取。可以根据自己的情况而定。由于我后端采用的是RBAC模型,也就是所谓的用户-角色-权限模型,而菜单属于权限,我需要先获取角色信息,然后获取菜单信息,下面是我的登录组件的登录方法,可能代码比较多,如果你已经获取了菜单,可以直接跳过此处
- import {reactive, ref, onMounted, watchEffect} from "vue";
- import {getCaptcha, login} from "@/api";
- import {useRouter} from "vue-router";
- import {setToken, removeToken, getToken} from "@/util/token/getToken";
- import {getMyRole} from '@/api/home/permission/role/index'
- import {useMenusStore} from '@/store/permission'
- import {getMYMenus} from "@/api/home/permission/menus/index";
-
- const store = useMenusStore()//pinia的操作
-
- async function loginsubmit() {
- /*登录*/
- await login(loginForm).then((res) => {
- if (res.data.code == 200) {
- window.sessionStorage.clear()
- setToken("token", res.data.data.token);
- }
- else {
-
- }
- })
- // 简单的健壮性判断
- if (getToken(`token`)) {
- /*获取角色信息*/
- await getMyRole().then((res) => {
- store.setRoles(res.data.data)
- })
- /*获取菜单信息*/
- await getMYMenus(store.roleIds).then(res => {
- store.setMenuList(res.data.data.menuPermission);
- router.push("/home")
- })
- }
- }

上面主要是发送请求获取菜单,然后将菜单数据缓存到pinia中,也就是上面代码的store,下面是pinia的代码(缓存菜单数据以及路由数据)
- import {defineStore} from "pinia" // 定义容器
- import {getRoleRouter} from "@/api/home/permission/menus";
- import {logout} from '@/api/index'
-
-
- export const useMenusStore = defineStore('permission', {
- /**
- * 存储全局状态,类似于vue2中的data里面的数据
- * 1.必须是箭头函数: 为了在服务器端渲染的时候避免交叉请求导致数据状态污染
- * 和 TS 类型推导
- */
- state: () => {
- return {
- menuList: [],//菜单信息
- roles: [],//角色信息
- menuRouter: [],//路由信息
- roleIds: [],//角色Id
- }
- },
- /**
- * 用来封装计算属性 有缓存功能 类似于computed
- */
- getters: {},
- /**
- * 编辑业务逻辑 类似于methods
- */
- actions: {
- //菜单
- setMenuList(data) {
- this.menuList = data
- },
- //设置路由:data-->后端传入的路由信息
- setMenuRouter(data) {
- data.forEach(item => {
- //定义一个对象-->routerInfo格式化后端传入的路由信息
- let routerInfo = {
- path: item.path,
- name: item.name,
- meta: item.name,
- component: () => import(`@/views/${item.linkUrl}`),
- }
- this.menuRouter.push(routerInfo)//加入到home子路由下
- })
- },
- setRoles(data) {
- this.roles = data
- this.roleIds = data.map(item => {
- return item.id
- })
- },
- generateRoutes() {
- //获取路由信息
- return new Promise((resolve, reject) => {
- getRoleRouter(this.roleIds).then(res => {
- if (res.data.code == 200) {
- this.setMenuRouter(res.data.data)
- resolve()
- } else {
- reject()
- }
- })
- })
- },
- logout() {
- /*注销登录*/
- return logout().then(res => {
- if (res.data.code == 200) {
- this.$reset()//清除状态管理所有数据
- window.sessionStorage.clear()//清除本地所有缓存
- return true
- }
- else return false
- })
- },
- },
- // 持久化设置
- persist: {
- enabled: true, //开启
- storage: sessionStorage, //修改存储位置
- key: 'permissions', //设置存储的key,在这里是存在sessionStorage时的键
- paths: ['menuList', 'hasRouter', `roles`, 'roleIds'],//指定要持久化的字段,menuRouter不需要缓存。因为每次路由跳转我们都可以重新获取
- },
- })

你可能会说,这直接粘一些垃圾代码,谁爱看啊,别担心,在上面的代码主要用到的是将角色、菜单信息缓存的操作,你需要关注的也就是下面的截图,其他的代码暂时不用理会,后面会讲


在上面的代码中,主要是将菜单信息和角色信息存入pinia,当然角色信息是否存储并不重要,因为他跟我们的主题并没有多大的相关
注意:这个菜单是菜单树的结构哦!如果想看看后端响应的数据结构,可以在文章最后面查看
好了到这咱们已经获取到了菜单了,下面就是生成菜单了
获取了菜单,我们就需要在首页中渲染菜单。
使用明确:element-plus中menu主要分为两种状态:有子菜单(目录)和没有子菜单,我们需要根据这两种分别渲染。

我们就可以粘代码修修改就可以了,
新建一个vue文件:我们假定这个组件是menus
- <template>
- <el-row>
- <el-col>
- <h5 class="mb-2">后台管理系统</h5>
- <el-menu
- active-text-color="#ffd04b"
- background-color="#545c64"
- class="el-menu-vertical-demo"
- default-active="0"
- text-color="#fff"
- router="true"
- >
- <menuTree :menuList="menuList"></menuTree>
- </el-menu>
- </el-col>
- </el-row>
- </template>
-
- <script setup>
- import menuTree from "@/components/menu/menuTree/index";//渲染菜单的组件
- import {useMenusStore} from "@/store/permission";
-
- let menuList = useMenusStore().menuList;//获取pinia的缓存的菜单数据
- </script>

你可能会问:怎么代码这么少?而且只有<el-menu>标签?渲染菜单的<el-sub-menu>和<el-menu-item>标签呢?
由于我们会采用递归,如果把所有代码都写到一个组件里面,那么所有元素都会重复了,比如说上面这个“后台管理系统”标题会被重复显示,那么这肯定是不行的,所以我们需要将渲染菜单的具体操作放在另一个vue文件里面,具体的代码就在这个<menuTree> 组件里面,当然我们还需要将pinia中存入的菜单信息传给这个组件,让其渲染
下面我们来看看这个<menuTree >组件
- <template>
- <div>
- <template v-for="item in menuList" :key="item.path">
- <!-- 分为两种方式渲染:有子菜单和没有子菜单-->
- <el-sub-menu
- :index="item.path"
- v-if="item.nodeType == 1"
- >
- <template #title>
- <span>{{ item.name }}</span>
- </template>
- <!-- 有子菜单的继续遍历(递归)-->
- <menuTree :menuList="item.children"></menuTree>
- </el-sub-menu>
- <!-- 没有子菜单-->
- <el-menu-item :index="item.path" v-if="item.nodeType==2">
- <span>{{ item.name }}</span>
- </el-menu-item>
- </template>
- </div>
- </template>
- <script>
- export default ({
- name: 'menuTree',
- props: {
- menuList: {
- type: Array
- }
- },
- setup(props) {
- return {};
- }
- })
- </script>

在上面的代码中:
1、我使用nodeType的值来判断是否存在子菜单。因为我在数据库存储的菜单属性中规定nodeType=1就是目录,具有子菜单,=2就是没有子菜单,是页面,=3就是按钮。你也可以通过其他的方法来判断,比如说,菜单children的长度是否大于零。如果有子菜单,那么我们还需要再一次遍历,也就是递归,无论我们有多少级菜单都能够遍历出来,当然,理论上层次深了会爆栈,但是正经人谁会弄那么深是吧?
2、我采用父子组件的传递数据的方法把菜单数据传入带渲染菜单的子组件中
好了到这里,基本就实现了动态菜单的效果了,但这还不行,虽然用户不能通过菜单的方式来访问与他无关的页面,但是他可以通过手动输入路由信息来跳转,所以我们需要使用动态路由来防止
功能:根据不同的权限创建对应的路由
实现步骤:获取路由----->处理路由格式并缓存------>创建路由(前置守卫)
我们查看pinia中的获取路由方法,具体代码就在上面的pinia那里
获取路由的请求

上面没什么好说的,就一个获取路由信息然后存入pinia中的操作,重点是下面的处理路由信息,这里我们需要将后端传过来的数据处理为路由格式。

注意:由于我这里所有路由都只有一层,并没有子路由,所以这样处理,如果你想要多嵌套几个路由,那么你需要将后端传来的路由信息处理为树结构(也可以在后端处理),然后再遍历处理(可能还需要递归遍历处理) 并添加到children属性里面,这里就不多说
好了,到现在我们已经做完准备,到了最终的环节:在哪里创建?怎么创建路由?
老样子先粘代码,再解说
这里是router/index.js代码,没什么特别的,主要是定义一些静态路由,和必要的路由配置创建
- import {createRouter, createWebHistory} from "vue-router";
-
- import {useMenusStore} from "@/store/index";
-
- //静态路由
- const constRoutes = [
-
- {
- path: '/',
- redirect: '/login'
- },
- {
- path: '/login',
- component: () => import('@/views/login')
- },
- {
- path:'/404',
- component:()=>import('@/views/CommonViews/404')
- },
-
- ]
- const routerHistory = createWebHistory()
- const router = createRouter({
- history: routerHistory,
- routes: constRoutes
- })
-
- export default router

我们采用的是在路由守卫中动态创建路由,下面就是router/permission.js。主要是定义一个前置守卫,守卫里面动态创建路由,
- import {createPinia} from 'pinia';
-
- const pinia = createPinia();
- import router from "@/router/index";
- import {useMenusStore} from "@/store/permission";
- import Home from '@/views/home/index.vue'
- import NProgress from 'nprogress' // 进度条
- import 'nprogress/nprogress.css'
- import {getUserMenus} from "@/api/home/permission/menus";
- import {getToken} from "@/util/token/getToken";
-
- //路由白名单
- const Whitelist = ['/', '/login', '/error', '/404']
-
- //全局路由守卫
- router.beforeEach((to, from, next) => {
- NProgress.start();
- const store = useMenusStore()
- const hasetoken = getToken('token')
- if (to.path == "/login" && hasetoken) {
- //如果已经登录还在访问登录页,直接跳转首页
- next('/home')
- }
- if (Whitelist.indexOf(to.path) == -1) {
- //没有在白名单里面
- if (store.menuRouter.length == 0 && hasetoken) {
- /*已经登录还未获取路由,就获取路由信息(pinia)*/
- store.generateRoutes().then(() => {
- const routerlist = store.menuRouter
- router.addRoute(
- {
- path: '/home',
- name: 'Home',
- component: Home,
- redirect: '/home/homepage',
- children: routerlist
- }
- )
- next({...to, replace: true})//确保路由添加成功
- })
- } else {
- /*两种情况进入:1:已经获取了路由(一定有token),2,没有登录(一定没有路由)*/
- NProgress.done()
- if (hasetoken && to.matched.length != 0) {
- /*to.matched.length === 0判断当前输入的路由是否具有,即使登陆了,如果输入不能访问的路由也要404*/
- /*已经获取了路由,并且访问路由合法,放行*/
- next()
- } else {
- next('/404')
- }/*没有登录、登录了但是访问路由不合法,都跳转404*/
- }
- } else {
- NProgress.done()
- /*在白名单里面,直接放行*/
- next()
- }
- })
-
-

在上面代码中:

中这个if()判断尤为重要,不然一直循环进入这个if()里面出不来,会导致浏览器白屏卡死 ,在if()之外一定要放行。在上面的代码中,我是采用pinia里面存储路由的数组长度判断,当然你也可以用其他的方法,比如当前的路由长度减去静态路由是否为0作为条件。当然也需要判断好其他情况,比如用户输入不存在的路由,我们需要将其跳转到404等。
4. 路由刷新会丢失,每次刷新需要重新创建。每一次刷新页面都会丢失动态添加的路由。在上面中我们使用的是判断pinia中路由数组的长度来判断的,在这样的情况下,我们就不能持久化pinia这个路由数组(如果是其他判断条件则可以持久化,一切以判断条件而立),不然页面刷新丢失路由,却没法进入这个if()里面(因为如果持久化,这个menrouter就不会为空,if判断条件不成立)。无法进入if创建动态创建路由,在上面的代码中就会一直进入404(如果没有404页面处理,就是白屏)

可以看到我并没有将这路由数组持久化。

我还是再说一遍,如果你是其他判断条件,比如说你是使用路由的长度判断(是否大于静态路由数组长度),那么就可以持久化,而在if()内部也不用每次都发送请求,不然持久化也失去了意义。
当然,就我而言,更倾向于每次刷新就发送请求,这样可以确保数据的准确性。
这里提一嘴,permission.js定义的路由守卫想要生效就需要在min.js中引入,如果你觉得麻烦就全定义在router/index.js中也是可行的,如果你是单独新建js实现的路由守卫,记得引入哦。

动态按钮的实现可以通过v-if或者v-show指令来动态渲染按钮(根据后端返回的权限码比对);
如果你觉得麻烦,你可以用vue中自定义组件来简化。这里就不再赘述了
主要分为三个步骤:获取权限码,缓存权限码,权限对比(v-if或者v-show)
假设我们在登录的时候就获取权限码,下面是login页面登录时发送的请求,将这个结果存入pinia

在store/permission.js中

提供对应的赋值方法以及缓存

在需要的页面中引入,并判断
场景:假设我们的商品上架只有超级管理员才能使用

indexof表示判断一个数组是否包含一个元素,如果包含就返回索引,不包含就返回-1。
大工告成!
管理员登录

商家登录

有没有发现还是太丑了一点?接下来我们来尝试美化一下它
这部分,分为快速篇和循序渐进篇两种,如果耐心不够,请看快速篇,如果你觉得还行,可以看循序渐进篇
使用element-plus的tabs标签
主要修改地方:
在pinia中定义标识激活标签页的值和标签页的数据数组,

以及对应的对外提供操作的接口

路由展示的地方添加标签代码如下

代码如下
- <template>
- <div class="common-layout">
- <el-container>
- <el-aside width="200px">
- <div>
- <!-- 菜单侧栏导航栏 -->
- <menus></menus>
- </div>
- </el-aside>
- <el-container>
- <el-header>
- <div class="head_class">
- <!-- 头部 -->
- <Crumbs></Crumbs>
- <div>
- <el-button type="primary" text @click="logOut">注销</el-button>
- </div>
- </div>
- </el-header>
- <el-main>
- <!-- 内容 -->
- <div>
- <el-tabs type="border-card" v-model="tabsActive" @tab-change="gotoActive" @tab-remove="closeTabs">
- <el-tab-pane :label="item.name" :key="item.name"
- v-for="item in tabsList" :name="item.path" :closable="item.isClose==1" >
- </el-tab-pane>
- <router-view></router-view>
- </el-tabs>
- </div>
- </el-main>
- </el-container>
- </el-container>
- </div>
- </template>
-
- <script setup>
- import menus from "@/components/menu/index.vue";
- import {ElMessage, ElMessageBox} from 'element-plus'
- import Crumbs from "@/components/CommonComponent/crumbs/index.vue";
- import {useMenusStore} from '@/store/permission'
- import {useRouter} from "vue-router";
- import {ref} from "vue";
- import { storeToRefs } from 'pinia'
-
- const store = useMenusStore()
- const router = useRouter()
- let {tabsList,tabsActive}=storeToRefs(store)
-
- function logOut() {
- ElMessageBox.confirm('确认退出登录?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }).then(async () => {
- /*点击的确认*/
- const b = await store.logout()
- if (b) {
- router.push('/login')
- } else {
- ElMessage({
- showClose: true,
- message: '注销失败',
- type: 'error',
- })
- }
- }).catch(() => {
- /*点击的取消*/
- })
- }
-
- function closeTabs(name){
- //判断删除的是否是活动页,如果是则删除并跳到首页,
- if(name==store.tabsActive){
- store.setActive('homepage')
- console.log("删除的是当前页",tabsActive)
- }
- store.delTabs(name)
- }
- function gotoActive(name){
- store.setActive(name)
- router.push(name)
- }
-
- </script>
-
- <style scoped>
-
- .head_class {
- display: flex;
- justify-content: space-between;
- }
-
- /*隐藏侧边栏滚动条*/
- .el-aside::-webkit-scrollbar {
- display: none;
- }
-
- .el-header {
- position: relative;
- width: 100%;
- height: 60px;
- }
-
- .el-aside {
- display: block;
- }
-
- .el-main {
- position: absolute;
- left: 200px;
- right: 0;
- top: 60px;
- bottom: 0;
- overflow-y: scroll;
- }
-
-
- </style>

在菜单渲组件中定义事件

代码如下
- <template>
- <div>
- <template v-for="item in menuList" :key="item.path">
- <!-- 分为两种方式渲染:有子菜单和没有子菜单-->
- <el-sub-menu
- :index="item.path"
- v-if="item.nodeType == 1"
- >
- <template #title>
- <!-- <el-icon v-show="item.iconId!=null"><img :src="sendIcon(item.iconInfo)" alt="" class="icon_class"/></el-icon>-->
- <span>{{ item.name }}</span>
- </template>
- <!-- 有子菜单的继续遍历(递归)-->
- <menuTree :menuList="item.children"></menuTree>
- </el-sub-menu>
- <!-- 没有子菜单-->
- <el-menu-item :index="item.path" v-if="item.nodeType==2" @click="clickOnMenu(item)">
- <!-- <el-icon v-show="item.iconId!=null">-->
- <!-- <img :src="sendIcon(item.iconInfo)" alt="" class="icon_class"/>-->
- <!-- </el-icon>-->
- <span>{{ item.name }}</span>
- </el-menu-item>
- </template>
- </div>
- </template>
-
- <script>
- import {getImage} from '@/api/home/file/index'
- import {getImageToBase64} from '@/util/file'
- import {useMenusStore} from "@/store/permission";
- import { storeToRefs } from 'pinia'
-
- export default ({
- name: 'menuTree',
- props: {
- menuList: {
- type: Array
- }
- },
- setup(props) {
- let store=useMenusStore()
- async function sendIcon(fileInfo) {
- // 获取菜单图片路径
- if (fileInfo != null) {
- return await getImageToBase64(fileInfo)
- }
- }
- // 点击菜单后添加进入tabs数组,需要注意tabs数组里面是否已经存在,
- function clickOnMenu(node){
- let hasNode=store.tabsList.filter(item=>item.path==node.path)
- if (hasNode.length==0 || hasNode==null){
- store.setTabs(node)
- }
- store.setActive(node.path)
- }
- return {
- sendIcon,
- clickOnMenu
- };
- }
- })
- </script>
-
- <style scoped>
- .icon_class {
- width: 30px;
- height: 30px;
- }
- </style>
-

大功告成,如果你不明白这些步骤并且想要了解,请看循序渐进篇。
我们从官网上复制代码,放在哪里呢?
- <el-tabs type="border-card">
- <el-tab-pane label="User">User</el-tab-pane>
- <el-tab-pane label="Config">Config</el-tab-pane>
- <el-tab-pane label="Role">Role</el-tab-pane>
- <el-tab-pane label="Task">Task</el-tab-pane>
- </el-tabs>
放在路由展示的地方,还记得我们前面的前期准备那里吗?
- <div class="common-layout">
- <el-container>
- <el-aside width="200px">
- <div>
- <!-- 菜单侧栏导航栏 -->
- <menus></menus>
- </div>
- </el-aside>
- <el-container>
- <el-header>
- <div class="head_class">
- <!-- 头部 -->
- <Crumbs></Crumbs>
- <div>
- <el-button type="primary" text @click="logOut">注销</el-button>
- </div>
- </div>
- </el-header>
- <el-main>
- <!-- 内容 -->
- <div>
- <el-tabs type="border-card">
- <el-tab-pane label="User">
-
- </el-tab-pane>
- <router-view></router-view>
- </div>
- </el-main>
- </el-container>
- </el-container>
- </div>
-
-
- </el-tabs>

当然,只是这样是不行的,由于这个标签页是动态的,我们需要一个数组来代替它的源数据,这个数组呢并不是固定的。在点击菜单栏的时候就需要添加一个,我们也可以在tabs上面点击删除按钮,删除一个标签页。也就是说,对这个数组的操作是跨页面跨组件的。所以我们有限考虑使用pinia来管理。
在前面的pinia基础上,我们新增一个tabsList

并提供一个新增和删除的方法

代码如下:
- import {defineStore} from "pinia" // 定义容器
- import {getRoleRouter} from "@/api/home/permission/menus";
- import {logout} from '@/api/index'
- import {isObjectValueEqualNew} from '@/util/Utils'
-
-
- export const useMenusStore = defineStore('permission', {
- /**
- * 存储全局状态,类似于vue2中的data里面的数据
- * 1.必须是箭头函数: 为了在服务器端渲染的时候避免交叉请求导致数据状态污染
- * 和 TS 类型推导
- */
- state: () => {
- return {
- menuList: [],//菜单信息
- roles: [],//角色信息
- menuRouter: [],//路由信息
- roleIds: [],//角色Id
- tabsList:[{id:1,name:'首页',path:'homepage'}],
- tabsActive:'homepage'
- }
- },
- /**
- * 用来封装计算属性 有缓存功能 类似于computed
- */
- getters: {},
- /**
- * 编辑业务逻辑 类似于methods
- */
- actions: {
- //菜单
- setMenuList(data) {
- this.menuList = data
- },
- //设置路由:data-->后端传入的路由信息
- setMenuRouter(data) {
- data.forEach(item => {
- //定义一个对象-->routerInfo格式化后端传入的路由信息
- let routerInfo = {
- path: '',
- name: '',
- meta: '',
- component: '',
- }
- routerInfo.path = `${item.path}`
- routerInfo.meta = {name: item.name}
- routerInfo.name = item.name
- routerInfo.component = () => import(`@/views/${item.linkUrl}`)
- this.menuRouter.push(routerInfo)
- })
- },
- setRoles(data) {
- this.roles = data
- this.roleIds = data.map(item => {
- return item.id
- })
- },
- generateRoutes() {
- //获取路由信息
- return new Promise((resolve, reject) => {
- getRoleRouter(this.roleIds).then(res => {
- if (res.data.code == 200) {
- this.setMenuRouter(res.data.data)
- resolve()
- } else {
- reject()
- }
- })
- })
- },
- logout() {
- /*注销登录*/
- return logout().then(res => {
- if (res.data.code == 200) {
- this.$reset()//清除状态管理所有数据
- window.sessionStorage.clear()//清除本地所有缓存
- return true
- }
- else return false
- })
- },
- setTabs(node){
- this.tabsList.push(node)
- },
- delTabs(node){
- /*返回和node不相同的元素,就相当于把相同的元素删掉了*/
- this.tabsList=this.tabsList.filter(item=>{
- if (item.path==node){
- return false
- }
- else return true
- })
- },
- setActive(value){
- this.tabsActive=value
- }
- },
- // 持久化设置
- persist: {
- enabled: true, //开启
- storage: sessionStorage, //修改存储位置
- key: 'permissions', //设置存储的key,在这里是存在sessionStorage时的键
- paths: ['menuList', 'hasRouter', `roles`, 'roleIds','tabsList','tabsActive'],//指定要持久化的字段,menuRouter不需要缓存。因为每次路由跳转我们都可以重新获取
- },
- })

现在,我们有了这样一个数组,就可以遍历生成了,我们在之前的基础上,修改这部分代码,
从pinia中引入tabsList数组,在标签页中遍历它
- <div>
- <el-tabs type="border-card">
- <el-tab-pane :label="item.name" :key="item.name"
- v-for="item in tabsList" :name="item.path" >
-
- </el-tab-pane>
- <router-view></router-view>
- </el-tabs>
- </div>
-
-
-
- import {useMenusStore} from '@/store/permission'
- import {useRouter} from "vue-router";
- import {ref} from "vue";
- import { storeToRefs } from 'pinia'
-
- const store = useMenusStore()
- const router = useRouter()
- let {tabsList,tabsActive}=storeToRefs(store)

分析
现在我们能够便遍历显示了,但是还不够,我们需要点击菜单的时候,想这个数组新增元素
我们找到前面的渲染菜单的组件,在点击没有子菜单的菜单上面添加按钮事件

- function clickOnMenu(node){
- let hasNode=store.tabsList.filter(item=>item.path==node.path)
- if (hasNode.length==0 || hasNode==null){
- //如果数组里面不存在,新增一个
- store.setTabs(node)
- }
-
- }
当点击菜单的时候,就需要新增一个标签页,如果已经存在,就直接跳转到这个标签页。那么问题来了,在上面的代码中,我们能够新增标签页了,但是如何跳转到这个标签页呢?
我们需要借助标签页的这个属性:
name属性标识了每一个标签页(tab-pane),而tab的v-model属性绑定了当前激活的标签页。tab绑定的值是哪一个tab-pane的name,那么哪一个tab-pane就是激活页
先不要着急定义这个激活值,我们仔细分析:在点击菜单的时候,不仅要向数组中添加一个新的标签页,还要切换到这个激活的标签页。也就是说,对这个激活值执行赋值的动作是在菜单组件中,而绑定(获取)这个激活值是在展示标签页的组件中,这又是一个跨页面跨组件的共享数据,我们仍然优先定义在pinia中。我们给他一个初始值,在进入页面的时候默认打开首页,

仍然需要提供一个修改的接口

代码在上面已经给出,这里就不在重复了
我们有了这个激活值,就需要绑定,并且考虑到在点击不同的标签页的时候需要切换路由,我们需要在激活值改变的时候,就切换到这个激活值对应的路由上。
修改标签页的代码,我们将数组中的路由路径(path)绑定为name值,closable表示当前标签是否可以被删除,我这里只有首页是不能被删除的
- <el-main>
- <!-- 内容 -->
- <div>
- <el-tabs type="border-card" v-model="tabsActive" @tab-change="gotoActive" @tab-remove="closeTabs">
- <el-tab-pane :label="item.name" :key="item.name"
- v-for="item in tabsList" :name="item.path" :closable="item.isClose==1" >
- <router-view></router-view>
- </el-tab-pane>
- </el-tabs>
- </div>
- </el-main>
-
- <script setup>
- import menus from "@/components/menu/index.vue";
- import {ElMessage, ElMessageBox} from 'element-plus'
- import Crumbs from "@/components/CommonComponent/crumbs/index.vue";
- import {useMenusStore} from '@/store/permission'
- import {useRouter} from "vue-router";
- import {ref} from "vue";
- import { storeToRefs } from 'pinia'
- import {getToken} from "@/util/token/getToken";
-
- const store = useMenusStore()
- const router = useRouter()
- let {tabsList,tabsActive}=storeToRefs(store)
-
- function logOut() {
- ElMessageBox.confirm('确认退出登录?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }).then(async () => {
- /*点击的确认*/
- const b = await store.logout()
- if (b) {
- router.push('/login')
- } else {
- ElMessage({
- showClose: true,
- message: '注销失败',
- type: 'error',
- })
- }
- }).catch(() => {
- /*点击的取消*/
- })
- }
-
- function closeTabs(name){
- //判断删除的是否是活动页,如果是则删除并跳到首页,
- if(name==store.tabsActive){
- store.setActive('homepage')
- console.log("删除的是当前页",tabsActive)
- }
- store.delTabs(name)
- }
- function gotoActive(name) {
- /*防止退出登录时清楚缓存时触发(active的值发生变动)*/
- if (getToken("token")) {
- store.setActive(name)
- router.push(name)
- }
- }
-
- </script>

还记得之前我们菜单点击哪里吗?那里只实现了添加标签页的功能还没有实现切换的功能,我们现在只需要在点击事件里面添加切换激活值就行了

后端响应的菜单数据
- {
- "menuPermission": [{
- "id": "1",
- "name": "首页",
- "menuCode": "homepage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/index.vue",
- "iconId": "1",
- "level": 0,
- "path": "homepage",
- "createTime": null,
- "isClose": 0,
- "children": [],
- "iconInfo": null
- }, {
- "id": "2",
- "name": "用户管理",
- "menuCode": "user_manage",
- "parentId": "0",
- "nodeType": 1,
- "sort": 0,
- "linkUrl": "home/UserManage",
- "iconId": "8",
- "level": 0,
- "path": "",
- "createTime": "2023-07-08 19:37:26",
- "isClose": 1,
- "children": [{
- "id": "201",
- "name": "后台用户管理",
- "menuCode": "system_user_manage",
- "parentId": "2",
- "nodeType": 2,
- "sort": 1,
- "linkUrl": "home/content/UserManage/System/index.vue",
- "iconId": "3",
- "level": 1,
- "path": "systemuser",
- "createTime": "2023-07-08 20:02:04",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "202",
- "name": "商家管理",
- "menuCode": "business_user_manage",
- "parentId": "2",
- "nodeType": 2,
- "sort": 1,
- "linkUrl": "home/content/UserManage/Business/index.vue",
- "iconId": "3",
- "level": 1,
- "path": "businessuser",
- "createTime": "2023-07-08 20:04:40",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "203",
- "name": "前台会员管理",
- "menuCode": "member_user_manage",
- "parentId": "2",
- "nodeType": 2,
- "sort": 1,
- "linkUrl": "home/content/UserManage/Member/index.vue",
- "iconId": "3",
- "level": 1,
- "path": "memberuser",
- "createTime": "2023-07-08 20:07:20",
- "isClose": 1,
- "children": [{
- "id": "20301",
- "name": "新增前台用户",
- "menuCode": "member_user_add",
- "parentId": "203",
- "nodeType": 3,
- "sort": 2,
- "linkUrl": null,
- "iconId": "3",
- "level": 2,
- "path": "",
- "createTime": "2023-07-18 10:30:56",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20302",
- "name": "删除用户",
- "menuCode": "member_user_delete",
- "parentId": "203",
- "nodeType": 3,
- "sort": 2,
- "linkUrl": null,
- "iconId": "3",
- "level": 2,
- "path": "\n",
- "createTime": null,
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }],
- "iconInfo": null
- }, {
- "id": "20303",
- "name": "用户新增",
- "menuCode": "user_add",
- "parentId": "2",
- "nodeType": 3,
- "sort": null,
- "linkUrl": null,
- "iconId": null,
- "level": null,
- "path": "",
- "createTime": "2023-08-24 21:06:30",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20304",
- "name": "用户修改",
- "menuCode": "user_update",
- "parentId": "2",
- "nodeType": 3,
- "sort": null,
- "linkUrl": null,
- "iconId": null,
- "level": null,
- "path": null,
- "createTime": "2023-08-24 21:25:00",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }],
- "iconInfo": null
- }, {
- "id": "3",
- "name": "字典管理",
- "menuCode": "dictionary_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/DictionaryManage/index.vue",
- "iconId": "9",
- "level": 0,
- "path": "dictionary",
- "createTime": "2023-07-13 16:45:54",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "4",
- "name": "菜单管理",
- "menuCode": "menu_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/MenuManage/index.vue",
- "iconId": "3",
- "level": 0,
- "path": "menu",
- "createTime": "2023-07-08 19:05:18",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "5",
- "name": "店铺管理",
- "menuCode": "store_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/ShopManage/index.vue",
- "iconId": "6",
- "level": 0,
- "path": "shop",
- "createTime": "2023-07-13 16:29:38",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "6",
- "name": "商品管理",
- "menuCode": "goods_manage",
- "parentId": "0",
- "nodeType": 1,
- "sort": 0,
- "linkUrl": "",
- "iconId": "5",
- "level": 0,
- "path": "goods",
- "createTime": "2023-07-13 16:42:17",
- "isClose": 1,
- "children": [{
- "id": "20305",
- "name": "商品发布",
- "menuCode": "goods_add",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/AddGoods.vue",
- "iconId": null,
- "level": null,
- "path": "addgoods",
- "createTime": "2023-08-26 14:39:21",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20307",
- "name": "分类管理",
- "menuCode": "category_manage",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/CategoryManage.vue",
- "iconId": null,
- "level": null,
- "path": "category",
- "createTime": "2023-08-28 11:49:34",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20308",
- "name": "属性分组",
- "menuCode": "attrGroup",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/AttrGroup",
- "iconId": null,
- "level": null,
- "path": "attr-group",
- "createTime": "2023-09-09 10:09:52",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20310",
- "name": "规格参数",
- "menuCode": "attribute_manage",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/AttributeManage.vue",
- "iconId": null,
- "level": null,
- "path": "attribute",
- "createTime": "2023-09-12 22:45:52",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20311",
- "name": "spu管理",
- "menuCode": "spu-manage",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/SpuManage.vue",
- "iconId": null,
- "level": null,
- "path": "spu-manage",
- "createTime": "2023-10-05 22:07:56",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20312",
- "name": "sku管理",
- "menuCode": "sku-manage",
- "parentId": "6",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/GoodsManage/SkuManage.vue",
- "iconId": null,
- "level": null,
- "path": "sku-manage",
- "createTime": "2023-10-08 20:41:06",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }],
- "iconInfo": null
- }, {
- "id": "7",
- "name": "订单管理",
- "menuCode": "order_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/OrderManage/index.vue",
- "iconId": "10",
- "level": 0,
- "path": "order",
- "createTime": "2023-07-13 16:43:15",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "8",
- "name": "库存系统",
- "menuCode": "warehouse_system",
- "parentId": "0",
- "nodeType": 1,
- "sort": 0,
- "linkUrl": "",
- "iconId": "4",
- "level": 0,
- "path": "warehouse",
- "createTime": "2023-07-13 16:44:36",
- "isClose": 1,
- "children": [{
- "id": "20313",
- "name": "商品库存",
- "menuCode": "goods_inventory",
- "parentId": "8",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/WarehouseManage/Inventory.vue",
- "iconId": null,
- "level": null,
- "path": "goods_inventory",
- "createTime": "2023-10-11 20:42:12",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20314",
- "name": "仓库维护",
- "menuCode": "warehouse_manage",
- "parentId": "8",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/WarehouseManage/Warehouse.vue",
- "iconId": null,
- "level": null,
- "path": "warehouse",
- "createTime": "2023-10-13 18:00:55",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20315",
- "name": "采购单维护",
- "menuCode": "procurement",
- "parentId": "8",
- "nodeType": 1,
- "sort": null,
- "linkUrl": null,
- "iconId": null,
- "level": null,
- "path": null,
- "createTime": "2023-10-14 11:06:24",
- "isClose": 1,
- "children": [{
- "id": "20316",
- "name": "采购需求",
- "menuCode": "purchase-detail",
- "parentId": "20315",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/WarehouseManage/ProcurementManage/PurchaseDetail.vue",
- "iconId": null,
- "level": null,
- "path": "purchase-detail",
- "createTime": "2023-10-14 20:50:05",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20317",
- "name": "采购单",
- "menuCode": "purchase",
- "parentId": "20315",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/WarehouseManage/ProcurementManage/Purchase.vue",
- "iconId": null,
- "level": null,
- "path": "purchase",
- "createTime": "2023-10-14 20:52:59",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20318",
- "name": "我的采购单",
- "menuCode": "my-purchase",
- "parentId": "20315",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/WarehouseManage/ProcurementManage/MyPurchase.vue",
- "iconId": null,
- "level": null,
- "path": "my-purchase",
- "createTime": "2023-10-17 10:57:03",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }],
- "iconInfo": null
- }],
- "iconInfo": null
- }, {
- "id": "9",
- "name": "文件管理",
- "menuCode": "file_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/FilesManage/index.vue",
- "iconId": "7",
- "level": 0,
- "path": "files",
- "createTime": "2023-07-28 12:25:50",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "10",
- "name": "角色管理",
- "menuCode": "role_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": 0,
- "linkUrl": "home/content/RoleManage/index.vue",
- "iconId": "2",
- "level": null,
- "path": "role",
- "createTime": "2023-07-31 16:21:36",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }, {
- "id": "20306",
- "name": "品牌管理",
- "menuCode": "brand_manage",
- "parentId": "0",
- "nodeType": 2,
- "sort": null,
- "linkUrl": "home/content/Brand/BrandManage",
- "iconId": null,
- "level": null,
- "path": "brand",
- "createTime": "2023-08-28 09:45:03",
- "isClose": 1,
- "children": [],
- "iconInfo": null
- }]
- }

进入这个仓库找到

对于实现动态路由这一块,不一定非要使用addRoute函数,也可以采用其他思路。
在上面说过,vue的路由初始化后不能直接修改路由数据(包括新增路由,删除路由等操作),既然在初始化之后不能操作,那我们可以在路由初始化之前操作啊!
比如说直接将获取到的路由数据直接在routes数组中添加,和静态路由一块加载。我们找到定义静态路由的route/index.js里面,大概思路如下

然后我们就不需要在路由守卫里使用addroute()函数了
需要注意的是:
局限性:能实现动态路由(加载路由时动态),不能实现动态路由(路由加载后就不是动态了)
这个也很好理解,前者表示加载的路由确实不是写死的,而是动态获取的。后者的意思就是加载后得路由就不能修改了,他就不是动态的了(除非又使用addRoute函数)
注:这个方法并不能完成我们文中的权限路由功能,为什么呢?
也很好理解,我们获取后端路由数据的时候是什么时候?是在登录成功后!这个时候路由早就初始化了!我们说过,这个方法需要保证获取路由数据在路由加载前就要完成!在路由完成加载前,你能登录吗?不能(都不能访问登录页呢。。。)。
该方法思路来源于一个粉丝私信提问引申而出。虽然他不能实现功能,我写这种方法呢,是希望采用这种方法的伙伴趁早打消这个念头,因为他并不能实现这个功能。他是一个假的“动态路由”!
以上对于该方法的描述均为猜测!!!哈哈,没想到吧?到底能不能实现,自个琢磨去吧
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。