你是不是也遇到过这种情况:layui 表格里明明有评分列,星星渲染出来了,但一点表头排序,要么纹丝不动,要么排出来的顺序完全是乱的,跟按分数排八竿子打不着。
我在实际项目里处理过好几回这种需求——用户列表要按评分排序、课程列表按星级排序、点评列表按好评度排序。最开始我也天真地以为,只要在<th>上挂个lay-sort就完事了,结果被狠狠教育了一顿。问题在于:你页面上看到的是星星,但星星背后的真实值是分数、是字符串、还是对象,排序依据完全不是一回事。如果不把这三层关系理顺,排序永远做不对。
这篇文章我就把 layui 表格中按星级评分排序这件事整个捋一遍,从默认排序机制、字段存储、渲染方式、前端排序到后端排序,最后再附上我实际踩过的坑和排查思路。不管你是刚接触 layui 的新手,还是已经被这问题折磨过的老手,看完应该都能直接把方案搬到项目里。
1. 先搞清楚 layui 表格排序的基本机制
1.1 表头排序的触发与参数传递
layui 的 table 模块在做排序时,其实靠的是表头<th>上的lay-sort属性。如果你写过类似下面这种代码,说明已经摸到门槛了:
<table id="demoTable" lay-filter="demoTable"> <thead> <tr> <th lay-data="{field:'score', sort:true}">评分</th> <th lay-data="{field:'name'}">名称</th> </tr> </thead> </table>当用户点击带sort: true的表头时,layui 会触发table.on('sort(filter)')事件,并且传递一个包含三个关键字段的对象:
table.on('sort(demoTable)', function(obj){ // obj.field —— 当前排序的字段名,就是 lay-data 里的 field // obj.type —— 'asc' 升序 / 'desc' 降序 / null 取消排序 // obj.init —— 是否初始化排序(一般用不到) console.log(obj); });这是 layui 排序的基础玩法。问题来了:obj.field传给你的只有那个字段名,至于这个字段存的是数字、字符串还是别的类型,layui 在默认情况下并不关心。它只关心你 lau-data 里写的 field 到底是什么。
所以很多人会想:既然排序事件能拿到 field,那我直接把当前表格的数据源重新排一下不就行了?理论上行,但实践里藏着几个坑。
1.2 为什么星级评分的排序不能直接使用默认 sort
常规的文本列、数字列,比如价格、时间戳,你把sort: true一开,layui 自带的前端排序就能工作。但星级评分不一样,它有特殊性:
第一,评分列渲染到页面的是星星图标,不是数字。如果你把field直接指定为展示星星的字段,这个字段存的是一个渲染后的 HTML 字符串,比如★★★★☆或者一个组装好的模板字符串。拿字符串去比大小,结果自然是按 Unicode 码点排的,数字顺序完全被打乱。比如 9、10、8 会排成 10、8、9,因为字符串比较时先比第一位字符。
第二,即使你存的是数字,如果把templet里的原始值和展示值搞混,排序时拿到的可能只是展示层的文本,而不是底层的数值。星星组件渲染后,原始数值早就被覆盖或者被隐藏了。
第三,layui 的默认前端排序是一个简单的数组排序,它不会去区分你的字段是'4.5'还是 4.5,如果从后端返回的数据里 score 是字符串'4.5',排序就会按字符串比较来,4.5 会被排在 4.12 前面。
所以核心结论是:星级排序列必须把“排序依据的真实值”和“展示星星的渲染值”分离。你是用真实值排序,渲染值只是给人看的。
1.3 需求拆解:排序依据是分数,不是星星
我们在做需求评审时,通常会把“按星级排序”拆成两层:
- 含义层:按多少颗星排序,本质是按用户打的分值排序,比如 5 分、4.5 分、4 分;
- 展示层:在表格单元格里显示成星星图案,让人一眼看出评分高低。
如果需求说“点击表头评分列按高到低排序”,那么排序一定发生在原始数据层面,而不是在已渲染的 DOM 上做文章。因此整体方案基本是:每行数据里维护一个纯数字的评分字段,比如score,用templet把它渲染成星星;表头的sort绑定这个score字段;排序时不管是前端排序还是后端排序,操作的始终是score这个数字字段。
明白这个基础逻辑后,后面所有方案都顺了。
2. 数据准备:评分字段的存储与渲染
2.1 两种常见的评分数据存储方式
我见过很多项目里评分字段的存储方式五花八门,但归纳起来就两种:
第一种:纯数字字段。比如score: 4.5,取值范围 0 到 5,支持一位或两位小数。这是最理想的排序字段,因为数字类型天然支持大小比较。
第二种:字符串或对象。比如score: '4.5',或者score: {value: 4.5, count: 18},甚至有的后端把评分和评价数量拼成一个字段,比如"4.5分(18人)"。这种数据如果直接拿来排序必出问题。
对于第二种,你需要和对接的后端商量:要么后端额外返回一个纯数字字段,比如scoreValue;要么你在前端拿到数据后自己预处理一次,把纯数字提取出来存到新的列字段里。我强烈建议新增一个隐藏字段也不要去改原字段的展示结构。
我在模拟项目 X 里是这么处理的:后端返回的数据结构是下面这样:
{ "code": 0, "data": [ {"id": 1, "courseName": "前端入门", "rating": "4.5分(128人)"}, {"id": 2, "courseName": "后端实战", "rating": "5分(89人)"} ] }如果在表格里直接对rating排序,肯定不行,因为里面有汉字、冒号、括号。我在前端parseData回调里做了预处理:
table.render({ elem: '#demoTable', url: '/api/course/list', parseData: function(res){ var data = res.data.map(function(item){ var match = item.rating.match(/^(\d+(\.\d+)?)/); item.score = match ? parseFloat(match[1]) : 0; return item; }); return { code: res.code, msg: res.msg, count: res.count, data: data }; } });这样每行就多了个纯数字的score字段,排序时直接用score,展示时仍用原来的rating或者重新渲染。
2.2 使用 templet 把数字渲染成星级
有了数字score后,下一步是把它渲染成星星。layui 自带rate组件,但你没法在templet里直接调用它的 JS 方法去实例化,因为templet返回的是 HTML 字符串,组件实例化需要额外的 DOM 操作。所以大多数人的做法是写一个简易的模板函数,生成星星 HTML。
我常用的渲染函数是这样的:
function renderStar(score){ score = Number(score) || 0; var full = Math.floor(score); var half = (score - full) >= 0.5 ? 1 : 0; var empty = 5 - full - half; var html = ''; for(var i = 0; i < full; i++){ html += '<i class="layui-icon layui-icon-rate-solid" style="color:#ffb800;"></i>'; } if(half){ html += '<i class="layui-icon layui-icon-rate-half" style="color:#ffb800;"></i>'; } for(var j = 0; j < empty; j++){ html += '<i class="layui-icon layui-icon-rate" style="color:#ffb800;"></i>'; } return html; }然后在cols配置里:
cols: [[ { field: 'courseName', title: '课程名称' }, { field: 'score', title: '评分', width: 160, align: 'center', sort: true, templet: function(d){ return renderStar(d.score) + '<span style="margin-left:6px;color:#999;">' + d.score + '</span>'; }} ]]这里关键点在于:field仍然是score,排序时 layui 拿到的是数字;templet只是影响了这个单元格展示的内容,并不影响排序字段的真实值。我甚至会把分数放在星星后面一起展示,这样用户既能看到星星,又能看到精确分数,排序结果也更直观。
2.3 隐藏原始分数列,但保留排序字段
有一种场景是:表格里不想直接展示数字分数,只想展示星星,但排序仍然要按分数。这时有些人会干脆建一个隐藏列,或者不显示分数文本。实现方式有两种:
方式一:仍然用score作为 field,templet 里只返回星星不返回分数。表头sort: true照旧。
方式二:如果你需要把原始分数留给别处用,但不想在表格里展示任何数字,你可以在cols里单独加一个隐藏列:
{ field: 'score', title: '评分', width: 0, hide: true, sort: true }不过实际操作中我不太推荐用hide: true来隐藏排序字段,因为 layui 在后续版本里隐藏列对排序事件的支持偶尔会有小问题。更稳妥的做法是:直接让展示列绑定score字段,templet 里只画星星。这样既没有数字露出来,排序字段也一直存在。
如果你用的是老版本 layui(比如 2.5.x),hide功能和排序组合可能会出现点击表头无反应的 bug。基于常见实践,优先选择前者。
3. 实现星级排序的完整方案
3.1 方案一:layui 自带 sort 配合纯数字字段
如果你的表格数据量不大(几百行以内),并且数据是一次性加载到前端的内存数组里,那么用 layui 自带排序是最省事的。
先看完整示例代码:
layui.use(['table', 'util'], function(){ var table = layui.table; var util = layui.util; // 模拟数据 var data = [ { id: 1, name: '课程A', score: 4.5 }, { id: 2, name: '课程B', score: 3.0 }, { id: 3, name: '课程C', score: 5.0 }, { id: 4, name: '课程D', score: 4.5 }, { id: 5, name: '课程E', score: 2.5 } ]; table.render({ elem: '#tableDemo', id: 'tableDemoId', data: data, cols: [[ { field: 'name', title: '课程名称' }, { field: 'score', title: '评分', width: 180, align: 'center', sort: true, templet: function(d){ return renderStar(d.score); }} ]], page: false }); // 监听排序事件 table.on('sort(tableDemo)', function(obj){ // 直接使用 layui 内置排序:注意需要重新载入数据 table.reloadData('tableDemoId', { initSort: obj, // 让表头排序图标状态同步 data: data.slice().sort(function(a, b){ var v1 = a[obj.field]; var v2 = b[obj.field]; if(v1 == null) v1 = -1; if(v2 == null) v2 = -1; if(obj.type === 'asc'){ return v1 - v2; } else if(obj.type === 'desc'){ return v2 - v1; } return 0; }) }); }); });这里有两个容易忽略的细节:
第一个是initSort: obj。如果你不传这个参数,点击表头排序后,表头上的箭头状态不会更新,用户会以为没排上。很多新手在这步卡住,总觉得功能没生效,其实数据已经变了,只是图标没变。
第二个是data.slice()。因为sort()是原地排序,会修改原数组,如果你重复点击不同字段排序,原数组顺序已经被改过了,再在已排序基础上排可能会出现奇怪结果。所以每次排序前先slice()复制一份,保证基于原始数据排序。
这种方案适合数据量小、纯前端表格的场景。我当年在做一个后台管理系统的课程列表时,数据只有几十条,就用这个方案,几行代码就解决了。
3.2 方案二:完全自定义排序事件,手动重载表格
有些场景是你不想用 layui 内置的 sort 逻辑,比如数据中 score 可能为空、可能需要根据评分人数作为次级排序、或者需要在前端做更复杂的权重计算。这时可以彻底摆脱data.sort(),在sort事件里自己控制要显示的数据列表。
思路是:维护一个全局的原始数据源,每次排序时,复制原始数据,按自定义规则排序后,再通过table.reloadData灌回去。
var allData = []; // 原始数据 // 模拟从接口获取 allData = [ { id: 1, name: '课程A', score: 4.5, count: 200 }, { id: 2, name: '课程B', score: 4.5, count: 100 }, { id: 3, name: '课程C', score: 3, count: 500 } ]; table.render({ elem: '#tableDemo', id: 'tableDemoId', data: allData, // cols 配置略 }); table.on('sort(tableDemo)', function(obj){ var sortedData = allData.slice(); if(obj.type){ sortedData.sort(function(a, b){ // 第一排序:分数 if(a.score !== b.score){ return obj.type === 'asc' ? a.score - b.score : b.score - a.score; } // 第二排序:评分人数 return obj.type === 'asc' ? a.count - b.count : b.count - a.count; }); } table.reloadData('tableDemoId', { initSort: obj, data: sortedData }); });这个方案的好处是排序规则完全透明,你可以任意组合字段,比如先按分数,再按人数,甚至再按创建时间。缺点是要自己保证原始数据不被污染,一旦经过其他操作(比如删除行、编辑行),allData也需要同步更新。
我遇到过一个需求:同一个评分数值下,希望评价人数多的排在前面。用内置 sort 只能排一个字段,但用了自定义排序后,两行代码就实现了。所以如果你的业务里也有类似的“并列怎么排”的需求,这个方案很适合。
3.3 方案三:后端排序,前端只传 sort 字段
数据量超过几千行时,前端一次性加载就不现实了。这时候必须走后端排序。流程是:
- 表头加
sort: true; - 监听
table.on('sort(...)'); - 在事件回调里重新请求数据,把排序字段和排序方式带给后端;
- 后端根据参数拼接 ORDER BY,返回新数据;
- 调用
table.reload重新加载表格,并传入initSort保持表头状态。
代码大致是:
table.on('sort(tableDemo)', function(obj){ var param = { page: 1, limit: 10, sortField: obj.field, sortOrder: obj.type || '' }; // 请求后端接口 $.ajax({ url: '/api/course/list', data: param, success: function(res){ table.reload('tableDemoId', { initSort: obj, data: res.data, page: { curr: 1 } }); } }); });后端伪代码(以常见框架为例):
-- 前端传来 sortField=score, sortOrder=desc SELECT * FROM course ORDER BY score DESC LIMIT 0, 10注意两点:
一是obj.type可能是 null,也就是用户点击的是“取消排序”,这时后端应该返回默认顺序。你可以在前端判断一下:
var sortOrder = obj.type ? (obj.type === 'asc' ? 'asc' : 'desc') : '';二是分页参数。排序后第一页数据很可能和之前完全不同,所以页码最好重置为 1,否则用户可能在第二页点排序,结果还停留在第二页,页面数据是排序后的第二页,体验很别扭。
我实际项目中更多的还是这种后端排序方案。因为业务表格几乎都会配分页,前端排序只能排当前页,数据一多就暴露问题。星级评分的排序字段在后端就是普通数值字段,处理起来非常简单,重点反而在前端如何正确传递obj.field和obj.type。
3.4 三种方案怎么选:决策建议
我根据自己的项目经验整理一个简单的选型参考:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 数据量小(几百行内),一次性加载 | 方案一(内置 sort + reloadData) | 代码少,维护简单,无额外请求 |
| 数据量中等,需要并列次排序 | 方案二(自定义排序事件) | 可自由控制排序规则,效果直观 |
| 数据量大、有分页、服务端渲染 | 方案三(后端排序) | 排序的是全量数据,性能最好,最严谨 |
| 表格里的评分同时用在其他联动场景 | 方案二或三 | 避免前端排序破坏原数据引用 |
如果你实在拿不准,就直接选方案三。后端排序是最不容易出错的,也不会因为浏览器端数据不一致导致排序错乱。唯一增加的工作量就是后端接口加一个ORDER BY参数,性价比很高。
4. 实战踩坑与问题排查
4.1 点击表头排序没反应,先检查这几处
排序功能看似简单,但实际项目中我遇到最多的问题就是“点了没反应”。排查看下来,90% 集中在下面几个原因:
第一,lay-filter和监听名称对不上。表格渲染时elem对应 HTML 里的id,但table.on('sort(filterName)')里的filterName是lay-filter的值,不是elem的 id。写错一个字母,事件永远不触发。
<table id="demoTable" lay-filter="demoTable"></table>table.render({ elem: '#demoTable', ... }); // 这里的 'demoTable' 是 lay-filter 的值 table.on('sort(demoTable)', function(obj){ ... });第二,表头lay-data里没写sort: true。你只在 JS 配置里写sort: true,但 HTML 表头里用的是lay-data,两者都得有。我一开始用纯 JS 配置 cols 时会忘记写,所以经常需要在表头 HTML 和 JS 配置之间反复检查。
第三,使用了自定义templet的列,但field名称写错。排序监听的obj.field是根据 cols 里的field回来的,如果你写的是score,但数据里字段是scoreValue,排序时比较的值就是 undefined,自然没反应或者排成乱序。
建议排查顺序:先在table.on('sort')回调里打一个console.log(obj),看点击表头后有没有触发。如果没触发,检查 filter 名和 lay-data;如果触发了,再打印数据内容,看 field 是否对应。
4.2 排序后星星不显示或者错位
有一种情况:用方案一排序后,星星消失了,或者出现错位。这通常不是排序逻辑的问题,而是templet函数执行时,你依赖的外部函数在重载时作用域丢了。
比如我在templet里调用了renderStar,但renderStar是在某个模块回调里定义的局部函数,重载表格后,由于渲染上下文变化,函数还在,但如果函数内部引用了错误的作用域变量,就可能报错,导致整列渲染失败。
解决办法是:把renderStar这类渲染函数定义在layui.use外层,或者挂到全局对象上,避免块级作用域带来的意外。
另外还有一种错位场景:你使用了hide: true隐藏了某个列,但隐藏列的字段和展示列字段在排序后因为数据顺序变化,出现了行数据与行 DOM 错位。这是 layui 表格自身的渲染机制决定的,通常刷新页面或者重新 render 能解决。但从根上避免,还是不要在同一列里既隐藏又排序,最好只保留一个不可见但真实存在的排序字段。
4.3 数字字符串导致的排序错乱
这是最容易踩的隐形坑。后端返回的 score 是字符串'4.5'、'5'、'3',前端直接用数字减法比较:
a.score - b.score如果 a.score 是'abc',减法结果是 NaN,排序直接乱。哪怕都是数字字符串,也会出现精度问题。我遇到过一次线上 bug:评分 4.5 的排在了 4.15 前面。因为字符串比较是按位比较,4.1 和 4.5 比较时,先比'4'再比'.'(同),再比'1'和'5',这一下就把 4.15 排到 4.5 前面了。
所以,排序前一定要做类型归一化:
function normalizeScore(v){ var n = Number(v); if(isNaN(n)) return 0; return n; }然后在排序比较里用normalizeScore(a.score) - normalizeScore(b.score)。
一个好的习惯是,在数据进入表格之前就统一转成 number。上面 2.1 节里的parseData预处理器就是干这个的。前端一旦保证score始终是数字类型,后续所有排序方案都稳了。
4.4 常见问题速查表
我把平时维护项目时遇到的高频问题整理成一个速查表,方便大家直接对号入座:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 点表头没反应 | filter 名字写错 / 未加 sort:true | 核对 lay-filter 和 table.on 名称;表头加 lay-sort 或 cols 配 sort:true |
| 排序结果不正确 | score 是字符串或带单位 | 统一用 parseFloat 预处理,生成纯数字字段 |
| 排序后表头箭头不更新 | 重载时没传 initSort | 在 reload/reloadData 里增加 initSort: obj |
| 星星排序后消失 | templet 函数作用域问题 | 把渲染函数提取到全局或 layui.use 外层 |
| 并列排序不定 | 只排了 score 一个字段 | 自定义 sort 事件,增加次级排序字段 |
| 分页状态下排序错乱 | 前端排序只排了当前页 | 改用后端排序,或一次性加载全量数据再前端排序 |
| 分数相同但顺序随机 | 缺少稳定排序规则 | 增加 id 或 count 作为兜底排序条件 |
4.5 一个完整的可运行示例
最后,我贴一个综合方案三(后端排序)思路但用本地数据模拟的完整示例,适合新手直接跑:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>layui 星级排序示例</title> <link rel="stylesheet" href="layui/css/layui.css"> </head> <body> <table id="scoreTable" lay-filter="scoreTable"></table> <script src="layui/layui.js"></script> <script> function renderStar(score){ score = Number(score) || 0; var full = Math.floor(score); var half = score - full >= 0.5 ? 1 : 0; var empty = 5 - full - half; var html = ''; for(var i = 0; i < full; i++){ html += '<i class="layui-icon layui-icon-rate-solid" style="color:#ffb800;"></i>'; } if(half){ html += '<i class="layui-icon layui-icon-rate-half" style="color:#ffb800;"></i>'; } for(var j = 0; j < empty; j++){ html += '<i class="layui-icon layui-icon-rate" style="color:#ffb800;"></i>'; } return html; } layui.use('table', function(){ var table = layui.table; var originData = [ { id: 1, name: '课程A', score: 4.5 }, { id: 2, name: '课程B', score: 4 }, { id: 3, name: '课程C', score: 5 }, { id: 4, name: '课程D', score: 3.5 }, { id: 5, name: '课程E', score: 4.5 }, { id: 6, name: '课程F', score: 2 } ]; table.render({ elem: '#scoreTable', id: 'scoreTableId', data: originData, cols: [[ { field: 'name', title: '课程名称' }, { field: 'score', title: '评分', width: 180, align: 'center', sort: true, templet: function(d){ return renderStar(d.score) + '<span style="margin-left:8px;">' + d.score + '</span>'; }} ]] }); table.on('sort(scoreTable)', function(obj){ var sorted = originData.slice().sort(function(a, b){ var v1 = Number(a[obj.field] || 0); var v2 = Number(b[obj.field] || 0); if(obj.type === 'asc'){ return v1 - v2; } if(obj.type === 'desc'){ return v2 - v1; } return a.id - b.id; }); table.reloadData('scoreTableId', { initSort: obj, data: sorted }); }); }); </script> </body> </html>把这段代码里的layui路径换成你自己的实际路径就能跑起来。里面我把取消排序时按 id 顺序复原也做了,这样体验更完整。
5. 进阶优化与经验建议
5.1 评分相同时的次级排序规则
真实业务里,评分相同的情况很多。比如 100 个商品里有三四十个都是 4.5 分,如果只按分数排,这三四十个的先后顺序在每次刷新时都可能变化,用户看起来就会觉得“排序不稳定”。
我的习惯是设置一个稳定的次级排序字段,优先级是这样:
- 主排序:指定字段(比如 score);
- 次级排序:score 相同的人数(比如 count 评价人数多的靠前);
- 兜底排序:主键 id 或创建时间,保证顺序绝对稳定。
代码里就是:
sortedData.sort(function(a, b){ if(a.score !== b.score){ return obj.type === 'asc' ? a.score - b.score : b.score - a.score; } if(a.count !== b.count){ return obj.type === 'asc' ? a.count - b.count : b.count - a.count; } return a.id - b.id; });这里有个小技巧:无论升序还是降序,兜底排序都建议用固定的 id 升序,因为在主次排序条件都相同的情况下,id 升序是最符合直觉的,也最容易保证稳定。你要是跟着主排序方向来,反而会让顺序每次刷新都不一样。
5.2 防止快速点击表头导致请求混乱
用后端排序方案时,用户如果手快,连续点了好几次表头,前端就可能发出多个并发的 Ajax 请求。后返回的响应可能不是最后一次请求的结果,于是表格数据被旧请求覆盖,出现排序状态和显示数据不一致的 bug。
我的处理方式有两种:
第一种是简单粗暴的防抖。在 sort 事件回调里加一个锁:
var isSorting = false; table.on('sort(scoreTable)', function(obj){ if(isSorting) return; isSorting = true; // 发请求,回调里面再 isSorting = false });第二种是记录最后一次请求的标记。每次发请求时生成一个自增 id,只有最新 id 的请求返回时才能更新表格:
var requestSeq = 0; table.on('sort(scoreTable)', function(obj){ var seq = ++requestSeq; $.ajax({ url: '/api/list', data: { sortField: obj.field, sortOrder: obj.type }, success: function(res){ if(seq !== requestSeq) return; // 过期请求直接丢弃 table.reload(...); } }); });第二种更好,它不会阻塞用户操作,只是丢弃旧响应。实际项目中我更喜欢用这种。
5.3 排序状态下其他操作的联动处理
表格如果支持行内编辑、删除、批量操作,排序后这些操作可能会影响到排序结果。比如当前按评分降序排列,用户删掉了一条高分行,那么下一行分数可能变成当前最高,但在显示层面它还在原来的位置,整体看起来像是“该排序了却没排”。
这种情况没法完全靠前端自动处理,我的经验是:在删除或编辑的操作回调里,再次触发当前表头的排序事件,让表格重新按当前排序字段排一次。实现方式不复杂,你可以在表格 reload 之后,手动调用:
// 当前排序状态 var currentSort = { field: 'score', type: 'desc' }; table.on('tool(scoreTable)', function(obj){ if(obj.event === 'delete'){ // 删除后重新按当前字段排序 sortData(currentSort.field, currentSort.type); } }); function sortData(field, type){ // 模拟点击表头,或者直接执行排序逻辑 // 具体实现和方案二/三一样 }这种方法对用户来说,体验是“删除后列表自动排好了”,不会出现空白或者顺序异常。
5.4 有关评分的细节体验优化
最后分享几个我在项目中积累的细节优化,不算必不可少,但加上了会更专业:
一是评分数字精度。排序用原始值,但展示时可以保留一位或两位小数。如果后端存的是 4.55,前端直接展示出来可能很长,你可以在templet里用d.score.toFixed(1)做显示。注意排序仍然用原始 d.score,而不是 toFixed 后的字符串。
二是半星的处理。layui 自带图标里有半星图标,但很多业务其实不需要展示半星。遇到 4.5 分时,有人显示 4 颗实星加一颗空星,有人显示 4 颗实星加半星,这两种都可以,但排序时都应该按 4.5 参与计算,而不是按 4。所以渲染函数里半星的实现只是为了视觉,和排序无关。
三是表尾合计行。如果表格启用了showFooter: true,某个字段配了totalRow: true,对评分列做合计是没意义的。我通常会在评分列不配置 totalRow,或者只展示平均值。如果非要展示平均值,可以用totalRow的回调自己算。
四是排序的默认状态。需求有时候要求首次进入页面就按评分降序排列,让高分靠前。这可以在table.render配置里通过initSort属性实现:
table.render({ elem: '#scoreTable', initSort: { field: 'score', type: 'desc' }, // 其他配置 });这样页面加载时表头就会自动带上降序箭头,数据也会按这个规则排序,用户一进来看到的就是“评分从高到低”的结果。
最后再分享一个小技巧
如果你在项目里遇到排序后星星明明正常,但分数文本却消失的情况,优先检查templet返回的字符串里是否包含 HTML 实体或者复杂的引号嵌套。我早期写渲染函数时,经常在拼接字符串时忘掉单双引号的转义,结果表格单元格渲染出错。后来我习惯把所有图标和文本全放进一个模板函数里,通过拼接变量生成,避免在 template 串里直接写死带引号的复杂 HTML。
另外再啰嗦一句:虽然 layui 的 Rate 组件很漂亮,但在表格排序这个场景里,尽量不要在templet里使用需要 JS 实例化的组件。表格每一行渲染时都是纯 HTML 输出,如果强行实例化 Rate,你会陷入“渲染后找不到节点”的泥潭。用纯 CSS + 字体图标拼星星绝对是最稳妥的。
我自己从第一次接触 layui 表格排序到现在,经历了从“完全不会”到“踩坑无数”再到“闭眼写”的过程,最大的体会就是:任何排序问题,先分清排序依据和展示内容,再考虑数据在哪一层解决。星级评分只是其中一个表象,背后其实是一个“字段映射”的通用问题。把这个思路想透了,以后遇到任何类似的自定义排序需求,都能很快定位到解决方案。希望这篇文章能帮你少走几次弯路。