Vue3.0實現(xiàn)無限級菜單
業(yè)務(wù)需求
菜單項是業(yè)務(wù)系統(tǒng)的重要組成部分,一般業(yè)務(wù)系統(tǒng)都要支持顯示多級業(yè)務(wù)菜單,但是根據(jù)每個業(yè)務(wù)人員的權(quán)責不同,看到的的菜單項也是不同的。
這就要求頁面可以支持無限極菜單顯示,根據(jù)每個用戶的權(quán)限不同,后臺服務(wù)返回對應的菜單項。
本文基于Vue 3.0實現(xiàn)了一個可配置的無限等級菜單,關(guān)鍵代碼如下:
后端返回的菜單項數(shù)據(jù)結(jié)構(gòu)
后端服務(wù)一般不會直接返回一個樹型結(jié)構(gòu)菜單集合給前端,這樣做也不合理。前端應該根據(jù)自己的具體需求,構(gòu)建自己的菜型單樹。后端返回的數(shù)據(jù)結(jié)構(gòu)一般包含以下一個字段:
- Id 菜單ID, 數(shù)字類型
- pId當前菜單的父級菜單ID, 數(shù)字類型
- title 菜單的標題
- link 菜單對應的鏈接
- order 同級菜單的排列順序,數(shù)字類型
其他業(yè)務(wù)字段需要具體問題具體分析,在這里不再贅述。本文不再討論后端如何進行菜單項的權(quán)限控制,所使用的菜單內(nèi)容,包括在一個JSON文件中,具體見附錄。
菜單內(nèi)容是一個足球數(shù)據(jù)管理系統(tǒng),包括多級菜單:
- 第一級菜單只有一項,是所有節(jié)點的祖先節(jié)點。
- 第二級菜單包括聯(lián)賽管理,俱樂部管理和球員管理
- 第三級菜單包括二級菜單內(nèi)容的CRUD。
關(guān)鍵代碼
為了支持無限級菜單,本文所有關(guān)鍵算法全部基于遞歸實現(xiàn)。主要包括:
1.后端數(shù)據(jù)轉(zhuǎn)換為樹形結(jié)構(gòu)
2.后端數(shù)據(jù)排序
3.基于菜單樹形結(jié)構(gòu)生成Vue的路由數(shù)據(jù)
4.菜單組件的遞歸調(diào)用
后端數(shù)據(jù)轉(zhuǎn)為樹形結(jié)構(gòu)
dataToTree函數(shù)調(diào)用的實參是附錄的JSON數(shù)據(jù),該代碼參考Vue 3.0的AST樹轉(zhuǎn)換的代碼,具體思想是:
1.將集合的數(shù)據(jù)分為父節(jié)點和子節(jié)集合,最外層的父節(jié)點為pId為0的節(jié)點。
2.在子節(jié)點中找到當前父節(jié)點的直接子節(jié)點,將其從當前子節(jié)點集合剔除。
3.遞歸回到1,尋找子節(jié)點的子節(jié)點。
4.如果當前子節(jié)點不是任何節(jié)點的父節(jié)點,將該子節(jié)點放入父節(jié)點的children集合中。
在生成當前樹型結(jié)構(gòu)菜單數(shù)據(jù)后,可以將該數(shù)據(jù)保存在vuex中,作為公共數(shù)據(jù)便于其他模塊使用。
function dataToTree(data) { ? const parents = data.filter((item) => item.pId === 0); ? const children = data.filter((item) => item.pId !== 0); ? toTree(parents, children); ? return parents; ? function toTree(parents, children) { ? ? for (var i = 0; i < parents.length; ++i) { ? ? ? for (var j = 0; j < children.length; ++j) { ? ? ? ? if (children[j].pId === parents[i].Id) { ? ? ? ? ? let _children = deepClone(children, []); ? ? ? ? ? toTree([children[j]], _children); ? ? ? ? ? if (parents[i].children) { ? ? ? ? ? ? parents[i].children.push(children[j]); ? ? ? ? ? } else { ? ? ? ? ? ? parents[i].children = [children[j]]; ? ? ? ? ? } ? ? ? ? } ? ? ? } ? ? } ? } } function deepClone(source, target) { ? var _tar = target || {}; ? let keys = Reflect.ownKeys(source); ? keys.map((key) => { ? ? if (typeof source[key] === "object") { ? ? ? _tar[key] = ? ? ? ? Object.prototype.toString.call(source[key]) === "[object Array]" ? ? ? ? ? ? [] ? ? ? ? ? : {}; ? ? ? deepClone(source[key], _tar[key]); ? ? } else { ? ? ? _tar[key] = source[key]; ? ? } ? }); ? return _tar; }
菜單項排序
根據(jù)同級節(jié)點的order值進行排序,本文沒有將該排序和上節(jié)的樹型結(jié)構(gòu)轉(zhuǎn)換放在一起,主要是考慮有些系統(tǒng)可能不需要排序。如果需要,每次添加元素都要進行一次排序,效率低下,所以在獲取樹型結(jié)構(gòu)后,再進行一次排序,具體排序函數(shù)如下:
function SortTree(tree) { ? tree = tree.sort((a, b) => a.order - b.order); ? tree.map((t) => { ? ? if (t.children) { ? ? ? t.children = SortTree(t.children); ? ? } ? }); ? return tree;
采用最簡單的遞歸方式,遍歷當前樹型集合,按照order字段的升序方式進行排序,如果當前節(jié)點有children項,遞歸排序。
基于菜單樹形結(jié)構(gòu)生成Vue的路由數(shù)據(jù)
在獲取樹型菜單后后,我們可以基于當前數(shù)據(jù),生成該用戶在App中要使用到的路由項,具體代碼如下:
function TreeToRoutes(treeData, routes) { ? routes = routes || []; ? for (var i = 0; i < treeData.length; ++i) { ? ? routes[i] = { ? ? ? path: treeData[i].link, ? ? ? name: treeData[i].name, ? ? ? component: () => import(`@/views/${treeData[i].name}`), ? ? }; ? ? if (treeData[i].children) { ? ? ? routes[i].children = TreeToRoutes( ? ? ? ? treeData[i].children, ? ? ? ? routes[i].children ? ? ? ); ? ? } ? } ? return routes; }
1.遍歷樹型菜單,將當前菜單項的link和tname復制到Vue路由數(shù)據(jù)的path和name上,component采用動態(tài)加載方式。
2.如果當前菜單項包含子節(jié)點children,遞歸調(diào)用,復制其子節(jié)點內(nèi)容。
在main.js方法中,將菜單數(shù)據(jù)通過vuex進行讀取,然后調(diào)用上述算法生成路由數(shù)據(jù)。將該數(shù)據(jù)直接加載到Vue的路由中,保證了如果當前用戶沒有某一個菜單的權(quán)限,即使通過URL進行訪問,也是訪問不到的,因為App只會為有權(quán)限的菜單項生成路由數(shù)據(jù)。如果用戶沒有某一個菜單的權(quán)限,也就不會從后端獲取到該菜單的數(shù)據(jù),也就不會為該菜單項生成路由。
菜單組件的遞歸調(diào)用
菜單組件代碼如下:
<template> ? <div> ? ? ? <ul v-if="data.children && data.children.length > 0"> ? ? ? ? ? <li><router-link :to="data.link">{{data.title}}</router-link></li>? ? ? ? ? ? <menu-item :data="item" :key="index" ?v-for="(item,index) in data.children"> ? ? ? </ul> ? ? ? <ul v-else> ? ? ? ? ? <li><router-link :to="data.link">{{data.title}}</router-link></li>? ? ? ? </ul> ? </div> </template> <script> export default { ? ? name: "MenuItem", ? ? props:{ ? ? ? ? data: Object ? ? } } </script>
如果當前菜單項包含子節(jié)點,則遞歸調(diào)用MenuItem組件自己
菜單組件調(diào)用的代碼如下:
<template> ? <div> ? ? ?<menu-item :data="item" :key="index" v-for="(item,index) in data" /> ? </div> </template> <script> import MenuItem from './MenuItem' export default { ? ? name: "Page", ? ? components:{ ? ? ? ? MenuItem ? ? } } </script>
由于生成的菜單數(shù)據(jù)結(jié)構(gòu)最外層是數(shù)據(jù),所以MenuItem組件需要進行循環(huán)調(diào)用。
附錄-菜單項數(shù)據(jù)
export default [ ? { ? ? Id: 15, ? ? pId: 0, ? ? name: "all", ? ? title: "all", ? ? link: "/all", ? ? order: 2, ? }, ? { ? ? Id: 1, ? ? pId: 15, ? ? name: "clubs", ? ? title: "Club Management", ? ? link: "/clubs", ? ? order: 2, ? }, ? { ? ? Id: 2, ? ? pId: 15, ? ? name: "leagues", ? ? title: "League Management", ? ? link: "/leagues", ? ? order: 1, ? }, ? { ? ? Id: 3, ? ? pId: 15, ? ? name: "players", ? ? title: "Player Management", ? ? link: "/players", ? ? order: 3, ? }, ? { ? ? Id: 5, ? ? pId: 2, ? ? name: "LeagueDelete", ? ? title: "Delete League", ? ? link: "/leagues/delete", ? ? order: 3, ? }, ? { ? ? Id: 6, ? ? pId: 2, ? ? name: "LeagueUpdate", ? ? title: "Update League", ? ? link: "/leagues/update", ? ? order: 2, ? }, ? { ? ? Id: 7, ? ? pId: 2, ? ? name: "LeagueAdd", ? ? title: "Add League", ? ? link: "/leagues/add", ? ? order: 1, ? }, ? { ? ? Id: 8, ? ? pId: 3, ? ? name: "PlayerAdd", ? ? title: "Add Player", ? ? link: "/players", ? ? order: 1, ? }, ? { ? ? Id: 9, ? ? pId: 3, ? ? name: "PlayerUpdate", ? ? title: "Update Player", ? ? link: "/players", ? ? order: 3, ? }, ? { ? ? Id: 10, ? ? pId: 3, ? ? name: "PlayerDelete", ? ? title: "Delete Player", ? ? link: "/players", ? ? order: 2, ? }, ? { ? ? Id: 11, ? ? pId: 1, ? ? name: "ClubAdd", ? ? title: "Add Club", ? ? link: "/clubs/add", ? ? order: 3, ? }, ? { ? ? Id: 12, ? ? pId: 1, ? ? name: "ClubUpdate", ? ? title: "Update Club", ? ? link: "/clubs/update", ? ? order: 1, ? }, ? { ? ? Id: 13, ? ? pId: 1, ? ? name: "ClubDelete", ? ? title: "Delete Club", ? ? link: "/clubs/delete", ? ? order: 2, ? }, ];
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
解決vue+ element ui 表單驗證有值但驗證失敗問題
這篇文章主要介紹了vue+ element ui 表單驗證有值但驗證失敗,本文通過實例代碼給大家分享解決方案,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01vue中關(guān)于redirect(重定向)初學者的坑
這篇文章主要介紹了vue中關(guān)于redirect(重定向)初學者的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08淺析Proxy可以優(yōu)化vue的數(shù)據(jù)監(jiān)聽機制問題及實現(xiàn)思路
這篇文章主要介紹了淺析Proxy可以優(yōu)化vue的數(shù)據(jù)監(jiān)聽機制問題及實現(xiàn)思路,需要的朋友可以參考下2018-11-11