徽标Badge
在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)
数据展示 规格 9 · 徽标 Badge 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-badge.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Badge.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
挂在图标右上角,绝对定位不改变图标占位;数值读屏可用 aria-label 补全(如「12 条未读消息」)。
查看代码(演示页原文 · 13 行)
<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>只表示「有新内容」不带数量,因此对读屏隐藏(装饰性标记)。
查看代码(演示页原文 · 13 行)
<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>宽度随文字增长,用于「新 / 热 / 限时」这类状态,不是数字计数。
查看代码(演示页原文 · 13 行)
<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 组件状态
数值超过 max 时显示 max+(本实现 max 默认 99),避免角标被长数字撑破。
查看代码(演示页原文 · 13 行)
<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 时不挂靠任何内容,常用于列表标题或段落前。
查看代码(演示页原文 · 11 行)
<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>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
shape | 'dot' | 'number' | 'text' | 'number' | 变体 shape:红点 / 数字 / 短文字(规格 §9.3) | N |
count | number | 0 | 数字角标的数值;为 0 且未开启 showZero 时整个角标隐藏(规格 §9.4) | N |
max | number | 99 | 数值上限;超过时显示 max+(规格 §9.4 overflow) | N |
text | string | '' | 变体 shape=text 时显示的短文字(规格 §9.3) | N |
standalone | boolean | false | 变体 standalone:独立使用,不挂靠在被包裹内容上(规格 §9.3) | N |
showZero | boolean | false | 数值为 0 时是否仍显示角标(规格 §9.4 hidden) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|
插槽
| 名称 | 说明 |
|---|---|
default | 被包裹的内容(图标 / 头像 / 按钮),standalone=true 时不渲染(规格 §9.2 wrap) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-badge-size | 18px | 数字角标高度(同时也是圆角半径的两倍) |
--kole-m-badge-dot-size | 8px | 红点直径 |
何时使用
- 在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)
- 角标本身不可点(可点的是被包裹的元素)
- 角标不改变被包裹元素的布局尺寸(绝对定位)
- 数字角标配 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 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
shape | dot / number / text | dot .kole-m-badge--dot number (由数据驱动,无专属类) text .kole-m-badge--text |
standalone | false / true | false (由数据驱动,无专属类) true .kole-m-badge--standalone |
代表变体
| 变体 | 标签 |
|---|---|
shape=number · standalone=false | 数字角标(挂在图标右上角) |
shape=dot · standalone=false | 红点(只表示有新内容) |
shape=text · standalone=false | 短文字角标 |
shape=number · standalone=true | 独立使用(不挂靠内容) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Badge.css · 纯样式(CSS) · 66 行
/* 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 行
<!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 行
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 行
<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 行
<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 行
<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 就够了,只有需要强调计数时才叠徽标"
}
]
}