一、前言

上篇博客中已经总体的说了一下权限系统的思路和表结构设计,那接下来我们就要进入正文了,先从菜单导航这个功能开始。

二、实现

这个页面基本不用什么需求分析了,大家都很明白,不过在这个页面要多维护一个东西,那就是定义页面中有哪些按钮,这个用弹出窗口做。
我们技术分析一下:
1、直在grid中在线编辑,使用easyui的treegrid控件可实现。
2、行编辑时选择父节点,使用easyui中的combotree控件,数据源直接在treegrid中取。
3、选择图标,这个没有控件可用,自己代码实现
4、弹出设置按钮窗口,使用easyui的window或dialog控件
5、按钮库管理窗口,使用easyui的datagrid控件。
6、前台交互逻辑使用ko,后台数据接口采用web api
经上面分析,技术上没有什么问题,唯一就是选择图标这个要自己实现比较麻烦点。

1、当然先从mvc控制器开始吧。创建MenuController.cs 里面只有一个index方法的空的mvc控件器,里面什么都不用写。

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

2、接下来再创建对应的视图,前台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="#" οnclick="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写的模板,弹出窗口时使用

3、前端viewModel

/**
* 模块名: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绑,即可以最少的代码实现这个功能
保存功能就不说了,一看就懂的。

接下来弹出设置按钮,调用我封装的com.dialog方法实现,然后写弹出窗口的viewModel即可。在弹出窗口时动态加载了它所需要的css,在这里处理中使用了ko中的observableArray的功能,对按钮数据进行监视,全选功能是使用subscribe函数实现的。这里引入了ko大家明显可以感觉到代码优雅了很多,而且以后也好维护。

弹出管理按钮库就更简单了,直接使用我封装好的com.editGridViewModel就实现了对datagrid的增删除改查了

4、后台web Api控制器

前台使用到的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/menupublic IEnumerable<dynamic> Get(){var UserCode = this.User.Identity.Name;return new sys_menuService().GetUserMenu(UserCode);}// GET api/menupublic dynamic GetEnabled(string id){var result = new sys_menuService().GetEnabledMenusAndButtons(id);return result;}// GET api/menupublic 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框架实现的,实现代码非常简洁,大家后台应该都有自己的一套我就不多说了。

三、效果图

我本来不想再上图的,但是想下大家看完了还得到第一篇中看看这个页面应该长的怎么样,麻烦,还是每篇都上两张图方便大家。

四、后述

上一篇博客受到很多博友的关注,首先非常感谢大家的支持! 
如果你觉得不错就帮我再【推荐】一下吧,你的支持才是我能坚持写完这个系列文章的动力。
技术交流QQ群:群一:328510073(已满),群二:167813846,欢迎大家来交流。

系列博客链接:

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(一)

转载于:https://www.cnblogs.com/xqin/p/3224226.html

我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(二)菜单导航相关推荐

  1. 基于MVC4+EF5+EasyUI技术实现通用权限管理系统(EpPlus、HignCharts、Reportviewer报表)...

    基于MVC4+EF5+EasyUI技术实现通用权限管理系统(EpPlus.HignCharts.Reportviewer报表) 适合人群:高级 课时数量:150课时 用到技术:MVC.EF.T4.Lo ...

  2. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(34)-文章发布系统①-简要分析...

    构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(34)-文章发布系统①-简要分析 原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入 ...

  3. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(10)-系统菜单栏[附源码]

    构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(10)-系统菜单栏[附源码] 原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后 ...

  4. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(40)-精准在线人数统计实现-【过滤器+Cache】...

    构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(40)-精准在线人数统计实现-[过滤器+Cache] 原文:构建ASP.NET MVC4+EF5+EasyUI+ ...

  5. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(12)-系统日志和异常的处理②...

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(12)-系统日志和异常的处理② 上一讲我们做了日志与异常的结果显示列表,这一节我们讲要把他应用系统中来. ...

  6. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(31)-MVC使用RDL报表

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(31)-MVC使用RDL报表 这次我们来演示MVC3怎么显示RDL报表,坑爹的微软把MVC升级到5都木有良 ...

  7. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(32)-swfupload多文件上传[附源码]...

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(32)-swfupload多文件上传[附源码] 文件上传这东西说到底有时候很痛,原来的asp.net服务器 ...

  8. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(39)-在线人数统计探讨

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(39)-在线人数统计探讨 系列目录 基于web的网站在线统计一直处于不是很精准的状态!基本上没有一种方法可 ...

  9. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(13)-系统日志和异常的处理③

    构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(13)-系统日志和异常的处理③ 参考文章: (1)构建ASP.NET MVC4+EF5+EasyUI+Unity ...

  10. 权限系统设计模型分析(DAC,MAC,RBAC,ABAC)

    此篇文章主要尝试将世面上现有的一些权限系统设计做一下简单的总结分析,个人水平有限,如有错误请不吝指出. 术语 这里对后面会用到的词汇做一个说明,老司机请直接翻到常见设计模式. 用户 发起操作的主体. ...

最新文章

  1. Android SurfaceView 黑背景的处理方法
  2. CListCtrl使用技巧汇总
  3. 关于对cross-browser支持的一些看法
  4. Windows10熄屏自动断开WiFi连接解决方法
  5. MySQL(一)——安装、创建数据库表、DML语言
  6. mysql自动提交 dcl语句_MySQL基础:DCL语句总结
  7. Python脚本做接口测试,抛弃接口测试工具是否可行?(一)
  8. web报表工具FineReport使用中遇到的常见报错及解决办法(一)
  9. 一名 IT 经理是如何把项目带崩的?
  10. 在 CentOS7 安装 ELK
  11. MNIST数据集下载与保存为图片格式
  12. 谷歌浏览器访问接口无返回
  13. Echart添加水印
  14. 计算机桌面上的微信图标不显示不出来的,电脑微信图标任务栏不见了怎么办
  15. Win7远程桌面连接不上问题解决方案
  16. unity tags的坑
  17. PNP三极管和NPN三极管的开关电路(EC极性接线判断简单明了)简单的技巧:三极管上箭头所在方向的二极管,只要二极管正向导通,那么三极管上下就能导通。
  18. 6.2.1.1UE maximum output power - EIRP and TRP——翻译
  19. Hark的数据结构与算法练习之圈排序
  20. 运用PS扭曲滤镜 将书法贴在人体上

热门文章

  1. API开放平台基于accessToken实现
  2. python(十一)接口开发、写日志、发邮件、python来发请求、手动添加环境变量...
  3. Unable to locate Spring NamespaceHandler for XML schema namespace
  4. Jmeter(五)录制功能
  5. java解析XML【转载】
  6. IE中getElementById的Bug
  7. 游戏开发中的数学和物理算法(18):缩放
  8. R中的子集选取运算符
  9. MATLAB怎么做出三叶玫瑰线,matlab复习题
  10. [PHP开发必备] -- 小巧强悍的MYSQL-Front中文版使用教程,附最新版下载地址