extjs+MVC4+PetaPoco+AutoFac+AutoMapper后台管理系统(附源码)
前言
本项目使用的开发环境及技术列举如下:
1、开发环境
IDE:VS2010+MVC4
数据库:SQLServer2008
2、技术
前端:Extjs
后端:
(1)、数据持久层:轻量级ORM框架PetaPoco
(2)、依赖注入:AutoFac
(3)、对象关系映射:AutoMapper
(4)、数据验证(MVC自带的验证封装使用)
(5)、SQL翻译机
(6)、缓存
以上使用都参考或直接借鉴使用了园子内牛人们的代码,只是学习交流使用而已,还请勿怪,我为了简便,没有分多个类库,而是以文
件夹的形式分的,大家可以根据文件夹分成类库也是一样的。好了,废话不多说,还是先上几张图大家看看吧,如果有兴趣再往下看
项目截图
要点一:Extjs
本项目虽然功能不多,但是基本已经涵盖了extjs的所有基本用法了,对于一般的extjs初学者或是简单应用的开发应该是足够了,后
台开发主要在tab、grid、treegrid的用法比较多,也是比较麻烦的地方,下面直接上代码,大家看看
首页布局:
1 /** 2 * 程序主入口 3 */ 4 Ext.onReady(function () { 5 /** 6 * 上,panel.Panel 7 */ 8 this.topPanel = Ext.create('Ext.panel.Panel', { 9 region: 'north', 10 height: 55 11 }); 12 /** 13 * 左,panel.Panel 14 */ 15 this.leftPanel = Ext.create('Ext.panel.Panel', { 16 region: 'west', 17 title: '主菜单', 18 iconCls: 'House', 19 width: 200, 20 layout: 'accordion', 21 collapsible: true 22 }); 23 /** 24 * 右,tab.Panel 25 */ 26 this.rightPanel = Ext.create('Ext.tab.Panel', { 27 region: 'center', 28 layout: 'fit', 29 id: 'mainContent', 30 collapisble: true, 31 tabWidth: 120, 32 items: [{ title: '首页', html: ' <iframe scrolling="auto" frameborder="0" width="100%" height="100%" src="' + DefaultUrl + '"> </iframe>'}] 33 }); 34 /** 35 * 下,panel.Panel 36 */ 37 this.bottomPanel = Ext.create('Ext.panel.Panel', { 38 region: 'south', 39 layout: 'fit', 40 id: 'foot', 41 collapisble: true, 42 height: 30, 43 html:'<div>欢迎您光临!</div>' 44 }); 45 /** 46 * 组建树 47 */ 48 Ext.define('TreeModelExtension', { 49 extend: 'Ext.data.Model', 50 //当Model实体类模型被用在某个TreeStore上,并且第一次实例化的时候 ,这些个属性会添加到Model实体类的的原型(prototype )上 (至于上述代码,则是通过把他设置为根节点的时候触发实例化处理的) 51 fields: [ 52 {name: 'text', type: 'string'}, 53 {name: 'url', type: 'string'} 54 ], 55 }); 56 var buildTree = function (json) { 57 return Ext.create('Ext.tree.Panel', { 58 rootVisible: false, 59 border: false, 60 store: Ext.create('Ext.data.TreeStore', { 61 model:'TreeModelExtension', 62 root: { 63 expanded: true, 64 children: json.children 65 } 66 }), 67 listeners: { 68 'itemdblclick': function (view, record, item, 69 index, e) { 70 var id = record.get('id'); 71 var text = record.get('text'); 72 var iconCls = record.get('iconCls'); 73 var leaf = record.get('leaf'); 74 var url = record.get('url'); 75 if (leaf) { //没有子节点时才创建新的tab显示 76 var tabs = Ext.getCmp('mainContent'); //获取布局页的Tab组件 77 var Loadtab = Ext.getCmp(id); //判断Tab是否已经加载 78 if(Loadtab==undefined){ //未加载则加载 79 tabs.add({ 80 id:id, 81 closable: true, 82 //这种方式采取了加载多个iframe的方式,优化看如何采取一个iframe的方式 83 html: ' <iframe scrolling="auto" frameborder="0" width="100%" height="100%" src="' + url + '"> </iframe>', 84 iconCls: iconCls, 85 title: text 86 }).show() 87 }else{ //已加载则设置为活动页 88 //tabs.setActiveTab(id); //适合于数据量较大,但是不需要实时改变得情况下,直接将已打开的tab设置为活动tab 89 90 //适合于需要实时展示最新数据的情况,先移除已打开的此tab,然后再重新加载 91 tabs.remove(id); 92 tabs.add({ 93 id: id, 94 closable: true, 95 //这种方式采取了加载多个iframe的方式,优化看如何采取一个iframe的方式 96 html: ' <iframe scrolling="auto" frameborder="0" width="100%" height="100%" src="' + url + '"> </iframe>', 97 iconCls: iconCls, 98 title: text 99 }).show() 100 } 101 } 102 }, 103 scope: this 104 } 105 }); 106 }; 107 /** 108 * 加载菜单树 109 */ 110 Ext.Ajax.request({ 111 url: AjaxPath, 112 113 success: function (response) { 114 var json = Ext.JSON.decode(response.responseText) 115 Ext.each(json.data, function (el) { 116 var panel = Ext.create( 117 'Ext.panel.Panel', { 118 id: el.id, 119 title: el.text, 120 iconCls:el.iconCls, 121 layout: 'fit' 122 }); 123 panel.add(buildTree(el)); 124 leftPanel.add(panel); 125 }); 126 }, 127 failure: function (request) { 128 Ext.MessageBox.show({ 129 title: '操作提示', 130 msg: "连接服务器失败", 131 buttons: Ext.MessageBox.OK, 132 icon: Ext.MessageBox.ERROR 133 }); 134 }, 135 method: 'post' 136 }); 137 /** 138 * Viewport 139 */ 140 Ext.create('Ext.container.Viewport', { 141 layout: 'border', 142 renderTo: Ext.getBody(), 143 items: [this.topPanel, this.leftPanel, this.rightPanel, this.bottomPanel] 144 }); 145 });
Grid行内增删改查:
1 Ext.onReady(function () { 2 // ExtJS组件自适应浏览器大小改变,看还有没有其他实现方式 3 Ext.EventManager.onWindowResize(function () { 4 Ext.ComponentManager.each(function (cmpId, cmp, length) { 5 if (cmp.hasOwnProperty("renderTo")) { 6 cmp.doLayout(); 7 } 8 }); 9 }); 10 var toolbar = Ext.create('Ext.toolbar.Toolbar', { 11 renderTo: document.body, 12 items: [ 13 // 使用右对齐容器 14 '->', // 等同 { xtype: 'tbfill' } 15 { 16 xtype: 'textfield', 17 name: 'roleName', 18 id: 'roleName', 19 emptyText: '输入角色名关键字', 20 listeners: { 21 specialkey: function (field, e) { 22 if (e.getKey() == Ext.EventObject.ENTER) { 23 store.load({ //传递查询条件参数 24 params: { 25 roleName: Ext.getCmp('roleName').getValue() 26 } 27 }); 28 } 29 } 30 } 31 }, 32 { 33 // xtype: 'button', // 默认的工具栏类型 34 text: '查询', 35 tooltip: '根据数据条件查询数据', 36 iconCls: "Zoom", 37 listeners: { 38 click: function () { 39 store.load({ //传递查询条件参数 40 params: { 41 roleName: Ext.getCmp('roleName').getValue() 42 } 43 }); 44 } 45 } 46 }, 47 // 添加工具栏项之间的垂直分隔条 48 '-', // 等同 {xtype: 'tbseparator'} 创建 Ext.toolbar.Separator 49 { 50 // xtype: 'button', // 默认的工具栏类型 51 text: '重置', 52 tooltip: '清空当前查询条件', 53 iconCls: "Arrowrotateanticlockwise", 54 handler: function () { //此事件可以代替click事件 55 Ext.getCmp('roleName').setValue(""); 56 } 57 }, 58 ] 59 }); 60 //1.定义Model 61 Ext.define("BeiDream.model.BeiDream_Role", { 62 extend: "Ext.data.Model", 63 fields: [ 64 { name: 'ID', type: 'int' }, 65 { name: 'Name', type: 'string' }, 66 { name: 'Description', type: 'string' }, 67 { name: 'IsUsed', type: 'boolean', defaultValue: true } 68 ] 69 }); 70 //2.创建store 71 var store = Ext.create("Ext.data.Store", { 72 model: "BeiDream.model.BeiDream_Role", 73 autoLoad: true, 74 pageSize: 10, 75 proxy: { 76 type: 'ajax', 77 api: { 78 read: RoleListUrl, //查询 79 create: AddUrl, //创建 80 update: UpdateUrl, //更新,必须真正修改了才会触发 81 destroy: RemoveUrl //删除 82 }, 83 reader: { 84 type: 'json', 85 root: 'data' 86 }, 87 writer: { 88 type: 'json', //默认格式 //MVC下后台使用模型自动进行转换,如果是普通webform,则配置root:'data',encode:'true',这样之后就可以使用request【data】获取 89 writeAllFields: true, //false只提交修改过的字段 90 allowSingle: false //默认为true,为true时,一条数据不以数组形式提交,为false时,都以数组形式提交,这样避免了提交了一条数据时,后台是list模型无法接收到数据问题 91 }, 92 listeners: { 93 exception: function (proxy, response, operation) { 94 grid.store.load(); //删除失败,数据重新加载 95 var resText = Ext.decode(response.responseText); 96 Ext.MessageBox.show({ 97 title: '服务器端异常', 98 msg: resText.msg, 99 icon: Ext.MessageBox.ERROR, 100 buttons: Ext.Msg.OK 101 }); 102 } 103 } 104 } 105 // sorters: [{ 106 // //排序字段。 107 // property: 'id' 108 // }] 109 }); 110 store.on('beforeload', function (store, options) { 111 var params = { roleName: Ext.getCmp('roleName').getValue() }; 112 Ext.apply(store.proxy.extraParams, params); 113 }); 114 var Gridtoolbar = Ext.create('Ext.toolbar.Toolbar', { 115 renderTo: document.body, 116 items: [{ 117 text: '新增', 118 tooltip: '新增一条数据', 119 iconCls: 'Add', 120 handler: function () { 121 RowEditing.cancelEdit(); 122 // Create a model instance 123 var r = new BeiDream.model.BeiDream_Role(); 124 Ext.getCmp('RoleGrid').getStore().insert(0, r); 125 RowEditing.startEdit(0, 0); 126 } 127 }, '-', { 128 text: '编辑', 129 tooltip: '编辑当前选择行数据', 130 iconCls: 'Pencil', 131 handler: function () { 132 RowEditing.cancelEdit(); 133 var data = Ext.getCmp("RoleGrid").getSelectionModel().getSelection(); 134 RowEditing.startEdit(data[0].index, 0); 135 }, 136 disabled: true 137 }, '-', { 138 itemId: 'removeUser', 139 text: '删除', 140 tooltip: '可以多选删除多条数据', 141 iconCls: 'Delete', 142 handler: function () { 143 Ext.MessageBox.confirm('提示', '确定删除该记录?', function (btn) { 144 if (btn != 'yes') { 145 return; 146 } 147 var sm = Ext.getCmp('RoleGrid').getSelectionModel(); 148 RowEditing.cancelEdit(); 149 150 var store = Ext.getCmp('RoleGrid').getStore(); 151 store.remove(sm.getSelection()); 152 store.sync(); //根据状态执行对应的服务器方法,delete,放在remove后才能成功执行服务器方法 153 if (store.getCount() > 0) { 154 sm.select(0); 155 } 156 }); 157 }, 158 disabled: true 159 }, '-', { 160 itemId: 'gridSync', 161 text: '保存', 162 tooltip: '保存到服务器', 163 iconCls: 'Disk', 164 handler: function () { 165 grid.store.sync(); 166 grid.store.commitChanges(); //执行commitChanges()提交数据修改。 167 } 168 }, '-', { 169 itemId: 'gridCancel', 170 text: '取消', 171 tooltip: '取消所有的已编辑数据', 172 iconCls: 'Decline', 173 handler: function () { 174 Ext.MessageBox.confirm('提示', '确定取消已编辑数据吗?', function (btn) { 175 if (btn != 'yes') { 176 return; 177 } 178 grid.store.rejectChanges(); //执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态 179 }); 180 } 181 }, '-', { 182 itemId: 'gridrefresh', 183 text: '刷新', 184 tooltip: '重新加载数据', 185 iconCls: 'Arrowrefresh', 186 handler: function () { 187 grid.store.load(); 188 } 189 }, '->', { 190 itemId: 'ImportExcel', 191 text: '导入Excel', 192 tooltip: '导入角色数据', 193 iconCls: 'Pageexcel', 194 handler: function () { 195 Ext.MessageBox.show({ 196 title: '暂未开放', 197 msg: '即将开放', 198 icon: Ext.MessageBox.ERROR, 199 buttons: Ext.Msg.OK 200 }); 201 } 202 }, '-', { 203 itemId: 'ExportExcel', 204 text: '导出Ecxel', 205 tooltip: '角色数据导出Excel', 206 iconCls: 'Pageexcel', 207 handler: function () { 208 Ext.MessageBox.show({ 209 title: '暂未开放', 210 msg: '即将开放', 211 icon: Ext.MessageBox.ERROR, 212 buttons: Ext.Msg.OK 213 }); 214 } 215 } 216 ] 217 }); 218 var RowEditing = Ext.create('Ext.grid.plugin.RowEditing', { // 行编辑模式 219 clicksToEdit: 2, //双击进行修改 1-单击 2-双击 220 autoCancel: false, 221 saveBtnText: '确定', 222 cancelBtnText: '取消', 223 errorsText: '错误', 224 dirtyText: '你要确认或取消更改', 225 listeners: { 226 cancelEdit: function (rowEditing, context) { 227 // Canceling editing of a locally added, unsaved record: remove it 228 if (context.record.phantom) { //服务器上是否有此条记录的标志,true为没有 229 store.remove(context.record); 230 } 231 }, 232 Edit: function (rowEditing, context) { 233 //store.sync(); //根据状态执行对应的服务器方法,Add/Edit 234 var IsValidate = ValidateInput(context.record.data, context.record.phantom); 235 if (!IsValidate) { 236 grid.store.rejectChanges(); 237 } 238 }, 239 validateedit: function (rowEditing, context) { 240 241 } 242 } 243 }); 244 function ValidateInput(data, IsAdd) { 245 var IsValidate; 246 Ext.Ajax.request({ 247 url: ValidateInputUrl, 248 method: 'POST', 249 jsonData: data, 250 params: { IsAdd: IsAdd }, 251 async: false, 252 success: function (response) { 253 var resText = Ext.decode(response.responseText); 254 if (resText.success) { 255 Ext.MessageBox.alert('警告', resText.msg); 256 IsValidate = false; 257 } else { 258 IsValidate = true; 259 } 260 }, 261 failure: function (response, options) { 262 Ext.MessageBox.alert('服务器异常', response.status); 263 } 264 }); 265 return IsValidate; 266 } 267 //多选框变化 268 function selectchange() { 269 var count = this.getCount(); 270 //删除 271 if (count == 0) { 272 Gridtoolbar.items.items[2].disable(); 273 Gridtoolbar.items.items[4].disable(); 274 } 275 else { 276 Gridtoolbar.items.items[2].enable(); 277 Gridtoolbar.items.items[4].enable(); 278 } 279 } 280 //3.创建grid 281 var grid = Ext.create("Ext.grid.Panel", { 282 id: "RoleGrid", 283 xtype: "grid", 284 store: store, 285 columnLines: true, 286 renderTo: Ext.getBody(), 287 selModel: { 288 injectCheckbox: 0, 289 listeners: { 290 'selectionchange': selectchange 291 }, 292 mode: "MULTI", //"SINGLE"/"SIMPLE"/"MULTI" 293 checkOnly: false //只能通过checkbox选择 294 }, 295 selType: "checkboxmodel", 296 columns: [ 297 { xtype: "rownumberer", text: "序号", width: 40, align: 'center' }, 298 { id: "id", text: "ID", width: 40, dataIndex: 'ID', sortable: true, hidden: true }, 299 { text: '角色名称', dataIndex: 'Name', flex: 1, editor: "textfield" }, 300 { text: '角色描述', dataIndex: 'Description', flex: 1, editor: "textfield" }, 301 { text: '是否启用', dataIndex: 'IsUsed', flex: 1, xtype: 'checkcolumn', editor: { xtype: 'checkbox', cls: 'x-grid-checkheader-editor'} } 302 ], 303 plugins: [RowEditing], 304 listeners: { 305 itemdblclick: function (me, record, item, index, e, eOpts) { 306 //双击事件的操作 307 } 308 }, 309 tbar: Gridtoolbar, 310 bbar: { xtype: "pagingtoolbar", store: store, displayInfo: true, emptyMsg: "没有记录" } 311 }); 312 });
TreeGrid展示:前台代码和后台模型结合
Ext.onReady(function () { // ExtJS组件自适应浏览器大小改变,看还有没有其他实现方式 Ext.EventManager.onWindowResize(function () { Ext.ComponentManager.each(function (cmpId, cmp, length) { if (cmp.hasOwnProperty("renderTo")) { cmp.doLayout(); } }); }); Ext.create('Ext.container.Viewport', { layout: 'border', renderTo: Ext.getBody(), items: [{ title: '主菜单模块', region: 'west', xtype: 'panel', margins: '5 0 0 5', width: 200, collapsible: true, // 可折叠/展开 id: 'NavigationMenucontainer', layout: 'fit' }, { title: '子菜单列表', region: 'center', // 必须指定中间区域 xtype: 'panel', layout: 'fit', id: 'Gridcontainer', margins: '5 5 0 0' }] }); var NavigationMenu=Ext.getCmp('NavigationMenucontainer'); /** * 加载菜单树 */ Ext.Ajax.request({ url: AjaxPath, success: function (response) { var json = Ext.JSON.decode(response.responseText) Ext.each(json.data, function (el) { var panel = Ext.create( 'Ext.panel.Panel', { id: el.id, layout: 'fit' }); var ShowGrid=CreateGrid(el.id); Gridcontainer.add(ShowGrid); //初始化,加载主菜单下的菜单 panel.add(buildTree(el)); NavigationMenu.add(panel); }); }, failure: function (request) { Ext.MessageBox.show({ title: '操作提示', msg: "连接服务器失败", buttons: Ext.MessageBox.OK, icon: Ext.MessageBox.ERROR }); }, method: 'post' }); var Gridcontainer=Ext.getCmp('Gridcontainer'); /** * 组建树 */ Ext.define('TreeModelExtension', { extend: 'Ext.data.Model', //当Model实体类模型被用在某个TreeStore上,并且第一次实例化的时候 ,这些个属性会添加到Model实体类的的原型(prototype )上 (至于上述代码,则是通过把他设置为根节点的时候触发实例化处理的) fields: [ {name: 'text', type: 'string'}, {name: 'url', type: 'string'} ], }); var buildTree = function (json) { return Ext.create('Ext.tree.Panel', { id:'MenuTree', rootVisible: true, border: false, store: Ext.create('Ext.data.TreeStore', { model:'TreeModelExtension', root: { id:json.id, text:json.text, iconCls: json.iconCls, expanded: json.expanded, children: json.children } }), listeners: { 'itemclick': function (view, record, item, index, e) { var ParentID = record.get('id'); var ShowGrid=CreateGrid(ParentID); Gridcontainer.add(ShowGrid); }, scope: this } }); }; function CreateGrid(ParentID) { var Gridtoolbar = Ext.create('Ext.toolbar.Toolbar', { renderTo: document.body, items: [{ text: '新增', tooltip: '新增一条数据', iconCls: 'Add', handler: function () { RowEditing.cancelEdit(); // Create a model instance var r = new BeiDream.model.BeiDream_NavigationMenu(); Ext.getCmp('NavigationMenuGrid').getStore().insert(0, r); RowEditing.startEdit(0, 0); } }, '-', { text: '编辑', tooltip: '编辑当前选择行数据', iconCls: 'Pencil', handler: function () { RowEditing.cancelEdit(); var data = Ext.getCmp("NavigationMenuGrid").getSelectionModel().getSelection(); RowEditing.startEdit(data[0].index, 0); }, disabled: true }, '-', { itemId: 'removeUser', text: '删除', tooltip: '可以多选删除多条数据', iconCls: 'Delete', handler: function () { Ext.MessageBox.confirm('提示', '确定删除该记录?', function (btn) { if (btn != 'yes') { return; } var sm = Ext.getCmp('NavigationMenuGrid').getSelectionModel(); RowEditing.cancelEdit(); var store = Ext.getCmp('NavigationMenuGrid').getStore(); store.remove(sm.getSelection()); store.sync(); //根据状态执行对应的服务器方法,delete,放在remove后才能成功执行服务器方法 if (store.getCount() > 0) { sm.select(0); } }); }, disabled: true }, '-', { itemId: 'gridSync', text: '保存', tooltip: '保存到服务器', iconCls: 'Disk', handler: function () { var grid=Ext.getCmp('NavigationMenuGrid'); grid.store.sync(); grid.store.commitChanges(); //执行commitChanges()提交数据修改。 } }, '-', { itemId: 'gridCancel', text: '取消', tooltip: '取消所有的已编辑数据', iconCls: 'Decline', handler: function () { Ext.MessageBox.confirm('提示', '确定取消已编辑数据吗?', function (btn) { if (btn != 'yes') { return; } var grid=Ext.getCmp('NavigationMenuGrid'); grid.store.rejectChanges(); //执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态 }); } }, '-', { itemId: 'gridrefresh', text: '刷新', tooltip: '重新加载数据', iconCls: 'Arrowrefresh', handler: function () { var grid=Ext.getCmp('NavigationMenuGrid'); grid.store.load(); } } ] }); var RowEditing = Ext.create('Ext.grid.plugin.RowEditing', { // 行编辑模式 clicksToEdit: 2, //双击进行修改 1-单击 2-双击 autoCancel: false, saveBtnText: '确定', cancelBtnText: '取消', errorsText: '错误', dirtyText: '你要确认或取消更改', listeners: { // beforeedit: function (rowEditing,e,context) { // if(e.colldx==2 && e.record.data.IsLeaf==false){ // return false; // }else{ // return true; // } // }, cancelEdit: function (rowEditing, context) { // Canceling editing of a locally added, unsaved record: remove it if (context.record.phantom) { //服务器上是否有此条记录的标志,true为没有 store.remove(context.record); } }, Edit: function (rowEditing, context) { //store.sync(); //根据状态执行对应的服务器方法,Add/Edit //var IsValidate = ValidateInput(context.record.data, context.record.phantom); // if (!IsValidate) { // grid.store.rejectChanges(); // } } } }); function ValidateInput(data, IsAdd) { var IsValidate; Ext.Ajax.request({ url: ValidateInputUrl, method: 'POST', jsonData: data, params: { IsAdd: IsAdd }, async: false, success: function (response) { var resText = Ext.decode(response.responseText); if (resText.success) { Ext.MessageBox.alert('警告', resText.msg); IsValidate = false; } else { IsValidate = true; } }, failure: function (response, options) { Ext.MessageBox.alert('服务器异常', response.status); } }); return IsValidate; } //多选框变化 function selectchange() { var count = this.getCount(); //删除 if (count == 0) { Gridtoolbar.items.items[2].disable(); Gridtoolbar.items.items[4].disable(); } else { Gridtoolbar.items.items[2].enable(); Gridtoolbar.items.items[4].enable(); } } //1.定义Model Ext.define("BeiDream.model.BeiDream_NavigationMenu", { extend: "Ext.data.Model", fields: [ { name: 'ID', type: 'int' }, { name: 'ParentID', type: 'int' }, { name: 'ShowName', type: 'string', defaultValue: '名称......' }, { name: 'IsLeaf', type: 'boolean', defaultValue: true }, { name: 'url', type: 'string' }, { name: 'OrderNo', type: 'int', defaultValue: 1 }, { name: 'iconCls', type: 'string' }, { name: 'Expanded', type: 'boolean', defaultValue: false } ] }); //2.创建store var store = Ext.create("Ext.data.Store", { model: "BeiDream.model.BeiDream_NavigationMenu", autoLoad: true, pageSize: 15, proxy: { type: 'ajax', api: { read: MenuListUrl, //查询 create: AddUrl, //创建 update: UpdateUrl, //更新,必须真正修改了才会触发 destroy: RemoveUrl //删除 }, reader: { type: 'json', root: 'data' }, writer: { type: 'json', //默认格式 //MVC下后台使用模型自动进行转换,如果是普通webform,则配置root:'data',encode:'true',这样之后就可以使用request【data】获取 writeAllFields: true, //false只提交修改过的字段 allowSingle: false //默认为true,为true时,一条数据不以数组形式提交,为false时,都以数组形式提交,这样避免了提交了一条数据时,后台是list模型无法接收到数据问题 }, listeners: { exception: function (proxy, response, operation) { // var grid=Ext.getCmp('NavigationMenuGrid'); // grid.store.load(); //删除失败,数据重新加载 var resText = Ext.decode(response.responseText); Ext.MessageBox.show({ title: '服务器端异常', msg: resText.msg, icon: Ext.MessageBox.ERROR, buttons: Ext.Msg.OK }); } } } }); store.on('beforeload', function (store, options) { var params = { ParentID: ParentID }; Ext.apply(store.proxy.extraParams, params); }); return Ext.create("Ext.grid.Panel", { id: "NavigationMenuGrid", xtype: "grid", store: store, columnLines: true, selModel: { injectCheckbox: 0, listeners: { 'selectionchange': selectchange }, mode: "SINGLE", //"SINGLE"/"SIMPLE"/"MULTI" checkOnly: false //只能通过checkbox选择 }, selType: "checkboxmodel", columns: [ { xtype: "rownumberer", text: "序号", width: 40, align: 'center' }, { id: "id", text: "ID", width: 40, dataIndex: 'ID', sortable: true, hidden: true }, { id: "id", text: "ParentID", width: 40, dataIndex: 'ParentID', sortable: true, hidden: true }, { text: '名称', dataIndex: 'ShowName', flex: 1, editor: { xtype: 'textfield', allowBlank: false } }, { text: '是否为模块', dataIndex: 'IsLeaf', flex: 1, xtype: 'checkcolumn', editor: { xtype: 'checkbox', cls: 'x-grid-checkheader-editor'} }, { text: '控制器路径', dataIndex: 'url', flex: 1, editor : { xtype: 'combobox', editable:false, listeners: { //点击下拉列表事件 expand: function (me, event, eOpts) { var grid=Ext.getCmp('NavigationMenuGrid'); var record = grid.getSelectionModel().getLastSelected(); if(record!=null){ if(record.data.IsLeaf==true){ currentComboBox = me; f_openSelectControllerWin(); }else{ Ext.MessageBox.alert('警告', '只有模块才拥有控制器!'); } } } } } }, { text: '排序号', dataIndex: 'OrderNo',align:"center", width: 48, flex: 1,editor: { xtype: 'numberfield', allowBlank: false, minValue: 1, maxValue: 150000 } }, { text: '图标', dataIndex: 'iconCls',align:"center", width: 48,renderer : function(value) { return "<div Align='center' style='height:16px;width:16px' class="+value+"></div>"; } ,editor : { xtype: 'combobox', editable:false, listeners: { //点击下拉列表事件 expand: function (me, event, eOpts) { currentComboBox = me; f_openIconsWin(); } } } }, { text: '是否展开', dataIndex: 'Expanded', flex: 1, xtype: 'checkcolumn', editor: { xtype: 'checkbox', cls: 'x-grid-checkheader-editor'} } ], plugins: [RowEditing], tbar: Gridtoolbar, bbar: { xtype: "pagingtoolbar", store: store, displayInfo: true, emptyMsg: "没有记录" } }); }; });
要点二:后台MVC的传参绑定,返回值自定义
MVC方便了Ajax的异步实现,并且方便的模型传参,代码如下
1 /// <summary> 2 /// 返回数据库新增后的实体,供前台的extjs的 grid的store更新数据,这样就不需要进行重新加载store了,删,改类似 3 /// </summary> 4 /// <param name="Roles"></param> 5 /// <returns></returns> 6 [Anonymous] 7 [HttpPost] 8 public ActionResult Add(List<BeiDream_Role> Roles) 9 { 10 List<BeiDream_Role> AddRoles = new List<BeiDream_Role>(); 11 List<Object> ListObj = RoleService.Add(Roles); 12 if (ListObj.Count == 0) 13 { 14 List<string> msg = new List<string>(); 15 msg.Add("添加角色失败!"); 16 return this.ExtjsJsonResult(false, msg); 17 } 18 else 19 { 20 foreach (var item in ListObj) 21 { 22 AddRoles.Add(RoleService.GetModelByID(item)); 23 } 24 List<string> msg = new List<string>(); 25 msg.Add("添加角色成功!"); 26 return this.ExtjsJsonResult(true, AddRoles, msg); 27 } 28 29 }
可以看到我直接通过后台模型来接收前台传递过来的参数,而不需要去一一解析参数值
返回值自定义:重写了ActionResult,实现了extjs需要的返回值
1 /// <summary> 2 /// 扩展的jsonResult模型,适用于extjs需要的json数据类型 3 /// </summary> 4 public class JsonResultExtension:ActionResult 5 { 6 public bool success { get; set; } 7 public string msg { get; set; } 8 public object data { get; set; } 9 public long? total { get; set; } 10 public Dictionary<string, string> errors { get; set; } 11 /// <summary> 12 /// 是否序列化为extjs需要的json格式,否则进行普通序列化 13 /// </summary> 14 public bool ExtjsUISerialize { get; set; } 15 public override void ExecuteResult(ControllerContext context) 16 { 17 18 if (context == null) 19 { 20 throw new ArgumentNullException("context"); 21 } 22 HttpResponseBase response = context.HttpContext.Response; 23 response.ContentType = "application/json"; 24 25 StringWriter sw = new StringWriter(); 26 //IsoDateTimeConverter timeFormat = new IsoDateTimeConverter(); 27 //timeFormat.DateTimeFormat = "yyyy-MM-dd HH:mm:ss"; 28 IsoDateTimeConverter timeFormat = new IsoDateTimeConverter(); 29 timeFormat.DateTimeFormat = "yyyy-MM-dd"; 30 JsonSerializer serializer = JsonSerializer.Create( 31 new JsonSerializerSettings 32 { 33 Converters = new JsonConverter[] { timeFormat }, 34 ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 35 NullValueHandling = NullValueHandling.Ignore //忽略为null的值序列化 36 37 } 38 ); 39 40 41 using (JsonWriter jsonWriter = new JsonTextWriter(sw)) 42 { 43 jsonWriter.Formatting = Formatting.Indented; 44 45 if (ExtjsUISerialize) 46 serializer.Serialize(jsonWriter, this); 47 else 48 serializer.Serialize(jsonWriter, data); 49 } 50 response.Write(sw.ToString()); 51 52 } 53 }
特性标注及权限验证,代码如下
1 /// <summary> 2 /// 权限拦截 3 /// </summary> 4 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 5 public class PermissionFilterAttribute : ActionFilterAttribute 6 { 7 /// <summary> 8 /// 权限拦截 9 /// </summary> 10 /// <param name="filterContext"></param> 11 public override void OnActionExecuting(ActionExecutingContext filterContext) 12 { 13 if (!this.CheckAnonymous(filterContext)) 14 { 15 //未登录验证 16 if (SessionHelper.Get("UserID") == null) 17 { 18 //跳转到登录页面 19 filterContext.RequestContext.HttpContext.Response.Redirect("~/Admin/User/Login"); 20 } 21 } 22 } 23 /// <summary> 24 /// [Anonymous标记]验证是否匿名访问 25 /// </summary> 26 /// <param name="filterContext"></param> 27 /// <returns></returns> 28 public bool CheckAnonymous(ActionExecutingContext filterContext) 29 { 30 //验证是否是匿名访问的Action 31 object[] attrsAnonymous = filterContext.ActionDescriptor.GetCustomAttributes(typeof(AnonymousAttribute), true); 32 //是否是Anonymous 33 var Anonymous = attrsAnonymous.Length == 1; 34 return Anonymous; 35 } 36 }
通过写一个BaseController来进行权限的验证,这样就不需要所有需要验证的Controller加标注了,当然BaseController还可以增加其他通用的处理
1 /// <summary> 2 /// Admin后台系统公共控制器(需要验证的模块) 3 /// </summary> 4 [PermissionFilter] 5 public class BaseController:Controller 6 { 7 8 }
要点三:轻量级ORM框架PetaPoco
非侵入性ORM框架,只需要一个PetaPoco.cs文件就OK了,不过我对其进行了再次封装,实现了工作单元,还是上代码吧
一:封装
1 public interface IDataRepository<TEntity> : IDependency where TEntity : class 2 { 3 #region 属性 4 5 /// <summary> 6 /// 获取 当前实体的查询数据集 7 /// </summary> 8 Database PetaPocoDB { get; } 9 10 #endregion 11 12 #region 公共方法 13 14 /// <summary> 15 /// 插入实体记录 16 /// </summary> 17 /// <param name="entity"> 实体对象 </param> 18 /// <returns> 操作影响的行数 </returns> 19 bool Add(TEntity entity); 20 21 /// <summary> 22 /// 批量插入实体记录集合 23 /// </summary> 24 /// <param name="entities"> 实体记录集合 </param> 25 /// <returns> 操作影响的行数 </returns> 26 List<object> Add(IEnumerable<TEntity> entities); 27 28 /// <summary> 29 /// 删除实体记录 30 /// </summary> 31 /// <param name="entity"> 实体对象 </param> 32 /// <returns> 操作影响的行数 </returns> 33 int Delete(TEntity entity); 34 35 /// <summary> 36 /// 删除实体记录集合 37 /// </summary> 38 /// <param name="entities"> 实体记录集合 </param> 39 /// <returns> 操作影响的行数 </returns> 40 bool Delete(IEnumerable<TEntity> entities); 41 42 /// <summary> 43 /// 更新实体记录 44 /// </summary> 45 /// <param name="entity"> 实体对象 </param> 46 /// <returns> 操作影响的行数 </returns> 47 int Update(TEntity entity); 48 49 /// <summary> 50 /// 更新实体记录 51 /// </summary> 52 /// <param name="entity"> 实体对象 </param> 53 /// <returns> 操作影响的行数 </returns> 54 bool Update(IEnumerable<TEntity> entities); 55 /// <summary> 56 /// 根据主键ID获取实体 57 /// </summary> 58 /// <param name="KeyID">主键ID</param> 59 /// <returns>实体</returns> 60 TEntity GetModelByID(object KeyID); 61 62 /// <summary> 63 /// 动态查询,返回dynamic类型的列表 64 /// 请使用标准SQL语句进行查询(SELECT ... FROM ...) 65 /// </summary> 66 /// <returns></returns> 67 PagedList<dynamic> DynamicPagedList(int pageIndex, int pageSize, Sql sql); 68 69 PagedList<TEntity> PagedList(int pageIndex, int pageSize, string sql, params object[] args); 70 71 PagedList<TEntity> PagedList(int pageIndex, int pageSize, Sql sql); 72 73 PagedList<TDto> PagedList<TDto>(int pageIndex, int pageSize, string sql, params object[] args); 74 75 PagedList<TDto> PagedList<TDto>(int pageIndex, int pageSize, Sql sql); 76 #endregion 77 }
二:使用,具体封装和使用,大家还是去下载源码看吧
1 public class NavigationMenuService : DbContextBase<BeiDream_NavigationMenu>, INavigationMenuService, IDependency 2 { 3 public NavigationMenuService(IUnitOfWork unitOfWork) 4 : base(unitOfWork) 5 { } 6 public List<NavigationMenu> GetNavigationMenu(int id) 7 { 8 var sql = Sql.Builder.Where("ParentID=@0",id); 9 sql.OrderBy("OrderNo ASC"); //默认ASC升序,降序为DESC 10 List<BeiDream_NavigationMenu> List = this.PetaPocoDB.Fetch<BeiDream_NavigationMenu>(sql); 11 return AutoMapperHelper.GetMapper(List); 12 } 13 public List<NavigationMenu> GetNavigationMenuNoLeaf(int id) 14 { 15 var sql = Sql.Builder.Where("ParentID=@0", id); 16 sql.Where("IsLeaf=@0", false); 17 sql.OrderBy("OrderNo ASC"); //默认ASC升序,降序为DESC 18 List<BeiDream_NavigationMenu> List = this.PetaPocoDB.Fetch<BeiDream_NavigationMenu>(sql); 19 return AutoMapperHelper.GetMapper(List); 20 } 21 /// <summary> 22 /// 递归查询产品分类列表 23 /// </summary> 24 /// <param name="list">父级产品分类列表</param> 25 public void GetNavigationMenus(ref List<NavigationMenu> list) 26 { 27 foreach (NavigationMenu season in list) 28 { 29 List<NavigationMenu> lstSeason = GetNavigationMenu(season.id); 30 season.children = lstSeason; 31 if (list.Count > 0) 32 { 33 GetNavigationMenus(ref lstSeason); 34 } 35 } 36 } 37 /// <summary> 38 /// 递归查询产品分类列表 39 /// </summary> 40 /// <param name="list">父级产品分类列表</param> 41 public void GetNavigationMenusNoLeaf(ref List<NavigationMenu> list) 42 { 43 foreach (NavigationMenu season in list) 44 { 45 List<NavigationMenu> lstSeason = GetNavigationMenuNoLeaf(season.id); 46 season.children = lstSeason; 47 if (list.Count > 0) 48 { 49 GetNavigationMenusNoLeaf(ref lstSeason); 50 } 51 } 52 } 53 }
要点四:依赖注入框架Autofac
目前使用心得最大的好处就是不需要配置即实现了面向接口编程,特别是和MVC结合,实现构造函数注入就更加方便了,当然它还有其他
功能,比如生命周期唯一实例,单例啊等等,暂时还研究不深,只是简单应用,大家看看具体实现吧
1 private static void AutofacMvcRegister() 2 { 3 ContainerBuilder builder = new ContainerBuilder(); 4 builder.RegisterGeneric(typeof(DbContextBase<>)).As(typeof(IDataRepository<>)); 5 Type baseType = typeof(IDependency); 6 Assembly[] assemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies() 7 .Select(Assembly.Load).ToArray(); 8 assemblies = assemblies.Union(new[] { Assembly.GetExecutingAssembly() }).ToArray(); 9 builder.RegisterAssemblyTypes(assemblies) 10 .Where(type => baseType.IsAssignableFrom(type) && !type.IsAbstract) 11 .AsImplementedInterfaces().InstancePerLifetimeScope();//InstancePerLifetimeScope 保证生命周期基于请求 12 13 //无效 14 //builder.RegisterType<DefaultCacheAdapter>().PropertiesAutowired().As<ICacheStorage>(); 15 16 builder.RegisterControllers(Assembly.GetExecutingAssembly()); 17 builder.RegisterFilterProvider(); 18 IContainer container = builder.Build(); 19 DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); 20 }
要点五;对象关系映射AutoMapper
目前也只是简单应用,先看代码,它是如何简化我们的工作量的
1 public static List<NavigationMenu> GetMapper(List<BeiDream_NavigationMenu> List) 2 { 3 List<NavigationMenu> NavigationMenuList = new List<NavigationMenu>(); 4 foreach (var item in List) 5 { 6 //NavigationMenu DaoModel = new NavigationMenu(); 7 //DaoModel.id = item.ID; 8 //DaoModel.text = item.ShowName; 9 //DaoModel.leaf = item.IsLeaf; 10 //DaoModel.url = item.url; 11 //DaoModel.Expanded = item.Expanded; 12 //DaoModel.children = null; 13 NavigationMenu DaoModel = item.ToDestination<BeiDream_NavigationMenu, NavigationMenu>(); 14 NavigationMenuList.Add(DaoModel); 15 } 16 return NavigationMenuList; 17 }
注释掉的是不使用automapper之前的代码,没注释掉的是使用automapper,扩展了方法直接一句代码实现转化,是不是很easy,当然实
现这些之前,我们需要给他定义规则,然后还要注册,代码如下,具体的请看源码
1 public class NavigationMenuProfile : Profile 2 { 3 protected override void Configure() 4 { 5 CreateMap<BeiDream_NavigationMenu, NavigationMenu>() 6 .ForMember(dest => dest.id, opt => opt.MapFrom(src => src.ID)) 7 .ForMember(dest => dest.text, opt => opt.MapFrom(src => src.ShowName)) 8 .ForMember(dest => dest.leaf, opt => opt.MapFrom(src => src.IsLeaf)); 9 } 10 }
要点六:数据验证
extjs前台验证我们已经做了,但是客户端传来的东西我们不能完全相信,后台需要再次验证,我们看到mvc的官方demo。一句话就实现
了验证,我们是不是可以自己做验证呢,看代码
1 [Anonymous] 2 public ActionResult SaveUser(BeiDream_User model, List<int> Roles) 3 { 4 var ValidateResult = Validation.Validate(model);//服务器端的验证 5 if (ValidateResult.IsValid) //验证成功 6 { 7 bool IsExitUser = UserService.PetaPocoDB.Exists<BeiDream_User>(model.ID); 8 if (!IsExitUser) 9 { 10 FilterGroup userRoleGroup = new FilterGroup(); 11 FilterHelper.CreateFilterGroup(userRoleGroup, null, "UserName", model.UserName, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.equal); 12 bool IsExist = UserService.IsExist(userRoleGroup); 13 if (IsExist) 14 { 15 List<string> errorName=new List<string>(); 16 errorName.Add("UserName"); 17 ValidationResult error = new ValidationResult("已存在相同的用户名", errorName); 18 ValidateResult.Add(error); 19 return this.ExtjsFromJsonResult(false,ValidateResult); 20 } 21 else 22 { 23 bool IsSaveSuccess = TransactionService.AddUserAndUserRole(model, Roles); 24 List<string> msg = new List<string>(); 25 msg.Add(IsSaveSuccess ? "用户信息保存成功!" : "用户信息保存失败!"); 26 return this.ExtjsFromJsonResult(true, null, msg); 27 } 28 } 29 else 30 { 31 FilterGroup userRoleGroup = new FilterGroup(); 32 FilterHelper.CreateFilterGroup(userRoleGroup, null, "UserName", model.UserName, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.equal); 33 FilterHelper.CreateFilterGroup(userRoleGroup, null, "ID", model.ID, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.notequal); 34 bool IsExist = UserService.IsExist(userRoleGroup); 35 if (IsExist) 36 { 37 List<string> errorName = new List<string>(); 38 errorName.Add("UserName"); 39 ValidationResult error = new ValidationResult("已存在相同的用户名", errorName); 40 ValidateResult.Add(error); 41 return this.ExtjsFromJsonResult(false, ValidateResult); 42 } 43 else 44 { 45 bool IsSaveSuccess = TransactionService.UpdateUserAndUserRole(model, Roles); 46 List<string> msg = new List<string>(); 47 msg.Add(IsSaveSuccess ? "用户信息保存成功!" : "用户信息保存失败!"); 48 return this.ExtjsFromJsonResult(true, null, msg); 49 } 50 } 51 } 52 else 53 { 54 return this.ExtjsFromJsonResult(false,ValidateResult); //验证失败,返回失败的验证结果,给出前台提示信息 55 } 56 }
大家可以看到前台传进来的参数,我们先进行验证 var ValidateResult = Validation.Validate(model),验证的条件我们是在模型上定义好的,然后判断验证是否通过,通过进行下一步动作,不通过,把验证的结果信息返回前台,提示给用户
要点七:SQL翻译机
这个只能算是一个简单的东西吧,并且感觉用起来麻烦,但是我觉得用的熟练了,还是很不错的,只是省了手拼SQL的问题嘛,减少了出
错几率,具体使用还是看代码吧
1 [Anonymous] 2 public ActionResult GetUserList(int page, int start, int limit, string UserKeyName, string RoleID) 3 { 4 RemoveSelectId(); 5 PagedList<BeiDream_User> PageList = null; 6 FilterGroup userGroup = GetQueryConditions(UserKeyName, RoleID); 7 PageList = UserService.GetPagedList(page, limit, userGroup); 8 return this.ExtjsGridJsonResult(PageList, PageList.TotalItemCount); 9 } 10 private FilterGroup GetQueryConditions(string UserKeyName, string RoleID) 11 { 12 FilterGroup userGroup = new FilterGroup(); 13 if (!string.IsNullOrEmpty(RoleID)) //用户角色不为空时 14 { 15 FilterGroup userRoleGroup = new FilterGroup(); 16 FilterHelper.CreateFilterGroup(userRoleGroup, null, "RoleID", RoleID, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.equal); 17 //先根据用户角色查出对应的用户ID 18 List<BeiDream_User_Role> List = UserRoleService.GetList(userRoleGroup); 19 if (List.Count != 0) //todo,此角色信息为空情况下,查到的用户也应该为空,目前未处理 20 { 21 if (string.IsNullOrEmpty(UserKeyName)) 22 { 23 //再根据此用户角色下的用户ID,因为查出所以用户ID,查询条件是或的关系GroupOperatorQueryEnum.or,翻译出对应用户的查询条件,最后查出对应用户 24 foreach (var item in List) 25 { 26 FilterHelper.CreateFilterGroup(userGroup, null, "ID", item.UserID, GroupOperatorQueryEnum.or, RuleOperatorQueryEnum.equal); 27 } 28 } 29 else 30 { 31 //先翻译出用户名查询条件,与其他查询条件是与的关系GroupOperatorQueryEnum.and 32 FilterHelper.CreateFilterGroup(userGroup, null, "UserName", UserKeyName, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.like); 33 //因为第二个查询条件是多个查询条件的结合组成再与第一个查询条件结合,故放到子FilterGroup中 34 List<FilterGroup> filterGroups = new List<FilterGroup>(); 35 FilterGroup userIDGroup = new FilterGroup(); 36 //再根据此用户角色下的用户ID,因为查出所以用户ID,查询条件是或的关系GroupOperatorQueryEnum.or,翻译出对应用户的查询条件,最后查出对应用户 37 foreach (var item in List) 38 { 39 FilterHelper.CreateFilterGroup(userIDGroup, null, "ID", item.UserID, GroupOperatorQueryEnum.or, RuleOperatorQueryEnum.equal); 40 } 41 filterGroups.Add(userIDGroup); 42 userGroup.groups = filterGroups; 43 } 44 } 45 } 46 else 47 { 48 if (!string.IsNullOrEmpty(UserKeyName)) 49 { 50 //先翻译出用户名查询条件,与其他查询条件是与的关系GroupOperatorQueryEnum.and 51 FilterHelper.CreateFilterGroup(userGroup, null, "UserName", UserKeyName, GroupOperatorQueryEnum.and, RuleOperatorQueryEnum.like); 52 } 53 } 54 return userGroup; 55 }
要点八:缓存
缓存也就简单应用Helper级别,主要用了.net自带缓存和分布式Memcached缓存,一个接口,两个实现
1 /// <summary> 2 /// 缓存接口 3 /// </summary> 4 public interface ICacheStorage 5 { 6 #region 缓存操作 7 /// <summary> 8 /// 添加缓存 9 /// </summary> 10 /// <param name="key"></param> 11 /// <param name="value"></param> 12 void Insert(string key, object value); 13 /// <summary> 14 /// 添加缓存(默认滑动时间为20分钟) 15 /// </summary> 16 /// <param name="key">key</param> 17 /// <param name="value">value</param> 18 /// <param name="expiration">绝对过期时间</param> 19 void Insert(string key, object value, DateTime expiration); 20 /// <summary> 21 /// 添加缓存 22 /// </summary> 23 /// <param name="key">key</param> 24 /// <param name="value">value</param> 25 /// <param name="expiration">过期时间</param> 26 void Insert(string key, object value, TimeSpan expiration); 27 /// <summary> 28 /// 获得key对应的value 29 /// </summary> 30 /// <param name="key"></param> 31 /// <returns></returns> 32 object Get(string key); 33 /// <summary> 34 /// 根据key删除缓存 35 /// </summary> 36 /// <param name="key"></param> 37 void Remove(string key); 38 /// <summary> 39 /// 缓存是否存在key的value 40 /// </summary> 41 /// <param name="key">key</param> 42 /// <returns></returns> 43 bool Exist(string key); 44 /// <summary> 45 /// 获取所有的缓存key 46 /// </summary> 47 /// <returns></returns> 48 List<string> GetCacheKeys(); 49 /// <summary> 50 /// 清空缓存 51 /// </summary> 52 void Flush(); 53 54 #endregion 55 }
写在最后
写博客真的是很累人的事,很敬佩那些能写连载博客的牛人们,虽然自己做的项目很小,但是觉得写成博客,要写的要点还是很多的
,上面我讲的很粗略,但是主要的知识点都讲出来了,这个项目其实没有做完,不打算再继续了,打算换了,接下来打算使用easyui
+knockout+ef来写一个完整的权限管理系统,涉及菜单权限、按钮权限、字段权限等等吧,路很长.....任重而道远
最后,大家如果觉得有帮助,请点推荐哦!源码下载地址: