詳解vue3+element-plus實現(xiàn)動態(tài)菜單和動態(tài)路由動態(tài)按鈕(前后端分離)
前言
本篇文章旨在從零搭建一個動態(tài)路由動態(tài)菜單的后臺管理系統(tǒng)初始環(huán)境,如果您只有個別地方?jīng)]有實現(xiàn),那么可以根據(jù)目錄選擇性的閱讀您所需的內(nèi)容
寫在前面
本文使用技術(shù):vue3,pinia狀態(tài)管理,element-plus,axios
注意在本文章中,有些方法并沒有粘出來,比如一些發(fā)送請求的方法,因為沒有必要,需要根據(jù)你們自己的情況進行修改,如果你看到一些方法并沒有寫出來,那多半就是發(fā)送請求的方法。
效果預覽


前期準備
本文需要使用axios,路由,pinia,安裝element-plus,并且本文vue3是基于js而非ts的,這些環(huán)境如何搭建不做描述,需要讀者自己完成。下面開始
在這之前我們需要一個布局

假設(shè)我們的home組件,是如下
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px">
<div>
<!-- 菜單側(cè)欄導航欄 -->
<menus></menus>
</div>
</el-aside>
<el-container>
<el-header>
<div class="head_class">
<!-- 頭部內(nèi)容 可以忽略-->
<Crumbs></Crumbs>
<div>
<el-button type="primary" text @click="logOut">注銷</el-button>
</div>
</div>
</el-header>
<el-main>
<!-- 主要內(nèi)容 -->
<div>
<router-view></router-view>
</div>
</el-main>
</el-container>
</el-container>
</div>
</template>話不多說,下面正片開始
動態(tài)菜單
功能:根據(jù)不同的角色顯示不同的菜單,菜單數(shù)據(jù)由后端提供,前端負責渲染即可
實現(xiàn)步驟:獲取菜單--->緩存--->渲染菜單
我們使用elemen-plus的menu菜單來渲染

獲取菜單:
首先我們需要先獲取到菜單,一般是在登錄的時候獲取。可以根據(jù)自己的情況而定。由于我后端采用的是RBAC模型,也就是所謂的用戶-角色-權(quán)限模型,而菜單屬于權(quán)限,我需要先獲取角色信息,然后獲取菜單信息,下面是我的登錄組件的登錄方法,可能代碼比較多,如果你已經(jīng)獲取了菜單,可以直接跳過此處
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")
})
}
}上面主要是發(fā)送請求獲取菜單,然后將菜單數(shù)據(jù)緩存到pinia中,也就是上面代碼的store,下面是pinia的代碼(緩存菜單數(shù)據(jù)以及路由數(shù)據(jù))
import {defineStore} from "pinia" // 定義容器
import {getRoleRouter} from "@/api/home/permission/menus";
import {logout} from '@/api/index'
export const useMenusStore = defineStore('permission', {
/**
* 存儲全局狀態(tài),類似于vue2中的data里面的數(shù)據(jù)
* 1.必須是箭頭函數(shù): 為了在服務(wù)器端渲染的時候避免交叉請求導致數(shù)據(jù)狀態(tài)污染
* 和 TS 類型推導
*/
state: () => {
return {
menuList: [],//菜單信息
roles: [],//角色信息
menuRouter: [],//路由信息
roleIds: [],//角色Id
}
},
/**
* 用來封裝計算屬性 有緩存功能 類似于computed
*/
getters: {},
/**
* 編輯業(yè)務(wù)邏輯 類似于methods
*/
actions: {
//菜單
setMenuList(data) {
this.menuList = data
},
//設(shè)置路由: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()//清除狀態(tài)管理所有數(shù)據(jù)
window.sessionStorage.clear()//清除本地所有緩存
return true
}
else return false
})
},
},
// 持久化設(shè)置
persist: {
enabled: true, //開啟
storage: sessionStorage, //修改存儲位置
key: 'permissions', //設(shè)置存儲的key,在這里是存在sessionStorage時的鍵
paths: ['menuList', 'hasRouter', `roles`, 'roleIds'],//指定要持久化的字段,menuRouter不需要緩存。因為每次路由跳轉(zhuǎn)我們都可以重新獲取
},
})你可能會說,這直接粘一些垃圾代碼,誰愛看啊,別擔心,在上面的代碼主要用到的是將角色、菜單信息緩存的操作,你需要關(guān)注的也就是下面的截圖,其他的代碼暫時不用理會,后面會講


在上面的代碼中,主要是將菜單信息和角色信息存入pinia,當然角色信息是否存儲并不重要,因為他跟我們的主題并沒有多大的相關(guān)
注意:這個菜單是菜單樹的結(jié)構(gòu)哦!如果想看看后端響應的數(shù)據(jù)結(jié)構(gòu),可以在文章最后面查看
好了到這咱們已經(jīng)獲取到了菜單了,下面就是生成菜單了
生成(渲染)菜單
獲取了菜單,我們就需要在首頁中渲染菜單。
使用明確:element-plus中menu主要分為兩種狀態(tài):有子菜單(目錄)和沒有子菜單,我們需要根據(jù)這兩種分別渲染。

我們就可以粘代碼修修改就可以了,
新建一個vue文件:我們假定這個組件是menus
<template>
<el-row>
<el-col>
<h5 class="mb-2">后臺管理系統(tǒng)</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的緩存的菜單數(shù)據(jù)
</script>你可能會問:怎么代碼這么少?而且只有<el-menu>標簽?渲染菜單的<el-sub-menu>和<el-menu-item>標簽呢?
由于我們會采用遞歸,如果把所有代碼都寫到一個組件里面,那么所有元素都會重復了,比如說上面這個“后臺管理系統(tǒng)”標題會被重復顯示,那么這肯定是不行的,所以我們需要將渲染菜單的具體操作放在另一個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>
<!-- 有子菜單的繼續(xù)遍歷(遞歸)-->
<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的值來判斷是否存在子菜單。因為我在數(shù)據(jù)庫存儲的菜單屬性中規(guī)定nodeType=1就是目錄,具有子菜單,=2就是沒有子菜單,是頁面,=3就是按鈕。你也可以通過其他的方法來判斷,比如說,菜單children的長度是否大于零。如果有子菜單,那么我們還需要再一次遍歷,也就是遞歸,無論我們有多少級菜單都能夠遍歷出來,當然,理論上層次深了會爆棧,但是正經(jīng)人誰會弄那么深是吧?
2、我采用父子組件的傳遞數(shù)據(jù)的方法把菜單數(shù)據(jù)傳入帶渲染菜單的子組件中
好了到這里,基本就實現(xiàn)了動態(tài)菜單的效果了,但這還不行,雖然用戶不能通過菜單的方式來訪問與他無關(guān)的頁面,但是他可以通過手動輸入路由信息來跳轉(zhuǎn),所以我們需要使用動態(tài)路由來防止
動態(tài)路由
功能:根據(jù)不同的權(quán)限創(chuàng)建對應的路由
實現(xiàn)步驟:獲取路由----->處理路由格式并緩存------>創(chuàng)建路由(前置守衛(wèi))
獲取路由
我們查看pinia中的獲取路由方法,具體代碼就在上面的pinia那里
獲取路由的請求

上面沒什么好說的,就一個獲取路由信息然后存入pinia中的操作,重點是下面的處理路由信息,這里我們需要將后端傳過來的數(shù)據(jù)處理為路由格式。
處理路由格式

注意:由于我這里所有路由都只有一層,并沒有子路由,所以這樣處理,如果你想要多嵌套幾個路由,那么你需要將后端傳來的路由信息處理為樹結(jié)構(gòu)(也可以在后端處理),然后再遍歷處理(可能還需要遞歸遍歷處理) 并添加到children屬性里面,這里就不多說
好了,到現(xiàn)在我們已經(jīng)做完準備,到了最終的環(huán)節(jié):在哪里創(chuàng)建?怎么創(chuàng)建路由?
創(chuàng)建路由
老樣子先粘代碼,再解說
這里是router/index.js代碼,沒什么特別的,主要是定義一些靜態(tài)路由,和必要的路由配置創(chuàng)建
import {createRouter, createWebHistory} from "vue-router";
import {useMenusStore} from "@/store/index";
//靜態(tài)路由
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我們采用的是在路由守衛(wèi)中動態(tài)創(chuàng)建路由,下面就是router/permission.js。主要是定義一個前置守衛(wèi),守衛(wèi)里面動態(tài)創(chuàng)建路由,
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']
//全局路由守衛(wèi)
router.beforeEach((to, from, next) => {
NProgress.start();
const store = useMenusStore()
const hasetoken = getToken('token')
if (to.path == "/login" && hasetoken) {
//如果已經(jīng)登錄還在訪問登錄頁,直接跳轉(zhuǎn)首頁
next('/home')
}
if (Whitelist.indexOf(to.path) == -1) {
//沒有在白名單里面
if (store.menuRouter.length == 0 && hasetoken) {
/*已經(jīng)登錄還未獲取路由,就獲取路由信息(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:已經(jīng)獲取了路由(一定有token),2,沒有登錄(一定沒有路由)*/
NProgress.done()
if (hasetoken && to.matched.length != 0) {
/*to.matched.length === 0判斷當前輸入的路由是否具有,即使登陸了,如果輸入不能訪問的路由也要404*/
/*已經(jīng)獲取了路由,并且訪問路由合法,放行*/
next()
} else {
next('/404')
}/*沒有登錄、登錄了但是訪問路由不合法,都跳轉(zhuǎn)404*/
}
} else {
NProgress.done()
/*在白名單里面,直接放行*/
next()
}
})
在上面代碼中:
- 我們采用addRoute方法來動態(tài)創(chuàng)建路由,這方法好像只能一次創(chuàng)建一個路由,并且還是根路由(一級路由),由于我們菜單路由是home(一級路由)下的子路由,所以就放在home的children里面。
- 我們采用next({...to, replace: true})來確保路由創(chuàng)建完畢,注意,他并不是直接放行,添加完路由后,他會再重新進入路由守衛(wèi)。
- 在上面2中說過,由于next({...to, replace: true})并不能直接放行,會再一次進入守衛(wèi),所以在

中這個if()判斷尤為重要,不然一直循環(huán)進入這個if()里面出不來,會導致瀏覽器白屏卡死 ,在if()之外一定要放行。在上面的代碼中,我是采用pinia里面存儲路由的數(shù)組長度判斷,當然你也可以用其他的方法,比如當前的路由長度減去靜態(tài)路由是否為0作為條件。當然也需要判斷好其他情況,比如用戶輸入不存在的路由,我們需要將其跳轉(zhuǎn)到404等。
4. 路由刷新會丟失,每次刷新需要重新創(chuàng)建。每一次刷新頁面都會丟失動態(tài)添加的路由。在上面中我們使用的是判斷pinia中路由數(shù)組的長度來判斷的,在這樣的情況下,我們就不能持久化pinia這個路由數(shù)組(如果是其他判斷條件則可以持久化,一切以判斷條件而立),不然頁面刷新丟失路由,卻沒法進入這個if()里面(因為如果持久化,這個menrouter就不會為空,if判斷條件不成立)。無法進入if創(chuàng)建動態(tài)創(chuàng)建路由,在上面的代碼中就會一直進入404(如果沒有404頁面處理,就是白屏)

可以看到我并沒有將這路由數(shù)組持久化。

我還是再說一遍,如果你是其他判斷條件,比如說你是使用路由的長度判斷(是否大于靜態(tài)路由數(shù)組長度),那么就可以持久化,而在if()內(nèi)部也不用每次都發(fā)送請求,不然持久化也失去了意義。
當然,就我而言,更傾向于每次刷新就發(fā)送請求,這樣可以確保數(shù)據(jù)的準確性。
這里提一嘴,permission.js定義的路由守衛(wèi)想要生效就需要在min.js中引入,如果你覺得麻煩就全定義在router/index.js中也是可行的,如果你是單獨新建js實現(xiàn)的路由守衛(wèi),記得引入哦。

動態(tài)按鈕
分析
動態(tài)按鈕的實現(xiàn)可以通過v-if或者v-show指令來動態(tài)渲染按鈕(根據(jù)后端返回的權(quán)限碼比對);
如果你覺得麻煩,你可以用vue中自定義組件來簡化。這里就不再贅述了
主要分為三個步驟:獲取權(quán)限碼,緩存權(quán)限碼,權(quán)限對比(v-if或者v-show)
獲取權(quán)限碼
假設(shè)我們在登錄的時候就獲取權(quán)限碼,下面是login頁面登錄時發(fā)送的請求,將這個結(jié)果存入pinia

緩存權(quán)限碼
在store/permission.js中

提供對應的賦值方法以及緩存

權(quán)限對比
在需要的頁面中引入,并判斷
場景:假設(shè)我們的商品上架只有超級管理員才能使用

indexof表示判斷一個數(shù)組是否包含一個元素,如果包含就返回索引,不包含就返回-1。
大工告成!
實現(xiàn)效果
管理員登錄

商家登錄

有沒有發(fā)現(xiàn)還是太丑了一點?接下來我們來嘗試美化一下它
+el-tabs美化
寫在前面
這部分,分為快速篇和循序漸進篇兩種,如果耐心不夠,請看快速篇,如果你覺得還行,可以看循序漸進篇
使用element-plus的tabs標簽

分析功能:
- 點擊菜單欄的時候,如果沒有這個tab頁,就新增,如果有就跳轉(zhuǎn)
- 點擊tabs的時候跳轉(zhuǎn)對應的路由
- tabs可以被刪除
快速篇
主要修改地方:
- pinia中定義標簽綁定所需的數(shù)據(jù)
- 在路由展示地方添加標簽頁代碼,并設(shè)置切換事件
- 在菜單渲染組件中添加新增標簽頁的事件
在pinia中定義標識激活標簽頁的值和標簽頁的數(shù)據(jù)數(shù)組,

以及對應的對外提供操作的接口

路由展示的地方添加標簽代碼如下

代碼如下
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px">
<div>
<!-- 菜單側(cè)欄導航欄 -->
<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>
<!-- 內(nèi)容 -->
<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>
</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;
}
/*隱藏側(cè)邊欄滾動條*/
.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>
<!-- 有子菜單的繼續(xù)遍歷(遞歸)-->
<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數(shù)組,需要注意tabs數(shù)組里面是否已經(jīng)存在,
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>大功告成,如果你不明白這些步驟并且想要了解,請看循序漸進篇。
循序漸進篇
我們從官網(wǎng)上復制代碼,放在哪里呢?
<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>
<!-- 菜單側(cè)欄導航欄 -->
<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>
<!-- 內(nèi)容 -->
<div>
<el-tabs type="border-card">
<el-tab-pane label="User">
<router-view></router-view>
</el-tab-pane>
</div>
</el-main>
</el-container>
</el-container>
</div>
</el-tabs>當然,只是這樣是不行的,由于這個標簽頁是動態(tài)的,我們需要一個數(shù)組來代替它的源數(shù)據(jù),這個數(shù)組呢并不是固定的。在點擊菜單欄的時候就需要添加一個,我們也可以在tabs上面點擊刪除按鈕,刪除一個標簽頁。也就是說,對這個數(shù)組的操作是跨頁面跨組件的。所以我們有限考慮使用pinia來管理。
在前面的pinia基礎(chǔ)上,我們新增一個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', {
/**
* 存儲全局狀態(tài),類似于vue2中的data里面的數(shù)據(jù)
* 1.必須是箭頭函數(shù): 為了在服務(wù)器端渲染的時候避免交叉請求導致數(shù)據(jù)狀態(tài)污染
* 和 TS 類型推導
*/
state: () => {
return {
menuList: [],//菜單信息
roles: [],//角色信息
menuRouter: [],//路由信息
roleIds: [],//角色Id
tabsList:[{id:1,name:'首頁',path:'homepage'}],
tabsActive:'homepage'
}
},
/**
* 用來封裝計算屬性 有緩存功能 類似于computed
*/
getters: {},
/**
* 編輯業(yè)務(wù)邏輯 類似于methods
*/
actions: {
//菜單
setMenuList(data) {
this.menuList = data
},
//設(shè)置路由: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()//清除狀態(tài)管理所有數(shù)據(jù)
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
}
},
// 持久化設(shè)置
persist: {
enabled: true, //開啟
storage: sessionStorage, //修改存儲位置
key: 'permissions', //設(shè)置存儲的key,在這里是存在sessionStorage時的鍵
paths: ['menuList', 'hasRouter', `roles`, 'roleIds','tabsList','tabsActive'],//指定要持久化的字段,menuRouter不需要緩存。因為每次路由跳轉(zhuǎn)我們都可以重新獲取
},
})現(xiàn)在,我們有了這樣一個數(shù)組,就可以遍歷生成了,我們在之前的基礎(chǔ)上,修改這部分代碼,
從pinia中引入tabsList數(shù)組,在標簽頁中遍歷它
<div>
<el-tabs type="border-card">
<el-tab-pane :label="item.name" :key="item.name"
v-for="item in tabsList" :name="item.path" >
<router-view></router-view>
</el-tab-pane>
</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)分析
- storeToRefs方法在這里的主要作用是將pinia中的數(shù)據(jù)變?yōu)轫憫?。如果不使用這個,數(shù)組修改后,標簽頁并不會隨之渲染出來
- 使用標簽頁的name屬性綁定tabsList存儲的路由路徑
現(xiàn)在我們能夠便遍歷顯示了,但是還不夠,我們需要點擊菜單的時候,想這個數(shù)組新增元素
我們找到前面的渲染菜單的組件,在點擊沒有子菜單的菜單上面添加按鈕事件

<el-main>
<!-- 內(nèi)容 -->
<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'
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>當點擊菜單的時候,就需要新增一個標簽頁,如果已經(jīng)存在,就直接跳轉(zhuǎn)到這個標簽頁。那么問題來了,在上面的代碼中,我們能夠新增標簽頁了,但是如何跳轉(zhuǎn)到這個標簽頁呢?
我們需要借助標簽頁的這個屬性:


name屬性標識了每一個標簽頁(tab-pane),而tab的v-model屬性綁定了當前激活的標簽頁。tab綁定的值是哪一個tab-pane的name,那么哪一個tab-pane就是激活頁
先不要著急定義這個激活值,我們仔細分析:在點擊菜單的時候,不僅要向數(shù)組中添加一個新的標簽頁,還要切換到這個激活的標簽頁。也就是說,對這個激活值執(zhí)行賦值的動作是在菜單組件中,而綁定(獲?。┻@個激活值是在展示標簽頁的組件中,這又是一個跨頁面跨組件的共享數(shù)據(jù),我們?nèi)匀粌?yōu)先定義在pinia中。我們給他一個初始值,在進入頁面的時候默認打開首頁,

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

代碼在上面已經(jīng)給出,這里就不在重復了
我們有了這個激活值,就需要綁定,并且考慮到在點擊不同的標簽頁的時候需要切換路由,我們需要在激活值改變的時候,就切換到這個激活值對應的路由上。
修改標簽頁的代碼,我們將數(shù)組中的路由路徑(path)綁定為name值,closable表示當前標簽是否可以被刪除,我這里只有首頁是不能被刪除的
{
"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": "商品發(fā)布",
"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": "規(guī)格參數(shù)",
"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": "庫存系統(tǒng)",
"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
}]
}還記得之前我們菜單點擊哪里嗎?那里只實現(xiàn)了添加標簽頁的功能還沒有實現(xiàn)切換的功能,我們現(xiàn)在只需要在點擊事件里面添加切換激活值就行了

后端響應的菜單數(shù)據(jù)
{
"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": "商品發(fā)布",
"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": "規(guī)格參數(shù)",
"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": "庫存系統(tǒng)",
"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
}]
}源碼地址
進入這個倉庫找到

到此這篇關(guān)于vue3+element-plus實現(xiàn)動態(tài)菜單和動態(tài)路由動態(tài)按鈕(前后端分離)的文章就介紹到這了,更多相關(guān)vue3 element-plus動態(tài)菜單內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue如何通過Vue.prototype定義原型屬性實現(xiàn)全局變量
在Vue.js開發(fā)中,通過原型屬性為Vue實例添加全局變量是一種常見做法,使用$前綴命名,可以避免與組件內(nèi)部的數(shù)據(jù)、方法或計算屬性產(chǎn)生命名沖突,這種方式簡單有效,確保了變量在所有Vue實例中的可用性,同時保持全局作用域的整潔2024-10-10

