移动端导航徽标

徽标Badge

在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)

数据展示 规格 9 · 徽标 Badge 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-badge.css">

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

演示

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

01 组件类型

数字角标shape=number

挂在图标右上角,绝对定位不改变图标占位;数值读屏可用 aria-label 补全(如「12 条未读消息」)。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Badge.html · number
<section class="demo-block" data-demo="number">
  <p class="demo-label">数字角标(shape=number:挂在图标右上角,绝对定位不改变图标占位)</p>
  <div class="demo-box">
    <span class="kole-m-badge" data-assert="badge-number">
      <span class="kole-m-badge__wrap"><span class="demo-icon">消息</span></span>
      <span class="kole-m-badge__count" aria-label="12 条未读消息">12</span>
    </span>
    <span class="kole-m-badge">
      <span class="kole-m-badge__wrap"><span class="demo-icon">待办</span></span>
      <span class="kole-m-badge__count" aria-label="5 条待办">5</span>
    </span>
  </div>
</section>
红点shape=dot

只表示「有新内容」不带数量,因此对读屏隐藏(装饰性标记)。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Badge.html · dot
<section class="demo-block" data-demo="dot">
  <p class="demo-label">红点(shape=dot:只表示「有新内容」,不带数量,对读屏隐藏)</p>
  <div class="demo-box">
    <span class="kole-m-badge kole-m-badge--dot" data-assert="badge-dot">
      <span class="kole-m-badge__wrap"><span class="demo-icon">通知</span></span>
      <span class="kole-m-badge__count" aria-hidden="true"></span>
    </span>
    <span class="kole-m-badge kole-m-badge--dot">
      <span class="kole-m-badge__wrap"><span class="demo-icon">动态</span></span>
      <span class="kole-m-badge__count" aria-hidden="true"></span>
    </span>
  </div>
</section>
短文字shape=text

宽度随文字增长,用于「新 / 热 / 限时」这类状态,不是数字计数。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Badge.html · text
<section class="demo-block" data-demo="text">
  <p class="demo-label">短文字(shape=text:宽度随文字增长,用于「新 / 热 / 试用」这类状态)</p>
  <div class="demo-box">
    <span class="kole-m-badge kole-m-badge--text" data-assert="badge-text">
      <span class="kole-m-badge__wrap"><span class="demo-icon">功能</span></span>
      <span class="kole-m-badge__count">新</span>
    </span>
    <span class="kole-m-badge kole-m-badge--text">
      <span class="kole-m-badge__wrap"><span class="demo-icon">活动</span></span>
      <span class="kole-m-badge__count">限时</span>
    </span>
  </div>
</section>

02 组件状态

超上限状态 overflow

数值超过 max 时显示 max+(本实现 max 默认 99),避免角标被长数字撑破。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Badge.html · overflow
<section class="demo-block" data-demo="overflow">
  <p class="demo-label">超上限(状态 overflow:数值超过 max 时显示 max+,而不是完整数字)</p>
  <div class="demo-box">
    <span class="kole-m-badge" data-assert="badge-overflow">
      <span class="kole-m-badge__wrap"><span class="demo-icon">未读</span></span>
      <span class="kole-m-badge__count" aria-label="超过 99 条未读">99+</span>
    </span>
    <span class="kole-m-badge">
      <span class="kole-m-badge__wrap"><span class="demo-icon">消息</span></span>
      <span class="kole-m-badge__count" aria-label="128 条未读消息">99+</span>
    </span>
  </div>
</section>
独立使用standalone=true

standalone=true 时不挂靠任何内容,常用于列表标题或段落前。

查看代码(演示页原文 · 11 行)
frameworks-mobile/Badge.html · standalone
<section class="demo-block" data-demo="standalone">
  <p class="demo-label">独立使用(standalone=true:不挂靠内容,常用于列表标题或段落前)</p>
  <div class="demo-box">
    <span class="kole-m-badge kole-m-badge--standalone" data-assert="badge-standalone">
      <span class="kole-m-badge__count" aria-label="3 条待处理">3</span>
    </span>
    <span class="kole-m-badge kole-m-badge--standalone kole-m-badge--text">
      <span class="kole-m-badge__count">未读</span>
    </span>
  </div>
</section>
数值为 0状态 hidden

未开启 showZero 时为 0 不渲染(is-hidden),图标回到干净状态;右侧为对照。

查看代码(演示页原文 · 13 行)
frameworks-mobile/Badge.html · hidden
<section class="demo-block" data-demo="hidden">
  <p class="demo-label">数值为 0(状态 hidden:未开启 showZero 时整个角标不渲染,图标回到干净状态)</p>
  <div class="demo-box" data-assert="badge-hidden">
    <span class="kole-m-badge is-hidden">
      <span class="kole-m-badge__wrap"><span class="demo-icon">已读</span></span>
      <span class="kole-m-badge__count" aria-hidden="true">0</span>
    </span>
    <span class="kole-m-badge">
      <span class="kole-m-badge__wrap"><span class="demo-icon">对照</span></span>
      <span class="kole-m-badge__count" aria-label="1 条未读">1</span>
    </span>
  </div>
</section>

API

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

Props

名称类型默认值说明必传
shape'dot' | 'number' | 'text''number'变体 shape:红点 / 数字 / 短文字(规格 §9.3)N
countnumber0数字角标的数值;为 0 且未开启 showZero 时整个角标隐藏(规格 §9.4)N
maxnumber99数值上限;超过时显示 max+(规格 §9.4 overflow)N
textstring''变体 shape=text 时显示的短文字(规格 §9.3)N
standalonebooleanfalse变体 standalone:独立使用,不挂靠在被包裹内容上(规格 §9.3)N
showZerobooleanfalse数值为 0 时是否仍显示角标(规格 §9.4 hidden)N

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

事件

名称参数说明

插槽

名称说明
default被包裹的内容(图标 / 头像 / 按钮),standalone=true 时不渲染(规格 §9.2 wrap)

CSS 变量

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

名称默认值说明
--kole-m-badge-size18px数字角标高度(同时也是圆角半径的两倍)
--kole-m-badge-dot-size8px红点直径

何时使用

  • 在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)
  • 角标本身不可点(可点的是被包裹的元素)
  • 角标不改变被包裹元素的布局尺寸(绝对定位)
  • 数字角标配 aria-label(如「12 条未读」),否则读屏只读出数字
  • 纯装饰红点 aria-hidden="true"

交互与触控

  • 角标本身不可点(可点的是被包裹的元素)
  • 角标不改变被包裹元素的布局尺寸(绝对定位)

无障碍

  • 数字角标配 aria-label(如「12 条未读」),否则读屏只读出数字
  • 纯装饰红点 aria-hidden="true"

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

组件何时用它而不是本组件
底部标签栏TabBar标签栏项上的未读提示就该用徽标,而不是把数字写进标签文字里
标签Tag标签承载文字语义与关闭动作,徽标只承载计数与红点,两者不要互相替代
单元格Cell单元格右侧的状态文字用 value 就够了,只有需要强调计数时才叠徽标

规格未定 / 禁止发明

类别条目
禁止发明角标内容的动画(出现 / 消失)
规格未定max 的默认取值(本实现取 99)
规格未定独立使用时是否需要背景色

结构(anatomy)

字段说明
badge根元素,可包裹子元素(角标形态)也可独立使用
count数字或短文字
dot红点形态(无内容)
wrap被包裹的内容(图标 / 头像 / 按钮)

变体维度与类名映射

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

维度取值对应类名 / 变量
shapedot / number / text
dot .kole-m-badge--dot
number (由数据驱动,无专属类)
text .kole-m-badge--text
standalonefalse / true
false (由数据驱动,无专属类)
true .kole-m-badge--standalone

代表变体

变体标签
shape=number · standalone=false数字角标(挂在图标右上角)
shape=dot · standalone=false红点(只表示有新内容)
shape=text · standalone=false短文字角标
shape=number · standalone=true独立使用(不挂靠内容)

用到的令牌

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

--kole-m-font-size-caption --kole-color-error --kole-color-text-inverse --kole-font-family --kole-radius-small --kole-space-4 --kole-m-badge-dot-size --kole-m-badge-size

6 端源码

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

frameworks-mobile/Badge.css · 纯样式(CSS) · 66 行
frameworks-mobile/Badge.css
/* Kole UI Mobile · Badge 样式 — 对齐移动端规格 §9
   徽标:数字 / 红点 / 短文字三种形态;角标绝对定位,不改变被包裹元素的布局尺寸。
   配色只用令牌(实心底 + 反色文字):文字与底色对比度亮色 5.57:1 / 暗色 6.19:1。 */

.kole-m-badge {
  --kole-m-badge-size: 18px;      /* 数字角标高度(同时也是圆角半径的两倍) */
  --kole-m-badge-dot-size: 8px;   /* 红点直径 */
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}

/* 被包裹的内容(图标 / 头像 / 文字) */
.kole-m-badge__wrap {
  display: inline-flex;
  align-items: center;
}

.kole-m-badge__count {
  position: absolute;
  top: 0;
  inset-inline-end: 0;
  transform: translate(50%, -50%);
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--kole-m-badge-size);
  height: var(--kole-m-badge-size);
  padding: 0 var(--kole-space-4);
  border-radius: calc(var(--kole-m-badge-size) / 2);
  background: var(--kole-color-error);
  color: var(--kole-color-text-inverse);
  font-family: var(--kole-font-family);
  font-size: var(--kole-m-font-size-caption);
  line-height: 1;
  white-space: nowrap;
}

/* 变体 shape=dot:纯红点(无内容,对读屏是装饰) */
.kole-m-badge--dot .kole-m-badge__count {
  width: var(--kole-m-badge-dot-size);
  min-width: var(--kole-m-badge-dot-size);
  height: var(--kole-m-badge-dot-size);
  padding: 0;
  border-radius: 50%;
  font-size: 0;
}

/* 变体 shape=text:短文字角标(宽度随文字增长,不做胶囊圆角) */
.kole-m-badge--text .kole-m-badge__count {
  border-radius: var(--kole-radius-small);
  font-size: var(--kole-m-font-size-caption);
}

/* 变体 standalone=true:独立使用,不挂靠在任何内容上 */
.kole-m-badge--standalone .kole-m-badge__count {
  position: static;
  transform: none;
}

/* 状态 hidden:数值为 0 且未开启 showZero 时不渲染(由宿主加类) */
.kole-m-badge.is-hidden {
  display: none;
}
frameworks-mobile/Badge.html · H5 原生(无框架) · 129 行
frameworks-mobile/Badge.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 · Badge(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Badge.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; align-items: center; gap: var(--kole-space-32); padding: var(--kole-space-24) var(--kole-m-gutter);
    background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
  /* 被包裹的内容:用方形占位块代表图标 / 头像,纯展示不承载交互 */
  .demo-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px;
    border-radius: var(--kole-radius-base); background: var(--kole-color-table-header-bg);
    color: var(--kole-color-text-secondary); font-size: var(--kole-m-font-size-label); }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="number">
    <p class="demo-label">数字角标(shape=number:挂在图标右上角,绝对定位不改变图标占位)</p>
    <div class="demo-box">
      <span class="kole-m-badge" data-assert="badge-number">
        <span class="kole-m-badge__wrap"><span class="demo-icon">消息</span></span>
        <span class="kole-m-badge__count" aria-label="12 条未读消息">12</span>
      </span>
      <span class="kole-m-badge">
        <span class="kole-m-badge__wrap"><span class="demo-icon">待办</span></span>
        <span class="kole-m-badge__count" aria-label="5 条待办">5</span>
      </span>
    </div>
  </section>

  <section class="demo-block" data-demo="dot">
    <p class="demo-label">红点(shape=dot:只表示「有新内容」,不带数量,对读屏隐藏)</p>
    <div class="demo-box">
      <span class="kole-m-badge kole-m-badge--dot" data-assert="badge-dot">
        <span class="kole-m-badge__wrap"><span class="demo-icon">通知</span></span>
        <span class="kole-m-badge__count" aria-hidden="true"></span>
      </span>
      <span class="kole-m-badge kole-m-badge--dot">
        <span class="kole-m-badge__wrap"><span class="demo-icon">动态</span></span>
        <span class="kole-m-badge__count" aria-hidden="true"></span>
      </span>
    </div>
  </section>

  <section class="demo-block" data-demo="text">
    <p class="demo-label">短文字(shape=text:宽度随文字增长,用于「新 / 热 / 试用」这类状态)</p>
    <div class="demo-box">
      <span class="kole-m-badge kole-m-badge--text" data-assert="badge-text">
        <span class="kole-m-badge__wrap"><span class="demo-icon">功能</span></span>
        <span class="kole-m-badge__count">新</span>
      </span>
      <span class="kole-m-badge kole-m-badge--text">
        <span class="kole-m-badge__wrap"><span class="demo-icon">活动</span></span>
        <span class="kole-m-badge__count">限时</span>
      </span>
    </div>
  </section>

  <section class="demo-block" data-demo="overflow">
    <p class="demo-label">超上限(状态 overflow:数值超过 max 时显示 max+,而不是完整数字)</p>
    <div class="demo-box">
      <span class="kole-m-badge" data-assert="badge-overflow">
        <span class="kole-m-badge__wrap"><span class="demo-icon">未读</span></span>
        <span class="kole-m-badge__count" aria-label="超过 99 条未读">99+</span>
      </span>
      <span class="kole-m-badge">
        <span class="kole-m-badge__wrap"><span class="demo-icon">消息</span></span>
        <span class="kole-m-badge__count" aria-label="128 条未读消息">99+</span>
      </span>
    </div>
  </section>

  <section class="demo-block" data-demo="standalone">
    <p class="demo-label">独立使用(standalone=true:不挂靠内容,常用于列表标题或段落前)</p>
    <div class="demo-box">
      <span class="kole-m-badge kole-m-badge--standalone" data-assert="badge-standalone">
        <span class="kole-m-badge__count" aria-label="3 条待处理">3</span>
      </span>
      <span class="kole-m-badge kole-m-badge--standalone kole-m-badge--text">
        <span class="kole-m-badge__count">未读</span>
      </span>
    </div>
  </section>

  <section class="demo-block" data-demo="hidden">
    <p class="demo-label">数值为 0(状态 hidden:未开启 showZero 时整个角标不渲染,图标回到干净状态)</p>
    <div class="demo-box" data-assert="badge-hidden">
      <span class="kole-m-badge is-hidden">
        <span class="kole-m-badge__wrap"><span class="demo-icon">已读</span></span>
        <span class="kole-m-badge__count" aria-hidden="true">0</span>
      </span>
      <span class="kole-m-badge">
        <span class="kole-m-badge__wrap"><span class="demo-icon">对照</span></span>
        <span class="kole-m-badge__count" aria-label="1 条未读">1</span>
      </span>
    </div>
  </section>
</div>
<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/Badge.jsx · React · 40 行
frameworks-mobile/Badge.jsx
import React from 'react';
import './Badge.css';

/* 徽标(移动端)— 规格 §9;角标绝对定位不改变被包裹元素的布局尺寸。
   shape=dot 时对读屏隐藏;数字/文字角标带 aria-label,否则读屏只读出数字。 */
export default function Badge({
  shape = 'number',
  count = 0,
  max = 99,
  text = '',
  standalone = false,
  showZero = false,
  children = null,
}) {
  const overflow = shape === 'number' && count > max;
  const hidden = shape === 'number' && !showZero && count === 0;
  const display = shape === 'text' ? text : overflow ? `${max}+` : String(count);
  const ariaLabel = shape === 'dot' ? undefined : shape === 'text' ? text : `${count} 条未读`;

  const cls =
    'kole-m-badge' +
    (shape === 'dot' ? ' kole-m-badge--dot' : '') +
    (shape === 'text' ? ' kole-m-badge--text' : '') +
    (standalone ? ' kole-m-badge--standalone' : '') +
    (hidden ? ' is-hidden' : '');

  return (
    <span className={cls}>
      {standalone ? null : <span className="kole-m-badge__wrap">{children}</span>}
      <span
        className="kole-m-badge__count"
        aria-label={shape === 'dot' ? undefined : ariaLabel}
        aria-hidden={shape === 'dot' ? 'true' : undefined}
      >
        {shape === 'dot' ? null : display}
      </span>
    </span>
  );
}
frameworks-mobile/Badge.vue2.vue · Vue 2 · 50 行
frameworks-mobile/Badge.vue2.vue
<template>
  <span class="kole-m-badge" :class="badgeClass">
    <span v-if="!standalone" class="kole-m-badge__wrap"><slot></slot></span>
    <span
      class="kole-m-badge__count"
      :aria-label="shape === 'dot' ? null : ariaLabel"
      :aria-hidden="shape === 'dot' ? 'true' : null"
    >{{ shape === 'dot' ? '' : display }}</span>
  </span>
</template>

<script>
export default {
  name: 'KoleMBadge',
  props: {
    shape: { type: String, default: 'number' },
    count: { type: Number, default: 0 },
    max: { type: Number, default: 99 },
    text: { type: String, default: '' },
    standalone: { type: Boolean, default: false },
    showZero: { type: Boolean, default: false }
  },
  computed: {
    overflow: function () {
      return this.shape === 'number' && this.count > this.max;
    },
    hidden: function () {
      return this.shape === 'number' && !this.showZero && this.count === 0;
    },
    display: function () {
      if (this.shape === 'text') return this.text;
      return this.overflow ? this.max + '+' : String(this.count);
    },
    ariaLabel: function () {
      return this.shape === 'text' ? this.text : this.count + ' 条未读';
    },
    badgeClass: function () {
      return [
        this.shape === 'dot' ? 'kole-m-badge--dot' : '',
        this.shape === 'text' ? 'kole-m-badge--text' : '',
        this.standalone ? 'kole-m-badge--standalone' : '',
        this.hidden ? 'is-hidden' : ''
      ].filter(Boolean);
    }
  }
};
</script>

<style src="./Badge.css"></style>
frameworks-mobile/Badge.vue3.vue · Vue 3 · 41 行
frameworks-mobile/Badge.vue3.vue
<template>
  <span class="kole-m-badge" :class="badgeClass">
    <span v-if="!standalone" class="kole-m-badge__wrap"><slot></slot></span>
    <span
      class="kole-m-badge__count"
      :aria-label="shape === 'dot' ? null : ariaLabel"
      :aria-hidden="shape === 'dot' ? 'true' : null"
    >{{ shape === 'dot' ? '' : display }}</span>
  </span>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  shape: { type: String, default: 'number' },
  count: { type: Number, default: 0 },
  max: { type: Number, default: 99 },
  text: { type: String, default: '' },
  standalone: { type: Boolean, default: false },
  showZero: { type: Boolean, default: false }
});

const overflow = computed(() => props.shape === 'number' && props.count > props.max);
const hidden = computed(() => props.shape === 'number' && !props.showZero && props.count === 0);
const display = computed(() => {
  if (props.shape === 'text') return props.text;
  return overflow.value ? props.max + '+' : String(props.count);
});
const ariaLabel = computed(() => (props.shape === 'text' ? props.text : props.count + ' 条未读'));

const badgeClass = computed(() => [
  props.shape === 'dot' ? 'kole-m-badge--dot' : '',
  props.shape === 'text' ? 'kole-m-badge--text' : '',
  props.standalone ? 'kole-m-badge--standalone' : '',
  hidden.value ? 'is-hidden' : ''
].filter(Boolean));
</script>

<style src="./Badge.css"></style>
frameworks-mobile/Badge.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 100 行
frameworks-mobile/Badge.uniapp.vue
<template>
  <view class="kole-m-badge" :class="badgeClass">
    <view v-if="!standalone" class="kole-m-badge__wrap"><slot></slot></view>
    <text
      class="kole-m-badge__count"
      :aria-label="shape === 'dot' ? '' : ariaLabel"
      :aria-hidden="shape === 'dot' ? 'true' : 'false'"
    >{{ shape === 'dot' ? '' : display }}</text>
  </view>
</template>

<script setup>
/* uni-app 端 · 徽标(移动端)— 规格 §9
   跨端差异:用 view / text 结构,尺寸用 rpx(2rpx ≈ 1px);角标位置靠绝对定位与
   translate(三端一致);不依赖 DOM,无手势。 */
import { computed } from 'vue';

const props = defineProps({
  shape: { type: String, default: 'number' },
  count: { type: Number, default: 0 },
  max: { type: Number, default: 99 },
  text: { type: String, default: '' },
  standalone: { type: Boolean, default: false },
  showZero: { type: Boolean, default: false }
});

const overflow = computed(() => props.shape === 'number' && props.count > props.max);
const hidden = computed(() => props.shape === 'number' && !props.showZero && props.count === 0);
const display = computed(() => {
  if (props.shape === 'text') return props.text;
  return overflow.value ? props.max + '+' : String(props.count);
});
const ariaLabel = computed(() => (props.shape === 'text' ? props.text : props.count + ' 条未读'));

const badgeClass = computed(() => [
  props.shape === 'dot' ? 'kole-m-badge--dot' : '',
  props.shape === 'text' ? 'kole-m-badge--text' : '',
  props.standalone ? 'kole-m-badge--standalone' : '',
  hidden.value ? 'is-hidden' : ''
].filter(Boolean));
</script>

<style>
.kole-m-badge {
  --kole-m-badge-size: 36rpx;
  --kole-m-badge-dot-size: 16rpx;
  --kole-m-font-size-caption: 22rpx;
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}

.kole-m-badge__wrap {
  display: inline-flex;
  align-items: center;
}

.kole-m-badge__count {
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--kole-m-badge-size);
  height: var(--kole-m-badge-size);
  padding: 0 8rpx;
  border-radius: 18rpx;
  background-color: var(--kole-color-error);
  color: var(--kole-color-text-inverse);
  font-size: var(--kole-m-font-size-caption);
  line-height: 1;
  white-space: nowrap;
}

.kole-m-badge--dot .kole-m-badge__count {
  width: var(--kole-m-badge-dot-size);
  min-width: var(--kole-m-badge-dot-size);
  height: var(--kole-m-badge-dot-size);
  padding: 0;
  border-radius: 50%;
  font-size: 0;
}

.kole-m-badge--text .kole-m-badge__count {
  border-radius: 8rpx;
}

.kole-m-badge--standalone .kole-m-badge__count {
  position: static;
  transform: none;
}

.kole-m-badge.is-hidden {
  display: none;
}
</style>

测试与回归

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

断言 16 条 · 全部通过 报告 2026-09-22 03:04:00

复现命令
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-badge.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "confidence": "high",
  "specSection": "9 · 徽标 Badge",
  "slug": "mobile-badge",
  "name": "徽标 Badge",
  "semanticTypeCandidates": [
    "badge",
    "indicator",
    "count"
  ],
  "variantDimensions": [
    {
      "name": "shape",
      "values": [
        "dot",
        "number",
        "text"
      ]
    },
    {
      "name": "standalone",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "shape": "number",
      "standalone": "false",
      "label": "数字角标(挂在图标右上角)"
    },
    {
      "shape": "dot",
      "standalone": "false",
      "label": "红点(只表示有新内容)"
    },
    {
      "shape": "text",
      "standalone": "false",
      "label": "短文字角标"
    },
    {
      "shape": "number",
      "standalone": "true",
      "label": "独立使用(不挂靠内容)"
    }
  ],
  "anatomy": {
    "badge": "根元素,可包裹子元素(角标形态)也可独立使用",
    "count": "数字或短文字",
    "dot": "红点形态(无内容)",
    "wrap": "被包裹的内容(图标 / 头像 / 按钮)"
  },
  "structurePatterns": {
    "shape": "dot / number / text",
    "standalone": "false(包裹在子元素上)/ true(独立使用)"
  },
  "usageHints": [
    "在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)",
    "角标本身不可点(可点的是被包裹的元素)",
    "角标不改变被包裹元素的布局尺寸(绝对定位)",
    "数字角标配 aria-label(如「12 条未读」),否则读屏只读出数字",
    "纯装饰红点 aria-hidden=\"true\""
  ],
  "doNotInvent": [
    "角标内容的动画(出现 / 消失)"
  ],
  "unknowns": [
    "max 的默认取值(本实现取 99)",
    "独立使用时是否需要背景色"
  ],
  "interaction": [
    "角标本身不可点(可点的是被包裹的元素)",
    "角标不改变被包裹元素的布局尺寸(绝对定位)"
  ],
  "accessibility": [
    "数字角标配 aria-label(如「12 条未读」),否则读屏只读出数字",
    "纯装饰红点 aria-hidden=\"true\""
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "shape",
        "type": "'dot' | 'number' | 'text'",
        "default": "'number'",
        "desc": "变体 shape:红点 / 数字 / 短文字(规格 §9.3)",
        "required": false
      },
      {
        "name": "count",
        "type": "number",
        "default": "0",
        "desc": "数字角标的数值;为 0 且未开启 showZero 时整个角标隐藏(规格 §9.4)",
        "required": false
      },
      {
        "name": "max",
        "type": "number",
        "default": "99",
        "desc": "数值上限;超过时显示 max+(规格 §9.4 overflow)",
        "required": false
      },
      {
        "name": "text",
        "type": "string",
        "default": "''",
        "desc": "变体 shape=text 时显示的短文字(规格 §9.3)",
        "required": false
      },
      {
        "name": "standalone",
        "type": "boolean",
        "default": "false",
        "desc": "变体 standalone:独立使用,不挂靠在被包裹内容上(规格 §9.3)",
        "required": false
      },
      {
        "name": "showZero",
        "type": "boolean",
        "default": "false",
        "desc": "数值为 0 时是否仍显示角标(规格 §9.4 hidden)",
        "required": false
      }
    ],
    "events": [],
    "slots": [
      {
        "name": "default",
        "desc": "被包裹的内容(图标 / 头像 / 按钮),standalone=true 时不渲染(规格 §9.2 wrap)"
      }
    ]
  },
  "variantClasses": {
    "shape": {
      "dot": [
        ".kole-m-badge--dot"
      ],
      "number": [],
      "text": [
        ".kole-m-badge--text"
      ]
    },
    "standalone": {
      "false": [],
      "true": [
        ".kole-m-badge--standalone"
      ]
    }
  },
  "demos": [
    {
      "id": "number",
      "group": "01 组件类型",
      "title": "数字角标",
      "desc": "挂在图标右上角,绝对定位不改变图标占位;数值读屏可用 aria-label 补全(如「12 条未读消息」)。",
      "variant": "shape=number"
    },
    {
      "id": "dot",
      "group": "01 组件类型",
      "title": "红点",
      "desc": "只表示「有新内容」不带数量,因此对读屏隐藏(装饰性标记)。",
      "variant": "shape=dot"
    },
    {
      "id": "text",
      "group": "01 组件类型",
      "title": "短文字",
      "desc": "宽度随文字增长,用于「新 / 热 / 限时」这类状态,不是数字计数。",
      "variant": "shape=text"
    },
    {
      "id": "overflow",
      "group": "02 组件状态",
      "title": "超上限",
      "desc": "数值超过 max 时显示 max+(本实现 max 默认 99),避免角标被长数字撑破。",
      "variant": "状态 overflow"
    },
    {
      "id": "standalone",
      "group": "02 组件状态",
      "title": "独立使用",
      "desc": "standalone=true 时不挂靠任何内容,常用于列表标题或段落前。",
      "variant": "standalone=true"
    },
    {
      "id": "hidden",
      "group": "02 组件状态",
      "title": "数值为 0",
      "desc": "未开启 showZero 时为 0 不渲染(is-hidden),图标回到干净状态;右侧为对照。",
      "variant": "状态 hidden"
    }
  ],
  "related": [
    {
      "slug": "tabbar",
      "why": "标签栏项上的未读提示就该用徽标,而不是把数字写进标签文字里"
    },
    {
      "slug": "mobile-tag",
      "why": "标签承载文字语义与关闭动作,徽标只承载计数与红点,两者不要互相替代"
    },
    {
      "slug": "cell",
      "why": "单元格右侧的状态文字用 value 就够了,只有需要强调计数时才叠徽标"
    }
  ]
}