移动端导航评分

评分Rate

让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分

数据录入 规格 36 · 评分 Rate 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌: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 组件类型

基础用法allowHalf=false

点第 N 颗即取 N 分;整行 44px 高,星形本体只有 24px。

查看代码(演示页原文 · 42 行)
frameworks-mobile/Rate.html · basic
<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

allowHalf=true:点某颗星的左半取 N-0.5 分、右半取 N 分。

查看代码(演示页原文 · 52 行)
frameworks-mobile/Rate.html · half
<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=default|small

size=small 星形 18px 用于行内展示;default 24px 用于可评分场景。

查看代码(演示页原文 · 45 行)
frameworks-mobile/Rate.html · size
<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

readonly=true:星形不再是按钮、点击不改值,整组用 role="img" 播报分值。

查看代码(演示页原文 · 25 行)
frameworks-mobile/Rate.html · readonly
<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>
禁用disabled=true

整条置灰且不可聚焦:读屏会播报不可用,点击不改值。

查看代码(演示页原文 · 41 行)
frameworks-mobile/Rate.html · disabled
<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

名称类型默认值说明必传
valuenumber0受控分值,允许半星(3.5)与 0(未评分)(规格 §36.5)N
maxnumber5满分星数,决定渲染几颗星(规格 §36.2 stars)N
allowHalfbooleanfalse变体 allowHalf:是否允许半星(点左半取 N-0.5 分)(规格 §36.3)N
size'default' | 'small''default'变体 size:星形 24px 还是 18px(行内展示用)(规格 §36.3)N
readonlybooleanfalse变体 readonly:只读展示(不渲染可点区域,整组用 role="img" 播报)(规格 §36.3)N
disabledbooleanfalse状态 disabled:置灰且不可聚焦,点击不改值(规格 §36.4)N
labelstring'评分'无障碍名称,落到 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-star24px星形字号(= 视觉边长)
--kole-m-rate-gap8px星与星之间的间距

何时使用

  • 让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分
  • 触屏上没有悬停预览:桌面上鼠标划过就能预告「点下去会是几分」,触屏上这个预告不存在
  • 填充比例(含半星)必须直接可读,且星形本体只有 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
sizedefault / small
default (由数据驱动,无专属类)
small .kole-m-rate--small
allowHalffalse / true
false (由数据驱动,无专属类)
true .kole-m-rate--half .kole-m-rate__half
readonlyfalse / 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 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-m-touch-target --kole-color-brand --kole-color-card-bg --kole-color-focus-ring --kole-color-icon-inactive --kole-color-text-body --kole-color-text-disabled --kole-color-text-secondary --kole-space-8 --kole-m-rate-gap --kole-m-rate-star

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/Rate.css · 纯样式(CSS) · 125 行
frameworks-mobile/Rate.css
/* 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 行
frameworks-mobile/Rate.html
<!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 行
frameworks-mobile/Rate.jsx
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 行
frameworks-mobile/Rate.vue2.vue
<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 行
frameworks-mobile/Rate.vue3.vue
<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 行
frameworks-mobile/Rate.uniapp.vue
<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"
    }
  ]
}