分段器Segmented
在 2~5 个互斥选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)
导航 规格 38 · 分段器 Segmented 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-segmented.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Segmented.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
三项互斥:选中项是白底滑块 + 品牌字色,靠形态而不是色相区分;容器 data-value 随选中同步。
查看代码(演示页原文 · 15 行)
<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:两项等分容器宽度,适合与筛选条同宽;选项少时也不出现半截空白。
查看代码(演示页原文 · 12 行)
<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:32px 高,用于卡片内的次级筛选;视觉更矮但命中区仍按 44px 计。
查看代码(演示页原文 · 14 行)
<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 行)
<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> 列表</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> 网格</button>
</div>
</div>
</section>整段置灰且不响应:选中项也不再高亮,点击与键盘都不改值,读屏播报不可用。
查看代码(演示页原文 · 12 行)
<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
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
value | string | '' | 当前选中值(受控;与选项的 data-value 对应)(规格 §38.5) | N |
options | Array<{ value, label, disabled? }> | [] | 选项列表,2~5 个(规格 §38.2 item) | N |
size | 'default' | 'small' | 'default' | 变体 size:default 44px / small 32px(规格 §38.3) | N |
block | boolean | false | 变体 block:占满容器且各项等分(规格 §38.3) | N |
disabled | boolean | false | 整段禁用(规格 §38.4) | N |
label | string | '分段选择' | 这组选项在选什么的说明,落到 aria-label(规格 §38.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
change | (value: string) | 选中项变化时回传目标值;值未变不发事件(规格 §38.5) |
插槽
| 名称 | 说明 |
|---|
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-segmented-height | var(--kole-m-touch-target) | 组件内部默认值,可在业务侧覆盖 |
--kole-m-segmented-padding | 2px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-segmented-radius | var(--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 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | default / small | default .kole-m-segmented--default small .kole-m-segmented--small |
block | false / true | false (由数据驱动,无专属类) true .kole-m-segmented--block |
代表变体
| 变体 | 标签 |
|---|---|
size=default · block=false | 三项基础(44px 高,宽度随内容) |
size=default · block=true | 等分铺满(与筛选条同宽) |
size=small · block=false | 小尺寸(32px 高,卡片内次级筛选) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Segmented.css · 纯样式(CSS) · 92 行
/* 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 行
<!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> 列表</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> 网格</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 行
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 行
<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 行
<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 行
<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": "选项需要文字说明或纵向排列时用单选框列表,横向挤在一行才用分段器"
}
]
}