MVC前端分页插件shenniu.pager.js使用指南与源码解析

作者:桌面漫游时间:2026-10-22 00:10:04浏览:93

本文详细介绍MVC架构下使用开源JS分页插件shenniu.pager.js的方法。文章对比了跳转与AJAX两种分页方式的优劣,展示了基于Bootstrap和jQuery的集成示例。内容涵盖后台接口的参数获取、MoPageResult类的使用、时间格式化处理,并深入解析了插件源码中的默认配置合并、AJAX请求及DOM渲染逻辑,帮助开发者快速实现无刷新分页功能。

引言与项目背景

近期工作重心在于公司手机端接口项目的开发与调试,目前项目已接近上线阶段。在繁忙之余,我完成了两项个人项目:一是基于aspnetcore 1.0.0、Redis、PostgreSQL及TaskMainForm服务构建的个人线上项目(目前仅支持IP访问,暂无域名),该项目后续计划开源;二是正在开发的后台管理框架“一叶子”,其中包含一个自研的JS分页插件shenniu.pager.js。本文将重点分享该插件的使用方法与源码解析,欢迎扫码支持。

  • 为什么采用js分页及效果图
  • 在view中如何使用及分享个后台方法
  • 开发者视角阅读shenniu.pager.js代码

下面一步一个脚印的来分享:

MVC分页方式对比与效果展示

为什么采用js分页及效果图

MVC开发中常见的分页方式主要有两种:跳转分页和AJAX分页。跳转分页通过重定向页面并传递分页参数,获取数据后通过Model绑定展示,但每次操作都会刷新页面,用户体验较差。AJAX分页则通过异步请求接口获取数据并更新界面,无需刷新页面,从而提供了更流畅的用户体验。

下面来看下这次分享的js分页插件效果图:

图一:

引言与项目背景

图二:

引言与项目背景

图三:

引言与项目背景

静态图片难以完全展示交互效果,后续计划补充GIF动图。以下将重点介绍核心代码实现。

插件集成与后台接口实现

在view中如何使用及分享个后台方法

为了提升页面美观度,本示例使用了Bootstrap结合ACE样式框架,效果如前文图片所示(此处省略样式和js文件链接)。由于该插件基于jQuery开发,需引用jquery.js。此外,示例中使用了基于HTML5设计的日期选择器,因此还需引入bootstrap-datepicker.min.js及其样式文件bootstrap-datepicker3.min.css。所有引用文件配置完毕后,截图如下:

引言与项目背景

接下来演示如何使用shenniu.pager.js。当用户点击“查询”按钮时,调用插件并获取后台接口返回的数据,最终绑定到页面展示。具体代码如下:


var snTool = new shenniuTool();
 $("button[id='btnSearch']").on("click", function () {
 var data = {
 txtName: $("input[name='txtName']").val(),
 nStatus: $("select option:selected").val(),
 dOperateTime: $("input[name='dOperateTime']").val()
 };
 snTool.listFun({
 showId: "divShowResult", //内容显示的div的Id
 url: "/Menu/Search",
 data: data,
 pageSize: 2, //每页N条
 headText: [
 { nickName: "全选", name: "Id", colType: "checkbox" },
 { nickName: "名称", name: "Name", colType: "label", isModalHeadText: true },
 { nickName: "链接", name: "Link" },
 { nickName: "状态", name: "EnableDes" },
 { nickName: "操作人", name: "OperatorDes" },
 { nickName: "操作时间", name: "OperateTime", format: "yyyy-MM-dd" },
 { nickName: "操作", name: "Id", colType: "operate" }
 ],
 editeOption: {
 url: "/Menu/Edit",
 title: "编辑",
 height: 500
 },
 viewOption: {
 url: "/Menu/Details",
 title: "查看",
 height: 500
 },
 delOption: {
 url: "/Menu/Delete",
 title: "删除",
 height: 500
 },
 modalExt: modalExt
 });
 });

注意参数url: "/Menu/Search",该地址指向后台接口。接下来查看后台实现代码:


[HttpGet]
 public JsonResult Search()
 {
 var moPageResult = new StageModel.MoPageResult<dynamic>();
 try
 {
 var txtName = Request.Params["txtName"];
 var nStatus = string.IsNullOrWhiteSpace(Request.Params["nStatus"]) ? -1 : Convert.ToInt32(Request.Params["nStatus"]);
 var dOperateTime = string.IsNullOrWhiteSpace(Request.Params["dOperateTime"]) ? Convert.ToDateTime("1991-01-01") : Convert.ToDateTime(Request.Params["dOperateTime"]);
 var data = db.MoMenus.AsQueryable();
 if (!string.IsNullOrWhiteSpace(txtName))
 {
 data = data.Where(b => b.Name.Contains(txtName));
 }
 if (nStatus >= 0)
 {
 data = data.Where(b => b.IsEnable == (nStatus == (int)StageEnumHelper.ComStatus.启用));
 }
 if (dOperateTime > Convert.ToDateTime("1991-01-01"))
 {
 data = data.Where(b => b.OperateTime >= dOperateTime && b.OperateTime < dOperateTime.AddDays(1));
 }
 moPageResult.MoPageContent(
 data,
 b => b.OperateTime,
 b => new
 {
 Id = b.Id,
 Name = b.Name,
 Link = b.Link,
 Des = b.Des,
 IsEnable = b.IsEnable,
 Operator = b.Operator,

 OperatorDes = b.MoUserInfo.NickName,
 EnableDes = b.IsEnable ? "启用" : "禁用",
 OperateTime = b.OperateTime
 });
 }
 catch (Exception ex)
 {
 }
 return Json(moPageResult, JsonRequestBehavior.AllowGet);
 }

后台接口通过Request.Params获取的参数分别对应视图中的名称、状态和操作时间等查询条件。


var data = {
 txtName: $("input[name='txtName']").val(),
 nStatus: $("select option:selected").val(),
 dOperateTime: $("input[name='dOperateTime']").val()
 }; 

后台代码中并未直接处理当前页数和分页记录数,而是封装在MoPageResult类的MoPageContent()方法中。MoPageResult类代码如下:


#region 分页数据返回
 public class MoPageResult<TResult> where TResult : class, new()
 {
 public MoPageResult()
 {
 }
 public IQueryable<TResult> MoResult;
 /// <summary>
 /// 总页数
 /// </summary>
 public int PageTotal { get; set; }
 /// <summary>
 /// 当前页数
 /// </summary>
 public int CurrentPage { get; set; }
 /// <summary>
 /// 分页记录数
 /// </summary>
 public int PageSize { get; set; }
 /// <summary>
 /// 分页方法
 /// </summary>
 /// <typeparam name="TKey"></typeparam>
 /// <param name="query"></param>
 /// <param name="order_desc"></param>
 public void MoPageContent<T, TKey>(IQueryable<T> query, Expression<Func<T, TKey>> desc, Expression<Func<T, TResult>> selector = null, bool isDesc = true) where T : class,new()
 {
 if (HttpContext.Current == null) { return; }
 var Request = HttpContext.Current.Request;
 this.PageSize = string.IsNullOrWhiteSpace(Request.Params["pageSize"]) ? 15 : Convert.ToInt32(Request.Params["pageSize"]);
 this.CurrentPage = string.IsNullOrWhiteSpace(Request.Params["currentPage"]) ? 1 : Convert.ToInt32(Request.Params["currentPage"]);
 var nTotal = query.Count();
 this.PageTotal = nTotal / this.PageSize + (nTotal % this.PageSize > 0 ? 1 : 0);
 if (selector != null)
 {
 if (isDesc)
 {
 query = query.OrderByDescending(desc);
 }
 else
 {
 query = query.OrderBy(desc);
 }
 this.MoResult = query.
 Skip((this.CurrentPage - 1) * this.PageSize).
 Take(this.PageSize).
 Select(selector);
 }
 }
 }
#endregion

MoPageContent()方法默认获取pagesize和currentpage参数,简化了用户操作,并负责计算总页数及执行分页查询。注意最后的查询语句Select(selector),其中selector类型为Expression<Func<T, TResult>>,且T需满足where T : class,new()约束。在调用该分页类时,我传递的T为dynamic类型,因为我觉得使用匿名类更为便捷。唯一的不足是,目前select输出暂时无法直接对某个属性直接使用方法。

最后,插件使用还需注意时间处理。如果后台未对DateTime类型进行特殊处理直接进行JSON格式化,插件获取的时间格式可能如下:

引言与项目背景

此时,在使用shenniu.pager.js插件时,需在属性headText中指定时间列的格式,例如:

{ nickName: "操作时间", name: "OperateTime", format: "yyyy-MM-dd" }

使用format属性格式化时间格式,该插件支持的格式包括:yyyy,MM,dd,HH,mm,ss,应能满足大部分需求。

shenniu.pager.js源码深度解析

开发者视角阅读shenniu.pager.js代码

首先从上而下查看插件属性定义:


var defOption = {
 showId: "divShowResult", //内容显示的div
 url: "", //ajax数据来源地址
 headText: [
 { nickName: "A", colType: "checkbox", name: "Id" },
 { nickName: "B", colType: "label", name: "Name", isModalHeadText: true } //isModalHeadText:是否是模式窗体头部显示的信息
 ],
 data: {}, //查询条件
 editeOption: {
 url: "",
 title: "编辑",
 width: 500,
 height: 500
 }, //编辑地址,不包括id
 viewOption: {
 url: "",
 title: "查看",
 width: 500,
 height: 500
 }, //查看地址
 delOption: {
 url: "",
 title: "删除",
 width: 500,
 height: 500
 }, //删除地址
 currentPage: 1, //当前页数
 pageSize: 15, //分页记录数
 showPageTab: 6, //展示6个页数
 modalExt: null, //模式窗体对象
 //可忽略
 callback: function () { }, //回调函数
 tabId: "tab001",
 loading: "努力加载中,等会吧...", //可以直接写出<img src=''/>
 sucFun: function (data) { },
 befFun: function () { },
 errFun: function () { },
 comFun: function () { },
 timeout: 60000 //超时60S
 };
$.extend(defOption, option);

属性中已包含注释说明,易于理解。代码$.extend(defOption, option);的作用是将用户传递的参数与插件默认参数合并,若用户提供了相同属性,则用户参数覆盖默认值。

接下来查看请求后台的方法:


//请求后台
 function ajaxFun(option) {
 if (option) {
 $.extend(defOption, option);
 }
 //获取分页参数
 var hidPageSize = defOption.pageSize;
 var hidCurrentPage = defOption.currentPage;
 if ($("form input[name='pageSize']").val()) {
 hidPageSize = $("form input[name='pageSize']").val();
 }
 if ($("form input[name='currentPage']").val()) {
 hidCurrentPage = $("form input[name='currentPage']").val();
 }
 //合并用户查询条件和分页参数条件
 var searchData = {
 pageSize: hidPageSize,
 currentPage: hidCurrentPage
 };
 $.extend(searchData, defOption.data);
 //请求后台数据
 $.ajax({
 url: defOption.url,
 type: "get",
 data: searchData,
 dataType: "json",
 timeout: defOption.timeout,
 async: true,
 beforeSend: defOption.befFun,
 success: defOption.sucFun,
 });
 }

该方法用于请求接口获取数据,默认从页面中获取pageSize和currentPage两个分页参数。此处采用GET方式请求,若后台支持POST,也可改为POST方式。

再看查询列表方法:


//查询列表
 listFun: function (option) {
 if (option) {
 $.extend(defOption, option);
 }
 //默认格式
 var tab = [];
 tab.push('<table id="' + defOption.tabId + '" class="table table-bordered table-hover">');
 tab.push('<thead><tr role="row">');
 for (var i in defOption.headText) {
 var head = defOption.headText[i];
 if (head.colType == "label") {
 tab.push('<th class="center" tabindex="0" rowspan="1" colspan="1">' + head.nickName + '</th>');
 } else if (head.colType == "checkbox") {
 tab.push('<th class="center " rowspan="1" colspan="1" aria-label="">');
 tab.push(' <label class="pos-rel">');
 tab.push(' <input type="checkbox" name="cbAll" class="ace">');
 tab.push(' <span class="lbl">' + head.nickName + '</span>');
 tab.push(' </label>');
 tab.push('</th>');
 } else {
 tab.push('<th class="center" tabindex="0" rowspan="1" colspan="1">' + head.nickName + '</th>');
 }
 }
 tab.push('</tr></thead>');
 tab.push('<tbody><tr><td class="text-center" colspan="' + defOption.headText.length + '">' + defOption.loading + '</td></tr></tbody>');
 tab.push('</table>');
 tab.push('<div id="divPager" class="text-center"></div>');
 $("#" + defOption.showId).html(tab.join(''));
 //全选事件
 $("input[type='checkbox'][name='cbAll']").on("click", function () {
 var cbStatus = $(this).is(":checked");
 if (cbStatus) {
 $("input[name='cb']:checkbox").prop("checked", true);
 } else {
 $("input[name='cb']:checkbox").prop("checked", false);
 }
 });
 //数据返回成功处理
 defOption.sucFun = function (data) {
 var head = $("table[id='" + defOption.tabId + "'] tbody");
 if (data) {
 if (data.MoResult) {
 //遍历table展示的数据
 var rows = [];
 $.each(data.MoResult, function (i, item) {
 rows.push('<tr>');
 var modalHeadText = "";
 for (var h_i in defOption.headText) {
 var head = defOption.headText[h_i];
 var item_val = item[head.name];
 if (item_val && typeof (item_val) != "undefined") { } else { item_val = ""; }
 //时间格式化
 if (head.format && item_val.length > 0) {
 console.log(item_val);
 var date = new Date(parseInt(item_val.replace("/Date(", "").replace(")/", ""), 10));
 item_val = head.format.
 replace("yyyy", date.getFullYear()).
 replace("MM", date.getMonth() + 1).
 replace("dd", date.getDate()).
 replace("HH", date.getHours()).
 replace("mm", date.getMinutes()).
 replace("ss", date.getMilliseconds());
 }
 //获取模式窗体头部信息
 if (modalHeadText.length <= 0) { modalHeadText = head.isModalHeadText ? item_val : "" };
 if (head.colType == "label") {
 rows.push('<td class="center">' + item_val + '</td>');
 } else if (head.colType == "checkbox") {
 rows.push('<td class="center">');
 rows.push(' <label class="pos-rel">');
 rows.push(' <input type="checkbox" name="cb" value="' + item_val + '" class="ace">');
 rows.push(' <span class="lbl"></span>');
 rows.push(' </label>');
 rows.push('</td>');
 } else if (head.colType == "operate") {
 rows.push('<td class="center"><div class="hidden-sm hidden-xs action-buttons">');
 if (defOption.editeOption.url.length > 0) {
 var editOption = $.extend({}, defOption.editeOption);
 editOption.url += "/" + item_val;
 editOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
 var op = JSON.stringify(editOption);
 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-pencil bigger-130"></i></a>');
 }
 if (defOption.viewOption.url.length > 0) {
 var viewOption = $.extend({}, defOption.viewOption);
 viewOption.url += "/" + item_val;
 viewOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
 var op = JSON.stringify(viewOption);
 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-search-plus bigger-130"></i></a>');
 }
 if (defOption.delOption.url.length > 0) {
 var delOption = $.extend({}, defOption.delOption);
 delOption.url += "/" + item_val;
 delOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
 var op = JSON.stringify(delOption);
 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-trash-o bigger-130"></i></a>');
 }
 rows.push('</div></td>');
 }
 else {
 rows.push('<td class="center">' + item_val + '</td>');
 }
 }
 rows.push('</tr>');
 });
 //页数展示
 if (data.MoResult.length > 0) {
 var pager = [];
 pager.push('<div class="text-center">');
 pager.push(' <ul class="pagination" style="margin-top:0px">');
 var nPager = defOption.showPageTab;//每次展示6个分页
 //上一页
 var nprev = (data.CurrentPage - 1 >= 1 ? data.CurrentPage - 1 : 1);
 pager.push(' <li class="paginate_button previous" aria-controls="dynamic-table" tabindex="0" id="dynamic-table_previous">');
 pager.push(' <a href="javascript:;" name="npager" data-page="' + nprev + '">上一页</a>');
 pager.push(' </li>');
 //当前页之前页码
 var preTotal = data.CurrentPage - nPager >= 1 ? data.CurrentPage - nPager : 1;
 for (var i = preTotal; i < data.CurrentPage ; i++) {
 pager.push(' <li class="paginate_button ' + (i == data.CurrentPage ? "active disabled" : "") + '" aria-controls="dynamic-table">');
 pager.push(' <a href="javascript:;" name="npager" data-page="' + i + '">' + i + '</a>');
 pager.push(' </li>');
 }
 //当前页
 pager.push(' <li class="paginate_button active disabled" aria-controls="dynamic-table">');
 pager.push(' <a href="javascript:;" name="npager" data-page="' + data.CurrentPage + '">' + data.CurrentPage + '</a>');
 pager.push(' </li>');
 //当前页以后页码
 var nextTotal = data.CurrentPage + nPager > data.PageTotal ? data.PageTotal : data.CurrentPage + nPager;
 for (var i = data.CurrentPage + 1; i <= nextTotal; i++) {
 pager.push(' <li class="paginate_button ' + (i == data.CurrentPage ? "active disabled" : "") + '" aria-controls="dynamic-table">');
 pager.push(' <a href="javascript:;" name="npager" data-page="' + i + '">' + i + '</a>');
 pager.push(' </li>');
 }
 //下一页
 var nnext = (data.PageTotal < data.CurrentPage + 1 ? data.PageTotal : data.CurrentPage + 1);
 pager.push(' <li class="paginate_button next" aria-controls="dynamic-table" tabindex="0" id="dynamic-table_next">');
 pager.push(' <a href="javascript:;" name="npager" data-page="' + nnext + '">下一页</a>');
 pager.push(' </li>');
 pager.push(' </ul>');
 //分页查询条件
 pager.push('<div style="display:none">');
 pager.push(' <form>');
 pager.push(' <input type="hidden" name="pageSize" value="' + defOption.pageSize + '"/>');
 pager.push(' <input type="hidden" name="currentPage" value="' + defOption.currentPage + '"/>');
 pager.push(' </form>');
 pager.push('</div>');
 pager.push('</div>');
 //移除加载中
 //head.html("");
 //添加结果
 head.html(rows.join(''));
 $("div[id='divPager']").html(pager.join(''));
 //操作按钮事件
 $("a[data-item]").on("click", function () {
 var data_Item = $(this).attr("data-item");
 if (data_Item) {
 var data_Item_Obj = JSON.parse(data_Item);
 defOption.modalExt.modalFun({
 width: data_Item_Obj.width,
 height: data_Item_Obj.height,
 url: data_Item_Obj.url,
 title: data_Item_Obj.title,
 callback: defOption.callback
 });
 }
 });
 //绑定分页按钮事件
 $("a[name='npager']").on("click", function () {
 var nPager = $(this).attr("data-page");
 if (nPager.length <= 0) { return; }
 $("form input[name='currentPage']").val(nPager);
 //执行请求后台
 ajaxFun(defOption);
 });
 } else {
 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据!</td></tr>");
 $("div[id='divPager']").html("");
 }
 } else {
 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据。</td></tr>");
 $("div[id='divPager']").html("");
 }
 } else {
 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据</td></tr>");
 $("div[id='divPager']").html("");
 }
 };
 if (option) {
 $.extend(defOption, option);
 }
 //执行请求后台
 ajaxFun(defOption);
 }

该方法体较长,主要操作包括:

默认格式展示列表头部并呈现加载提示、绑定复选框全选事件、创建数据返回成功函数sucFun()、调用请求后台方法ajaxFun()。

再来看函数sucFun(),该函数在数据返回后执行以下操作:

遍历JSON返回数据并展示到table中(包括时间格式化、复选框、label及操作按钮类型operate的初始化);展示页数及绑定事件(目前仅支持上一页、当前页之前页码、当前页、当前页以后页码、下一页的效果展示,生成分页查询条件(生成pagesize和currentPage隐藏控件),并绑定分页按钮事件)。

以上即为shenniu.pager.js插件的完整内容,代码不多且逻辑清晰。示例中用到的js文件和css文件可在以下地址下载:http://xiazai.jb51.net/201612/yuanma/shenniu.pager_jb51.rar

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!

作者最新文章