移动端导航分段器

分段器Segmented

在 2~5 个互斥选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)

导航 规格 38 · 分段器 Segmented 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-segmented.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/Segmented.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

基础用法size=default

三项互斥:选中项是白底滑块 + 品牌字色,靠形态而不是色相区分;容器 data-value 随选中同步。

查看代码(演示页原文 · 15 行)
frameworks-mobile/Segmented.html · basic
<section class="demo-block" data-demo="basic">
  <p class="demo-label">基础用法(三项互斥,点第二项切换;选中项 = 白底滑块 + 品牌字色)</p>
  <div class="demo-box">
    <div class="kole-m-segmented kole-m-segmented--default" id="seg-basic"
         role="radiogroup" aria-label="订单状态" data-assert="segmented-basic" data-value="all">
      <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
              data-value="all">全部</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="todo" data-behavior="click-sets-attr:#seg-basic|data-value|todo">待付款</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="done">已完成</button>
    </div>
    <p class="demo-out">容器上的 data-value 随选中项同步(第 2 项断言为 todo)</p>
  </div>
</section>
等分铺满block=true

block=true:两项等分容器宽度,适合与筛选条同宽;选项少时也不出现半截空白。

查看代码(演示页原文 · 12 行)
frameworks-mobile/Segmented.html · block
<section class="demo-block" data-demo="block">
  <p class="demo-label">等分铺满(block=true:两项等分容器宽度,适合与筛选条同宽)</p>
  <div class="demo-box">
    <div class="kole-m-segmented kole-m-segmented--default kole-m-segmented--block" id="seg-block"
         role="radiogroup" aria-label="出售方式" data-assert="segmented-block" data-value="buy">
      <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
              data-value="buy">我要买</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="sell" data-behavior="click-sets-attr:#seg-block|data-value|sell">我要卖</button>
    </div>
  </div>
</section>
小尺寸size=small

size=small:32px 高,用于卡片内的次级筛选;视觉更矮但命中区仍按 44px 计。

查看代码(演示页原文 · 14 行)
frameworks-mobile/Segmented.html · small
<section class="demo-block" data-demo="small">
  <p class="demo-label">小尺寸(size=small:32px 高,用于卡片内的次级筛选)</p>
  <div class="demo-box">
    <div class="kole-m-segmented kole-m-segmented--small" id="seg-small"
         role="radiogroup" aria-label="时间范围" data-assert="segmented-small" data-value="day">
      <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
              data-value="day">日</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="week" data-behavior="click-sets-attr:#seg-small|data-value|week">周</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="month">月</button>
    </div>
  </div>
</section>

02 组件状态

带图标选项含图标

选项内容可含装饰图标;图标 aria-hidden,语义全部由文字承担,读屏不会读出符号。

查看代码(演示页原文 · 12 行)
frameworks-mobile/Segmented.html · icon-text
<section class="demo-block" data-demo="icon-text">
  <p class="demo-label">带图标(选项内容可含装饰图标,图标 aria-hidden,语义由文字承担)</p>
  <div class="demo-box">
    <div class="kole-m-segmented kole-m-segmented--default" id="seg-icon"
         role="radiogroup" aria-label="视图切换" data-assert="segmented-icon-text" data-value="list">
      <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
              data-value="list"><span aria-hidden="true">☰</span>&nbsp;列表</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="grid" data-behavior="click-sets-attr:#seg-icon|data-value|grid"><span aria-hidden="true">▦</span>&nbsp;网格</button>
    </div>
  </div>
</section>
禁用disabled=true

整段置灰且不响应:选中项也不再高亮,点击与键盘都不改值,读屏播报不可用。

查看代码(演示页原文 · 12 行)
frameworks-mobile/Segmented.html · disabled
<section class="demo-block" data-demo="disabled">
  <p class="demo-label">禁用(整段置灰且不响应,选中项也不再高亮;点击与键盘都不改值)</p>
  <div class="demo-box">
    <div class="kole-m-segmented kole-m-segmented--default is-disabled" role="radiogroup"
         aria-label="已锁定的范围" aria-disabled="true" data-assert="segmented-disabled">
      <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
              data-value="now" disabled>本期</button>
      <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
              data-value="next" disabled>下期</button>
    </div>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
valuestring''当前选中值(受控;与选项的 data-value 对应)(规格 §38.5)N
optionsArray<{ value, label, disabled? }>[]选项列表,2~5 个(规格 §38.2 item)N
size'default' | 'small''default'变体 size:default 44px / small 32px(规格 §38.3)N
blockbooleanfalse变体 block:占满容器且各项等分(规格 §38.3)N
disabledbooleanfalse整段禁用(规格 §38.4)N
labelstring'分段选择'这组选项在选什么的说明,落到 aria-label(规格 §38.6)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
change(value: string)选中项变化时回传目标值;值未变不发事件(规格 §38.5)

插槽

名称说明

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
--kole-m-segmented-heightvar(--kole-m-touch-target)组件内部默认值,可在业务侧覆盖
--kole-m-segmented-padding2px组件内部默认值,可在业务侧覆盖
--kole-m-segmented-radiusvar(--kole-radius-base)组件内部默认值,可在业务侧覆盖

何时使用

  • 在 2~5 个互斥选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)
  • 触屏没有悬停预告,所以选中项必须靠形态(白底滑块 + 阴影)而非色相区分
  • 选中态是视觉与属性的双向同步:除类名外必须同时更新 aria-checked
  • 选中项再次点击不重复触发 change(值未变不发事件)
  • 受控:组件不存值,只回传目标值 change;宿主不采纳时视觉不变化

交互与触控

  • 选项切换是一次轻点:点击后该项 is-active、同组其它项复位;不响应长按、双击与拖动
  • 选中态是视觉与属性的双向同步:除类名外必须同时更新 aria-checked
  • 每项热区高 ≥ 44px(size=small 时视觉 32px,但触摸命中区仍按 44px 计,纵向不留死区)
  • 选中项再次点击不重复触发 change(值未变不发事件)
  • 切换动效 120ms(--kole-duration-fast),prefers-reduced-motion 下瞬时切换

无障碍

  • 容器 role="radiogroup" + aria-label;每个选项原生 button + role="radio" + aria-checked
  • 选项名称由文字承担;装饰性图标必须 aria-hidden="true"
  • 禁用时容器补 aria-disabled="true",选项用原生 disabled
  • 键盘:Tab 进入分组,左右方向键在选项间移动并选中

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
底部标签栏TabBar切换的是页面级内容区且选项 ≥ 4 个时用底部标签栏,分段器只用于同一区块内的轻量切换
标签Tag多选筛选条件用标签组(可同时选中多个),分段器只表达单选
单选框Radio选项需要文字说明或纵向排列时用单选框列表,横向挤在一行才用分段器

规格未定 / 禁止发明

类别条目
禁止发明多选(同时选中多个)—— 需要多选时改用标签组或多选框
禁止发明选项的横向滚动、换行与「更多」折叠(超过 5 个应换组件)
禁止发明选中项的下划线滑块动画(那是标签栏的视觉语言,不是分段器)
禁止发明选项禁用条件与业务权限的判断
规格未定选项数量上限是否应硬约束在 5 个(当前只写建议,不做运行时拦截)
规格未定size=small 的命中区是否需要在纵向自动补到 44px
规格未定是否需要「滑动经过即选中」(当前只认轻点,滑动不选中)

结构(anatomy)

字段说明
segmented容器,浅底 + 内边距,横向排列选项;role="radiogroup" 并带 aria-label 说明这组在选什么
item单个选项,原生 button + role="radio"(整项可点、可键盘聚焦),data-value 携带取值
label选项文字(可含装饰图标,图标 aria-hidden 由文字承担语义)

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
sizedefault / small
default .kole-m-segmented--default
small .kole-m-segmented--small
blockfalse / true
false (由数据驱动,无专属类)
true .kole-m-segmented--block

代表变体

变体标签
size=default · block=false三项基础(44px 高,宽度随内容)
size=default · block=true等分铺满(与筛选条同宽)
size=small · block=false小尺寸(32px 高,卡片内次级筛选)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-font-size-label --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-card-bg --kole-color-disabled-bg --kole-color-focus-ring --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-secondary --kole-duration-fast --kole-ease-standard --kole-font-family --kole-radius-base --kole-shadow-low --kole-space-12 --kole-space-16 --kole-m-segmented-height --kole-m-segmented-padding --kole-m-segmented-radius

6 端源码

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

frameworks-mobile/Segmented.css · 纯样式(CSS) · 92 行
frameworks-mobile/Segmented.css
/* Kole UI Mobile · Segmented 样式 — 对齐移动端规格 §38
   分段器:一段容器里排列 2~5 个互斥选项,选中项用「白色滑块 + 品牌字色」表达。
   容器浅底、选中项卡片底:这样选中态在浅色与暗色下都是「亮起来」而不是靠色相变化,
   色觉障碍用户也能分辨。热区:每项高度 ≥ 44px(small 档 32px 时靠 hit-slack 补齐到 44px)。 */

.kole-m-segmented {
  --kole-m-segmented-height: var(--kole-m-touch-target);
  --kole-m-segmented-padding: 2px;
  --kole-m-segmented-radius: var(--kole-radius-base);
  position: relative;
  box-sizing: border-box;
  display: inline-flex;
  align-items: stretch;
  padding: var(--kole-m-segmented-padding);
  border-radius: var(--kole-m-segmented-radius);
  background: var(--kole-color-table-header-bg);
  color: var(--kole-color-text-body);
  font-family: var(--kole-font-family);
  font-size: var(--kole-m-font-size-label);
  line-height: 1;
}

/* 变体 block=true:占满容器宽度,各项等分 */
.kole-m-segmented--block {
  display: flex;
  width: 100%;
}

.kole-m-segmented--block .kole-m-segmented__item { flex: 1 1 0; }

/* 变体 size:两档高度 */
.kole-m-segmented--default { --kole-m-segmented-height: var(--kole-m-touch-target); }
.kole-m-segmented--small { --kole-m-segmented-height: calc(var(--kole-m-touch-target) - var(--kole-space-12)); }

.kole-m-segmented__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 0;
  height: calc(var(--kole-m-segmented-height) - 2 * var(--kole-m-segmented-padding));
  margin: 0;
  padding: 0 var(--kole-space-16);
  border: 0;
  border-radius: calc(var(--kole-m-segmented-radius) - 1px);
  background: none;
  color: var(--kole-color-text-secondary);
  font-family: inherit;
  font-size: inherit;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color var(--kole-duration-fast) var(--kole-ease-standard),
    color var(--kole-duration-fast) var(--kole-ease-standard);
}

.kole-m-segmented__item:active { background: var(--kole-color-border); }

/* 状态 selected:白色滑块 + 品牌字色(暗色下卡片底自然抬升,规则同款) */
.kole-m-segmented__item.is-active {
  background: var(--kole-color-card-bg);
  color: var(--kole-color-brand);
  font-weight: 500;
  box-shadow: var(--kole-shadow-low);
}

.kole-m-segmented__item:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: -2px;
}

/* 状态 disabled:整段置灰,点击与键盘都不改值 */
.kole-m-segmented.is-disabled .kole-m-segmented__item,
.kole-m-segmented__item:disabled {
  color: var(--kole-color-text-disabled);
  cursor: not-allowed;
}

.kole-m-segmented.is-disabled .kole-m-segmented__item.is-active {
  background: var(--kole-color-disabled-bg);
  color: var(--kole-color-text-disabled);
  box-shadow: none;
}

.kole-m-segmented.is-disabled .kole-m-segmented__item:active { background: none; }
.kole-m-segmented.is-disabled .kole-m-segmented__item.is-active:active { background: var(--kole-color-disabled-bg); }

@media (prefers-reduced-motion: reduce) {
  .kole-m-segmented__item { transition: none; }
}
frameworks-mobile/Segmented.html · H5 原生(无框架) · 144 行
frameworks-mobile/Segmented.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 · Segmented(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Segmented.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-box { padding: var(--kole-space-12) var(--kole-m-gutter); background: var(--kole-color-card-bg);
    border-block: 1px solid var(--kole-color-border); }
  .demo-out { margin: var(--kole-space-8) 0 0; font-size: var(--kole-m-font-size-caption);
    color: var(--kole-color-text-tertiary); }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="basic">
    <p class="demo-label">基础用法(三项互斥,点第二项切换;选中项 = 白底滑块 + 品牌字色)</p>
    <div class="demo-box">
      <div class="kole-m-segmented kole-m-segmented--default" id="seg-basic"
           role="radiogroup" aria-label="订单状态" data-assert="segmented-basic" data-value="all">
        <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
                data-value="all">全部</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="todo" data-behavior="click-sets-attr:#seg-basic|data-value|todo">待付款</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="done">已完成</button>
      </div>
      <p class="demo-out">容器上的 data-value 随选中项同步(第 2 项断言为 todo)</p>
    </div>
  </section>

  <section class="demo-block" data-demo="block">
    <p class="demo-label">等分铺满(block=true:两项等分容器宽度,适合与筛选条同宽)</p>
    <div class="demo-box">
      <div class="kole-m-segmented kole-m-segmented--default kole-m-segmented--block" id="seg-block"
           role="radiogroup" aria-label="出售方式" data-assert="segmented-block" data-value="buy">
        <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
                data-value="buy">我要买</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="sell" data-behavior="click-sets-attr:#seg-block|data-value|sell">我要卖</button>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="small">
    <p class="demo-label">小尺寸(size=small:32px 高,用于卡片内的次级筛选)</p>
    <div class="demo-box">
      <div class="kole-m-segmented kole-m-segmented--small" id="seg-small"
           role="radiogroup" aria-label="时间范围" data-assert="segmented-small" data-value="day">
        <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
                data-value="day">日</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="week" data-behavior="click-sets-attr:#seg-small|data-value|week">周</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="month">月</button>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="icon-text">
    <p class="demo-label">带图标(选项内容可含装饰图标,图标 aria-hidden,语义由文字承担)</p>
    <div class="demo-box">
      <div class="kole-m-segmented kole-m-segmented--default" id="seg-icon"
           role="radiogroup" aria-label="视图切换" data-assert="segmented-icon-text" data-value="list">
        <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
                data-value="list"><span aria-hidden="true">☰</span>&nbsp;列表</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="grid" data-behavior="click-sets-attr:#seg-icon|data-value|grid"><span aria-hidden="true">▦</span>&nbsp;网格</button>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="disabled">
    <p class="demo-label">禁用(整段置灰且不响应,选中项也不再高亮;点击与键盘都不改值)</p>
    <div class="demo-box">
      <div class="kole-m-segmented kole-m-segmented--default is-disabled" role="radiogroup"
           aria-label="已锁定的范围" aria-disabled="true" data-assert="segmented-disabled">
        <button class="kole-m-segmented__item is-active" type="button" role="radio" aria-checked="true"
                data-value="now" disabled>本期</button>
        <button class="kole-m-segmented__item" type="button" role="radio" aria-checked="false"
                data-value="next" disabled>下期</button>
      </div>
    </div>
  </section>
</div>
<script>
  /* 演示页交互:点任一分段项 → 该项 is-active / aria-checked=true,同组其它项复位,
     并把选中值写回容器的 data-value(业务里从这里读值)。
     选中态是**视觉 + 属性**双向同步:只改颜色不改 aria-checked 会被无障碍断言判为缺口。 */
  (function () {
    function select(btn) {
      var group = btn.parentElement;
      var items = group.querySelectorAll('.kole-m-segmented__item');
      Array.prototype.forEach.call(items, function (it) {
        var on = it === btn;
        it.classList.toggle('is-active', on);
        it.setAttribute('aria-checked', on ? 'true' : 'false');
      });
      group.setAttribute('data-value', btn.getAttribute('data-value'));
      var out = group.parentElement ? group.parentElement.querySelector('.demo-out') : null;
      if (out) out.textContent = '当前选中:' + btn.getAttribute('data-value');
    }
    document.querySelectorAll('.kole-m-segmented').forEach(function (group) {
      group.addEventListener('click', function (e) {
        var btn = e.target.closest ? e.target.closest('.kole-m-segmented__item') : null;
        if (!btn || !group.contains(btn)) return;
        if (btn.disabled || group.classList.contains('is-disabled')) return;
        select(btn);
      });
    });
  })();
</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');
    var demoBox = document.querySelector('.demo');
    if (demoBox) demoBox.style.minHeight = 'auto';
    blocks.forEach(function (b) {
      var label = b.querySelector('.demo-label');
      if (label && !b.hidden) label.hidden = true;
    });
  })();
</script>
</body>
</html>
frameworks-mobile/Segmented.jsx · React · 49 行
frameworks-mobile/Segmented.jsx
import React from 'react';
import './Segmented.css';

/* 分段器(移动端)— 规格 §38
   2~5 个互斥选项横排,选中项是「白色滑块 + 品牌字色」,不是靠色相区分。
   受控:本端不存选中值,只回传目标值 onChange,是否采纳由宿主决定。
   每个选项是原生 button(整项可点、可键盘聚焦),role=radio 表达互斥语义。 */
export default function Segmented({
  value = '',
  options = [],
  size = 'default',
  block = false,
  disabled = false,
  label = '分段选择',
  onChange,
}) {
  const cls =
    'kole-m-segmented' +
    ` kole-m-segmented--${size}` +
    (block ? ' kole-m-segmented--block' : '') +
    (disabled ? ' is-disabled' : '');

  return (
    <div className={cls} role="radiogroup" aria-label={label} aria-disabled={disabled ? 'true' : undefined}>
      {options.map((op) => {
        const active = op.value === value;
        const off = disabled || !!op.disabled;
        return (
          <button
            key={op.value}
            className={'kole-m-segmented__item' + (active ? ' is-active' : '')}
            type="button"
            role="radio"
            aria-checked={active ? 'true' : 'false'}
            data-value={op.value}
            disabled={off}
            onClick={() => {
              if (off || active) return;
              if (onChange) onChange(op.value);
            }}
          >
            {op.label}
          </button>
        );
      })}
    </div>
  );
}
frameworks-mobile/Segmented.vue2.vue · Vue 2 · 49 行
frameworks-mobile/Segmented.vue2.vue
<template>
  <div class="kole-m-segmented" :class="segClass" role="radiogroup" :aria-label="label"
       :aria-disabled="disabled ? 'true' : null">
    <button
      v-for="op in options"
      :key="op.value"
      class="kole-m-segmented__item"
      :class="{ 'is-active': op.value === value }"
      type="button"
      role="radio"
      :aria-checked="op.value === value ? 'true' : 'false'"
      :data-value="op.value"
      :disabled="disabled || !!op.disabled"
      @click="onItemClick(op)"
    >{{ op.label }}</button>
  </div>
</template>

<script>
export default {
  name: 'KoleMSegmented',
  props: {
    value: { type: String, default: '' },
    options: { type: Array, default: function () { return []; } },
    size: { type: String, default: 'default' },
    block: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    label: { type: String, default: '分段选择' }
  },
  computed: {
    segClass: function () {
      return [
        'kole-m-segmented--' + this.size,
        this.block ? 'kole-m-segmented--block' : '',
        this.disabled ? 'is-disabled' : ''
      ].filter(Boolean);
    }
  },
  methods: {
    onItemClick: function (op) {
      if (this.disabled || op.disabled || op.value === this.value) return;
      this.$emit('change', op.value);
    }
  }
};
</script>

<style src="./Segmented.css"></style>
frameworks-mobile/Segmented.vue3.vue · Vue 3 · 45 行
frameworks-mobile/Segmented.vue3.vue
<template>
  <div class="kole-m-segmented" :class="segClass" role="radiogroup" :aria-label="label"
       :aria-disabled="disabled ? 'true' : null">
    <button
      v-for="op in options"
      :key="op.value"
      class="kole-m-segmented__item"
      :class="{ 'is-active': op.value === value }"
      type="button"
      role="radio"
      :aria-checked="op.value === value ? 'true' : 'false'"
      :data-value="op.value"
      :disabled="disabled || !!op.disabled"
      @click="onItemClick(op)"
    >{{ op.label }}</button>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  value: { type: String, default: '' },
  options: { type: Array, default: () => [] },
  size: { type: String, default: 'default' },
  block: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  label: { type: String, default: '分段选择' }
});
const emit = defineEmits(['change']);

const segClass = computed(() => [
  `kole-m-segmented--${props.size}`,
  props.block ? 'kole-m-segmented--block' : '',
  props.disabled ? 'is-disabled' : ''
].filter(Boolean));

function onItemClick(op) {
  if (props.disabled || op.disabled || op.value === props.value) return;
  emit('change', op.value);
}
</script>

<style src="./Segmented.css"></style>
frameworks-mobile/Segmented.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 101 行
frameworks-mobile/Segmented.uniapp.vue
<template>
  <view class="kole-m-segmented" :class="segClass" :role="'radiogroup'" :aria-label="label"
        :aria-disabled="disabled ? 'true' : 'false'">
    <view
      v-for="op in options"
      :key="op.value"
      class="kole-m-segmented__item"
      :class="{ 'is-active': op.value === value }"
      role="radio"
      :aria-checked="op.value === value ? 'true' : 'false'"
      :aria-disabled="disabled || op.disabled ? 'true' : 'false'"
      :data-value="op.value"
      @tap="onItemTap(op)"
    >
      <text class="kole-m-segmented__label">{{ op.label }}</text>
    </view>
  </view>
</template>

<script setup>
/* uni-app 端 · 分段器(移动端)— 规格 §38
   跨端差异:选项用 view + role="radio"(小程序没有 button 的原生 radio 语义),
   点击用 @tap;尺寸用 rpx(2rpx ≈ 1px,88rpx = 44px 触控最小边长)。
   受控:本端不存选中值,只回传 change。 */
import { computed } from 'vue';

const props = defineProps({
  value: { type: String, default: '' },
  options: { type: Array, default: () => [] },
  size: { type: String, default: 'default' },
  block: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  label: { type: String, default: '分段选择' }
});
const emit = defineEmits(['change']);

const segClass = computed(() => [
  `kole-m-segmented--${props.size}`,
  props.block ? 'kole-m-segmented--block' : '',
  props.disabled ? 'is-disabled' : ''
].filter(Boolean));

function onItemTap(op) {
  if (props.disabled || op.disabled || op.value === props.value) return;
  emit('change', op.value);
}
</script>

<style>
.kole-m-segmented {
  --kole-m-segmented-height: 88rpx;
  --kole-m-segmented-padding: 4rpx;
  --kole-m-segmented-radius: 8rpx;
  --kole-m-font-size-label: 28rpx;
  position: relative;
  box-sizing: border-box;
  display: flex;
  align-items: stretch;
  padding: var(--kole-m-segmented-padding);
  border-radius: var(--kole-m-segmented-radius);
  background-color: var(--kole-color-table-header-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-segmented--block { width: 100%; }

.kole-m-segmented--block .kole-m-segmented__item { flex: 1; }

.kole-m-segmented--default { --kole-m-segmented-height: 88rpx; }
.kole-m-segmented--small { --kole-m-segmented-height: 64rpx; }

.kole-m-segmented__item {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 0;
  height: var(--kole-m-segmented-height);
  padding: 0 32rpx;
  border-radius: 6rpx;
  color: var(--kole-color-text-secondary);
}

.kole-m-segmented__label { line-height: 1; }

.kole-m-segmented__item.is-active {
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-brand);
}

.kole-m-segmented.is-disabled .kole-m-segmented__item {
  color: var(--kole-color-text-disabled);
}

.kole-m-segmented.is-disabled .kole-m-segmented__item.is-active {
  background-color: var(--kole-color-disabled-bg);
  color: var(--kole-color-text-disabled);
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 19 条 · 全部通过 报告 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-segmented.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "38 · 分段器 Segmented",
  "confidence": "high",
  "slug": "mobile-segmented",
  "name": "分段器 Segmented",
  "semanticTypeCandidates": [
    "segmented-control",
    "tab-switcher",
    "radio-group"
  ],
  "variantDimensions": [
    {
      "name": "size",
      "values": [
        "default",
        "small"
      ]
    },
    {
      "name": "block",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "size": "default",
      "block": "false",
      "label": "三项基础(44px 高,宽度随内容)"
    },
    {
      "size": "default",
      "block": "true",
      "label": "等分铺满(与筛选条同宽)"
    },
    {
      "size": "small",
      "block": "false",
      "label": "小尺寸(32px 高,卡片内次级筛选)"
    }
  ],
  "anatomy": {
    "segmented": "容器,浅底 + 内边距,横向排列选项;role=\"radiogroup\" 并带 aria-label 说明这组在选什么",
    "item": "单个选项,原生 button + role=\"radio\"(整项可点、可键盘聚焦),data-value 携带取值",
    "label": "选项文字(可含装饰图标,图标 aria-hidden 由文字承担语义)"
  },
  "structurePatterns": {
    "size": "default 44px / small 32px(卡片内次级筛选)",
    "block": "false 宽度随内容 / true 占满容器且各项等分",
    "状态类": "is-active 选中 / is-disabled 整段禁用"
  },
  "usageHints": [
    "在 2~5 个互斥选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)",
    "触屏没有悬停预告,所以选中项必须靠形态(白底滑块 + 阴影)而非色相区分",
    "选中态是视觉与属性的双向同步:除类名外必须同时更新 aria-checked",
    "选中项再次点击不重复触发 change(值未变不发事件)",
    "受控:组件不存值,只回传目标值 change;宿主不采纳时视觉不变化"
  ],
  "doNotInvent": [
    "多选(同时选中多个)—— 需要多选时改用标签组或多选框",
    "选项的横向滚动、换行与「更多」折叠(超过 5 个应换组件)",
    "选中项的下划线滑块动画(那是标签栏的视觉语言,不是分段器)",
    "选项禁用条件与业务权限的判断"
  ],
  "unknowns": [
    "选项数量上限是否应硬约束在 5 个(当前只写建议,不做运行时拦截)",
    "size=small 的命中区是否需要在纵向自动补到 44px",
    "是否需要「滑动经过即选中」(当前只认轻点,滑动不选中)"
  ],
  "interaction": [
    "选项切换是一次轻点:点击后该项 is-active、同组其它项复位;不响应长按、双击与拖动",
    "选中态是视觉与属性的双向同步:除类名外必须同时更新 aria-checked",
    "每项热区高 ≥ 44px(size=small 时视觉 32px,但触摸命中区仍按 44px 计,纵向不留死区)",
    "选中项再次点击不重复触发 change(值未变不发事件)",
    "切换动效 120ms(--kole-duration-fast),prefers-reduced-motion 下瞬时切换"
  ],
  "accessibility": [
    "容器 role=\"radiogroup\" + aria-label;每个选项原生 button + role=\"radio\" + aria-checked",
    "选项名称由文字承担;装饰性图标必须 aria-hidden=\"true\"",
    "禁用时容器补 aria-disabled=\"true\",选项用原生 disabled",
    "键盘:Tab 进入分组,左右方向键在选项间移动并选中"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "value",
        "type": "string",
        "default": "''",
        "desc": "当前选中值(受控;与选项的 data-value 对应)(规格 §38.5)",
        "required": false
      },
      {
        "name": "options",
        "type": "Array<{ value, label, disabled? }>",
        "default": "[]",
        "desc": "选项列表,2~5 个(规格 §38.2 item)",
        "required": false
      },
      {
        "name": "size",
        "type": "'default' | 'small'",
        "default": "'default'",
        "desc": "变体 size:default 44px / small 32px(规格 §38.3)",
        "required": false
      },
      {
        "name": "block",
        "type": "boolean",
        "default": "false",
        "desc": "变体 block:占满容器且各项等分(规格 §38.3)",
        "required": false
      },
      {
        "name": "disabled",
        "type": "boolean",
        "default": "false",
        "desc": "整段禁用(规格 §38.4)",
        "required": false
      },
      {
        "name": "label",
        "type": "string",
        "default": "'分段选择'",
        "desc": "这组选项在选什么的说明,落到 aria-label(规格 §38.6)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "change",
        "params": "(value: string)",
        "desc": "选中项变化时回传目标值;值未变不发事件(规格 §38.5)"
      }
    ],
    "slots": []
  },
  "variantClasses": {
    "size": {
      "default": [
        ".kole-m-segmented--default"
      ],
      "small": [
        ".kole-m-segmented--small"
      ]
    },
    "block": {
      "false": [],
      "true": [
        ".kole-m-segmented--block"
      ]
    }
  },
  "demos": [
    {
      "id": "basic",
      "group": "01 组件类型",
      "title": "基础用法",
      "desc": "三项互斥:选中项是白底滑块 + 品牌字色,靠形态而不是色相区分;容器 data-value 随选中同步。",
      "variant": "size=default"
    },
    {
      "id": "block",
      "group": "01 组件类型",
      "title": "等分铺满",
      "desc": "block=true:两项等分容器宽度,适合与筛选条同宽;选项少时也不出现半截空白。",
      "variant": "block=true"
    },
    {
      "id": "small",
      "group": "01 组件类型",
      "title": "小尺寸",
      "desc": "size=small:32px 高,用于卡片内的次级筛选;视觉更矮但命中区仍按 44px 计。",
      "variant": "size=small"
    },
    {
      "id": "icon-text",
      "group": "02 组件状态",
      "title": "带图标",
      "desc": "选项内容可含装饰图标;图标 aria-hidden,语义全部由文字承担,读屏不会读出符号。",
      "variant": "选项含图标"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "禁用",
      "desc": "整段置灰且不响应:选中项也不再高亮,点击与键盘都不改值,读屏播报不可用。",
      "variant": "disabled=true"
    }
  ],
  "related": [
    {
      "slug": "tabbar",
      "why": "切换的是页面级内容区且选项 ≥ 4 个时用底部标签栏,分段器只用于同一区块内的轻量切换"
    },
    {
      "slug": "mobile-tag",
      "why": "多选筛选条件用标签组(可同时选中多个),分段器只表达单选"
    },
    {
      "slug": "mobile-radio",
      "why": "选项需要文字说明或纵向排列时用单选框列表,横向挤在一行才用分段器"
    }
  ]
}