进度条Progress
把一个过程的完成度可视化(上传、审核、额度耗尽)
反馈 规格 26 · 进度条 Progress 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-progress.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Progress.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
type=line + labelPlacement=right:文案固定在条右侧,数字用等宽数字避免跳动。
查看代码(演示页原文 · 19 行)
<section class="demo-block" data-demo="line">
<p class="demo-label">线形(type=line,labelPlacement=right:文案在条右侧)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 65"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="65" data-assert="progress-line-right">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 65%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">65%</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 30"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="30">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 30%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">30%</span>
</div>
</div>
</section>labelPlacement=inside 时条体加高到 20px 以容纳文字,适合窄屏与卡片内。
查看代码(演示页原文 · 11 行)
<section class="demo-block" data-demo="inside">
<p class="demo-label">文案在条内(labelPlacement=inside:条体加高到 20px,文案落在条体内部而非压在灰轨上)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--inside" style="--kole-m-progress-percent: 48"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="48" data-assert="progress-inside">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 48%"><span class="kole-m-progress__label" aria-hidden="true">48%</span></span>
</span>
</div>
</div>
</section>type=circle + labelPlacement=center:环心挖空与所在容器同底,文案在环心。
查看代码(演示页原文 · 19 行)
<section class="demo-block" data-demo="circle">
<p class="demo-label">环形(type=circle + labelPlacement=center:文案在环心)</p>
<div class="demo-box demo-box--center">
<div class="demo-row">
<div class="kole-m-progress kole-m-progress--circle" style="--kole-m-progress-percent: 72"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" data-assert="progress-circle">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">72%</span>
</span>
</div>
<div class="kole-m-progress kole-m-progress--circle" style="--kole-m-progress-percent: 24"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="24" aria-label="下载进度">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">24%</span>
</span>
</div>
</div>
</div>
</section>02 组件状态
进行中 / 成功 / 失败:色相不同,且都带文案与状态图标,不靠颜色单独表意。
查看代码(演示页原文 · 31 行)
<section class="demo-block" data-demo="status">
<p class="demo-label">三种状态(正在上传 / 上传成功 / 上传失败:色相不同,且都带文案)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 42"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="42" aria-valuetext="上传中 42%"
data-assert="progress-status-normal">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 42%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">上传中 42%</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right kole-m-progress--success" style="--kole-m-progress-percent: 100"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-valuetext="已完成 100%"
data-assert="progress-status-success">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 100%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">已完成</span>
<span class="kole-m-progress__status" aria-hidden="true">✓</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right kole-m-progress--error" style="--kole-m-progress-percent: 58"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="58" aria-valuetext="上传失败 58%"
data-assert="progress-status-error">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 58%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">上传失败</span>
<span class="kole-m-progress__status" aria-hidden="true">!</span>
</div>
</div>
</section>状态 paused:条体降透明度、进度保留,用户能看出「停住了」而不是「没开始」。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="paused">
<p class="demo-label">暂停(状态 paused:条体降透明度,进度保留)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right is-paused" style="--kole-m-progress-percent: 55"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" aria-valuetext="已暂停 55%"
data-assert="progress-paused">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 55%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">已暂停 55%</span>
</div>
</div>
</section>进度条本身不接收点击;需要打断时由宿主在旁边放按钮(演示里用按钮把进度推到 100%)。
查看代码(演示页原文 · 17 行)
<section class="demo-block" data-demo="clickable">
<p class="demo-label">进度不动(进度条本身不可交互:需要取消时旁边放按钮,点按钮推进演示进度)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" id="progress-live" style="--kole-m-progress-percent: 20"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="20" data-assert="progress-live">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" id="progress-bar" style="width: 20%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">20%</span>
</div>
<div class="demo-row">
<button class="demo-btn" type="button" id="progress-btn"
data-behavior="click-sets-attr:#progress-live|aria-valuenow|100">推进到 100%</button>
<span class="demo-hint">真实业务里由上传进度驱动</span>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
percentage | number | 0 | 完成度 0~100,超出范围按边界收敛(规格 §26.2 bar) | N |
type | 'line' | 'circle' | 'line' | 变体 type:线形 / 环形(规格 §26.3) | N |
status | 'normal' | 'success' | 'error' | 'normal' | 变体 status:进行中 / 成功 / 失败,决定条体色相(规格 §26.3) | N |
labelPlacement | 'inside' | 'right' | 'center' | 'right' | 变体 labelPlacement:文案位置;center 仅对环形有效(规格 §26.3) | N |
label | string | '' | 自定义文案,缺省显示「percentage%」(规格 §26.2 label) | N |
paused | boolean | false | 状态 paused:暂停,条体降透明度且进度保留(规格 §26.4) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
— | — | 无事件;进度条不可交互(规格 §26.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 追加在进度条之后的自定义内容(如「取消」按钮)(规格 §26.2 status) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-progress-height | 6px | 线形条体高度 |
--kole-m-progress-size | 88px | 环形直径 |
--kole-m-progress-stroke | 6px | 环形线宽 |
--kole-m-progress-duration | 240ms | 数值过渡时长 |
--kole-m-progress-percent | 0 | 当前进度(0~100),由宿主按 percentage 写入 |
--kole-m-progress-hole-bg | var(--kole-color-card-bg) | 环心挖空底色,需与所在容器同底 |
何时使用
- 把一个过程的完成度可视化(上传、审核、额度耗尽)
- 移动端的进度多数伴随文字出现,因此百分比文案与状态色是标配而非可选
- 进度变化要能被读屏播报而不只是画出来
- 进度条本身不可交互、不接收点击;需要取消时由宿主在旁边放按钮
- 数值变化用 CSS 宽度过渡(240ms),不做无限循环动画(省电且不干扰读屏)
交互与触控
- 进度条本身不可交互、不接收点击;需要取消时由宿主在旁边放按钮
- 数值变化用 CSS 宽度过渡(240ms),不做无限循环动画(省电且不干扰读屏)
- 无动画偏好(prefers-reduced-motion)下直接跳到目标宽度
无障碍
- 根节点 role="progressbar" + aria-valuemin="0" / aria-valuemax="100" / aria-valuenow
- 文案节点 aria-hidden="true",避免与 aria-valuenow 重复播报
- 不确定进度(无法给出百分比)用 aria-valuetext="进行中" 表达
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 步骤条Steps | 过程有明确的阶段(提交 → 审核 → 完成)用步骤条;只有完成度没有阶段时用进度条 |
| 下拉刷新PullRefresh | 等待短任务完成用下拉刷新的加载态;长过程需要量化完成度时用进度条 |
| 轻提示Toast | 过程瞬间完成只需一句轻提示;过程持续可见才需要进度条 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 环形进度的线宽与半径的自适应规则(由 size 决定,不做响应式推导) |
| 禁止发明 | 进度到达 100% 后的自动隐藏时机 |
| 规格未定 | 百分比是否四舍五入到整数 |
| 规格未定 | 环形进度是否支持渐变描边 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
progress | 根元素,承载轨道与文案 |
track | 轨道,未完成部分的底色 |
bar | 已完成部分,宽度由 percentage 决定 |
ring | 环形进度的圆环轨道(type=circle 时替代 track/bar) |
label | 百分比文案,可置于条内、条右侧或环心 |
status | 状态图标位(成功 / 失败),非进行中时显示 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
type | line / circle | line .kole-m-progress--line circle .kole-m-progress--circle |
status | normal / success / error | normal (由数据驱动,无专属类) success .kole-m-progress--success error .kole-m-progress--error |
labelPlacement | inside / right / center | inside .kole-m-progress--inside right .kole-m-progress--right center .kole-m-progress__hole --kole-m-progress-percent |
代表变体
| 变体 | 标签 |
|---|---|
type=line · status=normal · labelPlacement=right | 线形进行中(文案在条右侧) |
type=line · status=success · labelPlacement=inside | 线形成功(文案压在条内) |
type=circle · status=normal · labelPlacement=center | 环形进行中(文案在环心) |
type=line · status=error · labelPlacement=right | 线形失败(进度停在断点转错误色) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Progress.css · 纯样式(CSS) · 134 行
/* Kole UI Mobile · Progress 样式 — 对齐移动端规格 §26
进度条:线形 / 环形两形态,状态色三档(normal / success / error);
条体只用宽度过渡(240ms),不做无限循环动画;减少动态偏好下直接跳变。 */
.kole-m-progress {
--kole-m-progress-height: 6px; /* 线形条体高度 */
--kole-m-progress-size: 88px; /* 环形直径 */
--kole-m-progress-stroke: 6px; /* 环形线宽 */
--kole-m-progress-duration: 240ms; /* 数值过渡时长 */
--kole-m-progress-percent: 0; /* 当前进度(0~100),由宿主按 percentage 写入 */
--kole-m-progress-hole-bg: var(--kole-color-card-bg); /* 环心挖空底色,需与所在容器同底 */
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--kole-space-12);
width: 100%;
font-family: var(--kole-font-family);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
}
/* 变体 type=line:轨道 + 条体 */
.kole-m-progress--line { flex-direction: row; }
.kole-m-progress__track {
position: relative;
flex: 1 1 auto;
box-sizing: border-box;
overflow: hidden;
height: var(--kole-m-progress-height);
border-radius: calc(var(--kole-m-progress-height) / 2);
background: var(--kole-color-disabled-bg);
}
.kole-m-progress__bar {
display: block;
box-sizing: border-box;
width: 0;
height: 100%;
border-radius: inherit;
background: var(--kole-color-brand);
transition: width var(--kole-m-progress-duration) var(--kole-ease-out);
}
/* 变体 labelPlacement=inside:文案压在条体内(条体加高到 20px 以容纳文字)。
文案节点放在**条体内部**:条体是不透明品牌底,反色字对比度 5.85:1 达标;
若把反色字直接压在灰色轨道上就是真实的对比度缺陷(1.1:1)。
代价是条体过窄时文案会被裁掉 —— 所以 inside 只适合进度已经不小、或文案较短的场合。 */
.kole-m-progress--inside { --kole-m-progress-height: 20px; }
.kole-m-progress--inside .kole-m-progress__bar {
display: flex;
align-items: center;
justify-content: center;
}
.kole-m-progress--inside .kole-m-progress__label {
margin: 0 var(--kole-space-4);
overflow: hidden;
white-space: nowrap;
color: var(--kole-color-text-inverse);
font-size: var(--kole-m-font-size-caption);
line-height: 1;
}
/* 变体 labelPlacement=right:文案固定在条的右侧 */
.kole-m-progress--right .kole-m-progress__label {
flex: 0 0 auto;
min-width: 36px;
text-align: end;
font-variant-numeric: tabular-nums;
}
/* 变体 type=circle:环形用 conic-gradient 画(零依赖,不用 SVG) */
.kole-m-progress--circle {
display: inline-flex;
justify-content: center;
width: auto;
}
.kole-m-progress__ring {
position: relative;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--kole-m-progress-size);
height: var(--kole-m-progress-size);
border-radius: 50%;
background: conic-gradient(var(--kole-color-brand) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
transition: background var(--kole-m-progress-duration) var(--kole-ease-out);
}
/* 环心挖空:与所在容器同底,形成圆环(不引 SVG,也不用 mask 兼容性赌注) */
.kole-m-progress__hole {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: calc(var(--kole-m-progress-size) - var(--kole-m-progress-stroke) * 2);
height: calc(var(--kole-m-progress-size) - var(--kole-m-progress-stroke) * 2);
border-radius: 50%;
background: var(--kole-m-progress-hole-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
font-variant-numeric: tabular-nums;
}
/* 状态 normal:进行中(品牌色,默认)
状态 paused:暂停,条体降透明度(进度保留) */
.kole-m-progress.is-paused .kole-m-progress__bar,
.kole-m-progress.is-paused .kole-m-progress__ring { opacity: 0.5; }
/* 状态 success:完成 / 成功色 */
.kole-m-progress.kole-m-progress--success .kole-m-progress__bar { background: var(--kole-color-success); }
.kole-m-progress.kole-m-progress--success .kole-m-progress__ring {
background: conic-gradient(var(--kole-color-success) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
}
/* 状态 error:失败,进度停在断点并转错误色 */
.kole-m-progress.kole-m-progress--error .kole-m-progress__bar { background: var(--kole-color-error); }
.kole-m-progress.kole-m-progress--error .kole-m-progress__ring {
background: conic-gradient(var(--kole-color-error) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
}
/* 状态图标位:非进行中时显示(成功 / 失败) */
.kole-m-progress__status { flex: 0 0 auto; display: inline-flex; }
/* 减少动态偏好:不做过渡,直接跳到目标宽度 */
@media (prefers-reduced-motion: reduce) {
.kole-m-progress__bar,
.kole-m-progress__ring { transition: none; }
}
frameworks-mobile/Progress.html · H5 原生(无框架) · 185 行
<!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 · Progress(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Progress.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 { display: flex; flex-direction: column; gap: var(--kole-space-24); padding: var(--kole-space-24) var(--kole-m-gutter);
background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-box--center { align-items: center; }
.demo-row { display: flex; align-items: center; gap: var(--kole-space-12); }
/* 演示页专用按钮(不属于 Progress 组件;组件本身不可交互) */
.demo-btn { min-height: var(--kole-m-touch-target); padding: 0 var(--kole-m-gutter);
border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base);
background: var(--kole-color-card-bg); color: var(--kole-color-text-body);
font-family: inherit; font-size: var(--kole-m-font-size-label); cursor: pointer; touch-action: manipulation; }
.demo-btn:focus-visible { outline: 2px solid var(--kole-color-focus-ring); outline-offset: 2px; }
.demo-hint { font-size: var(--kole-m-font-size-caption); color: var(--kole-color-text-secondary); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="line">
<p class="demo-label">线形(type=line,labelPlacement=right:文案在条右侧)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 65"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="65" data-assert="progress-line-right">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 65%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">65%</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 30"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="30">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 30%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">30%</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="inside">
<p class="demo-label">文案在条内(labelPlacement=inside:条体加高到 20px,文案落在条体内部而非压在灰轨上)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--inside" style="--kole-m-progress-percent: 48"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="48" data-assert="progress-inside">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 48%"><span class="kole-m-progress__label" aria-hidden="true">48%</span></span>
</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="status">
<p class="demo-label">三种状态(正在上传 / 上传成功 / 上传失败:色相不同,且都带文案)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" style="--kole-m-progress-percent: 42"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="42" aria-valuetext="上传中 42%"
data-assert="progress-status-normal">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 42%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">上传中 42%</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right kole-m-progress--success" style="--kole-m-progress-percent: 100"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-valuetext="已完成 100%"
data-assert="progress-status-success">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 100%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">已完成</span>
<span class="kole-m-progress__status" aria-hidden="true">✓</span>
</div>
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right kole-m-progress--error" style="--kole-m-progress-percent: 58"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="58" aria-valuetext="上传失败 58%"
data-assert="progress-status-error">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 58%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">上传失败</span>
<span class="kole-m-progress__status" aria-hidden="true">!</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="circle">
<p class="demo-label">环形(type=circle + labelPlacement=center:文案在环心)</p>
<div class="demo-box demo-box--center">
<div class="demo-row">
<div class="kole-m-progress kole-m-progress--circle" style="--kole-m-progress-percent: 72"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" data-assert="progress-circle">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">72%</span>
</span>
</div>
<div class="kole-m-progress kole-m-progress--circle" style="--kole-m-progress-percent: 24"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="24" aria-label="下载进度">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">24%</span>
</span>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="paused">
<p class="demo-label">暂停(状态 paused:条体降透明度,进度保留)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right is-paused" style="--kole-m-progress-percent: 55"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="55" aria-valuetext="已暂停 55%"
data-assert="progress-paused">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" style="width: 55%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">已暂停 55%</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="clickable">
<p class="demo-label">进度不动(进度条本身不可交互:需要取消时旁边放按钮,点按钮推进演示进度)</p>
<div class="demo-box">
<div class="kole-m-progress kole-m-progress--line kole-m-progress--right" id="progress-live" style="--kole-m-progress-percent: 20"
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="20" data-assert="progress-live">
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" id="progress-bar" style="width: 20%"></span>
</span>
<span class="kole-m-progress__label" aria-hidden="true">20%</span>
</div>
<div class="demo-row">
<button class="demo-btn" type="button" id="progress-btn"
data-behavior="click-sets-attr:#progress-live|aria-valuenow|100">推进到 100%</button>
<span class="demo-hint">真实业务里由上传进度驱动</span>
</div>
</div>
</section>
</div>
<script>
/* 演示页脚本:把进度推到 100%(真实业务里由上传进度驱动)。
同时改三处:条体宽、根节点 aria-valuenow、文案。 */
(function () {
var btn = document.getElementById('progress-btn');
var root = document.getElementById('progress-live');
var bar = document.getElementById('progress-bar');
if (!btn || !root || !bar) return;
btn.addEventListener('click', function () {
root.style.setProperty('--kole-m-progress-percent', '100');
root.setAttribute('aria-valuenow', '100');
bar.style.width = '100%';
var label = root.querySelector('.kole-m-progress__label');
if (label) label.textContent = '100%';
});
})();
</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/Progress.jsx · React · 86 行
import React from 'react';
import './Progress.css';
/* 进度条(移动端)— 规格 §26
根节点 role="progressbar" + aria-valuemin/max/now;文案节点 aria-hidden(避免与 aria-valuenow 重复播报)。
线形与环形共用 --kole-m-progress-percent:环形用 conic-gradient 画,环心挖空与所在容器同底
(--kole-m-progress-hole-bg 可覆盖)。进度条本身不可交互(规格 §26.5)。 */
export default function Progress({
percentage = 0,
type = 'line',
status = 'normal',
labelPlacement = 'right',
label = '',
paused = false,
children = null,
}) {
const value = Math.max(0, Math.min(100, Number(percentage) || 0));
const percentStyle = { '--kole-m-progress-percent': String(value) };
const cls =
'kole-m-progress' +
` kole-m-progress--${type}` +
(type === 'line' ? ` kole-m-progress--${labelPlacement}` : '') +
(status === 'success' ? ' kole-m-progress--success' : '') +
(status === 'error' ? ' kole-m-progress--error' : '') +
(paused ? ' is-paused' : '');
const text = label || value + '%';
/* 文案在条内的线形:标签落在**条体内部**(品牌底 + 反色字才达标),
与 H5 演示页同一结构(规格 §26.3 labelPlacement=inside) */
const inlineLabel = (
<span className="kole-m-progress__label" aria-hidden="true">
{text}
</span>
);
const ring = (
<span className="kole-m-progress__ring">
<span className="kole-m-progress__hole" aria-hidden="true">
{text}
</span>
</span>
);
return (
<div
className={cls}
style={percentStyle}
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={value}
aria-valuetext={status === 'error' ? '失败 ' + value + '%' : undefined}
>
{type === 'circle' ? (
ring
) : (
<>
<span className="kole-m-progress__track">
<span className="kole-m-progress__bar" style={{ width: value + '%' }}>
{labelPlacement === 'inside' ? inlineLabel : null}
</span>
</span>
{labelPlacement !== 'inside' ? (
<span className="kole-m-progress__label" aria-hidden="true">
{text}
</span>
) : null}
</>
)}
{status === 'success' ? (
<span className="kole-m-progress__status" aria-hidden="true">
✓
</span>
) : null}
{status === 'error' ? (
<span className="kole-m-progress__status" aria-hidden="true">
!
</span>
) : null}
{children}
</div>
);
}
frameworks-mobile/Progress.vue2.vue · Vue 2 · 68 行
<template>
<div
:class="progressClass"
:style="percentStyle"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuenow="value"
:aria-valuetext="status === 'error' ? '失败 ' + value + '%' : null"
>
<template v-if="type === 'circle'">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">{{ text }}</span>
</span>
</template>
<template v-else>
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" :style="{ width: value + '%' }">
<span v-if="labelPlacement === 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</span>
</span>
</span>
<span v-if="labelPlacement !== 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</span>
</template>
<span v-if="status === 'success'" class="kole-m-progress__status" aria-hidden="true">✓</span>
<span v-if="status === 'error'" class="kole-m-progress__status" aria-hidden="true">!</span>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'KoleMProgress',
props: {
percentage: { type: Number, default: 0 },
type: { type: String, default: 'line' },
status: { type: String, default: 'normal' },
labelPlacement: { type: String, default: 'right' },
label: { type: String, default: '' },
paused: { type: Boolean, default: false }
},
computed: {
value: function () {
var n = Number(this.percentage) || 0;
return Math.max(0, Math.min(100, n));
},
text: function () {
return this.label || this.value + '%';
},
percentStyle: function () {
return { '--kole-m-progress-percent': String(this.value) };
},
progressClass: function () {
return [
'kole-m-progress--' + this.type,
this.type === 'line' ? 'kole-m-progress--' + this.labelPlacement : '',
this.status === 'success' ? 'kole-m-progress--success' : '',
this.status === 'error' ? 'kole-m-progress--error' : '',
this.paused ? 'is-paused' : ''
].filter(Boolean);
}
}
};
</script>
<style src="./Progress.css"></style>
frameworks-mobile/Progress.vue3.vue · Vue 3 · 59 行
<template>
<div
:class="progressClass"
:style="percentStyle"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuenow="value"
:aria-valuetext="status === 'error' ? '失败 ' + value + '%' : null"
>
<template v-if="type === 'circle'">
<span class="kole-m-progress__ring">
<span class="kole-m-progress__hole" aria-hidden="true">{{ text }}</span>
</span>
</template>
<template v-else>
<span class="kole-m-progress__track">
<span class="kole-m-progress__bar" :style="{ width: value + '%' }">
<span v-if="labelPlacement === 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</span>
</span>
</span>
<span v-if="labelPlacement !== 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</span>
</template>
<span v-if="status === 'success'" class="kole-m-progress__status" aria-hidden="true">✓</span>
<span v-if="status === 'error'" class="kole-m-progress__status" aria-hidden="true">!</span>
<slot></slot>
</div>
</template>
<script setup>
/* 进度条(移动端)— 规格 §26:文案节点 aria-hidden(不重复播报),进度条本身不可交互 */
import { computed } from 'vue';
const props = defineProps({
percentage: { type: Number, default: 0 },
type: { type: String, default: 'line' },
status: { type: String, default: 'normal' },
labelPlacement: { type: String, default: 'right' },
label: { type: String, default: '' },
paused: { type: Boolean, default: false }
});
const value = computed(() => Math.max(0, Math.min(100, Number(props.percentage) || 0)));
const text = computed(() => props.label || value.value + '%');
const percentStyle = computed(() => ({ '--kole-m-progress-percent': String(value.value) }));
const progressClass = computed(() => [
`kole-m-progress--${props.type}`,
props.type === 'line' ? `kole-m-progress--${props.labelPlacement}` : '',
props.status === 'success' ? 'kole-m-progress--success' : '',
props.status === 'error' ? 'kole-m-progress--error' : '',
props.paused ? 'is-paused' : ''
].filter(Boolean));
</script>
<style src="./Progress.css"></style>
frameworks-mobile/Progress.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 179 行
<template>
<view
class="kole-m-progress"
:class="progressClass"
:style="percentStyle"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuenow="value"
:aria-valuetext="status === 'error' ? '失败 ' + value + '%' : ''"
>
<template v-if="type === 'circle'">
<view class="kole-m-progress__ring">
<view class="kole-m-progress__hole" aria-hidden="true">
<text class="kole-m-progress__hole-text">{{ text }}</text>
</view>
</view>
</template>
<template v-else>
<view class="kole-m-progress__track">
<view class="kole-m-progress__bar" :style="{ width: value + '%' }">
<text v-if="labelPlacement === 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</text>
</view>
</view>
<text v-if="labelPlacement !== 'inside'" class="kole-m-progress__label" aria-hidden="true">{{ text }}</text>
</template>
<text v-if="status === 'success'" class="kole-m-progress__status" aria-hidden="true">✓</text>
<text v-if="status === 'error'" class="kole-m-progress__status" aria-hidden="true">!</text>
<slot></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 进度条(移动端)— 规格 §26
跨端差异:全部用 view / text;条体宽度用百分比内联样式(与 H5 端同一取值口径);
环形用 conic-gradient 画(H5 / App 目标直接支持;微信小程序基础库 2.9.0+ 亦支持),
环心挖空的底色用 --kole-m-progress-hole-bg 覆盖(与所在容器同底)。
尺寸用 rpx(88rpx = 375pt 下的 44px)。 */
import { computed } from 'vue';
const props = defineProps({
percentage: { type: Number, default: 0 },
type: { type: String, default: 'line' },
status: { type: String, default: 'normal' },
labelPlacement: { type: String, default: 'right' },
label: { type: String, default: '' },
paused: { type: Boolean, default: false }
});
const value = computed(() => Math.max(0, Math.min(100, Number(props.percentage) || 0)));
const text = computed(() => props.label || value.value + '%');
const percentStyle = computed(() => ({ '--kole-m-progress-percent': String(value.value) }));
const progressClass = computed(() => [
`kole-m-progress--${props.type}`,
props.type === 'line' ? `kole-m-progress--${props.labelPlacement}` : '',
props.status === 'success' ? 'kole-m-progress--success' : '',
props.status === 'error' ? 'kole-m-progress--error' : '',
props.paused ? 'is-paused' : ''
].filter(Boolean));
</script>
<style>
.kole-m-progress {
--kole-m-progress-height: 12rpx;
--kole-m-progress-size: 176rpx;
--kole-m-progress-stroke: 12rpx;
--kole-m-progress-duration: 240ms;
--kole-m-progress-percent: 0;
--kole-m-progress-hole-bg: var(--kole-color-card-bg);
--kole-m-font-size-label: 28rpx;
--kole-m-font-size-caption: 22rpx;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
}
.kole-m-progress__track {
position: relative;
flex: 1;
box-sizing: border-box;
overflow: hidden;
height: var(--kole-m-progress-height);
border-radius: 999rpx;
background-color: var(--kole-color-disabled-bg);
}
.kole-m-progress__bar {
display: block;
height: 100%;
border-radius: 999rpx;
background-color: var(--kole-color-brand);
transition: width var(--kole-m-progress-duration) ease-out;
}
/* 变体 labelPlacement=inside:条体加高到 40rpx,文案落在**条体内部**
(品牌底 + 反色字 5.85:1;压在灰色轨道上是真实缺陷)。 */
.kole-m-progress--inside { --kole-m-progress-height: 40rpx; }
.kole-m-progress--inside .kole-m-progress__bar {
display: flex;
align-items: center;
justify-content: center;
}
.kole-m-progress--inside .kole-m-progress__label {
overflow: hidden;
white-space: nowrap;
color: var(--kole-color-text-inverse);
font-size: var(--kole-m-font-size-caption);
line-height: 1;
}
.kole-m-progress--right .kole-m-progress__label {
flex-shrink: 0;
min-width: 72rpx;
padding-left: 16rpx;
text-align: right;
}
/* 变体 type=circle:环形(外圈 conic-gradient + 环心挖空) */
.kole-m-progress--circle {
display: flex;
justify-content: center;
width: auto;
}
.kole-m-progress__ring {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: var(--kole-m-progress-size);
height: var(--kole-m-progress-size);
border-radius: 50%;
background: conic-gradient(var(--kole-color-brand) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
transition: background var(--kole-m-progress-duration) ease-out;
}
.kole-m-progress__hole {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: calc(var(--kole-m-progress-size) - var(--kole-m-progress-stroke) * 2);
height: calc(var(--kole-m-progress-size) - var(--kole-m-progress-stroke) * 2);
border-radius: 50%;
background-color: var(--kole-m-progress-hole-bg);
}
.kole-m-progress__hole-text { color: var(--kole-color-text-body); font-size: var(--kole-m-font-size-label); }
.kole-m-progress.is-paused .kole-m-progress__bar,
.kole-m-progress.is-paused .kole-m-progress__ring { opacity: 0.5; }
.kole-m-progress.kole-m-progress--success .kole-m-progress__bar { background-color: var(--kole-color-success); }
.kole-m-progress.kole-m-progress--success .kole-m-progress__ring {
background: conic-gradient(var(--kole-color-success) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
}
.kole-m-progress.kole-m-progress--error .kole-m-progress__bar { background-color: var(--kole-color-error); }
.kole-m-progress.kole-m-progress--error .kole-m-progress__ring {
background: conic-gradient(var(--kole-color-error) calc(var(--kole-m-progress-percent) * 1%), var(--kole-color-disabled-bg) 0);
}
.kole-m-progress__status {
flex-shrink: 0;
padding-left: 12rpx;
color: var(--kole-color-text-secondary);
}
</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-progress.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "26 · 进度条 Progress",
"confidence": "high",
"slug": "mobile-progress",
"name": "进度条 Progress",
"semanticTypeCandidates": [
"progress",
"progress-bar",
"progress-ring"
],
"variantDimensions": [
{
"name": "type",
"values": [
"line",
"circle"
]
},
{
"name": "status",
"values": [
"normal",
"success",
"error"
]
},
{
"name": "labelPlacement",
"values": [
"inside",
"right",
"center"
]
}
],
"representativeVariants": [
{
"type": "line",
"status": "normal",
"labelPlacement": "right",
"label": "线形进行中(文案在条右侧)"
},
{
"type": "line",
"status": "success",
"labelPlacement": "inside",
"label": "线形成功(文案压在条内)"
},
{
"type": "circle",
"status": "normal",
"labelPlacement": "center",
"label": "环形进行中(文案在环心)"
},
{
"type": "line",
"status": "error",
"labelPlacement": "right",
"label": "线形失败(进度停在断点转错误色)"
}
],
"anatomy": {
"progress": "根元素,承载轨道与文案",
"track": "轨道,未完成部分的底色",
"bar": "已完成部分,宽度由 percentage 决定",
"ring": "环形进度的圆环轨道(type=circle 时替代 track/bar)",
"label": "百分比文案,可置于条内、条右侧或环心",
"status": "状态图标位(成功 / 失败),非进行中时显示"
},
"structurePatterns": {
"type": "line(线形)/ circle(环形)",
"status": "normal(进行中,品牌色)/ success(成功)/ error(失败)",
"labelPlacement": "inside(文案在条内)/ right(条右侧)/ center(环心,仅 circle)"
},
"usageHints": [
"把一个过程的完成度可视化(上传、审核、额度耗尽)",
"移动端的进度多数伴随文字出现,因此百分比文案与状态色是标配而非可选",
"进度变化要能被读屏播报而不只是画出来",
"进度条本身不可交互、不接收点击;需要取消时由宿主在旁边放按钮",
"数值变化用 CSS 宽度过渡(240ms),不做无限循环动画(省电且不干扰读屏)"
],
"doNotInvent": [
"环形进度的线宽与半径的自适应规则(由 size 决定,不做响应式推导)",
"进度到达 100% 后的自动隐藏时机"
],
"unknowns": [
"百分比是否四舍五入到整数",
"环形进度是否支持渐变描边"
],
"interaction": [
"进度条本身不可交互、不接收点击;需要取消时由宿主在旁边放按钮",
"数值变化用 CSS 宽度过渡(240ms),不做无限循环动画(省电且不干扰读屏)",
"无动画偏好(prefers-reduced-motion)下直接跳到目标宽度"
],
"accessibility": [
"根节点 role=\"progressbar\" + aria-valuemin=\"0\" / aria-valuemax=\"100\" / aria-valuenow",
"文案节点 aria-hidden=\"true\",避免与 aria-valuenow 重复播报",
"不确定进度(无法给出百分比)用 aria-valuetext=\"进行中\" 表达"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "percentage",
"type": "number",
"default": "0",
"desc": "完成度 0~100,超出范围按边界收敛(规格 §26.2 bar)",
"required": false
},
{
"name": "type",
"type": "'line' | 'circle'",
"default": "'line'",
"desc": "变体 type:线形 / 环形(规格 §26.3)",
"required": false
},
{
"name": "status",
"type": "'normal' | 'success' | 'error'",
"default": "'normal'",
"desc": "变体 status:进行中 / 成功 / 失败,决定条体色相(规格 §26.3)",
"required": false
},
{
"name": "labelPlacement",
"type": "'inside' | 'right' | 'center'",
"default": "'right'",
"desc": "变体 labelPlacement:文案位置;center 仅对环形有效(规格 §26.3)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "''",
"desc": "自定义文案,缺省显示「percentage%」(规格 §26.2 label)",
"required": false
},
{
"name": "paused",
"type": "boolean",
"default": "false",
"desc": "状态 paused:暂停,条体降透明度且进度保留(规格 §26.4)",
"required": false
}
],
"events": [
{
"name": "—",
"params": "—",
"desc": "无事件;进度条不可交互(规格 §26.5)"
}
],
"slots": [
{
"name": "default",
"desc": "追加在进度条之后的自定义内容(如「取消」按钮)(规格 §26.2 status)"
}
]
},
"variantClasses": {
"type": {
"line": [
".kole-m-progress--line"
],
"circle": [
".kole-m-progress--circle"
]
},
"status": {
"normal": [],
"success": [
".kole-m-progress--success"
],
"error": [
".kole-m-progress--error"
]
},
"labelPlacement": {
"inside": [
".kole-m-progress--inside"
],
"right": [
".kole-m-progress--right"
],
"center": [
".kole-m-progress__hole",
"--kole-m-progress-percent"
]
}
},
"demos": [
{
"id": "line",
"group": "01 组件类型",
"title": "线形",
"desc": "type=line + labelPlacement=right:文案固定在条右侧,数字用等宽数字避免跳动。",
"variant": "type=line / labelPlacement=right"
},
{
"id": "inside",
"group": "01 组件类型",
"title": "文案在条内",
"desc": "labelPlacement=inside 时条体加高到 20px 以容纳文字,适合窄屏与卡片内。",
"variant": "labelPlacement=inside"
},
{
"id": "status",
"group": "02 组件状态",
"title": "三种状态",
"desc": "进行中 / 成功 / 失败:色相不同,且都带文案与状态图标,不靠颜色单独表意。",
"variant": "status=normal|success|error"
},
{
"id": "circle",
"group": "01 组件类型",
"title": "环形",
"desc": "type=circle + labelPlacement=center:环心挖空与所在容器同底,文案在环心。",
"variant": "type=circle / labelPlacement=center"
},
{
"id": "paused",
"group": "02 组件状态",
"title": "暂停",
"desc": "状态 paused:条体降透明度、进度保留,用户能看出「停住了」而不是「没开始」。",
"variant": "paused=true"
},
{
"id": "clickable",
"group": "02 组件状态",
"title": "进度不可交互",
"desc": "进度条本身不接收点击;需要打断时由宿主在旁边放按钮(演示里用按钮把进度推到 100%)。",
"variant": "无事件"
}
],
"related": [
{
"slug": "mobile-steps",
"why": "过程有明确的阶段(提交 → 审核 → 完成)用步骤条;只有完成度没有阶段时用进度条"
},
{
"slug": "pullrefresh",
"why": "等待短任务完成用下拉刷新的加载态;长过程需要量化完成度时用进度条"
},
{
"slug": "mobile-toast",
"why": "过程瞬间完成只需一句轻提示;过程持续可见才需要进度条"
}
]
}