[ { title:"名稱", data:"字段名", type:"字段類型", value:"值" } ]
對於一個簡單的form表單,這個結構足以描述。太複雜的頁面?本身寫...。而就個人實踐經驗告訴我,這個結構足以知足90%後臺系統中的新增與修改。javascript
this.initForm = function () { var form = $('<form id="' + this.formId + '" class="form-horizontal layer-row-container" role="form"></form>'); form.appendTo($("body")); for (var i = 0, iLen = this.columns.length; i < iLen; i++) { var colType = this.columns[i]["type"]; if (!colType) continue; var row; if (["text", "number", "datepicker", "timepicker", "hide"].indexOf(colType) >= 0) { row = $('<div class="form-group"><label for="' + this.columns[i]["data"] + '" class="col-sm-3 control-label">' + this.columns[i]["title"] + ':</label><div class="col-sm-9"><input type="text" class="form-control" id="' + this.columns[i]["data"] + '"></div></div>'); if (colType === "number") { $.bode.tools.input.formatDiscount(row.find("#" + this.columns[i]["data"])); } else if (colType === "datepicker" || colType === "timepicker") { var showTime = colType === "timepicker"; $.bode.tools.input.formatTime(row.find("#" + this.columns[i]["data"]), showTime); } else if (colType === "hide") { row.hide(); } row.appendTo(form); } else if (["switch", "dropdown"].indexOf(colType) >= 0) { var source = this.columns[i]["source"]; var valueFiled = source.valueField || "value"; var textField = source.textField || "text"; row = $('<div class="form-group"><label for="' + this.columns[i]["data"] + '" class="col-sm-3 control-label">' + this.columns[i]["title"] + ':</label><div class="col-sm-9"><select class="form-control" id="' + this.columns[i]["data"] + '"></select></div></div>'); var select = row.find("#" + this.columns[i]["data"]); for (var j = 0, jLen = source["data"].length; j < jLen; j++) { var option = source["data"][j]; $('<option value="' + option[valueFiled] + '">' + option[textField ] + '</option>') .appendTo(select); } row.appendTo(form); } else if (colType === "img") { row = $('<div class="form-group"><label for="' + this.columns[i]["data"] + '" class="col-sm-3 control-label">' + this.columns[i]["title"] + ':</label><div class="col-sm-9"><div class="uploader-list"><div class="file-item thumbnail"><img style="width:160px;height:90px;" id="img_' + this.columns[i]["data"] + '" src="" /></div></div><div id="' + this.columns[i]["data"] + '">選擇圖片</div></div></div>'); row.appendTo(form); // 初始化Web Uploader var uploader = WebUploader.create({ auto: true, // 選完文件後,是否自動上傳。 swf: '/Content/js/plugs/webuploader/Uploader.swf', // swf文件路徑 server: this.imgSaveUrl, // 文件接收服務端。 // 選擇文件的按鈕。可選。 // 內部根據當前運行是建立,多是input元素,也多是flash. pick: '#' + this.columns[i]["data"], // 只容許選擇圖片文件。 accept: { title: 'Images', extensions: 'gif,jpg,jpeg,bmp,png', mimeTypes: 'image/*' } }); uploader.on("uploadSuccess", function (file, resp) { $("#img_" + this.options.pick.substring(1)).attr("src", resp); }); } else if (colType === "textarea") { row = $('<div class="form-group"><label for="' + this.columns[i]["data"] + '" class="col-sm-3 control-label">' + this.columns[i]["title"] + ':</label><div class="col-sm-9"><textarea class="form-control" id="' + this.columns[i]["data"] + '" style="overflow: hidden; word-wrap: break-word; resize: horizontal; height: 48px;"></textarea></div></div>'); row.appendTo(form); row.find('textarea').autosize({ append: "\n" }); } else if (colType === "richtext") { row = $('<div class="form-group"><label for="' + this.columns[i]["data"] + '" class="col-sm-3 control-label">' + this.columns[i]["title"] + ':</label><div class="col-sm-9"><textarea class="form-control" id="' + this.columns[i]["data"] + '" style="heght:150px;"></textarea></div></div>'); row.appendTo(form); var editor = new wangEditor(this.columns[i]["data"]); editor.config.uploadImgUrl = this.imgSaveUrl; editor.config.withCredentials = false; editor.create(); editorHash[this.columns[i]["data"]] = editor; } } $('<hr class="wide" />').appendTo(form); this.isFormInited = true; }
最後在公共的js裏將整個表單加入頁面上,這樣就完成了省去新增和編輯頁面的工做。頁面是省去了,可是數據如何保存,試想咱們通常的新增和編輯接口,其提交的數據結構大概是這樣:css
{
字段名1:值1,
字段名2:值2,
字段名3:值3
}
而從上文的數據結構中咱們能很容易的提取出須要提交的結構,因此咱們只須要一個提交的服務端地址便可,這即是省去新增、編輯頁面的整個思路。可是新增、編輯過程當中還會有不少細節性的問題,好比:html
{ "pageIndex":1, "pageSize":15, "sortConditions":[ { "sortField":"name", "listSortDirection":1 } ], "filterGroup":{ "rules":[ { "field":"name", "operate":"contains", "value":"a" } ] } }
排序很容易,難點在於篩選條件的處理,特別是結合EF的使用,如何將filterGroup轉化爲查詢的表達式,仍是看代碼吧:前端
using Abp.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace Abp.Application.Services.Query { /// <summary> /// 查詢表達式輔助操做類 /// </summary> public static class FilterHelper { #region 字段 private static readonly Dictionary<FilterOperate, Func<Expression, Expression, Expression>> ExpressionDict = new Dictionary<FilterOperate, Func<Expression, Expression, Expression>> { { FilterOperate.Equal, Expression.Equal }, { FilterOperate.NotEqual, Expression.NotEqual }, { FilterOperate.Less, Expression.LessThan }, { FilterOperate.Greater, Expression.GreaterThan }, { FilterOperate.LessOrEqual, Expression.LessThanOrEqual }, { FilterOperate.GreaterOrEqual, Expression.GreaterThanOrEqual }, { FilterOperate.StartsWith, (left, right) => { if (left.Type != typeof(string)) { throw new NotSupportedException("「StartsWith」比較方式只支持字符串類型的數據"); } return Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), right); } }, { FilterOperate.EndsWith, (left, right) => { if (left.Type != typeof(string)) { throw new NotSupportedException("「EndsWith」比較方式只支持字符串類型的數據"); } return Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), right); } }, { FilterOperate.Contains, (left, right) => { if (left.Type != typeof(string)) { throw new NotSupportedException("「Contains」比較方式只支持字符串類型的數據"); } return Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }), right); } } }; #endregion /// <summary> /// 獲取指定查詢條件組的查詢表達式 /// </summary> /// <typeparam name="T">表達式實體類型</typeparam> /// <param name="group">查詢條件組,若是爲null,則直接返回 true 表達式</param> public static Expression<Func<T, bool>> GetExpression<T>(FilterGroup group) { ParameterExpression param = Expression.Parameter(typeof(T), "m"); Expression body = GetExpressionBody(param, group); Expression<Func<T, bool>> expression = Expression.Lambda<Func<T, bool>>(body, param); return expression; } /// <summary> /// 獲取指定查詢條件的查詢表達式 /// </summary> /// <typeparam name="T">表達式實體類型</typeparam> /// <param name="rule">查詢條件,若是爲null,則直接返回 true 表達式</param> /// <returns></returns> public static Expression<Func<T, bool>> GetExpression<T>(FilterRule rule = null) { ParameterExpression param = Expression.Parameter(typeof(T), "m"); Expression body = GetExpressionBody(param, rule); Expression<Func<T, bool>> expression = Expression.Lambda<Func<T, bool>>(body, param); return expression; } /// <summary> /// 把查詢操做的枚舉表示轉換爲操做碼 /// </summary> /// <param name="operate">查詢操做的枚舉表示</param> public static string ToOperateCode(this FilterOperate operate) { Type type = operate.GetType(); MemberInfo[] members = type.GetMember(operate.To<string>()); if (members.Length > 0) { OperateCodeAttribute attribute = members[0].GetAttribute<OperateCodeAttribute>(); return attribute == null ? null : attribute.Code; } return null; } /// <summary> /// 獲取操做碼的查詢操做枚舉表示 /// </summary> /// <param name="code">操做碼</param> /// <returns></returns> public static FilterOperate GetFilterOperate(string code) { Type type = typeof(FilterOperate); MemberInfo[] members = type.GetMembers(BindingFlags.Public | BindingFlags.Static); foreach (MemberInfo member in members) { FilterOperate operate = member.Name.To<FilterOperate>(); if (operate.ToOperateCode() == code) { return operate; } } throw new NotSupportedException("獲取操做碼的查詢操做枚舉表示時不支持代碼:" + code); } #region 私有方法 private static Expression GetExpressionBody(ParameterExpression param, FilterGroup group) { //若是無條件或條件爲空,直接返回 true表達式 if (group == null || (group.Rules.Count == 0 && group.Groups.Count == 0)) { return Expression.Constant(true); } List<Expression> bodys = new List<Expression>(); bodys.AddRange(group.Rules.Select(rule => GetExpressionBody(param, rule))); bodys.AddRange(group.Groups.Select(subGroup => GetExpressionBody(param, subGroup))); if (group.Operate == FilterOperate.And) { return bodys.Aggregate(Expression.AndAlso); } if (group.Operate == FilterOperate.Or) { return bodys.Aggregate(Expression.OrElse); } throw new Exception("查詢參數序列化失敗"); } private static Expression GetExpressionBody(ParameterExpression param, FilterRule rule) { if (rule == null || rule.Value == null || string.IsNullOrEmpty(rule.Value.ToString())) { return Expression.Constant(true); } LambdaExpression expression = GetPropertyLambdaExpression(param, rule); Expression constant = ChangeTypeToExpression(rule, expression.Body.Type); return ExpressionDict[rule.Operate](expression.Body, constant); } private static LambdaExpression GetPropertyLambdaExpression(ParameterExpression param, FilterRule rule) { string[] propertyNames = rule.Field.Split('.'); Expression propertyAccess = param; Type type = param.Type; foreach (string propertyName in propertyNames) { PropertyInfo property = type.GetProperty(propertyName); if (property == null) { throw new InvalidOperationException(string.Format("指定屬性{0}在類型{1}中不存在.", rule.Field, type.FullName)); } type = property.PropertyType; propertyAccess = Expression.MakeMemberAccess(propertyAccess, property); } return Expression.Lambda(propertyAccess, param); } private static Expression ChangeTypeToExpression(FilterRule rule, Type conversionType) { Type elementType = conversionType.GetUnNullableType(); object value = rule.Value is string ? rule.Value.ToString().To(conversionType) : Convert.ChangeType(rule.Value, elementType); return Expression.Constant(value, conversionType); } #endregion } }
using Abp.Application.Services.Dto; using Abp.Domain.Entities; using System; using System.Linq; using System.Linq.Expressions; using Abp.Extensions; using System.Collections.Generic; using System.ComponentModel; namespace Abp.Application.Services.Query { /// <summary> /// 集合擴展輔助操做類 /// </summary> public static class CollectionExtensions { /// <summary> /// 從指定<see cref="IQueryable{T}"/>集合中查詢指定數據篩選的分頁信息 /// </summary> /// <typeparam name="TEntity">實體類型</typeparam> /// <typeparam name="TResult">分頁數據類型</typeparam> /// <param name="source">要查詢的數據集</param> /// <param name="predicate">查詢條件謂語表達式</param> /// <param name="pageCondition">分頁查詢條件</param> /// <param name="selector">數據篩選表達式</param> /// <returns>分頁結果信息</returns> public static PagedResultDto<TResult> ToPage<TEntity, TResult>(this IQueryable<TEntity> source, Expression<Func<TEntity, bool>> predicate, PageCondition pageCondition, Expression<Func<TEntity, TResult>> selector) { return source.ToPage(predicate, pageCondition.PageIndex, pageCondition.PageSize, pageCondition.SortConditions, selector); } /// <summary> /// 從指定<see cref="IQueryable{T}"/>集合中查詢指定數據篩選的分頁信息 /// </summary> /// <typeparam name="TEntity">實體類型</typeparam> /// <typeparam name="TResult">分頁數據類型</typeparam> /// <param name="source">要查詢的數據集</param> /// <param name="predicate">查詢條件謂語表達式</param> /// <param name="pageIndex">分頁索引</param> /// <param name="pageSize">分頁大小</param> /// <param name="sortConditions">排序條件集合</param> /// <param name="selector">數據篩選表達式</param> /// <returns>分頁結果信息</returns> public static PagedResultDto<TResult> ToPage<TEntity, TResult>(this IQueryable<TEntity> source, Expression<Func<TEntity, bool>> predicate, int pageIndex, int pageSize, SortCondition[] sortConditions, Expression<Func<TEntity, TResult>> selector) { int total; TResult[] data = source.Where(predicate, pageIndex, pageSize, out total, sortConditions).Select(selector).ToArray(); return new PagedResultDto<TResult>() { TotalCount = total, Items = data }; } /// <summary> /// 從指定<see cref="IQueryable{T}"/>集合中查詢指定分頁條件的子數據集 /// </summary> /// <typeparam name="TEntity">實體類型</typeparam> /// <param name="source">要查詢的數據集</param> /// <param name="predicate">查詢條件謂語表達式</param> /// <param name="pageCondition">分頁查詢條件</param> /// <param name="total">輸出符合條件的總記錄數</param> /// <returns></returns> public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, bool>> predicate, PageCondition pageCondition, out int total) { return source.Where(predicate, pageCondition.PageIndex, pageCondition.PageSize, out total, pageCondition.SortConditions); } /// <summary> /// 從指定<see cref="IQueryable{T}"/>集合中查詢指定分頁條件的子數據集 /// </summary> /// <typeparam name="TEntity">動態實體類型</typeparam> /// <param name="source">要查詢的數據集</param> /// <param name="predicate">查詢條件謂語表達式</param> /// <param name="pageIndex">分頁索引</param> /// <param name="pageSize">分頁大小</param> /// <param name="total">輸出符合條件的總記錄數</param> /// <param name="sortConditions">排序條件集合</param> /// <returns></returns> public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, bool>> predicate, int pageIndex, int pageSize, out int total, ICollection<SortCondition> sortConditions = null) { if (!typeof(TEntity).IsEntityType()) { string message = $"類型「{typeof(TEntity).FullName}」不是實體類型"; throw new InvalidOperationException(message); } total = source.Count(predicate); source = source.Where(predicate); if (sortConditions == null || sortConditions.Count == 0) { source = source.OrderBy("Id", ListSortDirection.Descending); } else { int count = 0; IOrderedQueryable<TEntity> orderSource = null; foreach (SortCondition sortCondition in sortConditions) { var sortField = sortCondition.SortField.ToPascalCase(); orderSource = count == 0 ? CollectionPropertySorter<TEntity>.OrderBy(source, sortField, sortCondition.ListSortDirection) : CollectionPropertySorter<TEntity>.ThenBy(orderSource, sortField, sortCondition.ListSortDirection); count++; } source = orderSource.ThenBy("Id", ListSortDirection.Descending); } return source?.Skip((pageIndex - 1) * pageSize).Take(pageSize) ?? Enumerable.Empty<TEntity>().AsQueryable(); } /// <summary> /// 從指定<see cref="IQueryable{T}"/>集合中查詢指定分頁條件的子數據集 /// </summary> /// <typeparam name="TEntity">動態實體類型</typeparam> /// <param name="source">要查詢的數據集</param> /// <param name="input">查詢條件</param> /// <param name="total">輸出符合條件的總記錄數</param> /// <returns></returns> public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> source, QueryListPagedRequestInput input, out int total) { Expression<Func<TEntity, bool>> predicate = p => true; if (input.FilterGroup != null) { foreach (var item in input.FilterGroup.Rules) { item.Field = item.Field.ToPascalCase(); //TODO:處理FilterGroup.Groups } predicate = FilterHelper.GetExpression<TEntity>(input.FilterGroup); } return source.Where<TEntity>(predicate, input.PageIndex, input.PageSize, out total, input.SortConditions); } } }
最後咱們爲IQueryable添加了擴展方法供業務層使用,因而乎,幾乎全部業務層表格的查詢代碼都能統一爲:java
/// <inheritdoc/> public async Task<PagedResultDto<GetActivityListOutput>> GetActivityPagedList(QueryListPagedRequestInput input) { int total; var list = await _activityRepository.GetAll().Where(input, out total).ToListAsync(); return new PagedResultDto<GetActivityListOutput>(total, list.MapTo<List<GetActivityListOutput>>()); }
簡潔卻又功能強大,減小了工做量的同時也規範了編碼。對於多表聯合查詢,只須要組裝須要的IQueryable後同樣可使用該方法。如此,咱們服務端的查詢幾乎能夠說是零代碼了,那麼前端呢?如何省去相關代碼,排序依然很容易,只是在表頭上添加事件便可,而篩選,動態添加dom結構並綁定事件難度也不高,咱們同樣在公共js中能夠完成相關工做,實現具體頁面中零代碼。源碼都在這裏,有興趣的能夠看看:jquery
@{ Layout = "~/Areas/Admin/Views/Shared/_AdminLayout.cshtml"; } @section header{ <link href="~/Content/css/dataTables.bootstrap.css" rel="stylesheet" type="text/css" /> <link href="~/Content/css/bootstrap-datetimepicker.min.css" rel="stylesheet" type="text/css" /> <link href="~/Content/js/plugs/wangEditor-2.1.23/dist/css/wangEditor.min.css" rel="stylesheet" type="text/css" /> <link href="~/Content/js/plugs/webuploader/webuploader.css" rel="stylesheet" type="text/css" /> <style> .query-input { height: 32px; line-height: 32px; width: 30%; vertical-align: middle; } .form-inline .radio input[type=radio], .form-inline .checkbox input[type=checkbox] { position: absolute; } </style> } @section footer{ <script src="~/Content/js/plugs/select2/select2.js" type="text/javascript"></script> <script src="~/Content/js/plugs/datetime/bootstrap-datetimepicker.min.js" type="text/javascript"></script> <script src="~/Content/js/plugs/datetime/bootstrap-datetimepicker.zh-CN.js" type="text/javascript"></script> <script src="~/Content/js/plugs/webuploader/webuploader.js" type="text/javascript"></script> <script src="~/Content/js/plugs/wangEditor-2.1.23/dist/js/wangEditor.js" type="text/javascript"></script> <script src="~/Content/js/plugs/textarea/jquery.autosize.js" type="text/javascript"></script> <script src="~/Content/js/zooming.js" type="text/javascript"></script> <script src="~/Content/js/bode/bode.grid.js" type="text/javascript"></script> <script type="text/javascript"> var datatable; var tableOption = { url: {}, columns: [], permission: {}, pageSize: 15, actions: [], formWidth: "40%", isBatch: false, extraFilters: [], imgSaveUrl: $.bode.config.imgSaveUrl, loadDataComplete: function (data) { } }; var startfunction = function () { }; var endfunction = function () { }; </script> @RenderSection("customScript", true) <script type="text/javascript"> $(function () { startfunction(); //初始化數據 datatable = new $.bode.grid("#dataTable", tableOption); endfunction(); }); </script> } @RenderSection("headHtml", false) <div class="page-container"> <div class="page-body" style="padding:0;"> <div class="row"> <div class="col-xs-12"> <div class="widget flat radius-bordered"> @*<div class="widget-header bg-info"> <span class="widget-caption"><strong>@ViewBag.Title</strong></span> </div>*@ <div class="widget-body"> <div role="grid" id="editabledatatable_wrapper" class="dataTables_wrapper form-inline no-footer"> <div class="row" style="padding-bottom: 10px;"> <div class="col-sm-4"> <select style="width: 25%"></select> <select style="width: 25%"></select> <input type="text" class="query-input"> <a class="btn btn-info btn-sm icon-only query-add" href="javascript:void(0);"><i class="fa fa-plus-square-o"></i></a> </div> <div class="col-sm-8"> <div class="form-group" style="float: right" id="actionArea"></div> </div> </div> <table class="table table-bordered table-hover table-striped dataTable no-footer" id="dataTable" aria-describedby="editabledatatable_info"> <thead> <tr></tr> </thead> <tbody></tbody> </table> </div> </div> </div> </div> </div> </div> </div> @RenderBody() @RenderSection("footHtml", false)
最後須要寫的頁面代碼就只剩下這麼點了: git
@{ ViewBag.Title = "Table"; Layout = "~/Areas/Admin/Views/Shared/_GridLayout.cshtml"; } @section customScript{ <script type="text/javascript"> var enums = @Html.Raw(Json.Encode(@ViewBag.Enums)); tableOption.url = { read: "/api/services/activity/activities/GetPagedList", add: "/api/services/activity/activities/Create", edit: "/api/services/activity/activities/Update", delete: "/api/services/activity/activities/Delete" }; tableOption.columns = [ { data: "id", title: "編號",type:"hide" }, { data: "name", title: "類別名稱", type: "text", query: true, editor: {},display:{} }, { data: "isStatic", title: "是否靜態的類別", type: "switch", query: true, editor: {} }, { data: "order", title: "排序號", type: "number", query: true, editor: {} }, { data: "creationTime", title: "建立時間", type: "datepicker", editor: {} }, { title: "操做選項", type: "command", actions: [ { name: "操做", icon: "fa-trash-o", onClick: function (d) { alert(d["id"]); } } ] } ]; </script> }
而且徹底實現了分頁、查詢、排序、新增、編輯、刪除等基礎功能,而且提供了自定義功能按鈕的支持。github
其餘功能web
通過一年多的使用與不斷的調整,我在表格的基礎上新增了不少的功能特性,包括:express
一、結合樹的使用,樹表操做和純表格相差無幾,一樣是極簡的思路。
二、結合abp的權限實現表格按鈕級的權限控制,沒有權限的按鈕不顯示。
三、支持非表格之外的字段篩選,而且保存時自動提交該字段。
因爲功能介紹不是本篇博客的重點,就暫不展開介紹。本文提供了節省增刪查改編碼量的思路與部分示例代碼,歡迎交流,也推薦你們本身動手嘗試。