加载Loading
告诉用户「系统正在处理,请等」并占住当前位置
反馈 规格 22 · 加载 Loading 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-loading.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Loading.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
转圈 + 文案同行;文案要说明「在等什么」,只写「加载中」等于没说。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(转圈 + 文案;文案说明「在等什么」,不要只写「加载中」)</p>
<div class="demo-box" data-assert="loading-basic">
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在加载订单…</span>
</div>
</div>
</section>small 用于列表内联,default 用于区块,large 用于页面级或空态。
查看代码(演示页原文 · 17 行)
<section class="demo-block" data-demo="size">
<p class="demo-label">三档尺寸(size=small 16 / default 20 / large 28)</p>
<div class="demo-box demo-box--col" data-assert="loading-size">
<div class="kole-m-loading kole-m-loading--small" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">列表内联(small)</span>
</div>
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">区块默认(default)</span>
</div>
<div class="kole-m-loading kole-m-loading--large" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">页面级(large)</span>
</div>
</div>
</section>vertical=true 让文案不挤压指示器,空态与全屏面板里更稳。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="vertical">
<p class="demo-label">上下排布(vertical=true:空态与全屏里更稳,文案不挤压指示器)</p>
<div class="demo-box" data-assert="loading-vertical">
<div class="kole-m-loading kole-m-loading--vertical kole-m-loading--large" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在生成报表…</span>
</div>
</div>
</section>02 组件状态
只有转圈时,上下文说明必须由宿主在别处给出,否则用户不知道在等什么。
查看代码(演示页原文 · 8 行)
<section class="demo-block" data-demo="text-only">
<p class="demo-label">无文案(text 为空时只有转圈;此时必须由宿主给出上下文说明)</p>
<div class="demo-box" data-assert="loading-text-only">
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
</div>
</div>
</section>遮罩期间下方不可操作,用来挡住提交 / 支付这类动作的重复点击(点按钮切换)。
查看代码(演示页原文 · 15 行)
<section class="demo-block" data-demo="fullscreen">
<p class="demo-label">全屏遮罩(点按钮切换;遮罩期间下方不可操作 —— 生产环境铺满视口)</p>
<div class="demo-frame" data-assert="loading-fullscreen">
<div class="demo-frame__body">
<button class="demo-trigger" type="button" id="loading-trigger"
data-behavior="click-toggles-class:#loading-fs|is-open">提交订单(显示遮罩)</button>
</div>
<div class="kole-m-loading is-fullscreen" id="loading-fs" role="status" aria-live="polite" aria-busy="false">
<div class="kole-m-loading__panel">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在提交订单…</span>
</div>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
size | 'small' | 'default' | 'large' | 'default' | 变体 size:三档指示器尺寸(16 / 20 / 28),文案字号随档位(规格 §22.3) | N |
text | string | '' | 文案,说明「在等什么」;为空时只有转圈(规格 §22.2) | N |
vertical | boolean | false | 变体 vertical:true 时指示器在上、文案在下(规格 §22.3) | N |
fullscreen | boolean | false | 变体 fullscreen:true 时铺一层遮罩并截获手势(规格 §22.3/§22.5) | N |
open | boolean | false | 全屏遮罩的显隐;仅 fullscreen=true 时有意义(规格 §22.4) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|
插槽
| 名称 | 说明 |
|---|---|
default | 自定义文案(与 text 二选一)(规格 §22.2 text) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-loading-size | 20px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-loading-duration | 800ms | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 告诉用户「系统正在处理,请等」并占住当前位置
- 加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)
- fullscreen=true 且 open 时遮罩截获手势,下方内容不可点;未展开时不截获
- 转圈动画 800ms/圈(--kole-m-loading-duration 可覆盖),时长恒定不随尺寸变化
- prefers-reduced-motion: reduce 下停止旋转(改为静态环),避免前庭不适
交互与触控
- 加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)
- fullscreen=true 且 open 时遮罩截获手势,下方内容不可点;未展开时不截获
- 转圈动画 800ms/圈(--kole-m-loading-duration 可覆盖),时长恒定不随尺寸变化
- prefers-reduced-motion: reduce 下停止旋转(改为静态环),避免前庭不适
- 加载超过一次会话的合理时长时应由宿主提供取消入口,本组件不自己造取消按钮
无障碍
- 容器 role="status" + aria-live="polite"(状态变化被播报,且不打断当前朗读)
- 加载中置 aria-busy="true";全屏遮罩未展开时置 "false"
- 转圈是装饰(aria-hidden="true"),语义全部由文案与 role="status" 承担
- 文案要具体(「正在提交订单…」而不是「加载中」),读屏用户与视力用户获得同样的信息量
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 轻提示Toast | 只提示结果(成功 / 失败)且不要求等待时用轻提示;需要用户等待才用加载 |
| 对话框Dialog | 需要用户先确认才能继续时用对话框;加载不征求用户意见 |
| 下拉刷新PullRefresh | 列表顶部下拉触发的刷新用它(自带手势与指示区);区块内的等待才用加载 |
| 图标Icon | 只需要一个静态字形(不是等待语义)时用图标,不要拿图标假装加载 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 加载耗时的进度百分比(本组件不假装知道进度) |
| 禁止发明 | 超时后的自动提示 / 自动重试 |
| 禁止发明 | 多条加载的排队与合并 |
| 规格未定 | 全屏加载持续多久后应提示「可能需要更长时间」 |
| 规格未定 | 文案的最大长度与换行策略 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
loading | 根元素,内联形态(转圈 + 文案同行或上下排);fullscreen=true 时它本身即遮罩层 |
spinner | 转圈,由 CSS 动画驱动(三端一致,不依赖图片或字体) |
text | 可选文案,说明「在等什么」;为空时只有转圈 |
panel | 全屏形态下的卡片面板,承载指示器与文案,保证遮罩上的对比度可控 |
mask | 全屏形态的遮罩底色(取令牌,语义等同弹窗遮罩) |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | small / default / large | small .kole-m-loading--small default --kole-m-loading-sizelarge .kole-m-loading--large |
vertical | false / true | false (由数据驱动,无专属类) true .kole-m-loading--vertical |
fullscreen | false / true | false (由数据驱动,无专属类) true .is-fullscreen .kole-m-loading__panel |
代表变体
| 变体 | 标签 |
|---|---|
size=default · vertical=false | 区块内加载(转圈 + 文案同行) |
size=small · vertical=false | 列表内联加载 |
size=large · vertical=true | 空态 / 页面级加载(上下排布) |
size=default · fullscreen=true | 全屏遮罩(不可中断的提交) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Loading.css · 纯样式(CSS) · 91 行
/* Kole UI Mobile · Loading 样式 — 对齐 .design_library/kole-ui-mobile/spec/移动端规格.md §22
加载指示:一个转圈 + 一行文案。移动端与桌面端的差别在于**它经常独占整个屏幕** ——
提交订单、支付这类动作用 fullscreen 铺一层遮罩,期间用户不能操作下方内容。
令牌:尺寸/颜色取 --kole-*,间距与字号取 --kole-m-*;组件级变量 --kole-m-loading-size
与 --kole-m-loading-duration 可在业务侧覆盖。 */
.kole-m-loading {
--kole-m-loading-size: 20px;
--kole-m-loading-duration: 800ms;
display: inline-flex;
align-items: center;
gap: var(--kole-space-8);
box-sizing: border-box;
color: var(--kole-color-text-secondary);
font-family: var(--kole-font-family);
font-size: var(--kole-m-font-size-label);
line-height: 1.4;
}
/* 变体 size:small 16 / default 20 / large 28(文案字号随档位走) */
.kole-m-loading--small { --kole-m-loading-size: var(--kole-icon-size-16); font-size: var(--kole-m-font-size-caption); }
.kole-m-loading--large { --kole-m-loading-size: 28px; font-size: var(--kole-m-font-size-body); }
/* 变体 layout:vertical=true 时指示器在上、文案在下(全屏与空态里更稳) */
.kole-m-loading--vertical { flex-direction: column; gap: var(--kole-space-12); text-align: center; }
.kole-m-loading__spinner {
flex: 0 0 auto;
box-sizing: border-box;
width: var(--kole-m-loading-size);
height: var(--kole-m-loading-size);
border: 2px solid var(--kole-color-brand-bg);
border-top-color: var(--kole-color-brand);
border-radius: 50%;
animation: kole-m-loading-spin var(--kole-m-loading-duration) linear infinite;
}
.kole-m-loading__text {
color: inherit;
word-break: break-word;
}
@keyframes kole-m-loading-spin {
to { transform: rotate(360deg); }
}
/* 变体 fullscreen=true:铺满视口的遮罩 + 居中的卡片面板。
面板用卡片底色而不是透明底 —— 遮罩上的白字/深字对比度不可控,面板能钉死对比度。 */
.kole-m-loading.is-fullscreen {
position: fixed;
inset: 0;
z-index: 2300;
display: flex;
align-items: center;
justify-content: center;
background: var(--kole-color-mask);
opacity: 0;
pointer-events: none;
transition: opacity var(--kole-m-duration-slide) var(--kole-m-ease-slide);
}
.kole-m-loading.is-fullscreen.is-open {
opacity: 1;
pointer-events: auto;
}
.kole-m-loading__panel {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--kole-space-12);
min-width: 112px;
padding: var(--kole-space-20) var(--kole-space-24);
border-radius: var(--kole-radius-large);
background: var(--kole-color-card-bg);
box-shadow: var(--kole-shadow-high);
color: var(--kole-color-text-body);
}
/* 状态 done:加载结束后由宿主换成结果提示(本组件只保留过渡,不自动消失) */
.kole-m-loading.is-done .kole-m-loading__spinner {
animation: none;
border-color: var(--kole-color-brand);
}
@media (prefers-reduced-motion: reduce) {
.kole-m-loading { transition: none; }
.kole-m-loading__spinner { animation: none; }
}
frameworks-mobile/Loading.html · H5 原生(无框架) · 128 行
<!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 · Loading(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Loading.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-16);
padding: var(--kole-m-gutter); background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-box--col { flex-direction: column; align-items: flex-start; }
/* 展示框:transform 建立包含块,全屏遮罩限定在框内(生产环境铺满视口) */
.demo-frame { position: relative; min-height: 200px; overflow: hidden; transform: translateZ(0);
background: var(--kole-color-page-bg); border-block: 1px solid var(--kole-color-border); }
.demo-frame__body { padding: var(--kole-m-gutter); }
.demo-trigger { min-height: var(--kole-m-touch-target); padding: 0 var(--kole-space-16);
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-trigger:focus-visible { outline: 2px solid var(--kole-color-focus-ring); outline-offset: 2px; }
.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" data-assert="loading-basic">
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在加载订单…</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="size">
<p class="demo-label">三档尺寸(size=small 16 / default 20 / large 28)</p>
<div class="demo-box demo-box--col" data-assert="loading-size">
<div class="kole-m-loading kole-m-loading--small" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">列表内联(small)</span>
</div>
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">区块默认(default)</span>
</div>
<div class="kole-m-loading kole-m-loading--large" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">页面级(large)</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="vertical">
<p class="demo-label">上下排布(vertical=true:空态与全屏里更稳,文案不挤压指示器)</p>
<div class="demo-box" data-assert="loading-vertical">
<div class="kole-m-loading kole-m-loading--vertical kole-m-loading--large" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在生成报表…</span>
</div>
</div>
</section>
<section class="demo-block" data-demo="text-only">
<p class="demo-label">无文案(text 为空时只有转圈;此时必须由宿主给出上下文说明)</p>
<div class="demo-box" data-assert="loading-text-only">
<div class="kole-m-loading" role="status" aria-live="polite" aria-busy="true">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
</div>
</div>
</section>
<section class="demo-block" data-demo="fullscreen">
<p class="demo-label">全屏遮罩(点按钮切换;遮罩期间下方不可操作 —— 生产环境铺满视口)</p>
<div class="demo-frame" data-assert="loading-fullscreen">
<div class="demo-frame__body">
<button class="demo-trigger" type="button" id="loading-trigger"
data-behavior="click-toggles-class:#loading-fs|is-open">提交订单(显示遮罩)</button>
</div>
<div class="kole-m-loading is-fullscreen" id="loading-fs" role="status" aria-live="polite" aria-busy="false">
<div class="kole-m-loading__panel">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span class="kole-m-loading__text">正在提交订单…</span>
</div>
</div>
</div>
</section>
</div>
<script>
/* 演示页交互:点按钮切换全屏加载(真实业务里由请求的 pending 状态驱动) */
(function () {
var trig = document.getElementById('loading-trigger');
var box = document.getElementById('loading-fs');
if (!trig || !box) return;
trig.addEventListener('click', function () {
var on = box.classList.toggle('is-open');
box.setAttribute('aria-busy', on ? 'true' : 'false');
});
})();
</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/Loading.jsx · React · 38 行
import React from 'react';
import './Loading.css';
/* 加载(移动端)— 规格 §22;指示器持续旋转、状态由 aria-busy 与 role=status 承担。
fullscreen=true 时铺一层遮罩(期间不能操作下方内容),由 open 控制显隐。 */
export default function Loading({
size = 'default',
text = '',
vertical = false,
fullscreen = false,
open = false,
children = null,
}) {
const cls =
'kole-m-loading' +
(size === 'small' ? ' kole-m-loading--small' : '') +
(size === 'large' ? ' kole-m-loading--large' : '') +
(vertical ? ' kole-m-loading--vertical' : '') +
(fullscreen ? ' is-fullscreen' : '') +
(open ? ' is-open' : '');
const body = (
<>
<span className="kole-m-loading__spinner" aria-hidden="true" />
{text || children ? (
<span className="kole-m-loading__text">{text || children}</span>
) : null}
</>
);
/* 规格 §22.6:容器 role=status + aria-live=polite(读屏播报一次),加载中置 aria-busy */
return (
<div className={cls} role="status" aria-live="polite" aria-busy={fullscreen && !open ? 'false' : 'true'}>
{fullscreen ? <div className="kole-m-loading__panel">{body}</div> : body}
</div>
);
}
frameworks-mobile/Loading.vue2.vue · Vue 2 · 44 行
<template>
<div class="kole-m-loading" :class="loadingClass" role="status" aria-live="polite" :aria-busy="busy">
<div v-if="fullscreen" class="kole-m-loading__panel">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span v-if="text || $slots.default" class="kole-m-loading__text"><slot>{{ text }}</slot></span>
</div>
<template v-else>
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span v-if="text || $slots.default" class="kole-m-loading__text"><slot>{{ text }}</slot></span>
</template>
</div>
</template>
<script>
var SIZE_CLASS = { small: 'kole-m-loading--small', large: 'kole-m-loading--large' };
export default {
name: 'KoleMLoading',
props: {
size: { type: String, default: 'default' },
text: { type: String, default: '' },
vertical: { type: Boolean, default: false },
fullscreen: { type: Boolean, default: false },
open: { type: Boolean, default: false }
},
computed: {
loadingClass: function () {
return [
SIZE_CLASS[this.size],
this.vertical ? 'kole-m-loading--vertical' : '',
this.fullscreen ? 'is-fullscreen' : '',
this.open ? 'is-open' : ''
].filter(Boolean);
},
/* 规格 §22.6:加载中 aria-busy=true;全屏遮罩未展开时置 false */
busy: function () {
return this.fullscreen && !this.open ? 'false' : 'true';
}
}
};
</script>
<style src="./Loading.css"></style>
frameworks-mobile/Loading.vue3.vue · Vue 3 · 42 行
<template>
<div class="kole-m-loading" :class="loadingClass" role="status" aria-live="polite" :aria-busy="busy">
<div v-if="fullscreen" class="kole-m-loading__panel">
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span v-if="text || $slots.default" class="kole-m-loading__text"><slot>{{ text }}</slot></span>
</div>
<template v-else>
<span class="kole-m-loading__spinner" aria-hidden="true"></span>
<span v-if="text || $slots.default" class="kole-m-loading__text"><slot>{{ text }}</slot></span>
</template>
</div>
</template>
<script setup>
import { computed } from 'vue';
const SIZE_CLASS = { small: 'kole-m-loading--small', large: 'kole-m-loading--large' };
const props = defineProps({
size: { type: String, default: 'default' },
text: { type: String, default: '' },
vertical: { type: Boolean, default: false },
fullscreen: { type: Boolean, default: false },
open: { type: Boolean, default: false }
});
/* 与 React 端同一套类名组合规则 */
const loadingClass = computed(() =>
[
SIZE_CLASS[props.size],
props.vertical ? 'kole-m-loading--vertical' : '',
props.fullscreen ? 'is-fullscreen' : '',
props.open ? 'is-open' : ''
].filter(Boolean)
);
/* 规格 §22.6:加载中 aria-busy=true;全屏遮罩未展开时置 false(此时没有进行中的加载) */
const busy = computed(() => (props.fullscreen && !props.open ? 'false' : 'true'));
</script>
<style src="./Loading.css"></style>
frameworks-mobile/Loading.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 99 行
<template>
<view class="kole-m-loading" :class="loadingClass" :aria-busy="busy">
<view v-if="fullscreen" class="kole-m-loading__panel">
<view class="kole-m-loading__spinner"></view>
<text v-if="text" class="kole-m-loading__text">{{ text }}</text>
</view>
<template v-else>
<view class="kole-m-loading__spinner"></view>
<text v-if="text" class="kole-m-loading__text">{{ text }}</text>
</template>
</view>
</template>
<script setup>
/* uni-app 端 · 加载(移动端)— 规格 §22
跨端差异:用 view / text;转圈是 CSS animation(三端一致);尺寸用 rpx
(750rpx = 视口宽度,40rpx = 20px @375pt)。不绑手势、不可点。 */
import { computed } from 'vue';
const SIZE_CLASS = { small: 'kole-m-loading--small', large: 'kole-m-loading--large' };
const props = defineProps({
size: { type: String, default: 'default' },
text: { type: String, default: '' },
vertical: { type: Boolean, default: false },
fullscreen: { type: Boolean, default: false },
open: { type: Boolean, default: false }
});
const loadingClass = computed(() =>
[
SIZE_CLASS[props.size],
props.vertical ? 'kole-m-loading--vertical' : '',
props.fullscreen ? 'is-fullscreen' : '',
props.open ? 'is-open' : ''
].filter(Boolean)
);
const busy = computed(() => (props.fullscreen && !props.open ? 'false' : 'true'));
</script>
<style>
.kole-m-loading {
--kole-m-loading-size: 40rpx;
--kole-m-loading-duration: 800ms;
display: flex;
align-items: center;
box-sizing: border-box;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
.kole-m-loading--small { --kole-m-loading-size: 32rpx; font-size: var(--kole-m-font-size-caption); }
.kole-m-loading--large { --kole-m-loading-size: 56rpx; font-size: var(--kole-m-font-size-body); }
.kole-m-loading--vertical { flex-direction: column; justify-content: center; }
.kole-m-loading__spinner {
box-sizing: border-box;
width: var(--kole-m-loading-size);
height: var(--kole-m-loading-size);
margin-right: 16rpx;
border: 4rpx solid var(--kole-color-brand-bg);
border-top-color: var(--kole-color-brand);
border-radius: 50%;
animation: kole-m-loading-spin var(--kole-m-loading-duration) linear infinite;
}
.kole-m-loading--vertical .kole-m-loading__spinner { margin-right: 0; margin-bottom: 24rpx; }
@keyframes kole-m-loading-spin {
to { transform: rotate(360deg); }
}
.kole-m-loading.is-fullscreen {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2300;
align-items: center;
justify-content: center;
background-color: var(--kole-color-mask);
}
.kole-m-loading__panel {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
min-width: 224rpx;
padding: 40rpx 48rpx;
border-radius: var(--kole-radius-large);
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 16 条 · 全部通过 报告 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-loading.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "22 · 加载 Loading",
"confidence": "high",
"slug": "mobile-loading",
"name": "加载 Loading",
"semanticTypeCandidates": [
"loading",
"spinner"
],
"variantDimensions": [
{
"name": "size",
"values": [
"small",
"default",
"large"
]
},
{
"name": "vertical",
"values": [
"false",
"true"
]
},
{
"name": "fullscreen",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"size": "default",
"vertical": "false",
"label": "区块内加载(转圈 + 文案同行)"
},
{
"size": "small",
"vertical": "false",
"label": "列表内联加载"
},
{
"size": "large",
"vertical": "true",
"label": "空态 / 页面级加载(上下排布)"
},
{
"size": "default",
"fullscreen": "true",
"label": "全屏遮罩(不可中断的提交)"
}
],
"anatomy": {
"loading": "根元素,内联形态(转圈 + 文案同行或上下排);fullscreen=true 时它本身即遮罩层",
"spinner": "转圈,由 CSS 动画驱动(三端一致,不依赖图片或字体)",
"text": "可选文案,说明「在等什么」;为空时只有转圈",
"panel": "全屏形态下的卡片面板,承载指示器与文案,保证遮罩上的对比度可控",
"mask": "全屏形态的遮罩底色(取令牌,语义等同弹窗遮罩)"
},
"structurePatterns": {
"size": "small 16px / default 20px / large 28px",
"vertical": "false 横行(默认)/ true 上下排布",
"fullscreen": "false 区块内 / true 遮罩全屏(open 控制显隐)"
},
"usageHints": [
"告诉用户「系统正在处理,请等」并占住当前位置",
"加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)",
"fullscreen=true 且 open 时遮罩截获手势,下方内容不可点;未展开时不截获",
"转圈动画 800ms/圈(--kole-m-loading-duration 可覆盖),时长恒定不随尺寸变化",
"prefers-reduced-motion: reduce 下停止旋转(改为静态环),避免前庭不适"
],
"doNotInvent": [
"加载耗时的进度百分比(本组件不假装知道进度)",
"超时后的自动提示 / 自动重试",
"多条加载的排队与合并"
],
"unknowns": [
"全屏加载持续多久后应提示「可能需要更长时间」",
"文案的最大长度与换行策略"
],
"interaction": [
"加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)",
"fullscreen=true 且 open 时遮罩截获手势,下方内容不可点;未展开时不截获",
"转圈动画 800ms/圈(--kole-m-loading-duration 可覆盖),时长恒定不随尺寸变化",
"prefers-reduced-motion: reduce 下停止旋转(改为静态环),避免前庭不适",
"加载超过一次会话的合理时长时应由宿主提供取消入口,本组件不自己造取消按钮"
],
"accessibility": [
"容器 role=\"status\" + aria-live=\"polite\"(状态变化被播报,且不打断当前朗读)",
"加载中置 aria-busy=\"true\";全屏遮罩未展开时置 \"false\"",
"转圈是装饰(aria-hidden=\"true\"),语义全部由文案与 role=\"status\" 承担",
"文案要具体(「正在提交订单…」而不是「加载中」),读屏用户与视力用户获得同样的信息量"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "size",
"type": "'small' | 'default' | 'large'",
"default": "'default'",
"desc": "变体 size:三档指示器尺寸(16 / 20 / 28),文案字号随档位(规格 §22.3)",
"required": false
},
{
"name": "text",
"type": "string",
"default": "''",
"desc": "文案,说明「在等什么」;为空时只有转圈(规格 §22.2)",
"required": false
},
{
"name": "vertical",
"type": "boolean",
"default": "false",
"desc": "变体 vertical:true 时指示器在上、文案在下(规格 §22.3)",
"required": false
},
{
"name": "fullscreen",
"type": "boolean",
"default": "false",
"desc": "变体 fullscreen:true 时铺一层遮罩并截获手势(规格 §22.3/§22.5)",
"required": false
},
{
"name": "open",
"type": "boolean",
"default": "false",
"desc": "全屏遮罩的显隐;仅 fullscreen=true 时有意义(规格 §22.4)",
"required": false
}
],
"events": [],
"slots": [
{
"name": "default",
"desc": "自定义文案(与 text 二选一)(规格 §22.2 text)"
}
]
},
"variantClasses": {
"size": {
"small": [
".kole-m-loading--small"
],
"default": [
"--kole-m-loading-size"
],
"large": [
".kole-m-loading--large"
]
},
"vertical": {
"false": [],
"true": [
".kole-m-loading--vertical"
]
},
"fullscreen": {
"false": [],
"true": [
".is-fullscreen",
".kole-m-loading__panel"
]
}
},
"demos": [
{
"id": "basic",
"group": "01 组件类型",
"title": "基础用法",
"desc": "转圈 + 文案同行;文案要说明「在等什么」,只写「加载中」等于没说。",
"variant": "size=default / vertical=false"
},
{
"id": "size",
"group": "01 组件类型",
"title": "三档尺寸",
"desc": "small 用于列表内联,default 用于区块,large 用于页面级或空态。",
"variant": "size=small | default | large"
},
{
"id": "vertical",
"group": "01 组件类型",
"title": "上下排布",
"desc": "vertical=true 让文案不挤压指示器,空态与全屏面板里更稳。",
"variant": "vertical=true"
},
{
"id": "text-only",
"group": "02 组件状态",
"title": "无文案",
"desc": "只有转圈时,上下文说明必须由宿主在别处给出,否则用户不知道在等什么。",
"variant": "text=''"
},
{
"id": "fullscreen",
"group": "02 组件状态",
"title": "全屏遮罩",
"desc": "遮罩期间下方不可操作,用来挡住提交 / 支付这类动作的重复点击(点按钮切换)。",
"variant": "fullscreen=true / open"
}
],
"related": [
{
"slug": "mobile-toast",
"why": "只提示结果(成功 / 失败)且不要求等待时用轻提示;需要用户等待才用加载"
},
{
"slug": "mobile-dialog",
"why": "需要用户先确认才能继续时用对话框;加载不征求用户意见"
},
{
"slug": "pullrefresh",
"why": "列表顶部下拉触发的刷新用它(自带手势与指示区);区块内的等待才用加载"
},
{
"slug": "mobile-icon",
"why": "只需要一个静态字形(不是等待语义)时用图标,不要拿图标假装加载"
}
]
}