一、前言

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

二、实现

这个页面基本不用什么需求分析了,大家都很明白,不过在这个页面要多维护一个东西,那就是定义页面中有哪些按钮,这个用弹出窗口做。
我们技术分析一下:
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="#" 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写的模板,弹出窗口时使用

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/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框架实现的,实现代码非常简洁,大家后台应该都有自己的一套我就不多说了。

三、效果图

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

四、后述

上一篇博客受到很多博友的关注,首先非常感谢大家的支持! 
如果你觉得不错就帮我再【推荐】一下吧,你的支持才是我能坚持写完这个系列文章的动力。

技术交流QQ群:群一:328510073(已满),群二:167813846,欢迎大家来交流。

系列博客链接:

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

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

  1. 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(四)授权代码维护

    一.前言 权限系统设计中,授权代码是用来控制数据访问权限的.授权代码说白了只是一树型结构的数据,没有什么其它的业务意义.那么这个页面的功能也就非常简单授权代码维护:新增.修改.删除授权代码数据. 二. ...

  2. 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(三)图形化机构树

    一.前言 组织机构是国内管理系统中很重要的一个概念,以前我们基本都是采用数据列表的形式展现,最多只是采用树形列表展现.虽然够用,但是如果能做成图形化当然是最好不过了.这里我不用任何图形控件,就用最原始 ...

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

    一.前言 之前的博客一直都还没写到框架的实现及权限系统,今天开始写我的权限系统,我以前做过的项目基本上都有权限管理这个模块,但各个系统都会有一些不太一样,有些简单点,有些稍微复杂一点,一句话,我们做的 ...

  4. 我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(五)框架及Web项目的组件化

    一.组件化印象 1.先给大家看一张截图 如果我告诉大家,这就是一个web管理系统发布后的所有内容,你们会不会觉得太简洁了,只有一个web.config.一个Global.asax文件,其它的都是dll ...

  5. 权限系统设计实现MVC4 + WebAPI + EasyUI + Knouckout

    权限系统设计实现MVC4 + WebAPI + EasyUI + Knouckout (一) 一.前言 之前的博客一直都还没写到框架的实现及权限系统,今天开始写我的权限系统,我以前做过的项目基本上都有 ...

  6. MVC4 + WebAPI + EasyUI + Knockout-授权代码维护

    我的权限系统设计实现MVC4 + WebAPI + EasyUI + Knockout(四)授权代码维护 一.前言 权限系统设计中,授权代码是用来控制数据访问权限的.授权代码说白了只是一树型结构的数据 ...

  7. SNF快速开发平台3.0之BS页面展示和九大优点-部分页面显示效果-Asp.net+MVC4.0+WebAPI+EasyUI+Knockout

    一)经过多年的实践不断优化.精心维护.运行稳定.功能完善: 能经得起不同实施策略下客户的折腾,能满足各种情况下客户的复杂需求. 二)编码实现简单易懂.符合设计模式等理念: 上手快,见效快.方便维护,能 ...

  8. 建筑材料系统 ASP.NET MVC4.0 + WebAPI + EasyUI + Knockout 的架构设计开发

    框架介绍: 1.基于 ASP.NET MVC4.0 + WebAPI + EasyUI + Knockout 的架构设计开发 2.采用MVC的框架模式,具有耦合性低.重用性高.生命周期成本低.可维护性 ...

  9. SNF快速开发平台3.0之-界面个性化配置+10种皮肤+7种菜单-Asp.net+MVC4.0+WebAPI+EasyUI+Knockout

    一.个性配置-首页:可以进行拖动保存配置,下次登录时就会按配置的进行加载 二.个人配置页面 7种菜单用户可自定义配置,和预览效果 10种皮肤自定义配置,和预览效果 皮肤和菜单可以随意组合-部分截图: ...

随机推荐

  1. C语言初学者代码中的常见错误与瑕疵(5)

    问题: 素数 在世博园某信息通信馆中,游客可利用手机等终端参与互动小游戏,与虚拟人物Kr. Kong 进行猜数比赛. 当屏幕出现一个整数X时,若你能比Kr. Kong更快的发出最接近它的素数答案,你将 ...

  2. tomcat支持多少并发

    作者:孟男男 来源:https://zhidao.baidu.com/question/1445941399668603020.html Tomcat的最大并发数是可以配置的,实际运用中,最大并发数与 ...

  3. OSX下VirtualBox安装CentOS

    1.OSX上下载安装VirtualBox 2.新建虚拟机(所有选项默认即可) 3.启动虚拟机,选择CentOS安装镜像 CentOS-6.7-x86_64-minimal.iso 此处下载的是最小镜像 ...

  4. java微信接口之二—获取用户组

    一.微信获取用户组接口简介 1.请求 该请求也是GET方式请求.请求的url格式如下: https://api.weixin.qq.com/cgi-bin/groups/get?access_toke ...

  5. spring 容器加载完成后执行某个方法

    理论 刚好再开发过程中遇到了要在项目启动后自动开启某个服务,由于使用了spring,我在使用了spring的listener,它有onApplicationEvent()方法,在Spring容器将所有 ...

  6. Office文件上传自动生成缩略图

    来源:微信公众号CodeL 前不久产品经理提出一个X的需求,说上传office文件的时候需要将首页自动截图,用于显示文件列表的时候将文件第一页缩略图展示给用户.实现的方式有多种,这里给大家介绍一个简单 ...

  7. 【读书笔记《Android游戏编程之从零开始》】6.Android 游戏开发常用的系统控件(TabHost、ListView)

    3.9 TabSpec与TabHost TabHost类官方文档地址:http://developer.android.com/reference/android/widget/TabHost.htm ...

  8. 匈牙利算法 codevs 2776 寻找代表元

    codevs 2776 寻找代表元  时间限制: 1 s  空间限制: 256000 KB  题目等级 : 黄金 Gold 题目描述 Description 广州二中苏元实验学校一共有n个社团,分别用 ...

  9. 2014 Super Training #7 B Continuous Login --二分

    原题:ZOJ 3768 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3768 一个结论:一个正整数总能用不超过三个前n项相 ...

  10. Android网络之数据解析----使用Google Gson解析Json数据

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...