评分Rate
让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分
数据录入 规格 36 · 评分 Rate 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-rate.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Rate.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
点第 N 颗即取 N 分;整行 44px 高,星形本体只有 24px。
查看代码(演示页原文 · 42 行)
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(点第 N 颗即取 N 分;整行 44px 高,星形本体只有 24px)</p>
<div class="demo-pad">
<div class="kole-m-rate" id="rate-basic" role="radiogroup" aria-label="商品评分"
data-score="4" data-max="5" data-assert="rate-basic">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星"
data-behavior="click-sets-attr:#rate-basic|data-score|1">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="4 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">4 分</span>
</div>
</div>
</section>allowHalf=true:点某颗星的左半取 N-0.5 分、右半取 N 分。
查看代码(演示页原文 · 52 行)
<section class="demo-block" data-demo="half">
<p class="demo-label">半星(allowHalf=true:点某颗星的左半取 N-0.5 分、右半取 N 分 —— 规则写进规格 §36.5)</p>
<div class="demo-pad">
<div class="kole-m-rate kole-m-rate--half" id="rate-half" role="radiogroup" aria-label="物流评分"
data-score="3.5" data-max="5" data-half="true" data-assert="rate-half">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3.5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 50%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"
data-behavior="click-sets-attr:#rate-half|data-score|4.5"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">3.5 分</span>
</div>
</div>
</section>size=small 星形 18px 用于行内展示;default 24px 用于可评分场景。
查看代码(演示页原文 · 45 行)
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=small 星形 18px,用于行内展示;default 24px 用于可评分场景)</p>
<div class="demo-pad" data-assert="rate-size">
<div class="kole-m-rate kole-m-rate--small kole-m-rate--readonly" role="img" aria-label="评分 3 分(满分 5 分)">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">small(18px)</span>
</div>
<div class="kole-m-rate kole-m-rate--readonly" role="img" aria-label="评分 3 分(满分 5 分)">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">default(24px)</span>
</div>
</div>
</section>02 组件状态
readonly=true:星形不再是按钮、点击不改值,整组用 role="img" 播报分值。
查看代码(演示页原文 · 25 行)
<section class="demo-block" data-demo="readonly">
<p class="demo-label">只读展示(readonly=true:用于列表里展示已有评分,星形不再是按钮、点击不改值)</p>
<div class="demo-pad">
<div class="kole-m-rate kole-m-rate--readonly" role="img" aria-label="评分 4.5 分(满分 5 分)" data-assert="rate-readonly">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 50%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">4.5 分 · 共 128 条评价</span>
</div>
</div>
</section>整条置灰且不可聚焦:读屏会播报不可用,点击不改值。
查看代码(演示页原文 · 41 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(整条置灰且不可聚焦:读屏会播报不可用,点击不改值)</p>
<div class="demo-pad">
<div class="kole-m-rate is-disabled" role="radiogroup" aria-label="满意度评分" aria-disabled="true"
data-score="2" data-max="5" data-assert="rate-disabled">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="3 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="4 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">2 分</span>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
value | number | 0 | 受控分值,允许半星(3.5)与 0(未评分)(规格 §36.5) | N |
max | number | 5 | 满分星数,决定渲染几颗星(规格 §36.2 stars) | N |
allowHalf | boolean | false | 变体 allowHalf:是否允许半星(点左半取 N-0.5 分)(规格 §36.3) | N |
size | 'default' | 'small' | 'default' | 变体 size:星形 24px 还是 18px(行内展示用)(规格 §36.3) | N |
readonly | boolean | false | 变体 readonly:只读展示(不渲染可点区域,整组用 role="img" 播报)(规格 §36.3) | N |
disabled | boolean | false | 状态 disabled:置灰且不可聚焦,点击不改值(规格 §36.4) | N |
label | string | '评分' | 无障碍名称,落到 radiogroup / img 的 aria-label(规格 §36.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
change | (value) | 点星或键盘步进改值时触发,回传新分值(可含 0.5)(规格 §36.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 数值文案位置的自定义内容(如「4.5 分 · 共 128 条评价」),替换默认的「N 分」(规格 §36.2 text) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-rate-star | 24px | 星形字号(= 视觉边长) |
--kole-m-rate-gap | 8px | 星与星之间的间距 |
何时使用
- 让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分
- 触屏上没有悬停预览:桌面上鼠标划过就能预告「点下去会是几分」,触屏上这个预告不存在
- 填充比例(含半星)必须直接可读,且星形本体只有 24px、热区要补到 44px 高
- 点第 N 颗星取 N 分;allowHalf=true 时点第 N 颗星的左半取 N-0.5 分、右半取 N 分
- 取值后立即触发 change 事件(回传 3.5 这类半星值也是合法输入)
交互与触控
- **取值规则**:点第 N 颗星取 N 分;allowHalf=true 时点第 N 颗星的**左半**取 N-0.5 分、**右半**取 N 分(半星只在开启该项时存在,默认整星)
- 星形本体视觉 24px(size=small 时 18px),但每颗星的可点区域高度是 44px;横向不做死区,相邻星的边界就是两半分界
- 一次触摸即取值,不需要二次确认;不响应长按与拖动(拖动选分在触屏上容易滑错,规格未纳入)
- 已选中的部分用**填充比例**表达(半星就是左半填充),不能只靠颜色深浅区分
- 取值后立即触发 change 事件(回传 3.5 这类半星值也是合法输入)
- readonly=true 时不渲染任何可点区域(只读展示不抢键盘序列)
无障碍
- 可评分形态:根是 role="radiogroup" + aria-label,每颗星是原生 button + role="radio" + aria-checked
- 星形的名称是「N 星」(aria-label),读屏播报的是分值而不是符号
- 只读形态:整组是一个 role="img" + aria-label(「评分 4.5 分(满分 5 分)」),组内星形 aria-hidden
- 禁用态用原生 disabled 并补 aria-disabled="true"
- 键盘:聚焦后左右方向键步进(整星模式步长 1,半星模式步长 0.5)
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 滑动选择器Slider | 连续区间的粗选用 Slider;离散档位(1~5 星)且带语义的用 Rate |
| 表单Form | 评分作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供 |
| 标签Tag | 只是标注一个既有结论(如「好评」「差评」)用 Tag;需要用户给出分值时用 Rate |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 评分的业务含义映射(如「4 分以上算好评」)与统计口径 |
| 禁止发明 | 拖动选分与悬停预览(触屏没有悬停,拖动容易滑错) |
| 禁止发明 | 评分理由 / 标签的联动采集(由宿主另行组合) |
| 禁止发明 | 异步提交与失败回滚(宿主负责) |
| 规格未定 | 分值是否允许与文案一一对应(如 1 分「很差」、5 分「很好」) |
| 规格未定 | 星形换用图标字体或 SVG 后填充比例的表达是否仍然一致 |
| 规格未定 | 只读形态是否要显示评价条数(当前由宿主用插槽补) |
结构(anatomy)
| 字段 | 说明 |
|---|---|
rate | 根元素,一行里放进「星组 + 数值文案」 |
stars | 星组,横向排列,只负责布局 |
star | 单颗星,可点区域(原生 button),自身撑满 44px 高 |
glyph | 星形本体(视觉 24px),由灰底星 + 品牌色覆盖层叠成 |
fill | 覆盖层,宽度即该颗星的填充比例(100% 整星 / 50% 半星 / 0% 未选) |
text | 数值文案,默认「N 分」,可由默认插槽替换 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | default / small | default (由数据驱动,无专属类) small .kole-m-rate--small |
allowHalf | false / true | false (由数据驱动,无专属类) true .kole-m-rate--half .kole-m-rate__half |
readonly | false / true | false (由数据驱动,无专属类) true .kole-m-rate--readonly |
代表变体
| 变体 | 标签 |
|---|---|
size=default · allowHalf=false · readonly=false | 默认(整星可评分,星形 24px) |
size=default · allowHalf=true · readonly=false | 支持半星(点左半取 N-0.5 分) |
size=small · allowHalf=false · readonly=true | 行内只读展示(星形 18px,不占键盘序列) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Rate.css · 纯样式(CSS) · 125 行
/* Kole UI Mobile · Rate 样式 — 对齐移动端规格 §36
评分:点第 N 颗星即取 N 分;allowHalf=true 时点某颗星的左半取 N-0.5 分(半星规则见 §36.5)。
触屏上没有鼠标悬停预览,所以「已选中的部分」必须靠填充比例表达(半星就是左半填充),
不能只靠颜色深浅;星形本体视觉只有 24px,热区要补到 44px 高。
填充的实现:每颗星是「灰底星 + 品牌色覆盖层」,覆盖层的宽度就是这半颗/整颗的填充比例
(而不是叠一个透明度,那样在浅色轨道上看不出分界)。 */
.kole-m-rate {
/* 组件级变量:业务侧可在容器上覆盖 */
--kole-m-rate-star: 24px; /* 星形字号(= 视觉边长) */
--kole-m-rate-gap: 8px; /* 星与星之间的间距 */
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--kole-space-8);
width: 100%;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-m-gutter);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-body);
line-height: 1.4;
}
/* 变体 size=small:紧凑(列表里展示已有评分时用,配 readonly) */
.kole-m-rate--small { --kole-m-rate-star: 18px; }
/* 星组:横向排列;每颗星自身撑到 44px 高(热区),所以组本身就够高 */
.kole-m-rate__stars {
flex: 0 0 auto;
display: flex;
align-items: center;
}
/* 交互形态:每颗星是独立可聚焦的按钮(radiogroup 里的一项) */
.kole-m-rate__star {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
height: var(--kole-m-touch-target);
padding: 0 calc(var(--kole-m-rate-gap) / 2);
margin: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
line-height: 1;
cursor: pointer;
touch-action: manipulation;
}
.kole-m-rate__star:disabled { cursor: not-allowed; }
/* 星形本体:灰底 + 覆盖层(覆盖层宽度 = 填充比例,表达半星) */
.kole-m-rate__glyph {
position: relative;
display: inline-block;
font-size: var(--kole-m-rate-star);
line-height: 1;
}
.kole-m-rate__base { color: var(--kole-color-icon-inactive); }
.kole-m-rate__fill {
position: absolute;
top: 0;
bottom: 0;
left: 0;
overflow: hidden;
white-space: nowrap;
color: var(--kole-color-brand);
}
/* 半星的点击分区:盖在星形本体左右各一半上(纵向由整颗星的 44px 承担)。
只在 allowHalf=true(.kole-m-rate--half)时出现 —— 整星模式下点击位置不影响取值。 */
.kole-m-rate__half {
display: none;
position: absolute;
top: 0;
bottom: 0;
width: 50%;
}
.kole-m-rate--half .kole-m-rate__half { display: block; }
.kole-m-rate__half--left { left: 0; }
.kole-m-rate__half--right { right: 0; }
/* 数值文案:默认「N 分」,可由默认插槽替换成任意内容 */
.kole-m-rate__text {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 变体 readonly=true:只读展示(值仍被读屏读出,但不可改;星形不再是按钮) */
.kole-m-rate--readonly { cursor: default; }
.kole-m-rate--readonly .kole-m-rate__star { cursor: default; }
.kole-m-rate--readonly .kole-m-rate__half { display: none; }
.kole-m-rate--readonly .kole-m-rate__text { color: var(--kole-color-text-secondary); }
/* 状态 disabled:整条置灰且不可聚焦 */
.kole-m-rate.is-disabled { cursor: not-allowed; }
.kole-m-rate.is-disabled .kole-m-rate__base,
.kole-m-rate.is-disabled .kole-m-rate__fill { color: var(--kole-color-text-disabled); }
.kole-m-rate.is-disabled .kole-m-rate__text { color: var(--kole-color-text-disabled); }
.kole-m-rate__star:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
frameworks-mobile/Rate.html · H5 原生(无框架) · 307 行
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · Rate(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Rate.css">
<style>
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
.demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
.demo-pad { background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(点第 N 颗即取 N 分;整行 44px 高,星形本体只有 24px)</p>
<div class="demo-pad">
<div class="kole-m-rate" id="rate-basic" role="radiogroup" aria-label="商品评分"
data-score="4" data-max="5" data-assert="rate-basic">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星"
data-behavior="click-sets-attr:#rate-basic|data-score|1">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="4 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">4 分</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="half">
<p class="demo-label">半星(allowHalf=true:点某颗星的左半取 N-0.5 分、右半取 N 分 —— 规则写进规格 §36.5)</p>
<div class="demo-pad">
<div class="kole-m-rate kole-m-rate--half" id="rate-half" role="radiogroup" aria-label="物流评分"
data-score="3.5" data-max="5" data-half="true" data-assert="rate-half">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="3.5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 50%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星">
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"
data-behavior="click-sets-attr:#rate-half|data-score|4.5"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">3.5 分</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="readonly">
<p class="demo-label">只读展示(readonly=true:用于列表里展示已有评分,星形不再是按钮、点击不改值)</p>
<div class="demo-pad">
<div class="kole-m-rate kole-m-rate--readonly" role="img" aria-label="评分 4.5 分(满分 5 分)" data-assert="rate-readonly">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 50%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">4.5 分 · 共 128 条评价</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=small 星形 18px,用于行内展示;default 24px 用于可评分场景)</p>
<div class="demo-pad" data-assert="rate-size">
<div class="kole-m-rate kole-m-rate--small kole-m-rate--readonly" role="img" aria-label="评分 3 分(满分 5 分)">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">small(18px)</span>
</div>
<div class="kole-m-rate kole-m-rate--readonly" role="img" aria-label="评分 3 分(满分 5 分)">
<span class="kole-m-rate__stars" aria-hidden="true">
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 100%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
<span class="kole-m-rate__star">
<span class="kole-m-rate__glyph"><span class="kole-m-rate__base">★</span><span class="kole-m-rate__fill" style="width: 0%">★</span></span>
</span>
</span>
<span class="kole-m-rate__text">default(24px)</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(整条置灰且不可聚焦:读屏会播报不可用,点击不改值)</p>
<div class="demo-pad">
<div class="kole-m-rate is-disabled" role="radiogroup" aria-label="满意度评分" aria-disabled="true"
data-score="2" data-max="5" data-assert="rate-disabled">
<span class="kole-m-rate__stars">
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="1 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="true" aria-label="2 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 100%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="3 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="4 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
<button class="kole-m-rate__star" type="button" role="radio" aria-checked="false" aria-label="5 星" disabled>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" style="width: 0%">★</span>
</span>
</button>
</span>
<span class="kole-m-rate__text" aria-live="polite">2 分</span>
</div>
</div>
</section>
</div>
<script>
/* 演示页脚本:真实评分。
半星规则(写进规格 §36.5):allowHalf=true 时,点第 N 颗星的**左半**取 N-0.5 分、右半取 N 分;
整星模式(默认)下点击位置不影响取值,一律取 N。
每次改值重算每颗星的填充比例(0% / 50% / 100%),并同步 aria-checked 与右侧文案。
真实业务里这份状态由宿主管理(受控 value + change 事件),此处是最小可运行实现。 */
(function () {
function scoreOf(root) {
return Number(root.getAttribute('data-score') || 0);
}
function render(root) {
var score = scoreOf(root);
var stars = root.querySelectorAll('.kole-m-rate__star');
Array.prototype.forEach.call(stars, function (star, i) {
var ratio = Math.max(0, Math.min(1, score - i));
var fill = star.querySelector('.kole-m-rate__fill');
if (fill) fill.style.width = (ratio * 100) + '%';
star.setAttribute('aria-checked', ratio >= 1 ? 'true' : 'false');
});
var text = root.querySelector('.kole-m-rate__text');
if (text) text.textContent = score + ' 分';
}
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-rate'), function (root) {
if (root.classList.contains('is-disabled') || root.classList.contains('kole-m-rate--readonly')) return;
if (root.getAttribute('aria-disabled') === 'true') return;
var half = root.getAttribute('data-half') === 'true' || root.classList.contains('kole-m-rate--half');
root.addEventListener('click', function (e) {
var star = e.target.closest ? e.target.closest('.kole-m-rate__star') : null;
if (!star || !root.contains(star)) return;
var stars = Array.prototype.slice.call(root.querySelectorAll('.kole-m-rate__star'));
var index = stars.indexOf(star) + 1; /* 第 N 颗(1 起) */
if (!index) return;
var halfEl = e.target.closest ? e.target.closest('.kole-m-rate__half') : null;
var isLeftHalf = half && halfEl && halfEl.getAttribute('data-half') === 'left';
root.setAttribute('data-score', String(isLeftHalf ? index - 0.5 : index));
render(root);
});
/* 键盘:左右方向键整星步进(半星模式下是 0.5) */
root.addEventListener('keydown', function (e) {
var delta = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : 0;
if (!delta) return;
e.preventDefault();
var max = Number(root.getAttribute('data-max') || 5);
var min = half ? 0.5 : 1;
var next = Math.round((scoreOf(root) + delta * (half ? 0.5 : 1)) * 2) / 2;
root.setAttribute('data-score', String(Math.max(min, Math.min(max, next))));
render(root);
});
});
})();
</script>
<script>
/* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
(function () {
var id = new URLSearchParams(location.search).get('demo');
if (!id) return;
var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
var hit = false;
blocks.forEach(function (b) {
var on = b.getAttribute('data-demo') === id;
if (on) hit = true;
b.hidden = !on;
});
if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
document.body.classList.add('demo-single');
blocks.forEach(function (b) {
var label = b.querySelector('.demo-label');
if (label && !b.hidden) label.hidden = true;
});
})();
</script>
</body>
</html>
frameworks-mobile/Rate.jsx · React · 103 行
import React from 'react';
import './Rate.css';
/* 评分(移动端)— 规格 §36
点第 N 颗星取 N 分;allowHalf=true 时点某颗星的左半取 N-0.5 分(规则见规格 §36.5)。
触屏上没有鼠标悬停预览,所以「已选中的部分」靠填充比例表达(半星 = 左半填充),
不能只靠颜色深浅;星形本体视觉 24px,热区撑到 44px 高。
readonly=true 时是纯展示(用 role="img" + aria-label 播报「N 分(满分 M 分)」),
此时不渲染任何可聚焦控件 —— 列表里一行一个评分不该抢走键盘序列。 */
export default function Rate({
value = 0,
max = 5,
allowHalf = false,
size = 'default',
readonly = false,
disabled = false,
label = '评分',
onChange,
children = null,
}) {
const cls =
'kole-m-rate' +
(allowHalf ? ' kole-m-rate--half' : '') +
(size === 'small' ? ' kole-m-rate--small' : '') +
(readonly ? ' kole-m-rate--readonly' : '') +
(disabled ? ' is-disabled' : '');
const stars = [];
for (let i = 0; i < max; i++) stars.push(i);
function pick(index, isLeftHalf) {
if (readonly || disabled) return;
const next = isLeftHalf ? index - 0.5 : index;
if (onChange) onChange(next);
}
const ratioOf = (i) => Math.max(0, Math.min(1, value - i));
/* 只读形态:整组是一个 role="img",名称由 aria-label 播报(不留下可聚焦的星) */
if (readonly) {
return (
<div className={cls} role="img" aria-label={`${label} ${value} 分(满分 ${max} 分)`}>
<span className="kole-m-rate__stars" aria-hidden="true">
{stars.map((i) => (
<span className="kole-m-rate__star" key={i}>
<span className="kole-m-rate__glyph">
<span className="kole-m-rate__base">★</span>
<span className="kole-m-rate__fill" style={{ width: ratioOf(i) * 100 + '%' }}>
★
</span>
</span>
</span>
))}
</span>
{children || <span className="kole-m-rate__text">{value} 分</span>}
</div>
);
}
return (
<div className={cls} role="radiogroup" aria-label={label} aria-disabled={disabled ? 'true' : undefined}>
<span className="kole-m-rate__stars">
{stars.map((i) => {
const index = i + 1;
const full = ratioOf(i) >= 1;
return (
<button
className="kole-m-rate__star"
key={index}
type="button"
role="radio"
aria-checked={full ? 'true' : 'false'}
aria-label={`${index} 星`}
disabled={disabled}
onClick={(e) => {
/* 半星判定:点在左半区取 N-0.5,右半区取 N(整星模式下位置不影响取值) */
const isLeftHalf =
allowHalf && e.target && e.target.getAttribute && e.target.getAttribute('data-half') === 'left';
pick(index, isLeftHalf);
}}
>
<span className="kole-m-rate__glyph" aria-hidden="true">
<span className="kole-m-rate__base">★</span>
<span className="kole-m-rate__fill" style={{ width: ratioOf(i) * 100 + '%' }}>
★
</span>
</span>
{allowHalf ? (
<>
<span className="kole-m-rate__half kole-m-rate__half--left" data-half="left" />
<span className="kole-m-rate__half kole-m-rate__half--right" data-half="right" />
</>
) : null}
</button>
);
})}
</span>
{children || <span className="kole-m-rate__text">{value} 分</span>}
</div>
);
}
frameworks-mobile/Rate.vue2.vue · Vue 2 · 122 行
<template>
<!-- 只读形态:整组是一个 role="img",名称由 aria-label 播报(不留下可聚焦的星) -->
<div
v-if="readonly"
class="kole-m-rate"
:class="rateClass"
role="img"
:aria-label="label + ' ' + value + ' 分(满分 ' + max + ' 分)'"
>
<span class="kole-m-rate__stars" aria-hidden="true">
<span v-for="(i, k) in starIndexes" :key="k" class="kole-m-rate__star">
<span class="kole-m-rate__glyph">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</span>
</span>
</span>
</span>
<slot>
<span class="kole-m-rate__text">{{ value }} 分</span>
</slot>
</div>
<div
v-else
class="kole-m-rate"
:class="rateClass"
role="radiogroup"
:aria-label="label"
:aria-disabled="disabled ? 'true' : null"
@keydown="onKeyDown"
>
<span class="kole-m-rate__stars">
<button
v-for="(i, k) in starIndexes"
:key="k"
class="kole-m-rate__star"
type="button"
role="radio"
:aria-checked="ratioOf(i) >= 1 ? 'true' : 'false'"
:aria-label="i + 1 + ' 星'"
:disabled="disabled"
@click="pick(i, $event)"
>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</span>
</span>
<template v-if="allowHalf">
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</template>
</button>
</span>
<slot>
<span class="kole-m-rate__text">{{ value }} 分</span>
</slot>
</div>
</template>
<script>
/* 评分(移动端)— 规格 §36
点第 N 颗星取 N 分;allowHalf=true 时点某颗星的左半取 N-0.5 分(规则见规格 §36.5)。
触屏上没有鼠标悬停预览,所以「已选中的部分」靠填充比例表达(半星 = 左半填充),
不能只靠颜色深浅;星形本体视觉 24px,热区撑到 44px 高。
readonly=true 时是纯展示(用 role="img" + aria-label 播报「N 分(满分 M 分)」),
此时不渲染任何可聚焦控件 —— 列表里一行一个评分不该抢走键盘序列。 */
export default {
name: 'KoleMRate',
props: {
value: { type: Number, default: 0 },
max: { type: Number, default: 5 },
allowHalf: { type: Boolean, default: false },
size: { type: String, default: 'default' },
readonly: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
label: { type: String, default: '评分' }
},
computed: {
starIndexes: function () {
var out = [];
for (var i = 0; i < this.max; i++) out.push(i);
return out;
},
rateClass: function () {
return [
this.allowHalf ? 'kole-m-rate--half' : '',
this.size === 'small' ? 'kole-m-rate--small' : '',
this.readonly ? 'kole-m-rate--readonly' : '',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
}
},
methods: {
ratioOf: function (i) {
return Math.max(0, Math.min(1, this.value - i));
},
pick: function (i, e) {
if (this.readonly || this.disabled) return;
/* 半星判定:点在左半区取 N-0.5,右半区取 N(整星模式下位置不影响取值) */
var el = e.target && e.target.getAttribute ? e.target : null;
var isLeftHalf = this.allowHalf && el && el.getAttribute('data-half') === 'left';
var index = i + 1;
this.$emit('change', isLeftHalf ? index - 0.5 : index);
},
onKeyDown: function (e) {
if (this.readonly || this.disabled) return;
var delta =
e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : 0;
if (!delta) return;
e.preventDefault();
var stepValue = this.allowHalf ? 0.5 : 1;
var min = this.allowHalf ? 0.5 : 1;
var next = Math.round((this.value + delta * stepValue) * 2) / 2;
this.$emit('change', Math.max(min, Math.min(this.max, next)));
}
}
};
</script>
<style src="./Rate.css"></style>
frameworks-mobile/Rate.vue3.vue · Vue 3 · 113 行
<template>
<!-- 只读形态:整组是一个 role="img",名称由 aria-label 播报(不留下可聚焦的星) -->
<div
v-if="readonly"
class="kole-m-rate"
:class="rateClass"
role="img"
:aria-label="`${label} ${value} 分(满分 ${max} 分)`"
>
<span class="kole-m-rate__stars" aria-hidden="true">
<span v-for="(i, k) in starIndexes" :key="k" class="kole-m-rate__star">
<span class="kole-m-rate__glyph">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</span>
</span>
</span>
</span>
<slot>
<span class="kole-m-rate__text">{{ value }} 分</span>
</slot>
</div>
<div
v-else
class="kole-m-rate"
:class="rateClass"
role="radiogroup"
:aria-label="label"
:aria-disabled="disabled ? 'true' : null"
@keydown="onKeyDown"
>
<span class="kole-m-rate__stars">
<button
v-for="(i, k) in starIndexes"
:key="k"
class="kole-m-rate__star"
type="button"
role="radio"
:aria-checked="ratioOf(i) >= 1 ? 'true' : 'false'"
:aria-label="(i + 1) + ' 星'"
:disabled="disabled"
@click="pick(i, $event)"
>
<span class="kole-m-rate__glyph" aria-hidden="true">
<span class="kole-m-rate__base">★</span>
<span class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</span>
</span>
<template v-if="allowHalf">
<span class="kole-m-rate__half kole-m-rate__half--left" data-half="left"></span>
<span class="kole-m-rate__half kole-m-rate__half--right" data-half="right"></span>
</template>
</button>
</span>
<slot>
<span class="kole-m-rate__text">{{ value }} 分</span>
</slot>
</div>
</template>
<script setup>
/* 评分(移动端)— 规格 §36
点第 N 颗星取 N 分;allowHalf=true 时点某颗星的左半取 N-0.5 分(规则见规格 §36.5)。
触屏上没有鼠标悬停预览,所以「已选中的部分」靠填充比例表达(半星 = 左半填充),
不能只靠颜色深浅;星形本体视觉 24px,热区撑到 44px 高。
readonly=true 时是纯展示(用 role="img" + aria-label 播报「N 分(满分 M 分)」),
此时不渲染任何可聚焦控件 —— 列表里一行一个评分不该抢走键盘序列。 */
import { computed } from 'vue';
const props = defineProps({
value: { type: Number, default: 0 },
max: { type: Number, default: 5 },
allowHalf: { type: Boolean, default: false },
size: { type: String, default: 'default' },
readonly: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
label: { type: String, default: '评分' }
});
const emit = defineEmits(['change']);
const starIndexes = computed(() => Array.from({ length: props.max }, (_, i) => i));
const rateClass = computed(() => [
props.allowHalf ? 'kole-m-rate--half' : '',
props.size === 'small' ? 'kole-m-rate--small' : '',
props.readonly ? 'kole-m-rate--readonly' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function ratioOf(i) {
return Math.max(0, Math.min(1, props.value - i));
}
function pick(i, e) {
if (props.readonly || props.disabled) return;
/* 半星判定:点在左半区取 N-0.5,右半区取 N(整星模式下位置不影响取值) */
const el = e.target && e.target.getAttribute ? e.target : null;
const isLeftHalf = props.allowHalf && el && el.getAttribute('data-half') === 'left';
const index = i + 1;
emit('change', isLeftHalf ? index - 0.5 : index);
}
function onKeyDown(e) {
if (props.readonly || props.disabled) return;
const delta = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : 0;
if (!delta) return;
e.preventDefault();
const stepValue = props.allowHalf ? 0.5 : 1;
const min = props.allowHalf ? 0.5 : 1;
const next = Math.round((props.value + delta * stepValue) * 2) / 2;
emit('change', Math.max(min, Math.min(props.max, next)));
}
</script>
<style src="./Rate.css"></style>
frameworks-mobile/Rate.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 199 行
<template>
<!-- 只读形态:整组是一个 role="img",名称由 aria-label 播报(不留下可聚焦的星) -->
<view
v-if="readonly"
class="kole-m-rate"
:class="rateClass"
role="img"
:aria-label="label + ' ' + value + ' 分(满分 ' + max + ' 分)'"
>
<view class="kole-m-rate__stars">
<view v-for="(i, k) in starIndexes" :key="k" class="kole-m-rate__star">
<view class="kole-m-rate__glyph">
<text class="kole-m-rate__base">★</text>
<text class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</text>
</view>
</view>
</view>
<slot>
<text class="kole-m-rate__text">{{ value }} 分</text>
</slot>
</view>
<view
v-else
class="kole-m-rate"
:class="rateClass"
role="radiogroup"
:aria-label="label"
:aria-disabled="disabled ? 'true' : 'false'"
>
<view class="kole-m-rate__stars">
<view
v-for="(i, k) in starIndexes"
:key="k"
class="kole-m-rate__star"
:role="disabled ? '' : 'radio'"
:aria-checked="ratioOf(i) >= 1 ? 'true' : 'false'"
:aria-label="i + 1 + ' 星'"
:aria-disabled="disabled ? 'true' : 'false'"
@tap="pick(i, $event)"
>
<view class="kole-m-rate__glyph">
<text class="kole-m-rate__base">★</text>
<text class="kole-m-rate__fill" :style="{ width: ratioOf(i) * 100 + '%' }">★</text>
</view>
<template v-if="allowHalf">
<!-- uni 端取不到 DOM 的 e.target,半星靠两个分区各自挂处理器(左半取 N-0.5) -->
<view class="kole-m-rate__half kole-m-rate__half--left" @tap.stop="pickLeft(i)"></view>
<view class="kole-m-rate__half kole-m-rate__half--right" @tap.stop="pick(i)"></view>
</template>
</view>
</view>
<slot>
<text class="kole-m-rate__text">{{ value }} 分</text>
</slot>
</view>
</template>
<script setup>
/* uni-app 端 · 评分(移动端)— 规格 §36
跨端差异:
① 星用 view + role="radio"(小程序没有可聚焦的原生 button 语义差异),点击用 @tap;
② 半星判定取不到 DOM 的 e.target,改用 @tap 在左右两个半区视图上的**各自处理器**:
左半区视图自己发 tap 就取 N-0.5,其余一律取 N(这是跨端下唯一可靠的分区方式);
③ 填充比例用内联 width 的百分比(与 H5 同一表达:灰底星 + 品牌色覆盖层);
④ 只读形态不渲染任何可点区域,整组一个 role="img"。
尺寸用 rpx:88rpx = 375pt 下的 44px 触控最小边长,星形 48rpx = 24px。 */
import { computed } from 'vue';
const props = defineProps({
value: { type: Number, default: 0 },
max: { type: Number, default: 5 },
allowHalf: { type: Boolean, default: false },
size: { type: String, default: 'default' },
readonly: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
label: { type: String, default: '评分' }
});
const emit = defineEmits(['change']);
const starIndexes = computed(() => Array.from({ length: props.max }, (_, i) => i));
const rateClass = computed(() => [
props.allowHalf ? 'kole-m-rate--half' : '',
props.size === 'small' ? 'kole-m-rate--small' : '',
props.readonly ? 'kole-m-rate--readonly' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function ratioOf(i) {
return Math.max(0, Math.min(1, props.value - i));
}
/* 星本体被点时取整星(半区视图有自己的处理器,命中它时不会走到这里) */
function pick(i) {
if (props.readonly || props.disabled) return;
emit('change', i + 1);
}
/* 左半区:取 N-0.5(仅 allowHalf) */
function pickLeft(i) {
if (props.readonly || props.disabled || !props.allowHalf) return;
emit('change', i + 1 - 0.5);
}
</script>
<style>
.kole-m-rate {
--kole-m-rate-star: 48rpx; /* 星形字号(= 视觉边长,24px @375pt) */
--kole-m-rate-gap: 16rpx; /* 星与星之间的间距 */
--kole-m-touch-target: 88rpx;
--kole-m-font-size-label: 28rpx;
--kole-m-gutter: 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
min-height: var(--kole-m-touch-target);
padding-left: var(--kole-m-gutter);
padding-right: var(--kole-m-gutter);
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: 32rpx;
}
/* 变体 size=small:紧凑(列表里展示已有评分,配 readonly) */
.kole-m-rate--small { --kole-m-rate-star: 36rpx; }
.kole-m-rate__stars {
flex-shrink: 0;
display: flex;
align-items: center;
}
/* 每颗星撑到 88rpx 高(热区),星形本体只占中间 48rpx */
.kole-m-rate__star {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: var(--kole-m-touch-target);
padding-left: 8rpx;
padding-right: 8rpx;
}
/* 星形本体:灰底 + 品牌色覆盖层(覆盖层宽度 = 填充比例,表达半星) */
.kole-m-rate__glyph {
position: relative;
display: flex;
align-items: center;
font-size: var(--kole-m-rate-star);
line-height: 1;
}
.kole-m-rate__base { color: var(--kole-color-icon-inactive); }
.kole-m-rate__fill {
position: absolute;
top: 0;
left: 0;
overflow: hidden;
white-space: nowrap;
color: var(--kole-color-brand);
}
/* 半星的点击分区:盖在星形本体左右各一半上(仅 allowHalf 时出现) */
.kole-m-rate__half {
display: none;
position: absolute;
top: 0;
bottom: 0;
width: 50%;
}
.kole-m-rate--half .kole-m-rate__half { display: block; }
.kole-m-rate__half--left { left: 0; }
.kole-m-rate__half--right { right: 0; }
.kole-m-rate__text {
flex: 1;
padding-left: 24rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 变体 readonly=true:只读展示(无半星分区、无按下反馈) */
.kole-m-rate--readonly .kole-m-rate__half { display: none; }
/* 状态 disabled:整条置灰 */
.kole-m-rate.is-disabled .kole-m-rate__base,
.kole-m-rate.is-disabled .kole-m-rate__fill { color: var(--kole-color-text-disabled); }
.kole-m-rate.is-disabled .kole-m-rate__text { color: var(--kole-color-text-disabled); }
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 17 条 · 全部通过 报告 2026-09-20 16:50:33
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性
设计契约
components/mobile-rate.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "36 · 评分 Rate",
"confidence": "high",
"slug": "mobile-rate",
"name": "评分 Rate",
"semanticTypeCandidates": [
"rate",
"star-rating",
"score-input"
],
"variantDimensions": [
{
"name": "size",
"values": [
"default",
"small"
]
},
{
"name": "allowHalf",
"values": [
"false",
"true"
]
},
{
"name": "readonly",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"size": "default",
"allowHalf": "false",
"readonly": "false",
"label": "默认(整星可评分,星形 24px)"
},
{
"size": "default",
"allowHalf": "true",
"readonly": "false",
"label": "支持半星(点左半取 N-0.5 分)"
},
{
"size": "small",
"allowHalf": "false",
"readonly": "true",
"label": "行内只读展示(星形 18px,不占键盘序列)"
}
],
"anatomy": {
"rate": "根元素,一行里放进「星组 + 数值文案」",
"stars": "星组,横向排列,只负责布局",
"star": "单颗星,可点区域(原生 button),自身撑满 44px 高",
"glyph": "星形本体(视觉 24px),由灰底星 + 品牌色覆盖层叠成",
"fill": "覆盖层,宽度即该颗星的填充比例(100% 整星 / 50% 半星 / 0% 未选)",
"text": "数值文案,默认「N 分」,可由默认插槽替换"
},
"structurePatterns": {
"size": "default(星形 24px)/ small(星形 18px,行内展示用)",
"allowHalf": "false(只能取整星)/ true(允许半星)",
"readonly": "false(可评分)/ true(只读展示已有评分)"
},
"usageHints": [
"让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分",
"触屏上没有悬停预览:桌面上鼠标划过就能预告「点下去会是几分」,触屏上这个预告不存在",
"填充比例(含半星)必须直接可读,且星形本体只有 24px、热区要补到 44px 高",
"点第 N 颗星取 N 分;allowHalf=true 时点第 N 颗星的左半取 N-0.5 分、右半取 N 分",
"取值后立即触发 change 事件(回传 3.5 这类半星值也是合法输入)"
],
"doNotInvent": [
"评分的业务含义映射(如「4 分以上算好评」)与统计口径",
"拖动选分与悬停预览(触屏没有悬停,拖动容易滑错)",
"评分理由 / 标签的联动采集(由宿主另行组合)",
"异步提交与失败回滚(宿主负责)"
],
"unknowns": [
"分值是否允许与文案一一对应(如 1 分「很差」、5 分「很好」)",
"星形换用图标字体或 SVG 后填充比例的表达是否仍然一致",
"只读形态是否要显示评价条数(当前由宿主用插槽补)"
],
"interaction": [
"**取值规则**:点第 N 颗星取 N 分;allowHalf=true 时点第 N 颗星的**左半**取 N-0.5 分、**右半**取 N 分(半星只在开启该项时存在,默认整星)",
"星形本体视觉 24px(size=small 时 18px),但每颗星的可点区域高度是 44px;横向不做死区,相邻星的边界就是两半分界",
"一次触摸即取值,不需要二次确认;不响应长按与拖动(拖动选分在触屏上容易滑错,规格未纳入)",
"已选中的部分用**填充比例**表达(半星就是左半填充),不能只靠颜色深浅区分",
"取值后立即触发 change 事件(回传 3.5 这类半星值也是合法输入)",
"readonly=true 时不渲染任何可点区域(只读展示不抢键盘序列)"
],
"accessibility": [
"可评分形态:根是 role=\"radiogroup\" + aria-label,每颗星是原生 button + role=\"radio\" + aria-checked",
"星形的名称是「N 星」(aria-label),读屏播报的是分值而不是符号",
"只读形态:整组是一个 role=\"img\" + aria-label(「评分 4.5 分(满分 5 分)」),组内星形 aria-hidden",
"禁用态用原生 disabled 并补 aria-disabled=\"true\"",
"键盘:聚焦后左右方向键步进(整星模式步长 1,半星模式步长 0.5)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "value",
"type": "number",
"default": "0",
"desc": "受控分值,允许半星(3.5)与 0(未评分)(规格 §36.5)",
"required": false
},
{
"name": "max",
"type": "number",
"default": "5",
"desc": "满分星数,决定渲染几颗星(规格 §36.2 stars)",
"required": false
},
{
"name": "allowHalf",
"type": "boolean",
"default": "false",
"desc": "变体 allowHalf:是否允许半星(点左半取 N-0.5 分)(规格 §36.3)",
"required": false
},
{
"name": "size",
"type": "'default' | 'small'",
"default": "'default'",
"desc": "变体 size:星形 24px 还是 18px(行内展示用)(规格 §36.3)",
"required": false
},
{
"name": "readonly",
"type": "boolean",
"default": "false",
"desc": "变体 readonly:只读展示(不渲染可点区域,整组用 role=\"img\" 播报)(规格 §36.3)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:置灰且不可聚焦,点击不改值(规格 §36.4)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "'评分'",
"desc": "无障碍名称,落到 radiogroup / img 的 aria-label(规格 §36.6)",
"required": false
}
],
"events": [
{
"name": "change",
"params": "(value)",
"desc": "点星或键盘步进改值时触发,回传新分值(可含 0.5)(规格 §36.5)"
}
],
"slots": [
{
"name": "default",
"desc": "数值文案位置的自定义内容(如「4.5 分 · 共 128 条评价」),替换默认的「N 分」(规格 §36.2 text)"
}
]
},
"variantClasses": {
"size": {
"default": [],
"small": [
".kole-m-rate--small"
]
},
"allowHalf": {
"false": [],
"true": [
".kole-m-rate--half",
".kole-m-rate__half"
]
},
"readonly": {
"false": [],
"true": [
".kole-m-rate--readonly"
]
}
},
"demos": [
{
"id": "basic",
"group": "01 组件类型",
"title": "基础用法",
"desc": "点第 N 颗即取 N 分;整行 44px 高,星形本体只有 24px。",
"variant": "allowHalf=false"
},
{
"id": "half",
"group": "01 组件类型",
"title": "半星",
"desc": "allowHalf=true:点某颗星的左半取 N-0.5 分、右半取 N 分。",
"variant": "allowHalf=true"
},
{
"id": "size",
"group": "01 组件类型",
"title": "尺寸两档",
"desc": "size=small 星形 18px 用于行内展示;default 24px 用于可评分场景。",
"variant": "size=default|small"
},
{
"id": "readonly",
"group": "02 组件状态",
"title": "只读展示",
"desc": "readonly=true:星形不再是按钮、点击不改值,整组用 role=\"img\" 播报分值。",
"variant": "readonly=true"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "整条置灰且不可聚焦:读屏会播报不可用,点击不改值。",
"variant": "disabled=true"
}
],
"related": [
{
"slug": "mobile-slider",
"why": "连续区间的粗选用 Slider;离散档位(1~5 星)且带语义的用 Rate"
},
{
"slug": "mobile-form",
"why": "评分作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供"
},
{
"slug": "mobile-tag",
"why": "只是标注一个既有结论(如「好评」「差评」)用 Tag;需要用户给出分值时用 Rate"
}
]
}