一、前情回顧
哈嘍大家好,在這個歡慶的日子里,老張祝大家工作都能蒸蒸日上!今天正好也是社團成立的第一天,我也是希望今天能是個紀念日,沾沾這個大喜慶!
更新:
這篇文章得到張善友,張隊的閱讀,並提供了另一個方案,大家可以看看,也是不錯的。
放假這兩天,倒是學到了很多東西,我這個也是承認的,昨天的事務提交,今天的按鈕級別的權限,都是群里小伙伴提供的方案和思路,我就是誠惶誠恐的寫到文章里了,我總怕會說我是知識的偷盜者,當然我這個完全是為了社區,我畢竟一分錢沒有得到,無論訪問量有多少,可能充其量就是數字好看。
言歸正傳,還記得半年前(2019.02.27)的時候,我的 vue 項目之二:Blog.Admin 正式開源(https://github.com/anjoy8/Blog.Admin),當時打算做一個簡單的權限后台系統,我自己想了常用的一些功能,當然有人說丑,有人說亂,但是也有人在自己項目和公司中使用,不過也是我付出心血的,而且也是完美的配合了 Blog.Core 項目,當時幾大設想功能中,遲遲有一個功能沒有實現,擱置了很久 —— 就是按鈕級別的權限配置:
當時我為啥沒有做這個呢,有兩點考慮,1、是因為超級管理員我沒讓大家訪問,就怕誤操作數據,對別人觀看權限有影響;2、另一個考慮,就是想把按鈕暴漏出來,看看是不是真的 test 測試賬號能不能刪除數據。后來我就開始思考,是時候把這個權限加進來了,就是沒有刪除的權限,刪除按鈕就不顯示,但是考慮了很久,被一個小知識點給卡住了,就是沒有想到如何動態事件綁定,這個不懂沒關系,我下文章會說到,前天由群管理 @大黃瓜和@Kawhi 提供了解決思路和方案,眼前一亮,終於實現了這個功能。
投稿作者:@大黃瓜 and @Kawhi;
效果預覽:我為了防止大改,目前只在 “角色管理” 頁,增加了這個功能,后期全部替換;
在線地址:http://vueadmin.neters.club
Github分支:主分支;
Tips : 目前我依然沒有開放 Admin 權限,所以如果想看全部效果,可以下載本地,自行配置查看。
下邊就開始正式講解,分成了兩部分,步驟+重點知識說明,所以看步驟的時候,直接動手操作就行了,不用管為什么,下邊的第三部分——重點知識的說明,會簡單說說。
二、詳細設計步驟
1、后端微調,保存按鈕相關信息
不知道還有沒有小伙伴記得,我現在后台的權限系統中,左側的導航條已經自動化了,所謂的自動化,就是已經完全交給了數據庫,無論增加多少權限,不用前端或者后端進行操作,只需要配置即可達到目的,當時呢,我把左側的菜單和按鈕揉到了一張表里,當時感覺很不合理,但是現在又改起來簡單,得益於這個設計思路,所以這次我們幾乎不用改什么,只需要把按鈕信息給放出來即可,這里有兩個小點:
1、Permission.cs 菜單表中,新建字段 Func ,用來存放當前按鈕所對應的方法事件;
2、/Permission/GetNavigationBar 接口中,把 IsButton==false 限制去掉,使之可以配合菜單進行遞歸;
//var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id) && d.IsButton == false)).OrderBy(c => c.OrderSort); var rolePermissionMoudles = (await _permissionServices.Query(d => pids.Contains(d.Id))).OrderBy(c => c.OrderSort);3、在 RecursionHelper.cs 中,增加 IsButton 屬性,將數據庫數據,拼車 Tree 返回到前端;
這樣我們就把按鈕數據配合着菜單數據一起返回前端了,你可以來查看下:
到這里,我們第一部分——后端數據就完成了,當然,如果你想更炫酷,可以多增加字段,比如按鈕的樣式,或者其他屬性等等等,這里你肯定明白,我就不細說了。
從下邊開始,我們就開始說 Blog.Admin 項目了,請打開 VSCode ,來修改我們的 Vue 項目:
2、修改后台權限管理,添加按鈕事件
這個步驟很簡單,就是把上邊我們建立的那個 Func 字段,給在頁面里增刪改查一下就好了,具體的代碼自行修改即可,填寫的按鈕事件就是【點擊的方法名不帶括號】。
注意這里必須要添加上事件,否則就算頁面顯示了按鈕,點擊也會沒有效果。
(我的菜單管理頁,使用了樹形Table的延遲加載,更直觀)
3、控制“按鈕”不要和“菜單”展示沖突
剛剛我們上邊說到了,把按鈕數據配合着菜單一起開放了出來,那這個時候我們要需要檢查一下,不能和菜單的展示起沖突,這里我就直接說修改的地方了:
1、修改 Sidebar.vue 組件,讓按鈕的數據不要進行展示,具體的看看代碼就明白了,很簡單;
2、修改 src\router\index.js 中的動態路由注入方法,過濾掉按鈕數據;
到了這里,我們的第二部分——准備工作就做完了,接下來,就是本文的重中之重的重頭戲,設計這個工具欄了,那具體怎么操作,這個時候我希望你可以先暫停一下,先不要往下看,先自己腦中考慮一下,按照我的思路,就是按鈕數據也已經有了,如何設計這個公共組件呢?考慮五分鍾吧......
五分鍾后,假設你已經考慮過了,那我就開始正式說明。
4、設計工具欄組件——Toolbar.vue
既然要做成自動化的組件,就一定要抽象出來,那第一步就是建立一個組件,不能每個頁面都寫相似的一堆代碼;
其實呢,我們也要可以配置,不能僅僅把按鈕給提出來,還應該有其他的比如<input />搜索框等等,都應該放到工具欄里;
一定要加載或者不加載,不能show or hide,這樣別人也會在查看元素的時候,看到;
綜上所述 ,我的設計是把表格里的按鈕,全部提到了頂部,先給大家一個展示的效果圖,這個刪除顏色是我手動加的,你也可以自己加個字段配置:
首先我們創建組件,src\components\Toolbar.vue ,具體的代碼如下:
<template> <el-col v-if="buttonList.length>0" :span="24" class="toolbar" style="padding-bottom: 0px;"> <el-form :inline="true" @submit.native.prevent> <el-form-item> <el-input v-model="searchVal" placeholder="請輸入內容"></el-input> </el-form-item> <!-- 這個就是當前頁面內,所有的btn列表 --> <el-form-item v-for="item in buttonList"> <!-- 這里觸發點擊事件 --> <el-button type="primary" @click="callFunc(item)">{{item.name}}</el-button> </el-form-item> </el-form> </el-col> </template> <script> export default { name: "Toolbar", data() { return { searchVal: "" //雙向綁定搜索內容 }; }, props: ["buttonList"], //接受父組件傳值 methods: { callFunc(item) { item.search = this.searchVal; this.$emit("callFunction", item); //將值傳給父組件 } } }; </script>
相信每個人都能看的懂,只是字面意思能看得懂,其中的核心知識點就是 List for渲染,父給子傳值,子給父傳值,我下文會重點講到,其中 buttonList 數組的格式,很簡單,你可以自己后端封裝一下,我這里就偷懶了,直接使用的菜單的數據結果,就是上邊我 localstorage.routes 中的結構,畢竟我把按鈕和菜單共有一套嘛。
那現在我們設計好了子組件——工具欄,接下來就要設計父組件了,傳遞數據和接受子組件廣播了。
5、將按鈕事件綁定到組件上
剛剛我們說到了 ,在 Toolbar.vue 中,核心的內容,就是把動態的事件方法給推送到一個個父組件上,這里是以 Role.vue 頁面舉例的,所有用到了 $emit("callFunction", item) 方法,這個如果你開發vue的話,肯定都知道這個的,這個父子通訊實例中,使用很多,具體的我在之前的文中中,也講到了,你可以看看,這里不細說,說白了一句話,就是子組件執行父組件方法。二十║Vue基礎終篇:傳值+組件+項目說明。其實到這個地方,我也想到了,但是問題來了:你可以先看看 emit 的用法,使用 emit 一般都是傳遞數據,但是如果傳遞 function 的話,肯定也是一個 name 的字符串,那父組件接受到這個 function name 的時候,很容易當成一個 data,如果強行執行,他們又不在一個對象里,因為有閉包,如何讓頁面執行這個 function 呢?我思考了很久(說明自己學的不到家)。
這個就是這兩個月來困擾我的地方,前邊的思路和后邊的 Table 隔離我都想到了,只是這里我沒有想到,看來還是需要一些高級前端的朋友喲,前天聽到了一個 apply 方法后,我豁然開朗,原來可以這樣,那下邊我就詳細的說一說,如何父組件執行事件:
在 src\views\User\Roles.vue 頁面呢,修改我們的工具欄使用:
這種引用組件,在data中,定義 buttonList ,就不說,重點還是要理解 @callFunction 這個必須要和子組件的 $emit 中的方法名一致。然后我們定義 callFunction,用來動態執行一個個事件:
callFunction(item) {//這個 item 就是我們的 permission.cs 數據 this.filters = { name: item.search };//這里是把子組件中的 search 內容,也接受過來 this[item.Func].apply(this, item);//核心就是要執行 apply 方法 },
是不是很簡單,難點就在於,.apply()這個方法,下文會說到。這個 this ,就是當然父組件的內容,就是我們執行可以在子組件來調用父組件的方法了
這里再說下
6、父組件獲取 ButtonList 數據
上邊我們也說到了,我們把 button 和 菜單揉在一起了,所以我們很簡單操作一下之前的數據就行,做一下篩選:
// 在 mounted 鈎子中,調用 router let routers = window.localStorage.router ? JSON.parse(window.localStorage.router) : []; this.getButtonList(routers); // 定義方法,目的我為了遞歸 getButtonList(routers) { let _this = this; routers.forEach(element => { let path = this.$route.path.toLowerCase(); if (element.path && element.path.toLowerCase() == path) { _this.buttonList = element.children; return; } else if (element.children) { _this.getButtonList(element.children); } }); }
OK,數據准備完畢。
7、修改 Table 組件,將工具欄與 Table 邏輯隔離
到了這里就是最后一步了,我們把之前的 tabel 右側 “操作欄” 刪掉,統一放到頂部,然后綁定數據,就可以加載出來了,
現在我們把操作欄給取消了,但是我們如何獲取 scope.row 呢?是不是很麻煩,要修改很多呢,其實不是的。
8、Table 改為單選,通過點擊,選擇某行
這個功能特別簡單,思路就是通過單擊某一行,來獲取這個 table 的 row,這個 element 官網寫的很詳細,我就簡單的說一下吧:
//觸發事件,獲取到這個row selectCurrentRow(val) { this.currentRow = val; }, <!--列表--> <el-table :data="users" highlight-current-row v-loading="listLoading" @current-change="selectCurrentRow" style="width: 100%;" >
然后只需要簡單的修改一下我們的 edit 和 delete 方法即可,因為我們已經拿到了這個 row:
如果不選中某項,會彈出警告:
搞定啦!是不是很簡單,幾乎沒有修改什么,感覺之前設計的方案還可以吧,至少擴展還是很不錯的!
到了這里,我們的動態按鈕權限功能,就已經完全做完了,一個八個步驟,大家動手起來,搞一搞吧。
三、重點知識解析
1、組件 ——子傳父 & 父傳子
這塊內容呢,其實我們都已經講過很多遍了,父傳子很簡單,只需要定一個自定屬性即可,然后子組件接受,比如上文中的:
<toolbar :buttonList="buttonList" @callFunction="callFunction"></toolbar> name: "Toolbar", data() { return { searchVal: "" //雙向綁定搜索內容 }; }, props: ["buttonList"], //接受父組件傳值
比較復雜的就是 子傳父 了,重點還是要了解一些 $emit 這個api,二十║Vue基礎終篇:傳值+組件+項目說明 我這篇文章寫的還算是詳細,如果還是不懂,咱們再一對一討論吧。
2、動態事件綁定—— apply
這個apply 有點兒想 call 回調函數,首先,每個函數都包含兩個非繼承而來的方法:.apply()和 .call()。這兩個方法的用途都是在特定的作用域中調用函數,實際等於設置函數體內this對象的值。
這兩個方法接收的參數可以分為兩個部分,
第一部分是在其中運行函數的作用域,如果就在當前函數體中運行,就可以直接使用this值,如果在window作用域中使用,可以傳入window值,這樣,可以實現擴充作用域;
第二部分是參數組,在apply中可以傳入Array實例,也可以是arguments對象;在call中,傳遞給函數的參數必須逐個列舉;如果沒有參數,這個部分可以省略。
首先我們來看看網上apply()方法的定義:
1. apply()方法能劫持另外一個對象的方法,繼承另外一個對象的屬性
2.Function.apply(obj,args)方法能接收兩個參數
3.obj:這個對象將代替Function類里this對象
4.args:這個是數組,它將作為參數傳給Function(args–>arguments)
舉個例子,如下所示:
function sum(num1,num2){ return num1+num2; } //兩個數相等就相加,不相等就相乘 function mul(num1,num2){ if(num1 != num2){ return num1*num2; }else{ return sum.apply(this,arguments); //可以為 sum.apply(this,[num1,num2])或sum.call(this,num1,num2); } } console.log(mul(5,6)); //30 console.log(mul(6,6)); //12
說句簡單的,我認為就是在其他地方,去調用某一個方法,很重要的一個點,就是 this 這個到底指向什么,自己可以好好調調。
3、動態路由過濾—— addRoutes
這個在上邊的步驟里我沒有說到,是因為我們把 按鈕 給放出來以后,在動態菜單路由的時候,會出現重復的問題,所以我們就需要坐下過濾,注意這個不是錯誤,是警告,意思就是我們把一些重復的東西添加到路由里了,路由會忽略掉,只不過給大家一個 warm 而已。
所以呢,我做了一個過濾,封裝了下 route.addRoutes——在 src\router\index.js 中,我們過濾下重復路由,還是遞歸:
router.$addRoutes = (params) => { var f = item => { if (item['children']) { item['children'] = item['children'].filter(f); return true; } else if (item['IsButton']) { return item['IsButton']===false; } else { return true; } } var paramsFilt = params.filter(f); router.addRoutes(paramsFilt) }
然后咋其他的地方,將 router.addRoutes 統一都換成 router.$addRoutes 。但是這個目前還有一些小問題,我會后期繼續優化。
四、Github && Gitee
Core: https://github.com/anjoy8/Blog.Core
Vue: https://github.com/anjoy8/Blog.Admin