個人權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(二)菜單導航

1、前言

上篇博客中已經整體的說了一下權限系統的思路和表結構設計,那接下來咱們就要進入正文了,先從菜單導航這個功能開始。javascript

2、實現

這個頁面基本不用什麼需求分析了,你們都很明白,不過在這個頁面要多維護一個東西,那就是定義頁面中有哪些按鈕,這個用彈出窗口作。
咱們技術分析一下:
一、直在grid中在線編輯,使用easyui的treegrid控件可實現。
二、行編輯時選擇父節點,使用easyui中的combotree控件,數據源直接在treegrid中取。
三、選擇圖標,這個沒有控件可用,本身代碼實現
四、彈出設置按鈕窗口,使用easyui的window或dialog控件
五、按鈕庫管理窗口,使用easyui的datagrid控件。
六、前臺交互邏輯使用ko,後臺數據接口採用web api
經上面分析,技術上沒有什麼問題,惟一就是選擇圖標這個要本身實現比較麻煩點。css

一、固然先從mvc控制器開始吧。建立MenuController.cs 裏面只有一個index方法的空的mvc控件器,裏面什麼都不用寫。 html

public class MenuController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}


二、接下來再建立對應的視圖,前臺razor頁面代碼以下,看完了我再給你們解釋 前端

@{
    ViewBag.Title = "title";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section scripts{
    @Scripts.Render("~/Resource/Sys/Menu.js")
    <script type="text/javascript">
        using(['lookup', 'validatebox', 'combotree', 'numberspinner'], easyuifix.datagrid_editor_extend);
        var formatterParent = function (value, row) { return row.ParentName };
        var formatterButton = function (value, row) { return (row.URL&&row.URL!='#')?'<a href="#" onclick="setButton(\'' 
+ row.MenuCode + '\')"><span class="icon icon-set2">&nbsp;</span>[設置按鈕]</a>':''; }; ko.bindingViewModel(new viewModel()); </script> } <div class="z-toolbar"> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-arrow_refresh" title="刷新" data-bind="click:refreshClick">刷新</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-add" title="新增" data-bind="click:addClick" >新增</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-edit" title="編輯" data-bind="click:editClick" >編輯</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-cross" title="刪除" data-bind="click:deleteClick" >刪除</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-save" title="保存" data-bind="click:saveClick" >保存</a> </div> <table id="gridlist" data-bind="treegrid:grid"> <thead> <tr> <th field="_id" hidden="true"></th> <th field="MenuName" align="left" width="150" editor="{type:'validatebox',options:{required: true }}">菜單名稱 </th> <th field="MenuCode" align="left" width="80" editor="{type:'validatebox',options:{required: true }}">編碼 </th> <th field="ParentCode" align="left" width="150" editor="combotree" formatter="formatterParent" >上級菜單 </th> <th field="IconClass" align="left" width="180" editor="{type:'lookup'}" >圖標 </th> <th field="URL" align="left" width="180" editor="text" >連接地址 </th> <th field="IsVisible" align="center" width="60" editor="{type: 'checkbox',options: {on: true,off: false}}" formatter="com.formatCheckbox" >是否可見</th> <th field="IsEnable" align="center" width="60" editor="{type: 'checkbox',options: {on: true,off: false}}" formatter="com.formatCheckbox" >是否啓用</th> <th field="MenuSeq" align="right" width="50" editor="text" >排序 </th> <th field="Button" align="center" width="100" formatter="formatterButton" >頁面按鈕 </th> </tr> </thead> </table> <script type="text/html" id="button-template"> <div style="margin:5px;height:320px;overflow:auto;"> <style type="text/css"> .listview{ margin:0 !important;} .listview li{width:100px !important;background-color:#ECECFF !important;float:left;margin:3px;overflow:hidden;} .listview span{ font-size:14px !important;height:auto !important; white-space: nowrap;} .listview .icon:before{content:"" !important} </style> <div style="border-bottom:1px solid #CCC; margin-bottom:5px;"> <span class="icon32 icon-settings32" style="padding-left:48px;font-weight:bold; font-size:14px;color:#666;">請選擇頁面按鈕</span> </div> <div class="metrouicss"> <label class="input-control checkbox" style="margin-top:6px;margin-left:3px;"> <input type="checkbox" data-bind="checked:checkAll"><span class="helper">全選</span> </label> <button class="image-button standart fg-color-white" style="float:right" data-bind="click:manageClick"> <i class="icon-grid-view bg-color-green"></i> 管理按鈕庫 </button> <ul class="listview" data-bind="foreach: buttons" style="clear:both"> <li data-bind="click:$parent.buttonClick,css:{selected:Selected()>0}"><span class="icon" data-bind="text:ButtonName,css:ButtonIcon"></span></li> </ul> </div> </div> <div style="text-align:center;"> <a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)" >肯定</a> <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> </div> </script> <script type="text/html" id="manage-template"> <style type="text/css"> .datagrid-wrap{border-width:0 0 1px 0;} .datagrid-toolbar{background-color:#E0ECFF !important} </style> <table data-bind="datagrid:grid"> <thead> <th field="ButtonCode" align="left" editor="{type:'validatebox',options:{required:true}}" width="80" >編碼 </th> <th field="ButtonName" align="left" editor="{type:'validatebox',options:{required:true}}" width="70" >名稱 </th> <th field="ButtonIcon" align="left" editor="{type:'validatebox',options:{required:true}}" width="120" >圖標 </th> <th field="ButtonSeq" align="left" editor="text" width="50" >排序 </th> <th field="Description" align="left" editor="text" width="200" >備註說明 </th> </thead> </table> <div style="text-align:center;margin:5px;"> <a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)" >肯定</a> <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> </div> </script>

這一段基本上都是html,你們都懂,要解釋的可能會有如下幾點:
@Scripts.Render("~/Resource/Sys/Menu.js") 這個是利用了mvc4下面的bundle技術對js文件進行壓縮捆綁。
using(['lookup', 'validatebox', 'combotree', 'numberspinner'], easyuifix.datagrid_editor_extend);  這句話的意思是這個頁面中動態載入這些組件,(我沒有默認加載全部的easyui控件,都是使用時動態加載的)  
<th field="_id"  hidden="true"></th> 這句是表示我有一個隱藏的_id字段,由於個人這個功能是能夠修改主鍵的,因此不能把主鍵作爲更新條件,只有建一個隱藏字段來當作更新條件才能實現。
<script type="text/html" id="xxxx-template"> 這個應該用的不少的利用script的type=text/html寫的模板,彈出窗口時使用 java

三、前端viewModel
node

/**
* 模塊名:mms viewModel
* 程序名: menu.js
* Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] 
**/
 
function viewModel() {
    var self = this;
    this.grid = {
        size: { w: 4, h: 40 },
        url: '/api/sys/menu/getall',
        idField: '_id',
        queryParams: ko.observable(),
        treeField: 'MenuName',
        loadFilter: function (d) {
            d = utils.copyProperty(d.rows || d, ["MenuCode", "IconClass"], ["_id", "iconCls"], false);
            return utils.toTreeData(d, '_id', 'ParentCode', "children");
        } 
    };
    this.refreshClick = function () {
        window.location.reload();
    };
    this.addClick = function () {
        if (self.grid.onClickRow()) {
            var row = { _id: utils.uuid(),MenuCode:'',MenuName:''};
            self.grid.treegrid('append', { parent: '', data: [row] });
            self.grid.treegrid('select', row._id);
            self.grid.$element().data("datagrid").insertedRows.push(row);
            self.editClick();
        }
    };
    this.editClick = function () {
        var row = self.grid.treegrid('getSelected');
        if (row) {
            self.grid.treegrid('beginEdit', row._id);
            self.edit_id = row._id;
            var eds = self.grid.treegrid('getEditors', row._id);
            var edt = function (field) { return $.grep(eds, function (n) { return n.field == field })[0]; };
            var treeData = JSON.parse(JSON.stringify(self.grid.treegrid('getData')).replace(/_id/g, "id").replace(/MenuName/g, "text"));
            treeData.unshift({ "id": 0, "text": "" });
            edt("ParentCode").target.combotree('loadData', treeData);
            self.afterCreateEditors(edt);
        }
    };
    this.afterCreateEditors = function (editors) {
        var iconInput = editors("IconClass").target;
        var onShowPanel = function () {
            iconInput.lookup('hidePanel');
            com.dialog({
                title: "&nbsp;選擇圖標",
                iconCls: 'icon-node_tree',
                width: 700,
                height: 500,
                url: "/Resource/page/icon.html",
                viewModel: function (w) {
                    w.find('#iconlist').css("padding", "5px");
                    w.find('#iconlist li').attr('style', 'float:left;border:1px solid #fff; line-height:20px; margin-right:4px;width:16px;cursor:pointer')
                     .click(function () {
                         iconInput.lookup('setValue',$(this).find('span').attr('class').split(" ")[1]);
                         w.dialog('close');
                     }).hover(function () {
                         $(this).css({ 'border': '1px solid red' });
                     }, function () {
                         $(this).css({ 'border': '1px solid #fff' });
                     });
                }
            });
        };
        iconInput.lookup({ customShowPanel: true, onShowPanel: onShowPanel, editable: true });
        iconInput.lookup('resize', iconInput.parent().width());
        iconInput.lookup('textbox').unbind();
    };
    this.grid.OnBeforeDestroyEditor = function (editors, row) {
        row.ParentName = editors['ParentCode'].target.combotree('getText');
        row.IconClass = editors["IconClass"].target.lookup('textbox').val();
    };
    this.deleteClick = function () {
        var row = self.grid.treegrid('getSelected');
        if (row) {
            self.grid.$element().treegrid('remove', row._id);
            self.grid.$element().data("datagrid").deletedRows.push(row);
        }
    };
    this.grid.onDblClickRow = self.editClick;
    this.grid.onClickRow = function () {
        var edit_id = self.edit_id;
        if (!!edit_id) {
            if (self.grid.treegrid('validateRow', edit_id)) { //經過驗證
                self.grid.treegrid('endEdit', edit_id);
              self.edit_id = undefined;
            }
            else { //未經過驗證
                self.grid.treegrid('select', edit_id);
              return false;
            }
        }
        return true;
    };
    this.saveClick = function () {
        var post = {};
        post.list = new com.editTreeGridViewModel(self.grid)
.getChanges(['_id', 'MenuName', 'MenuCode', 'ParentCode', 'IconClass', 'URL', 'IsVisible', 'IsEnable', 'MenuSeq']); if (self.grid.onClickRow() && post.list._changed) { com.ajax({ url: '/api/sys/menu/edit', data: ko.toJSON(post), success: function (d) { com.message('success', '保存成功!'); self.grid.treegrid('acceptChanges'); self.grid.queryParams({}); } }); } }; } var setButton = function (MenuCode) { com.dialog({ title: "設置按鈕", width: 555, height: 400, html: "#button-template", viewModel: function (w) { var self = this; com.loadCss('/Resource/css/metro/css/modern.css', parent.document); this.buttons = ko.observableArray(); this.refresh = function () { com.ajax({ url: '/api/sys/menu/getmenubuttons/' + MenuCode, type: 'GET', async: false, success: function (d) { self.buttons(ko.mapping.fromJS(d)()); } }); }; this.refresh(); this.checkAll = ko.observable(false); this.checkAll.subscribe(function (value) { $.each(self.buttons(), function () { this.Selected(value ? 1 : 0); }); }); this.buttonClick = function (row) { row.Selected(row.Selected() ? 0 : 1); }; this.confirmClick = function () { var data = utils.filterProperties($.grep(self.buttons(), function (row) { return row.Selected() > 0; }), ['ButtonCode']); com.ajax({ url: '/api/sys/menu/editmenubuttons/' + MenuCode, data: ko.toJSON(data), success: function (d) { com.message('success', '保存成功!'); self.cancelClick(); } }); }; this.manageClick = function () { com.dialog({ title: "管理按鈕庫", width: 600, height: 410, html: "#manage-template", viewModel: function (w_sub) { var that = this; this.grid = { width: 586, height: 340, pagination: false, pageSize: 10, url: "/api/sys/menu/getbuttons", queryParams: ko.observable() }; this.cancelClick = function () { w_sub.dialog('close'); }; this.gridEdit = new com.editGridViewModel(this.grid); this.grid.OnAfterCreateEditor = function (editors, row) { if (!row._isnew) com.readOnlyHandler('input')(editors["ButtonCode"].target, true); }; this.grid.onClickRow = that.gridEdit.ended; this.grid.onDblClickRow = that.gridEdit.begin; this.grid.toolbar = [ { text: '新增', iconCls: 'icon-add1', handler: function () { that.gridEdit.addnew(); } }, '-', { text: '編輯', iconCls: 'icon-edit', handler: that.gridEdit.begin }, '-', { text: '刪除', iconCls: 'icon-cross', handler: that.gridEdit.deleterow } ]; this.confirmClick = function () { if (!that.gridEdit.isChangedAndValid()) return; var list = that.gridEdit.getChanges(['ButtonCode', 'ButtonName','ButtonIcon', 'ButtonSeq', 'Description']); com.ajax({ url: '/api/sys/menu/editbutton', data: ko.toJSON({ list: list }), success: function (d) { that.cancelClick(); self.refresh(); com.message('success', '保存成功!'); } }); }; } }); }; this.cancelClick = function () { w.dialog('close'); }; } }); };

viewModel的寫法你們要先了解下knockout,再回過頭來看這個,各個按鈕對應的事件屬性就不說了,
this.grid={}這裏的grid是綁定到菜單樹上的,this.grid即treegrid的屬性
在實現彈出圖標頁面採,利用lookup控件,把lookup的彈出頁面換掉,事件也unbind綁,便可以最少的代碼實現這個功能
保存功能就不說了,一看就懂的。
web

接下來彈出設置按鈕,調用我封裝的com.dialog方法實現,而後寫彈出窗口的viewModel便可。在彈出窗口時動態加載了它所須要的css,在這裏處理中使用了ko中的observableArray的功能,對按鈕數據進行監視,全選功能是使用subscribe函數實現的。這裏引入了ko你們明顯能夠感受到代碼優雅了不少,並且之後也好維護。
ajax

彈出管理按鈕庫就更簡單了,直接使用我封裝好的com.editGridViewModel就實現了對datagrid的增刪除改查了api

四、後臺web Api控制器mvc

前臺使用到的ajax請求中的方法包括:
1 獲取treegrid中數據:/api/sys/menu/getall
2 保存菜單數據:/api/sys/menu/edit
3 獲取菜單中的按鈕:/api/sys/menu/getmenubuttons/menucode
4 保存菜單中的按鈕:/api/sys/menu/editmenubuttons/menucode
5 獲取按鈕列表:/api/sys/menu/getbuttons
6 保存按鈕的增刪除改的操做改動:/api/sys/menu/editbutton
直接看webapi的代碼

    public class MenuApiController : ApiController
    {
        // GET api/menu
        public IEnumerable<dynamic> Get()
        {
            var UserCode = this.User.Identity.Name;
            return new sys_menuService().GetUserMenu(UserCode);
        }

        // GET api/menu
        public dynamic GetEnabled(string id)
        {
            var result = new sys_menuService().GetEnabledMenusAndButtons(id);
            return result;
        }

        // GET api/menu
        public IEnumerable<dynamic> GetAll()
        {
            var MenuService = new sys_menuService();
            var pQuery = ParamQuery.Instance().Select("A.*,B.MenuName as ParentName")
                .From(@"sys_menu A left join sys_menu B on B.MenuCode = A.ParentCode")
                .OrderBy("A.MenuSeq,A.MenuCode");
            var result = MenuService.GetDynamicList(pQuery);
            return result;
        }

        // 地址:POST api/mms/send
        [System.Web.Http.HttpPost]
        public void Edit(dynamic data)
        {
            var listWrapper = RequestWrapper.Instance().LoadSettingXmlString(@"
<settings>
    <table>
        sys_menu
    </table>
    <where>
        <field name='MenuCode' cp='equal' variable='_Id'></field>
    </where>
</settings>");

            var service = new sys_menuService();
            var result = service.Edit(null, listWrapper, data);
        }

        public IEnumerable<dynamic> GetMenuButtons(string id)
        {
            return new sys_menuService().GetMenuButtons(id);
        }

        public IEnumerable<dynamic> GetButtons()
        {
            var pQuery = ParamQuery.Instance().OrderBy("ButtonSeq");
            return new sys_buttonService().GetModelList(pQuery);
        }

        [System.Web.Http.HttpPost]
        public void EditMenuButtons(string id, dynamic data)
        {
            var service = new sys_menuService();
            service.SaveMenuButtons(id, data as JToken);
        }

        [System.Web.Http.HttpPost]
        public void EditButton(dynamic data)
        {
            var listWrapper = RequestWrapper.Instance().LoadSettingXmlString(@"
<settings>
    <table>sys_button</table>
    <where>
        <field name='ButtonCode' cp='equal'></field>
    </where>
</settings>");
            var service = new sys_buttonService();
            var result = service.Edit(null, listWrapper, data);
        }
    }

webapi中要指定請求類型,好比GetEnable這個方法,默認是Get方法,其它請求是訪問不到的。這些代碼也基本是採用個人zephyr.net框架實現的,實現代碼很是簡潔,你們後臺應該都有本身的一套我就很少說了。

3、效果圖

我原本不想再上圖的,可是想下你們看完了還獲得第一篇中看看這個頁面應該長的怎麼樣,麻煩,仍是每篇都上兩張圖方便你們。
image

image

image

image

 

4、後述

上一篇博客受到不少博友的關注,首先很是感謝你們的支持! 
若是你以爲不錯就幫我再【推薦】一下吧,你的支持纔是我能堅持寫完這個系列文章的動力。
技術交流QQ羣:羣一:328510073(已滿),羣二:167813846,歡迎你們來交流。

 

系列博客連接:

個人權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(一)

相關文章
相關標籤/搜索