移动端导航加载

加载Loading

告诉用户「系统正在处理,请等」并占住当前位置

反馈 规格 22 · 加载 Loading 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">

<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-loading.css">

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

演示

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

01 组件类型

基础用法size=default / vertical=false

转圈 + 文案同行;文案要说明「在等什么」,只写「加载中」等于没说。

查看代码(演示页原文 · 9 行)
frameworks-mobile/Loading.html · basic
<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>
三档尺寸size=small | default | large

small 用于列表内联,default 用于区块,large 用于页面级或空态。

查看代码(演示页原文 · 17 行)
frameworks-mobile/Loading.html · size
<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

vertical=true 让文案不挤压指示器,空态与全屏面板里更稳。

查看代码(演示页原文 · 9 行)
frameworks-mobile/Loading.html · vertical
<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 组件状态

无文案text=''

只有转圈时,上下文说明必须由宿主在别处给出,否则用户不知道在等什么。

查看代码(演示页原文 · 8 行)
frameworks-mobile/Loading.html · text-only
<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>
全屏遮罩fullscreen=true / open

遮罩期间下方不可操作,用来挡住提交 / 支付这类动作的重复点击(点按钮切换)。

查看代码(演示页原文 · 15 行)
frameworks-mobile/Loading.html · fullscreen
<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
textstring''文案,说明「在等什么」;为空时只有转圈(规格 §22.2)N
verticalbooleanfalse变体 vertical:true 时指示器在上、文案在下(规格 §22.3)N
fullscreenbooleanfalse变体 fullscreen:true 时铺一层遮罩并截获手势(规格 §22.3/§22.5)N
openbooleanfalse全屏遮罩的显隐;仅 fullscreen=true 时有意义(规格 §22.4)N

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

事件

名称参数说明

插槽

名称说明
default自定义文案(与 text 二选一)(规格 §22.2 text)

CSS 变量

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

名称默认值说明
--kole-m-loading-size20px组件内部默认值,可在业务侧覆盖
--kole-m-loading-duration800ms组件内部默认值,可在业务侧覆盖

何时使用

  • 告诉用户「系统正在处理,请等」并占住当前位置
  • 加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)
  • 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
sizesmall / default / large
small .kole-m-loading--small
default --kole-m-loading-size
large .kole-m-loading--large
verticalfalse / true
false (由数据驱动,无专属类)
true .kole-m-loading--vertical
fullscreenfalse / 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 令牌(改一处两端生效)。

--kole-m-duration-slide --kole-m-ease-slide --kole-m-font-size-body --kole-m-font-size-caption --kole-m-font-size-label --kole-color-brand --kole-color-brand-bg --kole-color-card-bg --kole-color-mask --kole-color-text-body --kole-color-text-secondary --kole-font-family --kole-icon-size-16 --kole-radius-large --kole-shadow-high --kole-space-12 --kole-space-20 --kole-space-24 --kole-space-8 --kole-m-loading-duration --kole-m-loading-size

6 端源码

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

frameworks-mobile/Loading.css · 纯样式(CSS) · 91 行
frameworks-mobile/Loading.css
/* 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 行
frameworks-mobile/Loading.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · 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 行
frameworks-mobile/Loading.jsx
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 行
frameworks-mobile/Loading.vue2.vue
<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 行
frameworks-mobile/Loading.vue3.vue
<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 行
frameworks-mobile/Loading.uniapp.vue
<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": "只需要一个静态字形(不是等待语义)时用图标,不要拿图标假装加载"
    }
  ]
}