按钮Button
触发一个即时动作
通用 规格 6 · 按钮 Button 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-button.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Button.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
一个区域里只放一个主按钮;移动端默认用 large 档(44px 高)。
查看代码(演示页原文 · 6 行)
<section class="demo-block" data-demo="primary">
<p class="demo-label">主按钮(type=primary / size=large)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-primary">提交订单</button>
</div>
</section>白底描边,与主按钮并排时表示次要动作。
查看代码(演示页原文 · 6 行)
<section class="demo-block" data-demo="default">
<p class="demo-label">次按钮(type=default,与主按钮并排表示次要动作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--default" type="button" data-assert="btn-default">取消</button>
</div>
</section>无背景无边框,用于弱操作(如「跳过」「查看详情」)。
查看代码(演示页原文 · 6 行)
<section class="demo-block" data-demo="text">
<p class="demo-label">文字按钮(type=text,弱操作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--text" type="button" data-assert="btn-text">跳过这一步</button>
</div>
</section>删除类动作用错误色;不要和主按钮同屏并列出现两次。
查看代码(演示页原文 · 6 行)
<section class="demo-block" data-demo="danger">
<p class="demo-label">危险按钮(type=danger,删除类动作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--danger kole-m-btn--large" type="button" data-assert="btn-danger">删除订单</button>
</div>
</section>02 组件状态
large 44px 给主操作区,default 36px 给表单,small 28px 只用于行列内联。
查看代码(演示页原文 · 8 行)
<section class="demo-block" data-demo="sizes">
<p class="demo-label">三档尺寸(small 28 / default 36 / large 44)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--small" type="button" data-assert="btn-size-small">小</button>
<button class="kole-m-btn kole-m-btn--primary" type="button" data-assert="btn-size-default">默认</button>
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-size-large">大</button>
</div>
</section>置灰且不响应;禁用原因要写在按钮附近,别只靠置灰暗示。
查看代码(演示页原文 · 6 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(disabled:置灰且不响应;禁用原因写在按钮附近)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" disabled data-assert="btn-disabled" aria-label="提交(库存不足,暂不可用)">库存不足</button>
</div>
</section>显示指示器并置 aria-busy,重复点击不再触发(点按钮切换)。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="loading">
<p class="demo-label">加载中(loading:置 aria-busy,重复点击不再触发;点按钮切换)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" id="btn-loading"
data-assert="btn-loading" data-behavior="click-toggles-class:#btn-loading|is-loading" aria-busy="false">
<span class="kole-m-btn__label">提交</span>
</button>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
type | 'primary' | 'default' | 'text' | 'danger' | 'default' | 变体 type:主次层级靠颜色与边框区分(规格 §6.3) | N |
size | 'large' | 'default' | 'small' | 'default' | 变体 size;large 为 44px 移动端默认档(规格 §6.3) | N |
block | boolean | false | 块级形态,撑满容器宽度(规格 §6.2) | N |
disabled | boolean | false | 置灰且不响应点击(规格 §6.4) | N |
loading | boolean | false | 显示加载指示器并阻止重复触发(规格 §6.4) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
click | — | 点击时触发(disabled / loading 时不触发) |
插槽
| 名称 | 说明 |
|---|---|
default | 按钮文字(规格 §6.2) |
icon | 可选图标或加载指示器,与文字间距 4px(规格 §6.2) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
| 本组件没有组件级 CSS 变量 | ||
何时使用
- 触发一个即时动作
- 高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作
- 按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)
- loading 期间重复点击不触发第二次事件
- 仅有图标时必须给 aria-label
交互与触控
- 高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作
- 按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)
- loading 期间重复点击不触发第二次事件
无障碍
- 使用原生 button,天然可聚焦、可键盘触发
- 仅有图标时必须给 aria-label
- loading 时置 aria-busy="true" 且 disabled(避免重复提交)
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 动作面板ActionSheet | 需要从一个入口提供多个操作时用动作面板;单个即时动作才用按钮 |
| 单元格Cell | 整行进入下级用单元格;单个动作才用按钮 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 按钮内多行文字的排版规则 |
| 禁止发明 | 长按(long-press)的附加行为 |
| 规格未定 | 图标与文字同时存在时的最小宽度 |
| 规格未定 | 危险按钮是否需要二次确认 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
button | 根元素,用原生 button,圆角取令牌 |
label | 按钮文字,单行不换行 |
icon | 可选图标或加载指示器,与文字间距 4px |
block | 可选块级形态,撑满容器宽度 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
type | primary / default / text / danger | primary .kole-m-btn--primary default .kole-m-btn--default text .kole-m-btn--text danger .kole-m-btn--danger |
size | large / default / small | large .kole-m-btn--large default (由数据驱动,无专属类) small .kole-m-btn--small |
block | false / true | false (由数据驱动,无专属类) true .is-block |
代表变体
| 变体 | 标签 |
|---|---|
type=primary · size=large | 主按钮(移动端默认档) |
type=default · size=default | 次按钮 |
type=text · size=default | 文字按钮 |
type=danger · size=large | 危险按钮 |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Button.css · 纯样式(CSS) · 92 行
/* Kole UI Mobile · Button 样式 — 对齐 .design_library/kole-ui-mobile/spec/移动端规格.md §6
按钮:高度分三档(44 / 36 / 28),按下反馈用 :active 背景(移动端没有 hover)。
令牌:颜色/字体/圆角取 --kole-*,触控尺寸取 --kole-m-*。 */
.kole-m-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--kole-space-4);
box-sizing: border-box;
min-height: 36px;
padding: 0 var(--kole-space-16);
border: 1px solid transparent;
border-radius: var(--kole-radius-base);
background: transparent;
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-label);
line-height: 1.2;
cursor: pointer;
touch-action: manipulation;
-webkit-user-select: none;
user-select: none;
transition: background-color var(--kole-duration-fast) var(--kole-ease-standard),
border-color var(--kole-duration-fast) var(--kole-ease-standard),
color var(--kole-duration-fast) var(--kole-ease-standard);
}
/* 变体 size:large = 移动端主操作区(44px 触控最小边长) */
.kole-m-btn--large {
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-space-20);
font-size: var(--kole-m-font-size-body);
}
.kole-m-btn--small {
min-height: 28px;
padding: 0 var(--kole-space-12);
font-size: var(--kole-m-font-size-caption);
}
/* 变体 type */
.kole-m-btn--primary { background: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
.kole-m-btn--primary:active { background: var(--kole-color-brand-active); }
.kole-m-btn--default { background: var(--kole-color-card-bg); border-color: var(--kole-color-border); }
.kole-m-btn--default:active { border-color: var(--kole-color-brand); color: var(--kole-color-brand); }
.kole-m-btn--text { color: var(--kole-color-brand); }
.kole-m-btn--text:active { background: var(--kole-color-brand-bg); }
.kole-m-btn--danger { background: var(--kole-color-error); color: var(--kole-color-text-inverse); }
.kole-m-btn--danger:active { filter: brightness(.92); }
/* 变体 block:撑满容器 */
.kole-m-btn.is-block { display: flex; width: 100%; }
/* 状态:禁用 / 加载中 */
.kole-m-btn:disabled,
.kole-m-btn.is-disabled {
background: var(--kole-color-disabled-bg);
border-color: transparent;
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
.kole-m-btn.is-loading { cursor: progress; }
.kole-m-btn__icon { display: inline-flex; flex: 0 0 auto; }
.kole-m-btn__label { white-space: nowrap; }
.kole-m-btn__spinner {
width: 14px;
height: 14px;
box-sizing: border-box;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: kole-m-btn-spin .6s linear infinite;
}
@keyframes kole-m-btn-spin {
to { transform: rotate(360deg); }
}
.kole-m-btn:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.kole-m-btn__spinner { animation: none; }
}
frameworks-mobile/Button.html · H5 原生(无框架) · 122 行
<!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 · Button(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Button.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-wrap: wrap; align-items: center; gap: var(--kole-space-12);
padding: var(--kole-m-gutter); background: var(--kole-color-card-bg);
border-block: 1px solid var(--kole-color-border); }
.demo-box--block { display: block; }
.demo-block[hidden] { display: none; }
body.demo-single .demo-label:first-child { padding-top: var(--kole-space-12); }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="primary">
<p class="demo-label">主按钮(type=primary / size=large)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-primary">提交订单</button>
</div>
</section>
<section class="demo-block" data-demo="default">
<p class="demo-label">次按钮(type=default,与主按钮并排表示次要动作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--default" type="button" data-assert="btn-default">取消</button>
</div>
</section>
<section class="demo-block" data-demo="text">
<p class="demo-label">文字按钮(type=text,弱操作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--text" type="button" data-assert="btn-text">跳过这一步</button>
</div>
</section>
<section class="demo-block" data-demo="danger">
<p class="demo-label">危险按钮(type=danger,删除类动作)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--danger kole-m-btn--large" type="button" data-assert="btn-danger">删除订单</button>
</div>
</section>
<section class="demo-block" data-demo="sizes">
<p class="demo-label">三档尺寸(small 28 / default 36 / large 44)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--small" type="button" data-assert="btn-size-small">小</button>
<button class="kole-m-btn kole-m-btn--primary" type="button" data-assert="btn-size-default">默认</button>
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" data-assert="btn-size-large">大</button>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(disabled:置灰且不响应;禁用原因写在按钮附近)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" disabled data-assert="btn-disabled" aria-label="提交(库存不足,暂不可用)">库存不足</button>
</div>
</section>
<section class="demo-block" data-demo="loading">
<p class="demo-label">加载中(loading:置 aria-busy,重复点击不再触发;点按钮切换)</p>
<div class="demo-box">
<button class="kole-m-btn kole-m-btn--primary kole-m-btn--large" type="button" id="btn-loading"
data-assert="btn-loading" data-behavior="click-toggles-class:#btn-loading|is-loading" aria-busy="false">
<span class="kole-m-btn__label">提交</span>
</button>
</div>
</section>
</div>
<script>
/* 演示页脚本:loading 态切换(真实业务里由请求的 pending 状态驱动) */
(function () {
var btn = document.getElementById('btn-loading');
if (!btn) return;
var label = btn.querySelector('.kole-m-btn__label');
btn.addEventListener('click', function () {
if (btn.disabled) return;
var on = btn.classList.toggle('is-loading');
btn.setAttribute('aria-busy', on ? 'true' : 'false');
btn.disabled = on; /* 规格 §6.4:loading 期间阻止重复触发 */
if (on) {
btn.insertAdjacentHTML('afterbegin', '<span class="kole-m-btn__spinner" aria-hidden="true"></span>');
label.textContent = '提交中…';
} else {
var sp = btn.querySelector('.kole-m-btn__spinner');
if (sp) sp.remove();
label.textContent = '提交';
}
});
})();
</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/Button.jsx · React · 42 行
import React from 'react';
import './Button.css';
/* 按钮(移动端)— 规格 §6;三档高度(44 / 36 / 28),loading 期间阻止重复触发 */
export default function Button({
type = 'default',
size = 'default',
block = false,
disabled = false,
loading = false,
onClick,
children,
icon = null,
}) {
const cls =
'kole-m-btn' +
(type === 'primary' ? ' kole-m-btn--primary' : '') +
(type === 'text' ? ' kole-m-btn--text' : '') +
(type === 'danger' ? ' kole-m-btn--danger' : '') +
(type === 'default' ? ' kole-m-btn--default' : '') +
(size === 'large' ? ' kole-m-btn--large' : '') +
(size === 'small' ? ' kole-m-btn--small' : '') +
(block ? ' is-block' : '') +
(loading ? ' is-loading' : '');
return (
<button
className={cls}
type="button"
disabled={disabled || loading}
aria-busy={loading ? 'true' : 'false'}
onClick={() => {
if (disabled || loading) return;
if (onClick) onClick();
}}
>
{loading ? <span className="kole-m-btn__spinner" aria-hidden="true" /> : icon}
<span className="kole-m-btn__label">{children}</span>
</button>
);
}
frameworks-mobile/Button.vue2.vue · Vue 2 · 43 行
<template>
<button
class="kole-m-btn"
:class="btnClass"
type="button"
:disabled="disabled || loading"
:aria-busy="loading ? 'true' : 'false'"
@click="$emit('click')"
>
<span v-if="loading" class="kole-m-btn__spinner" aria-hidden="true"></span>
<span v-else-if="$slots.icon" class="kole-m-btn__icon"><slot name="icon"></slot></span>
<span class="kole-m-btn__label"><slot>按钮</slot></span>
</button>
</template>
<script>
var TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
var SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };
export default {
name: 'KoleMButton',
props: {
type: { type: String, default: 'default' },
size: { type: String, default: 'default' },
block: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false }
},
computed: {
btnClass: function () {
return [
TYPE_CLASS[this.type],
SIZE_CLASS[this.size],
this.block ? 'is-block' : '',
this.loading ? 'is-loading' : ''
].filter(Boolean);
}
}
};
</script>
<style src="./Button.css"></style>
frameworks-mobile/Button.vue3.vue · Vue 3 · 37 行
<template>
<button
class="kole-m-btn"
:class="btnClass"
type="button"
:disabled="disabled || loading"
:aria-busy="loading ? 'true' : 'false'"
@click="emit('click')"
>
<span v-if="loading" class="kole-m-btn__spinner" aria-hidden="true"></span>
<span v-else-if="$slots.icon" class="kole-m-btn__icon"><slot name="icon" /></span>
<span class="kole-m-btn__label"><slot>按钮</slot></span>
</button>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
type: { type: String, default: 'default' },
size: { type: String, default: 'default' },
block: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);
/* 与 React 端同一套类名组合规则 */
const TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
const SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };
const btnClass = computed(() =>
[TYPE_CLASS[props.type], SIZE_CLASS[props.size], props.block ? 'is-block' : '', props.loading ? 'is-loading' : ''].filter(Boolean)
);
</script>
<style src="./Button.css"></style>
frameworks-mobile/Button.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 95 行
<template>
<view
class="kole-m-btn"
:class="btnClass"
role="button"
:aria-disabled="disabled || loading ? 'true' : 'false'"
:aria-busy="loading ? 'true' : 'false'"
@tap="onTap"
>
<view v-if="loading" class="kole-m-btn__spinner"></view>
<text class="kole-m-btn__label"><slot></slot></text>
</view>
</template>
<script setup>
/* uni-app 端 · 按钮(移动端)— 规格 §6
跨端差异:用 uni 基础组件(view / text),点击用 @tap;尺寸用 rpx
(750rpx = 视口宽度,88rpx = 375pt 下的 44px 触控最小边长)。 */
import { computed } from 'vue';
const TYPE_CLASS = { primary: 'kole-m-btn--primary', default: 'kole-m-btn--default', text: 'kole-m-btn--text', danger: 'kole-m-btn--danger' };
const SIZE_CLASS = { large: 'kole-m-btn--large', small: 'kole-m-btn--small' };
const props = defineProps({
type: { type: String, default: 'default' },
size: { type: String, default: 'default' },
block: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false }
});
const emit = defineEmits(['click']);
const btnClass = computed(() =>
[TYPE_CLASS[props.type], SIZE_CLASS[props.size], props.block ? 'is-block' : '', props.loading ? 'is-loading' : ''].filter(Boolean)
);
function onTap() {
/* 规格 §6.5:loading 期间重复点击不触发第二次事件 */
if (props.disabled || props.loading) return;
emit('click');
}
</script>
<style>
.kole-m-btn {
--kole-m-touch-target: 88rpx;
--kole-m-font-size-body: 32rpx;
--kole-m-font-size-label: 28rpx;
--kole-m-font-size-caption: 22rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-height: 72rpx;
padding: 0 32rpx;
border: 1rpx solid transparent;
border-radius: var(--kole-radius-base);
background-color: transparent;
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
}
.kole-m-btn--large { min-height: var(--kole-m-touch-target); padding: 0 40rpx; font-size: var(--kole-m-font-size-body); }
.kole-m-btn--small { min-height: 56rpx; padding: 0 24rpx; font-size: var(--kole-m-font-size-caption); }
.kole-m-btn--primary { background-color: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
.kole-m-btn--default { background-color: var(--kole-color-card-bg); border-color: var(--kole-color-border); }
.kole-m-btn--text { color: var(--kole-color-brand); }
.kole-m-btn--danger { background-color: var(--kole-color-error); color: var(--kole-color-text-inverse); }
.kole-m-btn.is-block { width: 100%; }
.kole-m-btn[aria-disabled="true"] {
background-color: var(--kole-color-disabled-bg);
border-color: transparent;
color: var(--kole-color-text-disabled);
}
.kole-m-btn__label { line-height: 1.2; }
.kole-m-btn__spinner {
width: 28rpx;
height: 28rpx;
box-sizing: border-box;
border: 4rpx solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: kole-m-btn-spin 600ms linear infinite;
}
@keyframes kole-m-btn-spin {
to { transform: rotate(360deg); }
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 20 条 · 全部通过 报告 2026-09-22 23:01:05
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-button.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"confidence": "high",
"specSection": "6 · 按钮 Button",
"slug": "mobile-button",
"name": "按钮 Button",
"semanticTypeCandidates": [
"button",
"action"
],
"variantDimensions": [
{
"name": "type",
"values": [
"primary",
"default",
"text",
"danger"
]
},
{
"name": "size",
"values": [
"large",
"default",
"small"
]
},
{
"name": "block",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"type": "primary",
"size": "large",
"label": "主按钮(移动端默认档)"
},
{
"type": "default",
"size": "default",
"label": "次按钮"
},
{
"type": "text",
"size": "default",
"label": "文字按钮"
},
{
"type": "danger",
"size": "large",
"label": "危险按钮"
}
],
"anatomy": {
"button": "根元素,用原生 button,圆角取令牌",
"label": "按钮文字,单行不换行",
"icon": "可选图标或加载指示器,与文字间距 4px",
"block": "可选块级形态,撑满容器宽度"
},
"structurePatterns": {
"type": "primary / default / text / danger",
"size": "large(44px 高,移动端默认)/ default(36px)/ small(28px)"
},
"usageHints": [
"触发一个即时动作",
"高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作",
"按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)",
"loading 期间重复点击不触发第二次事件",
"仅有图标时必须给 aria-label"
],
"doNotInvent": [
"按钮内多行文字的排版规则",
"长按(long-press)的附加行为"
],
"unknowns": [
"图标与文字同时存在时的最小宽度",
"危险按钮是否需要二次确认"
],
"interaction": [
"高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作",
"按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)",
"loading 期间重复点击不触发第二次事件"
],
"accessibility": [
"使用原生 button,天然可聚焦、可键盘触发",
"仅有图标时必须给 aria-label",
"loading 时置 aria-busy=\"true\" 且 disabled(避免重复提交)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "type",
"type": "'primary' | 'default' | 'text' | 'danger'",
"default": "'default'",
"desc": "变体 type:主次层级靠颜色与边框区分(规格 §6.3)",
"required": false
},
{
"name": "size",
"type": "'large' | 'default' | 'small'",
"default": "'default'",
"desc": "变体 size;large 为 44px 移动端默认档(规格 §6.3)",
"required": false
},
{
"name": "block",
"type": "boolean",
"default": "false",
"desc": "块级形态,撑满容器宽度(规格 §6.2)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "置灰且不响应点击(规格 §6.4)",
"required": false
},
{
"name": "loading",
"type": "boolean",
"default": "false",
"desc": "显示加载指示器并阻止重复触发(规格 §6.4)",
"required": false
}
],
"events": [
{
"name": "click",
"params": "—",
"desc": "点击时触发(disabled / loading 时不触发)"
}
],
"slots": [
{
"name": "default",
"desc": "按钮文字(规格 §6.2)"
},
{
"name": "icon",
"desc": "可选图标或加载指示器,与文字间距 4px(规格 §6.2)"
}
]
},
"variantClasses": {
"type": {
"primary": [
".kole-m-btn--primary"
],
"default": [
".kole-m-btn--default"
],
"text": [
".kole-m-btn--text"
],
"danger": [
".kole-m-btn--danger"
]
},
"size": {
"large": [
".kole-m-btn--large"
],
"default": [],
"small": [
".kole-m-btn--small"
]
},
"block": {
"false": [],
"true": [
".is-block"
]
}
},
"demos": [
{
"id": "primary",
"group": "01 组件类型",
"title": "主按钮",
"desc": "一个区域里只放一个主按钮;移动端默认用 large 档(44px 高)。",
"variant": "type=primary / size=large"
},
{
"id": "default",
"group": "01 组件类型",
"title": "次按钮",
"desc": "白底描边,与主按钮并排时表示次要动作。",
"variant": "type=default"
},
{
"id": "text",
"group": "01 组件类型",
"title": "文字按钮",
"desc": "无背景无边框,用于弱操作(如「跳过」「查看详情」)。",
"variant": "type=text"
},
{
"id": "danger",
"group": "01 组件类型",
"title": "危险按钮",
"desc": "删除类动作用错误色;不要和主按钮同屏并列出现两次。",
"variant": "type=danger"
},
{
"id": "sizes",
"group": "02 组件状态",
"title": "三档尺寸",
"desc": "large 44px 给主操作区,default 36px 给表单,small 28px 只用于行列内联。",
"variant": "size=small | default | large"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "置灰且不响应;禁用原因要写在按钮附近,别只靠置灰暗示。",
"variant": "disabled=true"
},
{
"id": "loading",
"group": "02 组件状态",
"title": "加载中",
"desc": "显示指示器并置 aria-busy,重复点击不再触发(点按钮切换)。",
"variant": "loading=true"
}
],
"related": [
{
"slug": "actionsheet",
"why": "需要从一个入口提供多个操作时用动作面板;单个即时动作才用按钮"
},
{
"slug": "cell",
"why": "整行进入下级用单元格;单个动作才用按钮"
}
]
}