詳解vue3+element-plus實(shí)現(xiàn)動(dòng)態(tài)菜單和動(dòng)態(tài)路由動(dòng)態(tài)按鈕(前后端分離)
前言
本篇文章旨在從零搭建一個(gè)動(dòng)態(tài)路由動(dòng)態(tài)菜單的后臺管理系統(tǒng)初始環(huán)境,如果您只有個(gè)別地方?jīng)]有實(shí)現(xiàn),那么可以根據(jù)目錄選擇性的閱讀您所需的內(nèi)容
寫在前面
本文使用技術(shù):vue3,pinia狀態(tài)管理,element-plus,axios
注意在本文章中,有些方法并沒有粘出來,比如一些發(fā)送請求的方法,因?yàn)闆]有必要,需要根據(jù)你們自己的情況進(jìn)行修改,如果你看到一些方法并沒有寫出來,那多半就是發(fā)送請求的方法。
效果預(yù)覽
前期準(zhǔn)備
本文需要使用axios,路由,pinia,安裝element-plus,并且本文vue3是基于js而非ts的,這些環(huán)境如何搭建不做描述,需要讀者自己完成。下面開始
在這之前我們需要一個(gè)布局
假設(shè)我們的home組件,是如下
<template> <div class="common-layout"> <el-container> <el-aside width="200px"> <div> <!-- 菜單側(cè)欄導(dǎo)航欄 --> <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>
話不多說,下面正片開始
動(dòng)態(tài)菜單
功能:根據(jù)不同的角色顯示不同的菜單,菜單數(shù)據(jù)由后端提供,前端負(fù)責(zé)渲染即可
實(shí)現(xiàn)步驟:獲取菜單--->緩存--->渲染菜單
我們使用elemen-plus的menu菜單來渲染
獲取菜單:
首先我們需要先獲取到菜單,一般是在登錄的時(shí)候獲取??梢愿鶕?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í)候避免交叉請求導(dǎo)致數(shù)據(jù)狀態(tài)污染 * 和 TS 類型推導(dǎo) */ state: () => { return { menuList: [],//菜單信息 roles: [],//角色信息 menuRouter: [],//路由信息 roleIds: [],//角色I(xiàn)d } }, /** * 用來封裝計(jì)算屬性 有緩存功能 類似于computed */ getters: {}, /** * 編輯業(yè)務(wù)邏輯 類似于methods */ actions: { //菜單 setMenuList(data) { this.menuList = data }, //設(shè)置路由:data-->后端傳入的路由信息 setMenuRouter(data) { data.forEach(item => { //定義一個(gè)對象-->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時(shí)的鍵 paths: ['menuList', 'hasRouter', `roles`, 'roleIds'],//指定要持久化的字段,menuRouter不需要緩存。因?yàn)槊看温酚商D(zhuǎn)我們都可以重新獲取 }, })
你可能會說,這直接粘一些垃圾代碼,誰愛看啊,別擔(dān)心,在上面的代碼主要用到的是將角色、菜單信息緩存的操作,你需要關(guān)注的也就是下面的截圖,其他的代碼暫時(shí)不用理會,后面會講
在上面的代碼中,主要是將菜單信息和角色信息存入pinia,當(dāng)然角色信息是否存儲并不重要,因?yàn)樗覀兊闹黝}并沒有多大的相關(guān)
注意:這個(gè)菜單是菜單樹的結(jié)構(gòu)哦!如果想看看后端響應(yīng)的數(shù)據(jù)結(jié)構(gòu),可以在文章最后面查看
好了到這咱們已經(jīng)獲取到了菜單了,下面就是生成菜單了
生成(渲染)菜單
獲取了菜單,我們就需要在首頁中渲染菜單。
使用明確:element-plus中menu主要分為兩種狀態(tài):有子菜單(目錄)和沒有子菜單,我們需要根據(jù)這兩種分別渲染。
我們就可以粘代碼修修改就可以了,
新建一個(gè)vue文件:我們假定這個(gè)組件是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>標(biāo)簽?渲染菜單的<el-sub-menu>和<el-menu-item>標(biāo)簽?zāi)兀?/p>
由于我們會采用遞歸,如果把所有代碼都寫到一個(gè)組件里面,那么所有元素都會重復(fù)了,比如說上面這個(gè)“后臺管理系統(tǒng)”標(biāo)題會被重復(fù)顯示,那么這肯定是不行的,所以我們需要將渲染菜單的具體操作放在另一個(gè)vue文件里面,具體的代碼就在這個(gè)<menuTree> 組件里面,當(dāng)然我們還需要將pinia中存入的菜單信息傳給這個(gè)組件,讓其渲染
下面我們來看看這個(gè)<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的值來判斷是否存在子菜單。因?yàn)槲以跀?shù)據(jù)庫存儲的菜單屬性中規(guī)定nodeType=1就是目錄,具有子菜單,=2就是沒有子菜單,是頁面,=3就是按鈕。你也可以通過其他的方法來判斷,比如說,菜單children的長度是否大于零。如果有子菜單,那么我們還需要再一次遍歷,也就是遞歸,無論我們有多少級菜單都能夠遍歷出來,當(dāng)然,理論上層次深了會爆棧,但是正經(jīng)人誰會弄那么深是吧?
2、我采用父子組件的傳遞數(shù)據(jù)的方法把菜單數(shù)據(jù)傳入帶渲染菜單的子組件中
好了到這里,基本就實(shí)現(xiàn)了動(dòng)態(tài)菜單的效果了,但這還不行,雖然用戶不能通過菜單的方式來訪問與他無關(guān)的頁面,但是他可以通過手動(dòng)輸入路由信息來跳轉(zhuǎn),所以我們需要使用動(dòng)態(tài)路由來防止
動(dòng)態(tài)路由
功能:根據(jù)不同的權(quán)限創(chuàng)建對應(yīng)的路由
實(shí)現(xiàn)步驟:獲取路由----->處理路由格式并緩存------>創(chuàng)建路由(前置守衛(wèi))
獲取路由
我們查看pinia中的獲取路由方法,具體代碼就在上面的pinia那里
獲取路由的請求
上面沒什么好說的,就一個(gè)獲取路由信息然后存入pinia中的操作,重點(diǎn)是下面的處理路由信息,這里我們需要將后端傳過來的數(shù)據(jù)處理為路由格式。
處理路由格式
注意:由于我這里所有路由都只有一層,并沒有子路由,所以這樣處理,如果你想要多嵌套幾個(gè)路由,那么你需要將后端傳來的路由信息處理為樹結(jié)構(gòu)(也可以在后端處理),然后再遍歷處理(可能還需要遞歸遍歷處理) 并添加到children屬性里面,這里就不多說
好了,到現(xiàn)在我們已經(jīng)做完準(zhǔn)備,到了最終的環(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)中動(dòng)態(tài)創(chuàng)建路由,下面就是router/permission.js。主要是定義一個(gè)前置守衛(wèi),守衛(wèi)里面動(dòng)態(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' // 進(jìn)度條 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 { /*兩種情況進(jìn)入:1:已經(jīng)獲取了路由(一定有token),2,沒有登錄(一定沒有路由)*/ NProgress.done() if (hasetoken && to.matched.length != 0) { /*to.matched.length === 0判斷當(dāng)前輸入的路由是否具有,即使登陸了,如果輸入不能訪問的路由也要404*/ /*已經(jīng)獲取了路由,并且訪問路由合法,放行*/ next() } else { next('/404') }/*沒有登錄、登錄了但是訪問路由不合法,都跳轉(zhuǎn)404*/ } } else { NProgress.done() /*在白名單里面,直接放行*/ next() } })
在上面代碼中:
- 我們采用addRoute方法來動(dòng)態(tài)創(chuàng)建路由,這方法好像只能一次創(chuàng)建一個(gè)路由,并且還是根路由(一級路由),由于我們菜單路由是home(一級路由)下的子路由,所以就放在home的children里面。
- 我們采用next({...to, replace: true})來確保路由創(chuàng)建完畢,注意,他并不是直接放行,添加完路由后,他會再重新進(jìn)入路由守衛(wèi)。
- 在上面2中說過,由于next({...to, replace: true})并不能直接放行,會再一次進(jìn)入守衛(wèi),所以在
中這個(gè)if()判斷尤為重要,不然一直循環(huán)進(jìn)入這個(gè)if()里面出不來,會導(dǎo)致瀏覽器白屏卡死 ,在if()之外一定要放行。在上面的代碼中,我是采用pinia里面存儲路由的數(shù)組長度判斷,當(dāng)然你也可以用其他的方法,比如當(dāng)前的路由長度減去靜態(tài)路由是否為0作為條件。當(dāng)然也需要判斷好其他情況,比如用戶輸入不存在的路由,我們需要將其跳轉(zhuǎn)到404等。
4. 路由刷新會丟失,每次刷新需要重新創(chuàng)建。每一次刷新頁面都會丟失動(dòng)態(tài)添加的路由。在上面中我們使用的是判斷pinia中路由數(shù)組的長度來判斷的,在這樣的情況下,我們就不能持久化pinia這個(gè)路由數(shù)組(如果是其他判斷條件則可以持久化,一切以判斷條件而立),不然頁面刷新丟失路由,卻沒法進(jìn)入這個(gè)if()里面(因?yàn)槿绻志没?,這個(gè)menrouter就不會為空,if判斷條件不成立)。無法進(jìn)入if創(chuàng)建動(dòng)態(tài)創(chuàng)建路由,在上面的代碼中就會一直進(jìn)入404(如果沒有404頁面處理,就是白屏)
可以看到我并沒有將這路由數(shù)組持久化。
我還是再說一遍,如果你是其他判斷條件,比如說你是使用路由的長度判斷(是否大于靜態(tài)路由數(shù)組長度),那么就可以持久化,而在if()內(nèi)部也不用每次都發(fā)送請求,不然持久化也失去了意義。
當(dāng)然,就我而言,更傾向于每次刷新就發(fā)送請求,這樣可以確保數(shù)據(jù)的準(zhǔn)確性。
這里提一嘴,permission.js定義的路由守衛(wèi)想要生效就需要在min.js中引入,如果你覺得麻煩就全定義在router/index.js中也是可行的,如果你是單獨(dú)新建js實(shí)現(xiàn)的路由守衛(wèi),記得引入哦。
動(dòng)態(tài)按鈕
分析
動(dòng)態(tài)按鈕的實(shí)現(xiàn)可以通過v-if或者v-show指令來動(dòng)態(tài)渲染按鈕(根據(jù)后端返回的權(quán)限碼比對);
如果你覺得麻煩,你可以用vue中自定義組件來簡化。這里就不再贅述了
主要分為三個(gè)步驟:獲取權(quán)限碼,緩存權(quán)限碼,權(quán)限對比(v-if或者v-show)
獲取權(quán)限碼
假設(shè)我們在登錄的時(shí)候就獲取權(quán)限碼,下面是login頁面登錄時(shí)發(fā)送的請求,將這個(gè)結(jié)果存入pinia
緩存權(quán)限碼
在store/permission.js中
提供對應(yīng)的賦值方法以及緩存
權(quán)限對比
在需要的頁面中引入,并判斷
場景:假設(shè)我們的商品上架只有超級管理員才能使用
indexof表示判斷一個(gè)數(shù)組是否包含一個(gè)元素,如果包含就返回索引,不包含就返回-1。
大工告成!
實(shí)現(xiàn)效果
管理員登錄
商家登錄
有沒有發(fā)現(xiàn)還是太丑了一點(diǎn)?接下來我們來嘗試美化一下它
+el-tabs美化
寫在前面
這部分,分為快速篇和循序漸進(jìn)篇兩種,如果耐心不夠,請看快速篇,如果你覺得還行,可以看循序漸進(jìn)篇
使用element-plus的tabs標(biāo)簽
分析功能:
- 點(diǎn)擊菜單欄的時(shí)候,如果沒有這個(gè)tab頁,就新增,如果有就跳轉(zhuǎn)
- 點(diǎn)擊tabs的時(shí)候跳轉(zhuǎn)對應(yīng)的路由
- tabs可以被刪除
快速篇
主要修改地方:
- pinia中定義標(biāo)簽綁定所需的數(shù)據(jù)
- 在路由展示地方添加標(biāo)簽頁代碼,并設(shè)置切換事件
- 在菜單渲染組件中添加新增標(biāo)簽頁的事件
在pinia中定義標(biāo)識激活標(biāo)簽頁的值和標(biāo)簽頁的數(shù)據(jù)數(shù)組,
以及對應(yīng)的對外提供操作的接口
路由展示的地方添加標(biāo)簽代碼如下
代碼如下
<template> <div class="common-layout"> <el-container> <el-aside width="200px"> <div> <!-- 菜單側(cè)欄導(dǎo)航欄 --> <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('確認(rèn)退出登錄?', '提示', { confirmButtonText: '確定', cancelButtonText: '取消', type: 'warning', }).then(async () => { /*點(diǎn)擊的確認(rèn)*/ const b = await store.logout() if (b) { router.push('/login') } else { ElMessage({ showClose: true, message: '注銷失敗', type: 'error', }) } }).catch(() => { /*點(diǎn)擊的取消*/ }) } function closeTabs(name){ //判斷刪除的是否是活動(dòng)頁,如果是則刪除并跳到首頁, if(name==store.tabsActive){ store.setActive('homepage') console.log("刪除的是當(dāng)前頁",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è)邊欄滾動(dòng)條*/ .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) } } // 點(diǎn)擊菜單后添加進(jìn)入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>
大功告成,如果你不明白這些步驟并且想要了解,請看循序漸進(jìn)篇。
循序漸進(jìn)篇
我們從官網(wǎng)上復(fù)制代碼,放在哪里呢?
<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>
放在路由展示的地方,還記得我們前面的前期準(zhǔn)備那里嗎?
<div class="common-layout"> <el-container> <el-aside width="200px"> <div> <!-- 菜單側(cè)欄導(dǎo)航欄 --> <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>
當(dāng)然,只是這樣是不行的,由于這個(gè)標(biāo)簽頁是動(dòng)態(tài)的,我們需要一個(gè)數(shù)組來代替它的源數(shù)據(jù),這個(gè)數(shù)組呢并不是固定的。在點(diǎn)擊菜單欄的時(shí)候就需要添加一個(gè),我們也可以在tabs上面點(diǎn)擊刪除按鈕,刪除一個(gè)標(biāo)簽頁。也就是說,對這個(gè)數(shù)組的操作是跨頁面跨組件的。所以我們有限考慮使用pinia來管理。
在前面的pinia基礎(chǔ)上,我們新增一個(gè)tabsList
并提供一個(gè)新增和刪除的方法
代碼如下:
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í)候避免交叉請求導(dǎo)致數(shù)據(jù)狀態(tài)污染 * 和 TS 類型推導(dǎo) */ state: () => { return { menuList: [],//菜單信息 roles: [],//角色信息 menuRouter: [],//路由信息 roleIds: [],//角色I(xiàn)d tabsList:[{id:1,name:'首頁',path:'homepage'}], tabsActive:'homepage' } }, /** * 用來封裝計(jì)算屬性 有緩存功能 類似于computed */ getters: {}, /** * 編輯業(yè)務(wù)邏輯 類似于methods */ actions: { //菜單 setMenuList(data) { this.menuList = data }, //設(shè)置路由:data-->后端傳入的路由信息 setMenuRouter(data) { data.forEach(item => { //定義一個(gè)對象-->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不相同的元素,就相當(dāng)于把相同的元素刪掉了*/ 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時(shí)的鍵 paths: ['menuList', 'hasRouter', `roles`, 'roleIds','tabsList','tabsActive'],//指定要持久化的字段,menuRouter不需要緩存。因?yàn)槊看温酚商D(zhuǎn)我們都可以重新獲取 }, })
現(xiàn)在,我們有了這樣一個(gè)數(shù)組,就可以遍歷生成了,我們在之前的基礎(chǔ)上,修改這部分代碼,
從pinia中引入tabsList數(shù)組,在標(biāo)簽頁中遍歷它
<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)轫憫?yīng)式。如果不使用這個(gè),數(shù)組修改后,標(biāo)簽頁并不會隨之渲染出來
- 使用標(biāo)簽頁的name屬性綁定tabsList存儲的路由路徑
現(xiàn)在我們能夠便遍歷顯示了,但是還不夠,我們需要點(diǎn)擊菜單的時(shí)候,想這個(gè)數(shù)組新增元素
我們找到前面的渲染菜單的組件,在點(diǎn)擊沒有子菜單的菜單上面添加按鈕事件
<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('確認(rèn)退出登錄?', '提示', { confirmButtonText: '確定', cancelButtonText: '取消', type: 'warning', }).then(async () => { /*點(diǎn)擊的確認(rèn)*/ const b = await store.logout() if (b) { router.push('/login') } else { ElMessage({ showClose: true, message: '注銷失敗', type: 'error', }) } }).catch(() => { /*點(diǎn)擊的取消*/ }) } function closeTabs(name){ //判斷刪除的是否是活動(dòng)頁,如果是則刪除并跳到首頁, if(name==store.tabsActive){ store.setActive('homepage') console.log("刪除的是當(dāng)前頁",tabsActive) } store.delTabs(name) } function gotoActive(name){ store.setActive(name) router.push(name) } </script>
當(dāng)點(diǎn)擊菜單的時(shí)候,就需要新增一個(gè)標(biāo)簽頁,如果已經(jīng)存在,就直接跳轉(zhuǎn)到這個(gè)標(biāo)簽頁。那么問題來了,在上面的代碼中,我們能夠新增標(biāo)簽頁了,但是如何跳轉(zhuǎn)到這個(gè)標(biāo)簽頁呢?
我們需要借助標(biāo)簽頁的這個(gè)屬性:
name屬性標(biāo)識了每一個(gè)標(biāo)簽頁(tab-pane),而tab的v-model屬性綁定了當(dāng)前激活的標(biāo)簽頁。tab綁定的值是哪一個(gè)tab-pane的name,那么哪一個(gè)tab-pane就是激活頁
先不要著急定義這個(gè)激活值,我們仔細(xì)分析:在點(diǎn)擊菜單的時(shí)候,不僅要向數(shù)組中添加一個(gè)新的標(biāo)簽頁,還要切換到這個(gè)激活的標(biāo)簽頁。也就是說,對這個(gè)激活值執(zhí)行賦值的動(dòng)作是在菜單組件中,而綁定(獲?。┻@個(gè)激活值是在展示標(biāo)簽頁的組件中,這又是一個(gè)跨頁面跨組件的共享數(shù)據(jù),我們?nèi)匀粌?yōu)先定義在pinia中。我們給他一個(gè)初始值,在進(jìn)入頁面的時(shí)候默認(rèn)打開首頁,
仍然需要提供一個(gè)修改的接口
代碼在上面已經(jīng)給出,這里就不在重復(fù)了
我們有了這個(gè)激活值,就需要綁定,并且考慮到在點(diǎn)擊不同的標(biāo)簽頁的時(shí)候需要切換路由,我們需要在激活值改變的時(shí)候,就切換到這個(gè)激活值對應(yīng)的路由上。
修改標(biāo)簽頁的代碼,我們將數(shù)組中的路由路徑(path)綁定為name值,closable表示當(dāng)前標(biāo)簽是否可以被刪除,我這里只有首頁是不能被刪除的
{ "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": "倉庫維護(hù)", "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": "采購單維護(hù)", "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 }] }
還記得之前我們菜單點(diǎn)擊哪里嗎?那里只實(shí)現(xiàn)了添加標(biāo)簽頁的功能還沒有實(shí)現(xiàn)切換的功能,我們現(xiàn)在只需要在點(diǎn)擊事件里面添加切換激活值就行了
后端響應(yīng)的菜單數(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": "倉庫維護(hù)", "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": "采購單維護(hù)", "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 }] }
源碼地址
進(jìn)入這個(gè)倉庫找到
到此這篇關(guān)于vue3+element-plus實(shí)現(xiàn)動(dòng)態(tài)菜單和動(dòng)態(tài)路由動(dòng)態(tài)按鈕(前后端分離)的文章就介紹到這了,更多相關(guān)vue3 element-plus動(dòng)態(tài)菜單內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue如何通過Vue.prototype定義原型屬性實(shí)現(xiàn)全局變量
在Vue.js開發(fā)中,通過原型屬性為Vue實(shí)例添加全局變量是一種常見做法,使用$前綴命名,可以避免與組件內(nèi)部的數(shù)據(jù)、方法或計(jì)算屬性產(chǎn)生命名沖突,這種方式簡單有效,確保了變量在所有Vue實(shí)例中的可用性,同時(shí)保持全局作用域的整潔2024-10-10Vue 3開發(fā)中VueUse強(qiáng)大Hooks庫
VueUse提供了一個(gè)豐富且強(qiáng)大的Hooks庫,可以幫助開發(fā)者快速實(shí)現(xiàn)各種功能,提高開發(fā)效率,本文來詳細(xì)的介紹一下,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08vue項(xiàng)目實(shí)現(xiàn)添加圖片裁剪組件
這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目實(shí)現(xiàn)添加圖片裁剪組件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Vue實(shí)現(xiàn)導(dǎo)出excel表格功能
這篇文章主要介紹了Vue實(shí)現(xiàn)導(dǎo)出excel表格的功能,在文章末尾給大家提到了vue中excel表格的導(dǎo)入和導(dǎo)出代碼,需要的朋友可以參考下2018-03-03