尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ABP框架集成BootstrapTable:列表页分页排序搜索实战
ABP入门系列写到第14篇终于要碰点前端“看得见摸得着”的东西了。这一篇的主题是应用BootstrapTable表格插件把ABP后端已经写好的应用服务方法直接接到前端表格上让列表页从“一条条手写DOM”变成“配置驱动”。如果你正在维护老版ABPASP.NET Boilerplate的MVC模板项目或者公司里还有不少基于jQueryBootstrap搭建的中后台页面这篇内容大概率能直接“抄作业”。之前十几篇聊了很多后端的事应用服务、DTO、权限校验、自动API、工作单元、审计日志可列表页这个后台系统里最高频的页面一直缺一个像样的前端表格方案。ABP默认模板并没有内置表格组件Angular和Blazor版本另当别论但MVCjQuery模板下我们最常做的事就是自己拼接HTML、自己写分页、自己维护排序状态代码越堆越多痛点也越攒越深。后来我把BootstrapTable接进去整个列表页的开发模式瞬间变了个样。1. 表格选型的现实为什么老ABP项目里BootstrapTable依然能打1.1 这个系列走到现在前端到底缺了什么前面十几篇把ABP的“后端心法”过了一遍但后台管理系统里真正每天都在用的其实是那个最朴素的列表页查询、翻页、排序、编辑、删除。ABP的老版MVC模板自带jQuery和Bootstrap却一直没有提供表格组件等于把“最后一公里”留给开发者自己解决。没有表格插件之前我们常用的写法是后端返回一个List前端用JavaScript拼tr分页控件自己写一套排序字段自己存状态搜索条件手动拼到查询字符串里。如果只有一个页面这么干还能忍但后台系统动辄几十个列表页每一页都要重复这些活维护成本会直线上升。更要命的是每个开发者拼出来的表格风格还不一样这个页面点页码刷新那个页面点页码弹新窗口样式和交互完全没法统一。1.2 为什么是BootstrapTable而不是DataTables或LayUI当时也对比过几个方案。DataTables功能确实更强但对Bootstrap风格的适配没有BootstrapTable自然默认外观一眼看上去就是“别人家的表格”LayUI是另一套生态和ABP默认模板的Bootstrap样式混在一起视觉割裂感比较明显完全手写表格则等于重新发明轮子分页、排序、搜索这些通用能力都要自己实现。综合考虑在老ABPjQuery模板这个前提下BootstrapTable是改动成本最低、见效最快的选择。它本身就是一个jQuery插件配置项很集中服务端分页、客户端分页、排序、搜索、列模板、事件都覆盖到了文档也足够丰富。更重要的是它和Bootstrap是同一套视觉语言放进ABP默认模板几乎不需要额外调整样式。需要说明一下这篇内容的背景是老版ABP的MVC模板也就是传统服务端渲染加jQuery那一条技术路线。ABP Framework新版的Razor Class Library项目也可以参考这里的思路只是资源引用和命名空间细节略有差异。2. 动手前的对齐应用服务返回结构、DTO与页面引用了哪些“约定”2.1 先把后端方法补成“可被表格调用”的形态表格要的数据通常是一个分页列表ABP对这类场景早就有标准契约。输入侧用PagedAndSortedResultRequestDto输出侧用PagedResultDtoT。如果项目里用的还是AsyncCrudAppService会发现它已经默认实现了分页返回但搜索条件需要我们重写CreateFilteredQuery来补。拿一个很简单的人员管理页面举例输入DTO这样声明public class GetPersonListInput : PagedAndSortedResultRequestDto { public string Filter { get; set; } }应用服务继承AsyncCrudAppService然后重写过滤逻辑[AbpAuthorize] public class PersonAppService : AsyncCrudAppServicePerson, PersonDto, int, GetPersonListInput, PersonDto, PersonDto { public PersonAppService(IRepositoryPerson, int repository) : base(repository) { } protected override IQueryablePerson CreateFilteredQuery(GetPersonListInput input) { return base.CreateFilteredQuery(input) .WhereIf(!string.IsNullOrEmpty(input.Filter), p p.Name.Contains(input.Filter)); } protected override async TaskPagedResultDtoPersonDto CreatePagedResultAsync( IQueryablePerson query, GetPersonListInput input) { var totalCount await query.CountAsync(); var items await query .OrderBy(input.Sorting ?? nameof(Person.CreationTime)) .PageBy(input) .ToListAsync(); return new PagedResultDtoPersonDto( totalCount, ObjectMapper.MapListPersonDto(items) ); } }这里有个容易忽略的细节PagedAndSortedResultRequestDto自带了MaxResultCount、SkipCount、Sorting三个属性ABP自动API在接收前端请求时会自动绑定这三个参数所以前端只要把参数名对齐后端几乎不用额外写绑定代码。还要理解ABP的自动API机制。PersonAppService会被自动暴露成一个RESTful接口默认地址是/api/services/app/person/getAll同时前端也会生成一个叫abp.services.app.person的JavaScript代理。BootstrapTable可以直接请求这个URL也可以在编辑、删除等操作里直接用abp.services.app.person.delete(id)这样的代理方法两种方式各有适用场景后面会展开讲。2.2 页面资源引用的顺序有哪些讲究BootstrapTable依赖jQuery和Bootstrap资源引用的顺序必须严格错了就是白屏或控制台直接报错。引入顺序是jQueryBootstrap的CSS和JSbootstrap-table.min.cssbootstrap-table.min.js语言包bootstrap-table-zh-CN.min.js页面自己的脚本ABP的布局页_Layout.cshtml里通常已经引入了jQuery和Bootstrap这时候新增BootstrapTable就只需要把后三个加到表单页或布局页里。如果BootstrapTable的JS被放在了jQuery之前浏览器控制台会报类似BootstrapTable requires jQuery的错误这个问题虽然低级但我见过不止一次。语言包是很多人忽略的一步。不引语言包分页组件显示的是英文的Pagination、Total这些字样引入中文语言包之后分页信息、按钮提示都会变成中文体验差别很大。3. 前端核心对接把ABP的result翻译成BootstrapTable需要的total和rows3.1 表格初始化页面里放一个空table标签就够BootstrapTable会在初始化时动态生成表头、分页条、工具栏。我的表单页结构大概是这样div classrow div classcol-md-12 div classibox float-e-margins div classibox-title h5人员列表/h5 /div div classibox-content div classrow div classcol-sm-3 input typetext idFilterKeyword classform-control placeholder搜索姓名 / /div div classcol-sm-1 button idSearchButton classbtn btn-primary查询/button /div /div table idPersonTable/table /div /div /div /div接着在页面脚本里初始化var personTable $(#PersonTable).bootstrapTable({ url: abp.appPath api/services/app/person/getAll, method: get, pagination: true, sidePagination: server, pageSize: 10, pageList: [10, 20, 50], showRefresh: true, queryParams: function (params) { return { skipCount: params.offset, maxResultCount: params.limit, sorting: params.sort ? params.sort params.order : creationTime desc, filter: $(#FilterKeyword).val() }; }, responseHandler: function (res) { if (res.success res.result) { return { total: res.result.totalCount, rows: res.result.items }; } return { total: 0, rows: [] }; }, columns: [ [ { field: name, title: 姓名, sortable: true }, { field: email, title: 邮箱 }, { field: phone, title: 电话 }, { field: creationTime, title: 创建时间, formatter: dateTimeFormatter }, { field: id, title: 操作, formatter: operateFormatter, events: operateEvents } ] ] });这段代码是整篇文章的核心如果只记一个片段就记responseHandler和queryParams这两块。3.2 responseHandler为什么必不可少ABP的自动API返回结构是外层带一个result包装的{ result: { totalCount: 100, items: [ { id: 1, name: 张三, email: ... } ] }, success: true, error: null, targetUrl: null }但BootstrapTable在服务端分页模式下期望的返回结构是{ total: 100, rows: [] }两个结构对不上所以必须在responseHandler里做翻译。刚开始接的时候我没写这一层直接把res.result当成返回值结果表格只显示当前页数据总页数永远是1翻页点不动。后来加上翻译问题立刻消失。这里可以多说一句为什么ABP要包这层壳因为它需要统一表达success、error、targetUrl这些状态信息任何一个应用服务方法抛异常ABP都能把错误信息封装成标准格式返回。代价就是前端对接方必须多写一层适配。理解了这一点就不会觉得responseHandler是多余代码。3.3 queryParams里的三个字段名是ABP的命门BootstrapTable默认发给后端的参数是limit、offset、search、sort、order这几个但ABP认识的是skipCount、maxResultCount、sorting。所以必须用queryParams做一次重写params.offset对应skipCountparams.limit对应maxResultCountparams.sort和params.order拼接成sorting格式是字段名 ASC/DESC例如name ascABP在模型绑定的时候对参数名大小写并不敏感传skipcount也能绑上但为了规范统一前端还是用小驼峰写法。sorting的默认值也很关键。不要留空字符串我一般给一个creationTime desc否则ABP在排序字段为空时可能按数据库默认顺序返回列表顺序不稳定。4. 翻页、排序、搜索联动三个高频坑位的位置与解法4.1 总条数的计算和显示问题服务端分页模式下总条数必须来自数据库的Count(*)结果而不是当前页返回的数组长度。ABP的totalCount就是这个值只要responseHandler把它放到了total字段BootstrapTable就能正确计算总页数。有个很容易踩的坑是后端某个开发者图省事直接返回一个ListT而不是PagedResultDtoT前端表格就只能拿到数组本身totalCount恒等于items.length。症状是“第一页有10条数据但底部显示总共只有10条”翻页完全无效。遇到这种情况不用怀疑BootstrapTable配置先去后端看返回类型是不是PagedResultDto。4.2 排序参数怎么拼接才不会报错ABP的Sorting字段长这样name ASC、creationTime DESC。而BootstrapTable传过来的params.sort是列field名比如nameparams.order是asc或者desc。如果直接拼接sorting就变成了name asc这个值会直接传给EF的OrderBy。问题来了DTO字段名是Name前端field是name拼出来的name asc到了EF那里找不到叫name的属性就会抛“无法排序”的异常。一是可以在列定义里用sortName指定后端实体属性名二是更稳妥的做法在后端做排序字段白名单映射。我推荐后端白名单方案private static readonly Dictionarystring, string SortMap new Dictionarystring, string { { name, nameof(Person.Name) }, { creationTime, nameof(Person.CreationTime) } };在排序方法里先解析前端传过来的排序字段做一层翻译翻译不出来的字段直接忽略并回退到默认值。这样即使前端传了一个后端实体里不存在的字段名也不会抛异常。protected override IQueryablePerson SortQuery(IQueryablePerson query, GetPersonListInput input) { var sorting input.Sorting; if (string.IsNullOrWhiteSpace(sorting)) { sorting nameof(Person.CreationTime) desc; } else { var parts sorting.Split( ); var key parts[0].ToLower(); if (SortMap.ContainsKey(key)) { sorting SortMap[key] (parts.Length 1 ? parts[1] : ); } else { sorting nameof(Person.CreationTime) desc; } } input.Sorting sorting; return query.OrderBy(sorting); }4.3 搜索框和表格怎么联动搜索联动有两种常见写法一种是点“查询”按钮时手动调用$(#SearchButton).on(click, function () { $(#PersonTable).bootstrapTable(refresh); });因为queryParams里已经在实时读取#FilterKeyword的值所以不需要额外传参。这是第一种写法的好处搜索词永远来自当前输入框刷新表格自动带上。第二种是用BootstrapTable自带的search属性把它设成true表格会自己渲染一个搜索框。但在服务端分页模式下这个自带的搜索框默认只做前端过滤不是发到后端的所以实际意义不大。我更建议关掉内置search自己放一个输入框配合queryParams读值逻辑更直白。如果要做输入即搜也就是每敲一个字就刷新那一定记得做防抖var searchTimer null; $(#FilterKeyword).on(keyup, function () { clearTimeout(searchTimer); searchTimer setTimeout(function () { $(#PersonTable).bootstrapTable(refresh); }, 300); });不加防抖每敲一个字母就发一次请求后端瞬间就会被几十个查询淹没页面也会卡顿。5. 操作列闭环编辑弹窗、删除确认与表格区域的安全刷新5.1 操作列渲染列表页最后一列通常是操作按钮BootstrapTable用formatter来渲染这一列function operateFormatter(value, row, index) { return [ button typebutton classbtn btn-xs btn-primary edit-person , data-id row.id 编辑/button, button typebutton classbtn btn-xs btn-danger delete-person , data-id row.id 删除/button ].join( ); }注意formatter里返回的是HTML字符串按钮上的>$(body).on(click, .edit-person, function () { var id $(this).data(id); editPerson(id); }); $(body).on(click, .delete-person, function () { var id $(this).data(id); deletePerson(id); });这样无论表格翻到第几页、刷新多少次、行怎么重建事件都依然有效。因为事件绑定在祖先节点body上借助事件冒泡捕获子元素点击天然适合表格这种DOM不断重建的场景。5.3 删除确认与ABP消息弹窗的配合删除操作不能直接调接口必须给用户一个确认机会。ABP老版自带abp.message.confirm用法很顺手function deletePerson(id) { abp.message.confirm(确定要删除这条记录吗, 提示, function (isConfirmed) { if (!isConfirmed) { return; } abp.services.app.person.delete(id).done(function () { abp.notify.success(删除成功); $(#PersonTable).bootstrapTable(refresh); }).fail(function (error) { abp.notify.error(error.message || 删除失败); }); }); }这里有两个细节。第一abp.message.confirm的回调参数isConfirmed点击确认是true取消是false不要写反。第二删除成功后调用bootstrapTable(refresh)因为queryParams会重新读取当前搜索框、页码和排序状态表格会自动回到当前页体验比较自然。5.4 refresh和load的差别很多人在刷新表格时纠结refresh和load的区别。简单说refresh用当前表格已有的queryParams重新发请求常用于查询条件变化、删除、编辑后刷新当前列表。load直接给表格塞一份新数据同时重置分页状态常用于外部跳转带入的全新数据集。在编辑、删除这类场景下应该用refresh而不是load因为refresh会保留当前页码和排序用户编辑完第3页的一行数据刷新后还停在那个位置附近不会被拽回第1页。6. 细节打磨日期格式、请求参数瘦身与表格性能取舍6.1 日期时间的显示问题ABP的CreationTime之类的DateTime属性经过JSON序列化后通常是一串ISO字符串比如2025-01-20T10:30:00Z直接显示在表格里又长又难读。处理方式是加一个列格式化函数function dateTimeFormatter(value) { if (!value) { return -; } var d new Date(value); if (isNaN(d.getTime())) { return value; } var pad function (n) { return n 10 ? 0 n : n; }; return d.getFullYear() - pad(d.getMonth() 1) - pad(d.getDate()) pad(d.getHours()) : pad(d.getMinutes()); }列定义里加上{ field: creationTime, title: 创建时间, formatter: dateTimeFormatter }这里有个关于时区的点值得留意。如果ABP返回的时间字符串以Z结尾浏览器会按本地时区解析展示出来的时间和数据库里的UTC时间会有时差。如果后端统一存UTC前端展示时按本地时间换算其实问题不大但如果你发现表格时间比预期早了8小时就要去检查后端序列化是不是强制转成了UTC以及在显示时是否需要按固定时区格式化。6.2 请求参数瘦身与防抖已经做了还能优化什么接BootstrapTable之后最影响性能的往往是请求参数太肥。有些人图方便在queryParams里把一整个表单序列化塞进去后端一收到就是一堆用不上的字段。建议只传接口真正需要的那几项skipCount、maxResultCount、sorting、filter其他参数一概不传。在pageList里控制好每页条数默认pageSize设成10或20就够了不要一上来就传100条后台列表页大多数时候用户只用得到前几页。如果表格里还有金额、状态这类字段也可以统一用formatter做枚举值翻译和后端状态码映射让表格显示的是“已启用/停用”而不是0/1这样可读性会提高不少。6.3 关于BootstrapTable和ABP自动API代理的混用体会最后分享一下我实际用下来的一套组合拳。表格初始化时的url指向ABP自动API的GET接口让BootstrapTable自己负责分页参数和请求发送但编辑、删除、批量操作这些动作我不走表格的url而是直接用abp.services.app.person代理调用。原因很简单abp.services代理自带ABP的错误处理、请求头、防伪造令牌注入比手动$.ajax去拼URL省心得多而且不用自己去管ABP封装结构。这两套方式在公司内部项目里已经稳定跑了大半年中间只因为EF Core版本升级踩过一次排序字段的坑其余时间几乎没为表格本身再操过心。如果你也打算在ABP项目里引入BootstrapTable我建议先把responseHandler和queryParams这两段配置想清楚再往后端补充排序白名单和分页契约整个页面链路基本就通了。至于更花哨的树形表格、可见列切换、导出Excel等基础版本跑起来之后再按需加不会影响现有结构。
RELATED

相关推荐

ABP框架邮件发送全指南:IEmailSender配置与模板实战

ABP框架邮件发送全指南:IEmailSender配置与模板实战

组里新人第一次接手ABP项目,第一个问题往往是:“我要给用户发个验证码邮件,是直接找一段SmtpClient代码,还是自己封装一个邮件工具类?”如果你也在纠结这件事,那我建议你先把这段纠结放一放。这篇是ABP入门…

📅 2026/10/17 10:54:34
大模型链接记录

大模型链接记录

豆包; Kimi.ai - 会推理解析,能深度思考的AI助手; DeepSeek https://yuanbao.tencent.com/chat/naQivTmsDa

📅 2026/10/17 10:54:34
NumPy eye() vs identity():One-hot编码的工程本质与避坑指南

NumPy eye() vs identity():One-hot编码的工程本质与避坑指南

1. 为什么One-hot不是“画个0和1的表格”那么简单刚接触机器学习时,我见过太多人把One-hot编码理解成“手动写个列表,该填1填1,该填0填0”。某次带新人做文本分类项目,A同学直接用Excel手敲了500个词对应的One-hot向量&#xff0c…

📅 2026/10/17 10:49:34
MORE NEWS

更多资讯

📰

YOLOv8实战:无人地面车辆检测系统从数据到部署全流程

这次想分享的是一套真正跑通了“数据→训练→改进→部署”全链路的小型无人地面车辆检测系统。和网上那些“训练完就结束”的教程不同,这套项目把YOLOv8目标检测从数据集构建到一键训练,再到Web前端展示全部串了起来——你拿到的不是一个孤立的best.pt模…

📰

基于YOLO11与CaFormer的太阳黑子检测模型设计与部署实践

太阳黑子检测这几年在天文图像处理里算是比较热门的方向。太阳黑子作为太阳活动最直观的表征之一,对空间天气预报、通信环境评估都有实际意义,但传统的检测方式依赖人工目视判读,效率低且标准不统一。把这活儿交给深度学习模型来做&#xff0…

📰

YOLOv8实战:小型无人地面车辆检测的数据、训练、改进与Web部署

“完整源码数据集部署教程”“一键训练”“Web前端展示”,这几个词凑在一起,说明这已经不光是给你一个模型文件让你自己玩,而是把整个目标检测项目从数据到服务全链条都做了。小型无人地面车辆的检测任务,放到实际场景里比表面上看…

📰

基于YOLOv8的包装材料回收分类检测系统构建与实战

包装材料回收分类检测这套东西,我前后做过不止一版。最初拿到一堆杂乱的垃圾图片,直接套预训练模型去训,结果一到真实场景就抓瞎:瓶子被压扁、表面有污渍、光线忽明忽暗,模型框得稀烂。后来把重心从“换更牛的网络”转…

📰

VirtualBox共享文件夹配置全指南:Ubuntu 18.04与Windows高效传文件

1. 项目概述:为什么在VirtualBox里配共享文件夹,比直接拖文件强十倍? 如果你正在用VirtualBox跑Ubuntu 18.04作为开发环境,又得频繁往里面传代码、日志、配置文件,或者从Linux里导出编译好的二进制、测试报告、截图——…

📰

AI辅助专著生成全流程指南:从选题到终稿的实操方法论

看到“AI专著生成工具”这几个字,我猜大部分人跟我的第一反应一样:这东西靠谱吗?是不是生成一堆正确的废话硬凑字数?我在内容写作这行待了十几年,帮人整理过专著、教材、行业白皮书,大大小小也经手了几十本…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬