
1. 评分展示这件事远比你想的要细做微信小程序开发这些年我接过不少看起来简单的需求星星评分展示绝对算一个。产品经理拿着原型图过来说就一个五星评分用户点一下打分展示的时候支持半颗星听起来十分钟能搞定的事实际动手才发现坑一个接一个。半颗星怎么渲染用图片还是字体图标用户点击时怎么判断点到了第几颗星、是整颗还是半颗展示态和交互态怎么复用评分数据从后端拿到的是4.3分前端怎么把它拆成四颗满星加一颗半星这些问题不提前想清楚写出来的代码就是一堆硬编码的坐标判断改一个需求要动全身。这篇文章我想把微信小程序星星评分展示、半颗星展示这个主题彻底讲透。不管你是刚接触小程序的新手还是做过几个项目但每次写评分都要重新查资料的老手都能从这里拿到可以直接抄作业的方案。我会从整体设计思路讲起把图片方案、字体图标方案、纯CSS方案各自的取舍说清楚然后深入到半颗星的三种主流实现方式再给出完整的交互逻辑和数据结构设计最后把我踩过的坑和排查技巧整理成速查表。核心关键词就三个微信小程序、星星评分、半颗星展示全文围绕它们展开不跑偏。先说清楚这个功能到底解决什么问题。评分组件在电商、外卖、内容社区、服务评价里几乎无处不在用户需要快速给出一个分数同时也要能直观看到别人给出的平均分。整颗星好办难的是半颗星——因为真实世界的平均分很少是整数4.7分、3.2分这种才是常态。如果只支持整颗星要么四舍五入丢失精度要么显示得很难看。所以半颗星展示不是锦上添花而是评分组件的刚需。适合阅读这篇文章的人包括正在做小程序评价模块的开发者、想封装一个通用评分组件的前端、以及被半颗星渲染折磨过的同行。2. 整体设计思路与方案选型拆解2.1 先想清楚展示态和交互态是两套逻辑很多人一上来就写代码结果写着写着发现展示和交互混在一起越写越乱。我的经验是动手前先把这两个状态彻底分开。展示态指的是只读场景比如商品详情页显示4.6分这时候星星只是视觉呈现用户不能点。它的核心诉求是给定一个0到5之间的小数准确渲染出满星、半星、空星的数量。交互态指的是用户打分场景比如请为本次服务评分用户点击星星时要能选中而且通常只允许选整颗或半颗。它的核心诉求是把用户的点击位置换算成分数并实时反馈。为什么一定要分开因为两者的数据流方向相反。展示态是分数→视觉交互态是点击→分数→视觉。如果用一个组件硬扛两种模式props会变得极其复杂维护成本飙升。我通常的做法是写一个基础渲染组件负责分数→星星再包一层交互逻辑处理点击通过一个readonly或mode属性切换。这样展示态复用时不用带任何点击逻辑交互态也能独立测试。提示如果你的项目里评分出现在多个页面强烈建议抽成自定义组件而不是复制粘贴。半颗星的渲染逻辑一旦要改集中在一处改比满项目找要省心得多。2.2 三种渲染方案到底选哪个半颗星的渲染方式直接决定了实现难度和视觉效果主流有三种我把它们的取舍列成表格你对着自己的项目选。方案实现方式优点缺点适用场景图片方案准备满星、半星、空星三张图按数量拼接视觉可控兼容性最好需要切图改颜色要重新出图对视觉要求高、有设计资源字体图标方案用iconfont的星形字体通过叠加裁剪实现半星矢量清晰改色方便半星需要额外处理字体加载有成本追求清晰度和灵活性纯CSS方案用两层星星叠加上层用宽度百分比裁剪无需图片字体改色改尺寸都方便需要理解裁剪原理边界处理要细心想零依赖、快速实现我个人的偏好是纯CSS叠加裁剪方案原因很实在不用等设计出图不用引入字体文件增加包体积改颜色就是改一个CSS变量的事。它的核心原理其实很简单——底层铺一排空星上层铺一排满星然后给上层容器设置一个宽度百分比超出的部分用overflow: hidden裁掉。比如4.6分就是4.6除以5等于92%上层容器宽度设为92%自然就露出了4.6颗星的视觉效果。半颗星那只是92%这个数字恰好落在半颗星位置而已根本不需要单独处理半颗这个概念。这个思路一旦想通半颗星就不再是特殊case而是连续百分比的自然结果。这也是我推荐它的根本原因把离散的半颗星问题转化成连续的宽度百分比问题逻辑一下子简单了。2.3 数据结构设计分数怎么存、怎么传在动手写渲染之前得先把数据约定好。我见过太多项目因为分数格式不统一导致前端一会儿拿到4.6一会儿拿到4.6分一会儿拿到46放大十倍每次都要转换容易出错。我的约定是这样的分数统一用0到5的浮点数表示保留一位小数。后端返回score: 4.6前端直接用。如果需要显示百分比或者十分制在展示层转换不要污染原始数据。为什么保留一位小数因为半颗星对应0.5一位小数足够表达所有半星场景再多就是过度设计。如果业务需要更精细比如0.1星那说明你需要的不是半星而是十分之一星方案要重新考虑但绝大多数场景0.5的粒度就够了。另外要区分两个概念平均分和用户评分。平均分是展示态可以是任意小数用户评分是交互态通常只允许0.5的整数倍。这两个值在组件里要分开管理不要混用同一个变量否则用户点了一下半星平均分的精度就被破坏了。3. 半颗星渲染的核心细节与实操要点3.1 纯CSS叠加裁剪方案完整实现这是我最推荐的方案先把核心代码给出来再逐行解释为什么这么写。!-- 组件结构 -- view classstar-rating !-- 底层空星 -- view classstar-layer star-empty text wx:for{{5}} wx:keyindex classstar-icon★/text /view !-- 上层满星宽度由分数决定 -- view classstar-layer star-full stylewidth: {{percent}}%; text wx:for{{5}} wx:keyindex classstar-icon★/text /view /view.star-rating { position: relative; display: inline-block; font-size: 32rpx; line-height: 1; } .star-layer { display: flex; white-space: nowrap; } .star-empty { color: #d9d9d9; /* 空星颜色 */ } .star-full { position: absolute; top: 0; left: 0; overflow: hidden; /* 关键裁掉超出宽度的部分 */ color: #ffb800; /* 满星颜色 */ } .star-icon { flex-shrink: 0; }// 计算宽度百分比 Component({ properties: { score: { type: Number, value: 0, observer(newVal) { // 限制在0-5之间防止脏数据 const safe Math.max(0, Math.min(5, newVal)); this.setData({ percent: (safe / 5) * 100 }); } } }, data: { percent: 0 } });这段代码的关键点有三个。第一两层星星必须完全对齐所以上层用position: absolute叠在底层上面top和left都是0。第二上层容器必须overflow: hidden这是裁剪生效的前提没有它满星会全部露出来。第三星星图标要flex-shrink: 0否则在flex布局里可能被压缩导致两层星星宽度不一致裁剪位置就错位了。为什么用★这个字符而不是图片因为它本身就是字体改color就能改颜色改font-size就能改大小零成本。有人担心不同设备上这个字符渲染不一致实测下来在主流机型上表现稳定实在不放心可以用iconfont的星形图标替代原理完全一样。3.2 半颗星的边界处理那些容易翻车的地方百分比方案虽然优雅但边界情况必须处理否则会出现看起来差一点点的诡异效果。第一个坑是分数超出范围。后端如果返回5.1或者-0.2百分比就会超过100%或变成负数星星显示就乱了。所以observer里一定要做Math.max(0, Math.min(5, newVal))的钳制。我见过线上事故就是因为后端某次返回了5.0001前端没防上层宽度100.002%虽然视觉上看不出来但配合某些动画就露馅了。第二个坑是精度导致的半星偏移。比如4.5分理论上是4颗半但4.5/5*100 90%如果星星总宽度是奇数像素90%可能落在第4.5颗星偏左或偏右一两个像素的位置看起来半星就不正。解决办法是给星星容器一个能被5整除的宽度或者干脆接受这一两个像素的误差——用户根本看不出来。如果产品对精度极其苛刻那就回到图片方案用三张图精确拼接。第三个坑是空星和满星的字符宽度不一致。理论上同一个字符宽度一样但如果你的空星用了不同的字体或者加了字重宽度就可能不同导致裁剪错位。所以两层星星的字体设置必须完全一致只改颜色不改其他。注意如果你的星星之间有间距比如每颗星之间留4rpx那么百分比计算要考虑间距。因为间距也占宽度90%的宽度可能裁到间距上而不是星星上。这种情况建议把间距做进星星字符本身用letter-spacing或者干脆不留间距靠字符自带的边距。3.3 交互态点击如何换算成半星展示态搞定后交互态是另一个重点。用户点击星星时我们要判断他点的是第几颗、是整颗还是半颗。这里有两种常见交互设计得先跟产品确认。设计A只允许整颗星。用户点第3颗就是3分简单直接。设计B允许半颗星。点某颗星的左半边是半星右半边是整星。设计B更精细但实现要复杂一些需要拿到点击位置相对于星星的偏移。我以设计B为例讲实现。核心是拿到点击事件的detail.x相对于组件的横坐标然后除以单颗星的宽度得到点到了第几颗星的什么位置。// 假设每颗星宽度为 starWidth单位px需与渲染一致 handleTap(e) { const x e.detail.x; // 相对于组件左上角的横坐标 const starWidth this.data.starWidth; // 计算点到了第几颗星从0开始 const starIndex Math.floor(x / starWidth); // 计算在这颗星内的偏移比例 const offsetInStar (x % starWidth) / starWidth; // 左半边算半星右半边算整星 let score; if (offsetInStar 0.5) { score starIndex 0.5; } else { score starIndex 1; } // 钳制范围 score Math.max(0.5, Math.min(5, score)); this.setData({ score }); this.triggerEvent(change, { score }); }这里有个细节e.detail.x是相对于触发元素的坐标如果星星外面还包了一层容器坐标原点可能不是你想要的。稳妥的做法是用e.currentTarget配合boundingClientRect拿到组件的位置再和e.detail.x做换算。或者更简单直接给每颗星单独绑定点击事件用>