移动端导航消息通知

消息通知Message

在页面顶部给一条不打断操作的结果提示(提交成功、网络异常、库存告警、操作回执)

反馈 规格 42 · 消息通知 Message 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-message.css">

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

演示

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

01 组件类型

四种语气tone=info|success|warning|error

信息 / 成功 / 警告 / 错误:图标与侧边描边同族变化,文字保持正文色以保证对比度不随语气波动。

查看代码(演示页原文 · 40 行)
frameworks-mobile/Message.html · tones
<section class="demo-block" data-demo="tones">
  <p class="demo-label">四种语气(tone=info / success / warning / error:图标与侧边描边同族,文字保持正文色)</p>
  <div class="demo-frame" data-assert="message-tones">
    <div class="demo-frame__body">
      <div class="demo-actions">
        <button class="demo-trigger" type="button" id="msg-info-trigger"
                data-behavior="click-toggles-class:#msg-info|is-open">信息</button>
        <button class="demo-trigger" type="button" id="msg-success-trigger"
                data-behavior="click-toggles-class:#msg-success|is-open">成功</button>
        <button class="demo-trigger" type="button" id="msg-warning-trigger"
                data-behavior="click-toggles-class:#msg-warning|is-open">警告</button>
        <button class="demo-trigger" type="button" id="msg-error-trigger"
                data-behavior="click-toggles-class:#msg-error|is-open">错误</button>
      </div>
    </div>
    <div class="kole-m-message">
      <div class="kole-m-message__item kole-m-message__item--info" id="msg-info"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
        <span class="kole-m-message__text">已为你切换到最近使用的收货地址</span>
      </div>
      <div class="kole-m-message__item kole-m-message__item--success" id="msg-success"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">✓</span>
        <span class="kole-m-message__text">提交成功,预计 2 小时内审核完成</span>
      </div>
      <div class="kole-m-message__item kole-m-message__item--warning" id="msg-warning"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">⚠</span>
        <span class="kole-m-message__text">库存仅剩 2 件,下单后可能延迟发货</span>
      </div>
      <div class="kole-m-message__item kole-m-message__item--error" id="msg-error"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">✕</span>
        <span class="kole-m-message__text">网络异常,请检查连接后重试</span>
      </div>
    </div>
  </div>
  <p class="demo-hint">四条消息同一层并列,各自独立开关;点一次展开、再点一次收起。</p>
</section>
可关闭closable=true

closable=true:右侧 × 是原生 button、热区 44px;需要用户读完的长文案给一条显式关闭路径。

查看代码(演示页原文 · 17 行)
frameworks-mobile/Message.html · closable
<section class="demo-block" data-demo="closable">
  <p class="demo-label">可关闭(closable=true:右侧 × 是原生 button,热区 44px,点它立即收起)</p>
  <div class="demo-frame" data-assert="message-closable">
    <div class="demo-frame__body">
      <button class="demo-trigger" type="button" id="msg-closable-trigger"
              data-behavior="click-toggles-class:#msg-closable|is-open">显示可关闭消息</button>
    </div>
    <div class="kole-m-message">
      <div class="kole-m-message__item kole-m-message__item--info" id="msg-closable"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
        <span class="kole-m-message__text">本次更新包含 3 项改进,点右侧 ✕ 立即收起</span>
        <button class="kole-m-message__close" type="button" aria-label="关闭消息">✕</button>
      </div>
    </div>
  </div>
</section>

02 组件状态

自动消失duration=3000

duration 到点回传 close 由宿主收起;计时归宿主 —— 组件不持有默认时长以外的隐式行为。

查看代码(演示页原文 · 17 行)
frameworks-mobile/Message.html · duration
<section class="demo-block" data-demo="duration">
  <p class="demo-label">自动消失(duration=3000:到点回传 close 由宿主收起;duration=0 时不自动收)</p>
  <div class="demo-frame" data-assert="message-duration">
    <div class="demo-frame__body">
      <button class="demo-trigger" type="button" id="msg-duration-trigger"
              data-behavior="click-sets-attr:#msg-duration|data-open|true">显示并在 3 秒后自动收起</button>
      <p class="demo-hint" id="msg-duration-tip">计时归宿主:组件只负责显示态,到点回传 close</p>
    </div>
    <div class="kole-m-message">
      <div class="kole-m-message__item kole-m-message__item--success" id="msg-duration"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">✓</span>
        <span class="kole-m-message__text">已保存,3 秒后这条消息自动收起</span>
      </div>
    </div>
  </div>
</section>
多条并列多条同时 open

消息层纵向排列,多条同时在场各自独立;同屏条数上限与合并策略由宿主决定。

查看代码(演示页原文 · 16 行)
frameworks-mobile/Message.html · stack
<section class="demo-block" data-demo="stack">
  <p class="demo-label">多条并列(消息层纵向排列:新消息追加在下方,可同时在场)</p>
  <div class="demo-frame" data-assert="message-stack">
    <div class="demo-frame__body">两条消息同时在场,各自语气独立。</div>
    <div class="kole-m-message">
      <div class="kole-m-message__item kole-m-message__item--success is-open" data-open="true">
        <span class="kole-m-message__icon" aria-hidden="true">✓</span>
        <span class="kole-m-message__text">图片上传完成(1/2)</span>
      </div>
      <div class="kole-m-message__item kole-m-message__item--warning is-open" data-open="true">
        <span class="kole-m-message__icon" aria-hidden="true">⚠</span>
        <span class="kole-m-message__text">第 2 张超出 5MB,已跳过</span>
      </div>
    </div>
  </div>
</section>
默认形态closable=false

不带关闭按钮也不阻断操作:消息层自身 pointer-events: none,下方页面仍可正常点击与滚动。

查看代码(演示页原文 · 17 行)
frameworks-mobile/Message.html · plain
<section class="demo-block" data-demo="plain">
  <p class="demo-label">默认形态(不带关闭按钮、不阻断操作:消息层自身 pointer-events: none)</p>
  <div class="demo-frame" data-assert="message-plain">
    <div class="demo-frame__body">
      <button class="demo-trigger" type="button" id="msg-plain-trigger"
              data-behavior="click-sets-attr:#msg-plain|data-open|true">显示默认消息</button>
      <p class="demo-hint">消息浮在顶部,下面这行文字仍然可以正常选中与点击。</p>
    </div>
    <div class="kole-m-message">
      <div class="kole-m-message__item kole-m-message__item--info" id="msg-plain"
           role="status" aria-live="polite" data-open="false">
        <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
        <span class="kole-m-message__text">已复制到剪贴板</span>
      </div>
    </div>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
tone'info' | 'success' | 'warning' | 'error''info'变体 tone:语气配色与图标形态(规格 §42.3)N
openbooleanfalse状态 open:展开可见(规格 §42.4)N
textstring''消息文字;与默认插槽二选一,text 优先(规格 §42.2 text)N
durationnumber3000自动关闭时长(毫秒);0 表示不自动关闭(规格 §42.5)N
closablebooleanfalse变体 closable:是否显示右侧关闭按钮(规格 §42.3)N

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

事件

名称参数说明
close—点关闭按钮或 duration 到点时触发;是否收起由宿主决定(规格 §42.5)

插槽

名称说明
default消息内容(与 text 二选一;text 优先)

CSS 变量

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

名称默认值说明
--kole-m-message-duration240ms组件内部默认值,可在业务侧覆盖
--kole-m-message-gapvar(--kole-space-8)组件内部默认值,可在业务侧覆盖

何时使用

  • 在页面顶部给一条不打断操作的结果提示(提交成功、网络异常、库存告警、操作回执)
  • 触屏上页面就是有限的可视区域,所以消息浮在内容之上(fixed)且不吃手势(pointer-events: none),页面不跳
  • 与 Toast 的分工:Toast 占据视口中央用于「结果就是你此刻唯一关心的事」,Message 贴顶部一条用于「要告诉你但不该拦着你」
  • 组件形态:受控 open + 内容 props;duration 到点回传 close,由宿主决定是否收起
  • 命令式调用由宿主侧组装 —— 命令式 API 需要单例容器与跨端定时器治理,组件不提供全局方法

交互与触控

  • 消息层不吃手势(根 pointer-events: none),只有关闭按钮自己接收手势
  • 自动消失时长由宿主决定:duration=0 表示不自动关闭
  • 入场从上滑下并淡入 240ms(--kole-m-duration-slide),减少动态偏好下瞬时切换
  • 多条并列时纵向排列,新的追加在下方;同屏条数上限由宿主决定

无障碍

  • 每条消息 role="status" + aria-live="polite":读屏朗读一次,不打断用户当前操作
  • 语气图标 aria-hidden="true",语义全部由文字承担(颜色不是唯一的信息通道)
  • 关闭按钮是原生 button 并带 aria-label,可用 Tab 聚焦、Enter 触发
  • 消息不抢焦点:出现时不移动焦点,用户正在输入的内容不受影响

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
轻提示Toast需要占据视口中央、让用户只看结果时用轻提示;顶部一条且不打断操作用消息通知
通知栏NoticeBar常驻在页面里、随内容滚动的提醒用通知栏;消息通知浮在顶部且会消失
对话框Dialog需要用户先做决定再继续时用对话框,消息通知从不阻断操作

规格未定 / 禁止发明

类别条目
禁止发明自动关闭的默认时长与「超时后是否保留」的策略
禁止发明多条消息的排队、合并、去重与同屏上限
禁止发明命令式全局方法(Message.success() 这类)与其单例容器
禁止发明消息内的操作按钮与跳转链接(需要动作时请用通知栏或对话框)
规格未定tone=warning 与 error 是否需要不同的停留时长(当前统一由宿主传 duration)
规格未定顶部多条同时出现时是否该限制为最多两条(当前不限,由宿主控制)
规格未定是否需要在消息层上提供「点整条跳详情」的交互(当前只有可选的关闭按钮可点)

结构(anatomy)

字段说明
message消息层根元素,顶部固定的纵向列表容器;pointer-events: none 让下方页面照常可点
item单条消息,一行「图标 + 文字(+ 可选关闭)」,带 role="status" 与 aria-live="polite"
icon语气图标(装饰),aria-hidden="true",语义由文字承担
text消息文字,超长换行,不截断
close可选的关闭按钮(原生 button,热区 44px)

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
toneinfo / success / warning / error
info .kole-m-message__item--info
success .kole-m-message__item--success
warning .kole-m-message__item--warning
error .kole-m-message__item--error
closablefalse / true
false (由数据驱动,无专属类)
true .kole-m-message__close

代表变体

变体标签
tone=info · closable=false信息(默认形态)
tone=success · closable=false成功
tone=warning · closable=true警告(需要用户读完,带关闭)
tone=error · closable=true错误(带关闭)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-ease-slide --kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-m-safe-top --kole-m-touch-target --kole-color-border --kole-color-card-bg --kole-color-error --kole-color-focus-ring --kole-color-info --kole-color-success --kole-color-table-header-bg --kole-color-text-body --kole-color-text-secondary --kole-color-warning --kole-font-family --kole-icon-size-16 --kole-radius-base --kole-shadow-medium --kole-space-12 --kole-space-8 --kole-m-message-duration --kole-m-message-gap

6 端源码

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

frameworks-mobile/Message.css · 纯样式(CSS) · 114 行
frameworks-mobile/Message.css
/* Kole UI Mobile · Message 样式 — 对齐移动端规格 §42
   轻量消息通知:顶部居中,一行「图标 + 文字」,四态(success / warning / error / info)。
   与轻提示 Toast 的分工:Toast 占据视口中央并阻断交互,Message 贴在顶部、**不阻断**任何操作
   (只占顶部一条,下面的页面照常可点),因此它的 z-index 低于遮罩与弹窗。
   自动消失时长由宿主的定时器控制,本组件只负责显示态与退出动画。 */

.kole-m-message {
  --kole-m-message-duration: 240ms;
  --kole-m-message-gap: var(--kole-space-8);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1900;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--kole-m-message-gap);
  padding: calc(var(--kole-m-safe-top) + var(--kole-space-12)) var(--kole-m-gutter) 0;
  pointer-events: none;
}

.kole-m-message__item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--kole-space-8);
  width: 100%;
  max-width: 343px;
  min-height: var(--kole-m-touch-target);
  padding: var(--kole-space-8) var(--kole-space-12);
  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: var(--kole-font-family);
  font-size: var(--kole-m-font-size-label);
  line-height: 1.5;
  box-shadow: var(--kole-shadow-medium);
  /* 入场:从上方滑下 + 淡入;出场沿用同一个过渡(宿主移除 is-open 即可) */
  opacity: 0;
  transform: translateY(-100%);
  transition: opacity var(--kole-m-message-duration) var(--kole-m-ease-slide),
    transform var(--kole-m-message-duration) var(--kole-m-ease-slide);
}

.kole-m-message__item.is-open {
  opacity: 1;
  transform: translateY(0);
}

/* 变体 tone:四种语气(图标字形 + 侧边描边同族,文字保持正文色以保证对比度) */
.kole-m-message__item--info { border-inline-start: 3px solid var(--kole-color-info); }
.kole-m-message__item--success { border-inline-start: 3px solid var(--kole-color-success); }
.kole-m-message__item--warning { border-inline-start: 3px solid var(--kole-color-warning); }
.kole-m-message__item--error { border-inline-start: 3px solid var(--kole-color-error); }

.kole-m-message__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--kole-icon-size-16);
  font-size: var(--kole-m-font-size-body);
  line-height: 1;
}

.kole-m-message__item--info .kole-m-message__icon { color: var(--kole-color-info); }
.kole-m-message__item--success .kole-m-message__icon { color: var(--kole-color-success); }
.kole-m-message__item--warning .kole-m-message__icon { color: var(--kole-color-warning); }
.kole-m-message__item--error .kole-m-message__icon { color: var(--kole-color-error); }

.kole-m-message__text {
  flex: 1 1 auto;
  min-width: 0;
  word-break: break-word;
}

/* 关闭按钮(可选):原生 button,24px 视觉 + 44px 触控热区(靠 min-height 撑住) */
.kole-m-message__close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: var(--kole-m-touch-target);
  height: var(--kole-m-touch-target);
  margin-block: calc(-1 * var(--kole-space-8));
  margin-inline-end: calc(-1 * var(--kole-space-8));
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--kole-color-text-secondary);
  font-family: inherit;
  font-size: var(--kole-m-font-size-body);
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  pointer-events: auto;
}

.kole-m-message__close:active { background: var(--kole-color-table-header-bg); }

.kole-m-message__close:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .kole-m-message__item { transition: none; }
}
frameworks-mobile/Message.html · H5 原生(无框架) · 224 行
frameworks-mobile/Message.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 · Message(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Message.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); }
  /* 展示框:transform 建立包含块,让 position: fixed 的消息层落在本框内(生产环境落在视口顶部)。
     不做这一步,同页并列的多个消息层会全部叠在视口顶部,看起来「只渲染了一个」。 */
  .demo-frame { position: relative; max-width: 375px; margin: 0 auto; height: 200px; overflow: hidden;
    transform: translateZ(0); background: var(--kole-color-card-bg);
    border-block: 1px solid var(--kole-color-border); }
  .demo-frame__body { padding: var(--kole-space-12) var(--kole-m-gutter);
    font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); line-height: 1.7; }
  .demo-actions { display: flex; flex-wrap: wrap; gap: var(--kole-space-8); }
  .demo-trigger { min-height: var(--kole-m-touch-target); padding: 0 var(--kole-space-12);
    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-hint { margin: 0; padding: var(--kole-space-8) var(--kole-m-gutter) 0;
    font-size: var(--kole-m-font-size-caption); color: var(--kole-color-text-tertiary); }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="tones">
    <p class="demo-label">四种语气(tone=info / success / warning / error:图标与侧边描边同族,文字保持正文色)</p>
    <div class="demo-frame" data-assert="message-tones">
      <div class="demo-frame__body">
        <div class="demo-actions">
          <button class="demo-trigger" type="button" id="msg-info-trigger"
                  data-behavior="click-toggles-class:#msg-info|is-open">信息</button>
          <button class="demo-trigger" type="button" id="msg-success-trigger"
                  data-behavior="click-toggles-class:#msg-success|is-open">成功</button>
          <button class="demo-trigger" type="button" id="msg-warning-trigger"
                  data-behavior="click-toggles-class:#msg-warning|is-open">警告</button>
          <button class="demo-trigger" type="button" id="msg-error-trigger"
                  data-behavior="click-toggles-class:#msg-error|is-open">错误</button>
        </div>
      </div>
      <div class="kole-m-message">
        <div class="kole-m-message__item kole-m-message__item--info" id="msg-info"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
          <span class="kole-m-message__text">已为你切换到最近使用的收货地址</span>
        </div>
        <div class="kole-m-message__item kole-m-message__item--success" id="msg-success"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">✓</span>
          <span class="kole-m-message__text">提交成功,预计 2 小时内审核完成</span>
        </div>
        <div class="kole-m-message__item kole-m-message__item--warning" id="msg-warning"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">⚠</span>
          <span class="kole-m-message__text">库存仅剩 2 件,下单后可能延迟发货</span>
        </div>
        <div class="kole-m-message__item kole-m-message__item--error" id="msg-error"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">✕</span>
          <span class="kole-m-message__text">网络异常,请检查连接后重试</span>
        </div>
      </div>
    </div>
    <p class="demo-hint">四条消息同一层并列,各自独立开关;点一次展开、再点一次收起。</p>
  </section>

  <section class="demo-block" data-demo="closable">
    <p class="demo-label">可关闭(closable=true:右侧 × 是原生 button,热区 44px,点它立即收起)</p>
    <div class="demo-frame" data-assert="message-closable">
      <div class="demo-frame__body">
        <button class="demo-trigger" type="button" id="msg-closable-trigger"
                data-behavior="click-toggles-class:#msg-closable|is-open">显示可关闭消息</button>
      </div>
      <div class="kole-m-message">
        <div class="kole-m-message__item kole-m-message__item--info" id="msg-closable"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
          <span class="kole-m-message__text">本次更新包含 3 项改进,点右侧 ✕ 立即收起</span>
          <button class="kole-m-message__close" type="button" aria-label="关闭消息">✕</button>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="duration">
    <p class="demo-label">自动消失(duration=3000:到点回传 close 由宿主收起;duration=0 时不自动收)</p>
    <div class="demo-frame" data-assert="message-duration">
      <div class="demo-frame__body">
        <button class="demo-trigger" type="button" id="msg-duration-trigger"
                data-behavior="click-sets-attr:#msg-duration|data-open|true">显示并在 3 秒后自动收起</button>
        <p class="demo-hint" id="msg-duration-tip">计时归宿主:组件只负责显示态,到点回传 close</p>
      </div>
      <div class="kole-m-message">
        <div class="kole-m-message__item kole-m-message__item--success" id="msg-duration"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">✓</span>
          <span class="kole-m-message__text">已保存,3 秒后这条消息自动收起</span>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="stack">
    <p class="demo-label">多条并列(消息层纵向排列:新消息追加在下方,可同时在场)</p>
    <div class="demo-frame" data-assert="message-stack">
      <div class="demo-frame__body">两条消息同时在场,各自语气独立。</div>
      <div class="kole-m-message">
        <div class="kole-m-message__item kole-m-message__item--success is-open" data-open="true">
          <span class="kole-m-message__icon" aria-hidden="true">✓</span>
          <span class="kole-m-message__text">图片上传完成(1/2)</span>
        </div>
        <div class="kole-m-message__item kole-m-message__item--warning is-open" data-open="true">
          <span class="kole-m-message__icon" aria-hidden="true">⚠</span>
          <span class="kole-m-message__text">第 2 张超出 5MB,已跳过</span>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="plain">
    <p class="demo-label">默认形态(不带关闭按钮、不阻断操作:消息层自身 pointer-events: none)</p>
    <div class="demo-frame" data-assert="message-plain">
      <div class="demo-frame__body">
        <button class="demo-trigger" type="button" id="msg-plain-trigger"
                data-behavior="click-sets-attr:#msg-plain|data-open|true">显示默认消息</button>
        <p class="demo-hint">消息浮在顶部,下面这行文字仍然可以正常选中与点击。</p>
      </div>
      <div class="kole-m-message">
        <div class="kole-m-message__item kole-m-message__item--info" id="msg-plain"
             role="status" aria-live="polite" data-open="false">
          <span class="kole-m-message__icon" aria-hidden="true">ℹ</span>
          <span class="kole-m-message__text">已复制到剪贴板</span>
        </div>
      </div>
    </div>
  </section>
</div>
<script>
  /* 演示页交互:类名与 data-open 由**同一个函数**写入(避免视觉与状态分叉)。
     每块自带开关,互不依赖:触发器 id 与消息 id 成对写在下方映射里,
     因此任一 `?demo=<id>` 单块预览都能独立工作。 */
  (function () {
    function setOpen(el, open) {
      el.classList.toggle('is-open', open);
      el.setAttribute('data-open', open ? 'true' : 'false');
    }
    function bind(triggerId, itemId, mode) {
      var trig = document.getElementById(triggerId);
      var item = document.getElementById(itemId);
      if (!trig || !item) return;
      trig.addEventListener('click', function () {
        if (mode === 'show') setOpen(item, true);
        else setOpen(item, !item.classList.contains('is-open'));
      });
    }
    /* 四种语气:点一次展开、再点一次收起 */
    bind('msg-info-trigger', 'msg-info', 'toggle');
    bind('msg-success-trigger', 'msg-success', 'toggle');
    bind('msg-warning-trigger', 'msg-warning', 'toggle');
    bind('msg-error-trigger', 'msg-error', 'toggle');
    bind('msg-closable-trigger', 'msg-closable', 'toggle');
    bind('msg-plain-trigger', 'msg-plain', 'show');

    /* 可关闭:点 × 只关它自己那一条 */
    document.querySelectorAll('.kole-m-message__close').forEach(function (btn) {
      btn.addEventListener('click', function () {
        var item = btn.closest('.kole-m-message__item');
        if (item) setOpen(item, false);
      });
    });

    /* 自动消失:时长由宿主决定(这里演示 3000ms),到点收起。
       组件不持有默认时长 —— 规格 §42.7 明确把这条留给宿主。 */
    var dur = document.getElementById('msg-duration');
    var durTrig = document.getElementById('msg-duration-trigger');
    var tip = document.getElementById('msg-duration-tip');
    var timer = null;
    if (dur && durTrig) {
      durTrig.addEventListener('click', function () {
        if (timer) clearTimeout(timer);
        setOpen(dur, true);
        if (tip) tip.textContent = '显示中…3 秒后自动收起';
        timer = setTimeout(function () {
          setOpen(dur, false);
          timer = null;
          if (tip) tip.textContent = '已自动收起(计时归宿主:组件只负责显示态)';
        }, 3000);
      });
    }
  })();
</script>
<script>
  /* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
  (function () {
    var id = new URLSearchParams(location.search).get('demo');
    if (!id) return;
    var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
    var hit = false;
    blocks.forEach(function (b) {
      var on = b.getAttribute('data-demo') === id;
      if (on) hit = true;
      b.hidden = !on;
    });
    if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
    document.body.classList.add('demo-single');
    var demoBox = document.querySelector('.demo');
    if (demoBox) demoBox.style.minHeight = 'auto';
    blocks.forEach(function (b) {
      var label = b.querySelector('.demo-label');
      if (label && !b.hidden) label.hidden = true;
    });
  })();
</script>
</body>
</html>
frameworks-mobile/Message.jsx · React · 59 行
frameworks-mobile/Message.jsx
import React, { useEffect, useRef } from 'react';
import './Message.css';

/* 消息通知(移动端)— 规格 §42
   顶部居中、不阻断操作(根是 pointer-events: none,只有关闭按钮自己接收手势)。
   **两种用法**:
   ① 组件形态(本文件导出的即此形态):受控 open + 内容 props,计时到点回传 onClose;
   ② 命令式调用:宿主自己维护一个列表与定时器(见规格 §42.5),本组件不提供全局方法 ——
      命令式 API 需要单例容器与跨端定时器,属于宿主或框架层的职责(规格 §42.7)。
   本组件只暴露组件形态的 props / events。 */
export default function Message({
  tone = 'info',
  open = false,
  text = '',
  duration = 3000,
  closable = false,
  onClose,
  children = null,
}) {
  /* 自动消失:计时归宿主(duration 由调用方决定),组件只负责「到点回传 close」。
     duration=0 表示不自动关闭。open / duration 变化时重新计时。
     回调放 ref:把 onClose 写进依赖会让「宿主传内联箭头函数」的常见写法每次渲染都重置计时器,
     计时永远到不了点(定时器形同虚设),而这不是调用方的错。 */
  const closeRef = useRef(onClose);
  closeRef.current = onClose;
  useEffect(() => {
    if (!open || !duration) return undefined;
    const timer = setTimeout(() => {
      if (closeRef.current) closeRef.current();
    }, duration);
    return () => clearTimeout(timer);
  }, [open, duration]);

  const cls = 'kole-m-message__item kole-m-message__item--' + tone + (open ? ' is-open' : '');

  return (
    <div className="kole-m-message">
      <div className={cls} role="status" aria-live="polite" data-open={open ? 'true' : 'false'}>
        <span className="kole-m-message__icon" aria-hidden="true">
          {tone === 'success' ? '✓' : tone === 'warning' ? '⚠' : tone === 'error' ? '✕' : 'ℹ'}
        </span>
        <span className="kole-m-message__text">{text || children}</span>
        {closable ? (
          <button
            className="kole-m-message__close"
            type="button"
            aria-label="关闭消息"
            onClick={() => {
              if (onClose) onClose();
            }}
          >
            ✕
          </button>
        ) : null}
      </div>
    </div>
  );
}
frameworks-mobile/Message.vue2.vue · Vue 2 · 85 行
frameworks-mobile/Message.vue2.vue
<template>
  <div class="kole-m-message">
    <div
      class="kole-m-message__item"
      :class="itemClass"
      role="status"
      aria-live="polite"
      :data-open="open ? 'true' : 'false'"
    >
      <span class="kole-m-message__icon" aria-hidden="true">{{ glyph }}</span>
      <span class="kole-m-message__text"><slot>{{ text }}</slot></span>
      <button
        v-if="closable"
        class="kole-m-message__close"
        type="button"
        aria-label="关闭消息"
        @click="$emit('close')"
      >✕</button>
    </div>
  </div>
</template>

<script>
var GLYPH = { info: 'ℹ', success: '✓', warning: '⚠', error: '✕' };

export default {
  name: 'KoleMMessage',
  props: {
    tone: { type: String, default: 'info' },
    open: { type: Boolean, default: false },
    text: { type: String, default: '' },
    duration: { type: Number, default: 3000 },
    closable: { type: Boolean, default: false }
  },
  data: function () {
    return { timer: null };
  },
  computed: {
    glyph: function () {
      return GLYPH[this.tone] || GLYPH.info;
    },
    itemClass: function () {
      return [
        'kole-m-message__item--' + this.tone,
        this.open ? 'is-open' : ''
      ].filter(Boolean);
    }
  },
  watch: {
    open: function () {
      this.schedule();
    },
    duration: function () {
      this.schedule();
    }
  },
  mounted: function () {
    this.schedule();
  },
  beforeDestroy: function () {
    this.clear();
  },
  methods: {
    clear: function () {
      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = null;
      }
    },
    /* 自动消失:计时归宿主(duration 由调用方决定),组件只负责「到点回传 close」 */
    schedule: function () {
      this.clear();
      var self = this;
      if (this.open && this.duration) {
        this.timer = setTimeout(function () {
          self.$emit('close');
        }, this.duration);
      }
    }
  }
};
</script>

<style src="./Message.css"></style>
frameworks-mobile/Message.vue3.vue · Vue 3 · 77 行
frameworks-mobile/Message.vue3.vue
<template>
  <div class="kole-m-message">
    <div
      class="kole-m-message__item"
      :class="itemClass"
      role="status"
      aria-live="polite"
      :data-open="open ? 'true' : 'false'"
    >
      <span class="kole-m-message__icon" aria-hidden="true">{{ glyph }}</span>
      <span class="kole-m-message__text">{{ content }}</span>
      <button
        v-if="closable"
        class="kole-m-message__close"
        type="button"
        aria-label="关闭消息"
        @click="onCloseClick"
      >✕</button>
    </div>
  </div>
</template>

<script setup>
import { computed, onBeforeUnmount, useSlots, watch } from 'vue';

const props = defineProps({
  tone: { type: String, default: 'info' },
  open: { type: Boolean, default: false },
  text: { type: String, default: '' },
  duration: { type: Number, default: 3000 },
  closable: { type: Boolean, default: false }
});
const emit = defineEmits(['close']);

const slots = useSlots();
const content = computed(() => {
  if (props.text) return props.text;
  const nodes = slots.default ? slots.default() : [];
  return nodes.map((n) => (typeof n.children === 'string' ? n.children : '')).join('');
});

const GLYPH = { info: 'ℹ', success: '✓', warning: '⚠', error: '✕' };
const glyph = computed(() => GLYPH[props.tone] || GLYPH.info);

const itemClass = computed(() => [
  `kole-m-message__item--${props.tone}`,
  props.open ? 'is-open' : ''
].filter(Boolean));

/* 自动消失:计时归宿主(duration 由调用方决定),组件只负责「到点回传 close」。
   duration=0 表示不自动关闭;open 变化时重新计时。 */
let timer = null;
function clear() {
  if (timer) {
    clearTimeout(timer);
    timer = null;
  }
}
watch(
  () => [props.open, props.duration],
  () => {
    clear();
    if (props.open && props.duration) {
      timer = setTimeout(() => emit('close'), props.duration);
    }
  },
  { immediate: true }
);
onBeforeUnmount(clear);

function onCloseClick() {
  emit('close');
}
</script>

<style src="./Message.css"></style>
frameworks-mobile/Message.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 155 行
frameworks-mobile/Message.uniapp.vue
<template>
  <view class="kole-m-message">
    <view
      class="kole-m-message__item"
      :class="itemClass"
      :role="'status'"
      :aria-label="content"
      :data-open="open ? 'true' : 'false'"
    >
      <text class="kole-m-message__icon" aria-hidden="true">{{ glyph }}</text>
      <text class="kole-m-message__text">{{ content }}</text>
      <view
        v-if="closable"
        class="kole-m-message__close"
        role="button"
        aria-label="关闭消息"
        @tap="onCloseTap"
      >
        <text>✕</text>
      </view>
    </view>
  </view>
</template>

<script setup>
/* uni-app 端 · 消息通知(移动端)— 规格 §42
   跨端差异:小程序 / App 端的读屏主要认 aria-label(aria-live 支持不一致),
   因此这里把整条消息的名称写在 aria-label 上,图标 aria-hidden。
   自动消失仍由本端定时器控制(duration 由调用方决定),到点回传 close。
   尺寸用 rpx(2rpx ≈ 1px,88rpx = 44px 触控最小边长)。 */
import { computed, onBeforeUnmount, watch } from 'vue';

const props = defineProps({
  tone: { type: String, default: 'info' },
  open: { type: Boolean, default: false },
  text: { type: String, default: '' },
  duration: { type: Number, default: 3000 },
  closable: { type: Boolean, default: false }
});
const emit = defineEmits(['close']);

const GLYPH = { info: 'ℹ', success: '✓', warning: '⚠', error: '✕' };

const glyph = computed(() => GLYPH[props.tone] || GLYPH.info);
const content = computed(() => props.text);

const itemClass = computed(() => [
  `kole-m-message__item--${props.tone}`,
  props.open ? 'is-open' : ''
].filter(Boolean));

let timer = null;
function clear() {
  if (timer) {
    clearTimeout(timer);
    timer = null;
  }
}
watch(
  () => [props.open, props.duration],
  () => {
    clear();
    if (props.open && props.duration) {
      timer = setTimeout(() => emit('close'), props.duration);
    }
  },
  { immediate: true }
);
onBeforeUnmount(clear);

function onCloseTap() {
  emit('close');
}
</script>

<style>
.kole-m-message {
  --kole-m-message-duration: 240ms;
  --kole-m-touch-target: 88rpx;
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-gutter: 32rpx;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1900;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24rpx var(--kole-m-gutter) 0;
}

.kole-m-message__item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--kole-m-touch-target);
  padding: 16rpx 24rpx;
  border: 2rpx solid var(--kole-color-border);
  border-radius: 8rpx;
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-label);
  line-height: 1.5;
  box-shadow: var(--kole-shadow-medium);
  opacity: 0;
  transform: translateY(-100%);
  transition: opacity var(--kole-m-message-duration) ease-out;
}

.kole-m-message__item.is-open {
  opacity: 1;
  transform: translateY(0);
}

.kole-m-message__item--info { border-left: 6rpx solid var(--kole-color-info); }
.kole-m-message__item--success { border-left: 6rpx solid var(--kole-color-success); }
.kole-m-message__item--warning { border-left: 6rpx solid var(--kole-color-warning); }
.kole-m-message__item--error { border-left: 6rpx solid var(--kole-color-error); }

.kole-m-message__icon {
  flex: 0 0 auto;
  width: 32rpx;
  font-size: var(--kole-m-font-size-body);
  line-height: 1;
}

.kole-m-message__item--info .kole-m-message__icon { color: var(--kole-color-info); }
.kole-m-message__item--success .kole-m-message__icon { color: var(--kole-color-success); }
.kole-m-message__item--warning .kole-m-message__icon { color: var(--kole-color-warning); }
.kole-m-message__item--error .kole-m-message__icon { color: var(--kole-color-error); }

.kole-m-message__text {
  flex: 1;
  min-width: 0;
  padding-left: 16rpx;
}

.kole-m-message__close {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: var(--kole-m-touch-target);
  height: var(--kole-m-touch-target);
  margin-right: -16rpx;
  border-radius: 50%;
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-body);
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 22 条 · 全部通过 报告 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-message.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "42 · 消息通知 Message",
  "confidence": "high",
  "slug": "mobile-message",
  "name": "消息通知 Message",
  "semanticTypeCandidates": [
    "message",
    "notification",
    "banner"
  ],
  "variantDimensions": [
    {
      "name": "tone",
      "values": [
        "info",
        "success",
        "warning",
        "error"
      ]
    },
    {
      "name": "closable",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "tone": "info",
      "closable": "false",
      "label": "信息(默认形态)"
    },
    {
      "tone": "success",
      "closable": "false",
      "label": "成功"
    },
    {
      "tone": "warning",
      "closable": "true",
      "label": "警告(需要用户读完,带关闭)"
    },
    {
      "tone": "error",
      "closable": "true",
      "label": "错误(带关闭)"
    }
  ],
  "anatomy": {
    "message": "消息层根元素,顶部固定的纵向列表容器;pointer-events: none 让下方页面照常可点",
    "item": "单条消息,一行「图标 + 文字(+ 可选关闭)」,带 role=\"status\" 与 aria-live=\"polite\"",
    "icon": "语气图标(装饰),aria-hidden=\"true\",语义由文字承担",
    "text": "消息文字,超长换行,不截断",
    "close": "可选的关闭按钮(原生 button,热区 44px)"
  },
  "structurePatterns": {
    "tone": "info / success / warning / error —— 图标字形与侧边描边同族,文字保持正文色",
    "closable": "false 读完自己消失 / true 右侧出现关闭按钮",
    "状态类": "is-open 展开(滑下淡入 240ms)"
  },
  "usageHints": [
    "在页面顶部给一条不打断操作的结果提示(提交成功、网络异常、库存告警、操作回执)",
    "触屏上页面就是有限的可视区域,所以消息浮在内容之上(fixed)且不吃手势(pointer-events: none),页面不跳",
    "与 Toast 的分工:Toast 占据视口中央用于「结果就是你此刻唯一关心的事」,Message 贴顶部一条用于「要告诉你但不该拦着你」",
    "组件形态:受控 open + 内容 props;duration 到点回传 close,由宿主决定是否收起",
    "命令式调用由宿主侧组装 —— 命令式 API 需要单例容器与跨端定时器治理,组件不提供全局方法"
  ],
  "doNotInvent": [
    "自动关闭的默认时长与「超时后是否保留」的策略",
    "多条消息的排队、合并、去重与同屏上限",
    "命令式全局方法(Message.success() 这类)与其单例容器",
    "消息内的操作按钮与跳转链接(需要动作时请用通知栏或对话框)"
  ],
  "unknowns": [
    "tone=warning 与 error 是否需要不同的停留时长(当前统一由宿主传 duration)",
    "顶部多条同时出现时是否该限制为最多两条(当前不限,由宿主控制)",
    "是否需要在消息层上提供「点整条跳详情」的交互(当前只有可选的关闭按钮可点)"
  ],
  "interaction": [
    "消息层不吃手势(根 pointer-events: none),只有关闭按钮自己接收手势",
    "自动消失时长由宿主决定:duration=0 表示不自动关闭",
    "入场从上滑下并淡入 240ms(--kole-m-duration-slide),减少动态偏好下瞬时切换",
    "多条并列时纵向排列,新的追加在下方;同屏条数上限由宿主决定"
  ],
  "accessibility": [
    "每条消息 role=\"status\" + aria-live=\"polite\":读屏朗读一次,不打断用户当前操作",
    "语气图标 aria-hidden=\"true\",语义全部由文字承担(颜色不是唯一的信息通道)",
    "关闭按钮是原生 button 并带 aria-label,可用 Tab 聚焦、Enter 触发",
    "消息不抢焦点:出现时不移动焦点,用户正在输入的内容不受影响"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "tone",
        "type": "'info' | 'success' | 'warning' | 'error'",
        "default": "'info'",
        "desc": "变体 tone:语气配色与图标形态(规格 §42.3)",
        "required": false
      },
      {
        "name": "open",
        "type": "boolean",
        "default": "false",
        "desc": "状态 open:展开可见(规格 §42.4)",
        "required": false
      },
      {
        "name": "text",
        "type": "string",
        "default": "''",
        "desc": "消息文字;与默认插槽二选一,text 优先(规格 §42.2 text)",
        "required": false
      },
      {
        "name": "duration",
        "type": "number",
        "default": "3000",
        "desc": "自动关闭时长(毫秒);0 表示不自动关闭(规格 §42.5)",
        "required": false
      },
      {
        "name": "closable",
        "type": "boolean",
        "default": "false",
        "desc": "变体 closable:是否显示右侧关闭按钮(规格 §42.3)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "close",
        "params": "—",
        "desc": "点关闭按钮或 duration 到点时触发;是否收起由宿主决定(规格 §42.5)"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "消息内容(与 text 二选一;text 优先)"
      }
    ]
  },
  "variantClasses": {
    "tone": {
      "info": [
        ".kole-m-message__item--info"
      ],
      "success": [
        ".kole-m-message__item--success"
      ],
      "warning": [
        ".kole-m-message__item--warning"
      ],
      "error": [
        ".kole-m-message__item--error"
      ]
    },
    "closable": {
      "false": [],
      "true": [
        ".kole-m-message__close"
      ]
    }
  },
  "demos": [
    {
      "id": "tones",
      "group": "01 组件类型",
      "title": "四种语气",
      "desc": "信息 / 成功 / 警告 / 错误:图标与侧边描边同族变化,文字保持正文色以保证对比度不随语气波动。",
      "variant": "tone=info|success|warning|error"
    },
    {
      "id": "closable",
      "group": "01 组件类型",
      "title": "可关闭",
      "desc": "closable=true:右侧 × 是原生 button、热区 44px;需要用户读完的长文案给一条显式关闭路径。",
      "variant": "closable=true"
    },
    {
      "id": "duration",
      "group": "02 组件状态",
      "title": "自动消失",
      "desc": "duration 到点回传 close 由宿主收起;计时归宿主 —— 组件不持有默认时长以外的隐式行为。",
      "variant": "duration=3000"
    },
    {
      "id": "stack",
      "group": "02 组件状态",
      "title": "多条并列",
      "desc": "消息层纵向排列,多条同时在场各自独立;同屏条数上限与合并策略由宿主决定。",
      "variant": "多条同时 open"
    },
    {
      "id": "plain",
      "group": "02 组件状态",
      "title": "默认形态",
      "desc": "不带关闭按钮也不阻断操作:消息层自身 pointer-events: none,下方页面仍可正常点击与滚动。",
      "variant": "closable=false"
    }
  ],
  "related": [
    {
      "slug": "mobile-toast",
      "why": "需要占据视口中央、让用户只看结果时用轻提示;顶部一条且不打断操作用消息通知"
    },
    {
      "slug": "mobile-noticebar",
      "why": "常驻在页面里、随内容滚动的提醒用通知栏;消息通知浮在顶部且会消失"
    },
    {
      "slug": "mobile-dialog",
      "why": "需要用户先做决定再继续时用对话框,消息通知从不阻断操作"
    }
  ]
}