头像Avatar
用一张图或一两个字符代表一个主体(用户、企业、群组);移动端列表与详情页里大量出现,必须能单手扫读,因此尺寸只有三档、形状只有两种
数据展示 规格 23 · 头像 Avatar 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-avatar.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Avatar.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
small 32 / default 40 / large 56,文字字号随尺寸放大,小尺寸下仍可读。
查看代码(演示页原文 · 8 行)
<section class="demo-block" data-demo="size">
<p class="demo-label">三档尺寸(small 32 / default 40 / large 56,文字随尺寸放大)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--small kole-m-avatar--circle" role="img" aria-label="王小明 头像" data-assert="avatar-size-small"><span class="kole-m-avatar__text">王</span></span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="李小红 头像" data-assert="avatar-size-default"><span class="kole-m-avatar__text">李</span></span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--circle" role="img" aria-label="赵小刚 头像" data-assert="avatar-size-large"><span class="kole-m-avatar__text">赵</span></span>
</div>
</section>circle 用于人(成员、好友),square 用于企业或群组(标志、团队)。
查看代码(演示页原文 · 8 行)
<section class="demo-block" data-demo="shape">
<p class="demo-label">两种形状(circle 用于人 / square 用于企业、群组)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="张 头像" data-assert="avatar-circle"><span class="kole-m-avatar__text">张</span></span>
<span class="kole-m-avatar kole-m-avatar--square" role="img" aria-label="云启科技 标志" data-assert="avatar-square"><span class="kole-m-avatar__text">云启</span></span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" role="img" aria-label="设计中心 标志"><span class="kole-m-avatar__text">设计</span></span>
</div>
</section>type=image 时图片铺满容器并按形状裁切,语义交给 <img alt>。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="image">
<p class="demo-label">图片头像(type=image:铺满容器并按形状裁切)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" data-assert="avatar-image-circle">
<img class="kole-m-avatar__image" alt="示例用户 头像"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjMkY1NEVCIi8+PGNpcmNsZSBjeD0iNDAiIGN5PSIzMCIgcj0iMTMiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNMTMgNzRjNC0xNSAxNC0yMiAyNy0yMnMyMyA3IDI3IDIyeiIgZmlsbD0iI0ZGRkZGRiIvPjwvc3ZnPg==">
</span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" data-assert="avatar-image-square">
<img class="kole-m-avatar__image" alt="示例企业 标志"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjMkY1NEVCIi8+PGNpcmNsZSBjeD0iNDAiIGN5PSIzMCIgcj0iMTMiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNMTMgNzRjNC0xNSAxNC0yMiAyNy0yMnMyMyA3IDI3IDIyeiIgZmlsbD0iI0ZGRkZGRiIvPjwvc3ZnPg==">
</span>
</div>
</section>02 组件状态
状态 fallback:图片不可用时退回文字,底色转中性底,绝不出现裂图。
查看代码(演示页原文 · 12 行)
<section class="demo-block" data-demo="fallback">
<p class="demo-label">图片兜底(状态 fallback:图片不可用时退回文字,不出现裂图)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle is-fallback" role="img" aria-label="头像加载失败,显示 陈" data-assert="avatar-fallback">
<span class="kole-m-avatar__text">陈</span>
</span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="周 头像">
<span class="kole-m-avatar__text">周</span>
</span>
<span class="demo-tag">图片失败 → 文字</span>
</div>
</section>角标位挂在线状态或未读数,绝对定位不改变头像尺寸。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="badge">
<p class="demo-label">带角标(badge 位:挂在线状态或未读数,不改变头像尺寸)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="吴 头像,在线" data-assert="avatar-badge-online">
<span class="kole-m-avatar__text">吴</span>
<span class="kole-m-avatar__badge" aria-hidden="true"></span>
</span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" role="img" aria-label="研发一组 标志,在线">
<span class="kole-m-avatar__text">研发</span>
<span class="kole-m-avatar__badge" aria-hidden="true"></span>
</span>
</div>
</section>状态 disabled:置灰用于已离职或已停用的主体。
查看代码(演示页原文 · 11 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(状态 disabled:置灰,用于已离职或已停用的主体)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle is-disabled" role="img" aria-label="孙 头像(已停用)" data-assert="avatar-disabled" aria-disabled="true">
<span class="kole-m-avatar__text">孙</span>
</span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="钱 头像">
<span class="kole-m-avatar__text">钱</span>
</span>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
size | 'small' | 'default' | 'large' | 'default' | 变体 size:三档尺寸 32 / 40 / 56px,文字随尺寸放大(规格 §23.3) | N |
shape | 'circle' | 'square' | 'circle' | 变体 shape:circle 用于人,square 用于企业或群组(规格 §23.3) | N |
type | 'text' | 'image' | 'text' | 变体 type:文字头像 / 图片头像,图片失败退回文字(规格 §23.3) | N |
src | string | '' | 图片地址,type=image 时必填(规格 §23.2 image) | N |
alt | string | '' | 可访问名:图片头像作为 <img alt>,文字头像作为根节点 aria-label(规格 §23.6) | N |
disabled | boolean | false | 状态 disabled:置灰(如成员已离职)(规格 §23.4) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
error | (event) | 图片加载失败时触发一次;是否替换资源由宿主决定(规格 §23.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 文字内容(姓名首字或简称)(规格 §23.2 text) |
badge | 右下角角标位,挂在线状态或未读数(规格 §23.2 badge) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-avatar-size | 40px | 三档尺寸:small 32 / default 40 / large 56 |
--kole-m-avatar-radius | 50% | 形状:circle 50% / square 圆角方形 |
--kole-m-avatar-font-size | 14px | 文字随尺寸放大,保证小尺寸下仍可读 |
--kole-m-avatar-badge-size | 10px | 右下角角标位直径 |
--kole-m-avatar-size | 32px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-avatar-font-size | 12px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-avatar-badge-size | 8px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-avatar-size | 56px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-avatar-font-size | 18px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-avatar-badge-size | 12px | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 用一张图或一两个字符代表一个主体(用户、企业、群组);移动端列表与详情页里大量出现,必须能单手扫读,因此尺寸只有三档、形状只有两种
- 图片不可用时必须立刻退回文字,不能出现裂图
- 头像本身不是按钮;可点时必须由宿主包一层原生 button 或 a,热区不小于 44×44
- 图片 alt 或根节点 aria-label 必填其一,读屏读「姓名 + 头像」
- 图片加载失败只触发一次 error 事件,是否替换资源由宿主决定
交互与触控
- 头像本身不是按钮;可点时必须由宿主包一层原生 button 或 a,热区不小于 44×44
- 图片加载失败只触发一次 error 事件,是否替换资源由宿主决定
无障碍
- 文字头像根节点 role="img" 并带 aria-label(内容为姓名)
- 图片头像直接用 <img alt>,alt 为空时视为装饰并对读屏隐藏
- 角标不改变头像的可访问名
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 列表List | 头像作为列表行的前缀位时用列表承载整行;只有单张头像时用本组件 |
| 徽标Badge | 需要数字角标时用徽标组件包裹头像;只表示在线状态用本组件自带的 badge 位 |
| 标签Tag | 要用文字标注状态或分类时用标签;头像只承载主体身份 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 图片裁剪的 focal point(人脸居中)算法 |
| 禁止发明 | 角标位置随形状(圆 / 方)的微调规则 |
| 规格未定 | 文字头像的底色是否按名字散列取多色 |
| 规格未定 | 姓名超过两个汉字时的截断规则 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
avatar | 根元素,正方形圆角容器,尺寸由 size 决定 |
image | 图片,铺满容器并按形状裁切 |
text | 文字内容(姓名首字或简称),图片缺失时它就是主体 |
fallback | 兜底节点,图片加载失败后退回的形状(文字或图标) |
badge | 可选右下角角标位,挂在线状态或未读数 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | small / default / large | small .kole-m-avatar--small default (由数据驱动,无专属类) large .kole-m-avatar--large |
shape | circle / square | circle .kole-m-avatar--circle square .kole-m-avatar--square |
type | text / image | text (由数据驱动,无专属类) image .kole-m-avatar__image |
代表变体
| 变体 | 标签 |
|---|---|
size=default · shape=circle · type=text | 文字头像(圆形,用户默认形态) |
size=large · shape=square · type=image | 图片头像(圆角方形,企业 / 群组) |
size=small · shape=circle · type=text | 小尺寸头像(列表行前缀位) |
size=default · shape=circle · type=image | 图片加载失败退回文字(状态 fallback) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Avatar.css · 纯样式(CSS) · 94 行
/* Kole UI Mobile · Avatar 样式 — 对齐移动端规格 §23
头像:三档尺寸 × 两种形状 × 文字 / 图片两种类型;
文字头像用品牌底 + 反色字(亮色 5.85:1 / 暗色 5.87:1),图片失败退回文字(is-fallback)。 */
.kole-m-avatar {
--kole-m-avatar-size: 40px; /* 三档尺寸:small 32 / default 40 / large 56 */
--kole-m-avatar-radius: 50%; /* 形状:circle 50% / square 圆角方形 */
--kole-m-avatar-font-size: 14px; /* 文字随尺寸放大,保证小尺寸下仍可读 */
--kole-m-avatar-badge-size: 10px; /* 右下角角标位直径 */
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
box-sizing: border-box;
width: var(--kole-m-avatar-size);
height: var(--kole-m-avatar-size);
overflow: hidden;
border-radius: var(--kole-m-avatar-radius);
background: var(--kole-color-brand);
color: var(--kole-color-text-inverse);
font-family: var(--kole-font-family);
font-size: var(--kole-m-avatar-font-size);
line-height: 1;
user-select: none;
}
/* 变体 size:三档(small 32 / default 40 / large 56) */
.kole-m-avatar--small {
--kole-m-avatar-size: 32px;
--kole-m-avatar-font-size: 12px;
--kole-m-avatar-badge-size: 8px;
}
.kole-m-avatar--large {
--kole-m-avatar-size: 56px;
--kole-m-avatar-font-size: 18px;
--kole-m-avatar-badge-size: 12px;
}
/* 变体 shape:circle 用于人,square 用于企业 / 群组 */
.kole-m-avatar--circle { --kole-m-avatar-radius: 50%; }
.kole-m-avatar--square { --kole-m-avatar-radius: var(--kole-radius-medium); }
/* 变体 type=image:图片铺满容器并按形状裁切 */
.kole-m-avatar__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 变体 type=text:一到两个字符居中 */
.kole-m-avatar__text {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 状态 fallback:图片不可用时退回文字,底色转为中性底 */
.kole-m-avatar.is-fallback {
background: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
}
/* 状态 disabled:置灰(如成员已离职) */
.kole-m-avatar.is-disabled {
background: var(--kole-color-disabled-bg);
color: var(--kole-color-text-disabled);
}
/* 角标位:挂在线状态或未读数,内容由宿主插槽提供 */
.kole-m-avatar__badge {
position: absolute;
bottom: 0;
right: 0;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--kole-m-avatar-badge-size);
height: var(--kole-m-avatar-badge-size);
border: 2px solid var(--kole-color-card-bg);
border-radius: 50%;
background: var(--kole-color-success);
}
/* 头像本身是内容不是控件;宿主把它包进 button / a 时才需要键盘焦点环 */
.kole-m-avatar:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
frameworks-mobile/Avatar.html · H5 原生(无框架) · 116 行
<!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 · Avatar(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Avatar.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-16); padding: var(--kole-space-24) var(--kole-m-gutter);
background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-tag { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 11px;
background: var(--kole-color-brand-bg); color: var(--kole-color-brand); font-size: var(--kole-m-font-size-caption); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="size">
<p class="demo-label">三档尺寸(small 32 / default 40 / large 56,文字随尺寸放大)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--small kole-m-avatar--circle" role="img" aria-label="王小明 头像" data-assert="avatar-size-small"><span class="kole-m-avatar__text">王</span></span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="李小红 头像" data-assert="avatar-size-default"><span class="kole-m-avatar__text">李</span></span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--circle" role="img" aria-label="赵小刚 头像" data-assert="avatar-size-large"><span class="kole-m-avatar__text">赵</span></span>
</div>
</section>
<section class="demo-block" data-demo="shape">
<p class="demo-label">两种形状(circle 用于人 / square 用于企业、群组)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="张 头像" data-assert="avatar-circle"><span class="kole-m-avatar__text">张</span></span>
<span class="kole-m-avatar kole-m-avatar--square" role="img" aria-label="云启科技 标志" data-assert="avatar-square"><span class="kole-m-avatar__text">云启</span></span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" role="img" aria-label="设计中心 标志"><span class="kole-m-avatar__text">设计</span></span>
</div>
</section>
<section class="demo-block" data-demo="image">
<p class="demo-label">图片头像(type=image:铺满容器并按形状裁切)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" data-assert="avatar-image-circle">
<img class="kole-m-avatar__image" alt="示例用户 头像"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjMkY1NEVCIi8+PGNpcmNsZSBjeD0iNDAiIGN5PSIzMCIgcj0iMTMiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNMTMgNzRjNC0xNSAxNC0yMiAyNy0yMnMyMyA3IDI3IDIyeiIgZmlsbD0iI0ZGRkZGRiIvPjwvc3ZnPg==">
</span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" data-assert="avatar-image-square">
<img class="kole-m-avatar__image" alt="示例企业 标志"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSIjMkY1NEVCIi8+PGNpcmNsZSBjeD0iNDAiIGN5PSIzMCIgcj0iMTMiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNMTMgNzRjNC0xNSAxNC0yMiAyNy0yMnMyMyA3IDI3IDIyeiIgZmlsbD0iI0ZGRkZGRiIvPjwvc3ZnPg==">
</span>
</div>
</section>
<section class="demo-block" data-demo="fallback">
<p class="demo-label">图片兜底(状态 fallback:图片不可用时退回文字,不出现裂图)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle is-fallback" role="img" aria-label="头像加载失败,显示 陈" data-assert="avatar-fallback">
<span class="kole-m-avatar__text">陈</span>
</span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="周 头像">
<span class="kole-m-avatar__text">周</span>
</span>
<span class="demo-tag">图片失败 → 文字</span>
</div>
</section>
<section class="demo-block" data-demo="badge">
<p class="demo-label">带角标(badge 位:挂在线状态或未读数,不改变头像尺寸)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="吴 头像,在线" data-assert="avatar-badge-online">
<span class="kole-m-avatar__text">吴</span>
<span class="kole-m-avatar__badge" aria-hidden="true"></span>
</span>
<span class="kole-m-avatar kole-m-avatar--large kole-m-avatar--square" role="img" aria-label="研发一组 标志,在线">
<span class="kole-m-avatar__text">研发</span>
<span class="kole-m-avatar__badge" aria-hidden="true"></span>
</span>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(状态 disabled:置灰,用于已离职或已停用的主体)</p>
<div class="demo-box">
<span class="kole-m-avatar kole-m-avatar--circle is-disabled" role="img" aria-label="孙 头像(已停用)" data-assert="avatar-disabled" aria-disabled="true">
<span class="kole-m-avatar__text">孙</span>
</span>
<span class="kole-m-avatar kole-m-avatar--circle" role="img" aria-label="钱 头像">
<span class="kole-m-avatar__text">钱</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/Avatar.jsx · React · 54 行
import React, { useState } from 'react';
import './Avatar.css';
/* 头像(移动端)— 规格 §23
文字头像用 role="img" + aria-label(读屏读「姓名 头像」);图片头像的语义由 <img alt> 承担,
加载失败只回传一次 error 事件并把内容退回文字(状态 fallback),宿主决定要不要换资源。 */
export default function Avatar({
size = 'default',
shape = 'circle',
type = 'text',
src = '',
alt = '',
disabled = false,
onError,
children = null,
badge = null,
}) {
const [failed, setFailed] = useState(false);
const showImage = type === 'image' && !!src && !failed;
const fallback = type === 'image' && failed;
const cls =
'kole-m-avatar' +
` kole-m-avatar--${size}` +
` kole-m-avatar--${shape}` +
(fallback ? ' is-fallback' : '') +
(disabled ? ' is-disabled' : '');
function handleError(e) {
setFailed(true);
if (onError) onError(e);
}
return (
<span
className={cls}
role={showImage ? undefined : 'img'}
aria-label={showImage ? undefined : alt || undefined}
aria-disabled={disabled ? 'true' : undefined}
>
{showImage ? (
<img className="kole-m-avatar__image" src={src} alt={alt} onError={handleError} />
) : (
<span className="kole-m-avatar__text">{children}</span>
)}
{badge ? (
<span className="kole-m-avatar__badge" aria-hidden="true">
{badge}
</span>
) : null}
</span>
);
}
frameworks-mobile/Avatar.vue2.vue · Vue 2 · 58 行
<template>
<span
class="kole-m-avatar"
:class="avatarClass"
:role="showImage ? null : 'img'"
:aria-label="showImage ? null : (alt || null)"
:aria-disabled="disabled ? 'true' : null"
>
<img
v-if="showImage"
class="kole-m-avatar__image"
:src="src"
:alt="alt"
@error="onImageError"
>
<span v-else class="kole-m-avatar__text"><slot /></span>
<span v-if="$slots.badge" class="kole-m-avatar__badge" aria-hidden="true"><slot name="badge" /></span>
</span>
</template>
<script>
export default {
name: 'KoleMAvatar',
props: {
size: { type: String, default: 'default' },
shape: { type: String, default: 'circle' },
type: { type: String, default: 'text' },
src: { type: String, default: '' },
alt: { type: String, default: '' },
disabled: { type: Boolean, default: false }
},
data: function () {
return { failed: false };
},
computed: {
showImage: function () {
return this.type === 'image' && !!this.src && !this.failed;
},
avatarClass: function () {
return [
'kole-m-avatar--' + this.size,
'kole-m-avatar--' + this.shape,
this.type === 'image' && this.failed ? 'is-fallback' : '',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
}
},
methods: {
onImageError: function (e) {
this.failed = true;
this.$emit('error', e);
}
}
};
</script>
<style src="./Avatar.css"></style>
frameworks-mobile/Avatar.vue3.vue · Vue 3 · 52 行
<template>
<span
class="kole-m-avatar"
:class="avatarClass"
:role="showImage ? null : 'img'"
:aria-label="showImage ? null : (alt || null)"
:aria-disabled="disabled ? 'true' : null"
>
<img
v-if="showImage"
class="kole-m-avatar__image"
:src="src"
:alt="alt"
@error="onImageError"
>
<span v-else class="kole-m-avatar__text"><slot /></span>
<span v-if="$slots.badge" class="kole-m-avatar__badge" aria-hidden="true"><slot name="badge" /></span>
</span>
</template>
<script setup>
/* 头像(移动端)— 规格 §23:图片加载失败退回文字(状态 fallback),并 emit error 一次 */
import { computed, ref } from 'vue';
const props = defineProps({
size: { type: String, default: 'default' },
shape: { type: String, default: 'circle' },
type: { type: String, default: 'text' },
src: { type: String, default: '' },
alt: { type: String, default: '' },
disabled: { type: Boolean, default: false }
});
const emit = defineEmits(['error']);
const failed = ref(false);
const showImage = computed(() => props.type === 'image' && !!props.src && !failed.value);
const avatarClass = computed(() => [
`kole-m-avatar--${props.size}`,
`kole-m-avatar--${props.shape}`,
props.type === 'image' && failed.value ? 'is-fallback' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function onImageError(e) {
failed.value = true;
emit('error', e);
}
</script>
<style src="./Avatar.css"></style>
frameworks-mobile/Avatar.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 131 行
<template>
<view
class="kole-m-avatar"
:class="avatarClass"
:role="showImage ? '' : 'img'"
:aria-label="showImage ? '' : (alt || '')"
:aria-disabled="disabled ? 'true' : 'false'"
>
<image
v-if="showImage"
class="kole-m-avatar__image"
:src="src"
:alt="alt"
mode="aspectFill"
@error="onImageError"
></image>
<text v-else class="kole-m-avatar__text"><slot></slot></text>
<view v-if="$slots.badge" class="kole-m-avatar__badge" aria-hidden="true">
<slot name="badge"></slot>
</view>
</view>
</template>
<script setup>
/* uni-app 端 · 头像(移动端)— 规格 §23
跨端差异:图片用 <image mode="aspectFill"> 承担裁切(小程序没有 object-fit 语义),
加载失败走 @error 回退文字(与 H5 端的 is-fallback 同一类名);
尺寸用 rpx(88rpx = 375pt 下的 44px,故 40px 头像 = 80rpx)。 */
import { computed, ref } from 'vue';
const props = defineProps({
size: { type: String, default: 'default' },
shape: { type: String, default: 'circle' },
type: { type: String, default: 'text' },
src: { type: String, default: '' },
alt: { type: String, default: '' },
disabled: { type: Boolean, default: false }
});
const emit = defineEmits(['error']);
const failed = ref(false);
const showImage = computed(() => props.type === 'image' && !!props.src && !failed.value);
const avatarClass = computed(() => [
`kole-m-avatar--${props.size}`,
`kole-m-avatar--${props.shape}`,
props.type === 'image' && failed.value ? 'is-fallback' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function onImageError(e) {
failed.value = true;
emit('error', e);
}
</script>
<style>
.kole-m-avatar {
--kole-m-avatar-size: 80rpx; /* 三档尺寸:small 64rpx / default 80rpx / large 112rpx */
--kole-m-avatar-radius: 50%;
--kole-m-avatar-font-size: 28rpx;
--kole-m-avatar-badge-size: 20rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--kole-m-avatar-size);
height: var(--kole-m-avatar-size);
overflow: hidden;
border-radius: var(--kole-m-avatar-radius);
background-color: var(--kole-color-brand);
color: var(--kole-color-text-inverse);
font-size: var(--kole-m-avatar-font-size);
line-height: 1;
}
.kole-m-avatar--small {
--kole-m-avatar-size: 64rpx;
--kole-m-avatar-font-size: 24rpx;
--kole-m-avatar-badge-size: 16rpx;
}
.kole-m-avatar--large {
--kole-m-avatar-size: 112rpx;
--kole-m-avatar-font-size: 36rpx;
--kole-m-avatar-badge-size: 24rpx;
}
.kole-m-avatar--circle { --kole-m-avatar-radius: 50%; }
.kole-m-avatar--square { --kole-m-avatar-radius: 12rpx; }
.kole-m-avatar__image {
display: block;
width: 100%;
height: 100%;
}
.kole-m-avatar__text {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.kole-m-avatar.is-fallback {
background-color: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
}
.kole-m-avatar.is-disabled {
background-color: var(--kole-color-disabled-bg);
color: var(--kole-color-text-disabled);
}
.kole-m-avatar__badge {
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: var(--kole-m-avatar-badge-size);
height: var(--kole-m-avatar-badge-size);
border: 4rpx solid var(--kole-color-card-bg);
border-radius: 50%;
background-color: var(--kole-color-success);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 20 条 · 全部通过 报告 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-avatar.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "23 · 头像 Avatar",
"confidence": "high",
"slug": "mobile-avatar",
"name": "头像 Avatar",
"semanticTypeCandidates": [
"avatar",
"user-image",
"profile-photo"
],
"variantDimensions": [
{
"name": "size",
"values": [
"small",
"default",
"large"
]
},
{
"name": "shape",
"values": [
"circle",
"square"
]
},
{
"name": "type",
"values": [
"text",
"image"
]
}
],
"representativeVariants": [
{
"size": "default",
"shape": "circle",
"type": "text",
"label": "文字头像(圆形,用户默认形态)"
},
{
"size": "large",
"shape": "square",
"type": "image",
"label": "图片头像(圆角方形,企业 / 群组)"
},
{
"size": "small",
"shape": "circle",
"type": "text",
"label": "小尺寸头像(列表行前缀位)"
},
{
"size": "default",
"shape": "circle",
"type": "image",
"label": "图片加载失败退回文字(状态 fallback)"
}
],
"anatomy": {
"avatar": "根元素,正方形圆角容器,尺寸由 size 决定",
"image": "图片,铺满容器并按形状裁切",
"text": "文字内容(姓名首字或简称),图片缺失时它就是主体",
"fallback": "兜底节点,图片加载失败后退回的形状(文字或图标)",
"badge": "可选右下角角标位,挂在线状态或未读数"
},
"structurePatterns": {
"size": "small(32px)/ default(40px)/ large(56px)",
"shape": "circle(圆形,用于人)/ square(圆角方形,用于企业或群组)",
"type": "text(文字头像)/ image(图片头像,失败退文字)"
},
"usageHints": [
"用一张图或一两个字符代表一个主体(用户、企业、群组);移动端列表与详情页里大量出现,必须能单手扫读,因此尺寸只有三档、形状只有两种",
"图片不可用时必须立刻退回文字,不能出现裂图",
"头像本身不是按钮;可点时必须由宿主包一层原生 button 或 a,热区不小于 44×44",
"图片 alt 或根节点 aria-label 必填其一,读屏读「姓名 + 头像」",
"图片加载失败只触发一次 error 事件,是否替换资源由宿主决定"
],
"doNotInvent": [
"图片裁剪的 focal point(人脸居中)算法",
"角标位置随形状(圆 / 方)的微调规则"
],
"unknowns": [
"文字头像的底色是否按名字散列取多色",
"姓名超过两个汉字时的截断规则"
],
"interaction": [
"头像本身不是按钮;可点时必须由宿主包一层原生 button 或 a,热区不小于 44×44",
"图片加载失败只触发一次 error 事件,是否替换资源由宿主决定"
],
"accessibility": [
"文字头像根节点 role=\"img\" 并带 aria-label(内容为姓名)",
"图片头像直接用
,alt 为空时视为装饰并对读屏隐藏",
"角标不改变头像的可访问名"
],
"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:三档尺寸 32 / 40 / 56px,文字随尺寸放大(规格 §23.3)",
"required": false
},
{
"name": "shape",
"type": "'circle' | 'square'",
"default": "'circle'",
"desc": "变体 shape:circle 用于人,square 用于企业或群组(规格 §23.3)",
"required": false
},
{
"name": "type",
"type": "'text' | 'image'",
"default": "'text'",
"desc": "变体 type:文字头像 / 图片头像,图片失败退回文字(规格 §23.3)",
"required": false
},
{
"name": "src",
"type": "string",
"default": "''",
"desc": "图片地址,type=image 时必填(规格 §23.2 image)",
"required": false
},
{
"name": "alt",
"type": "string",
"default": "''",
"desc": "可访问名:图片头像作为
,文字头像作为根节点 aria-label(规格 §23.6)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:置灰(如成员已离职)(规格 §23.4)",
"required": false
}
],
"events": [
{
"name": "error",
"params": "(event)",
"desc": "图片加载失败时触发一次;是否替换资源由宿主决定(规格 §23.5)"
}
],
"slots": [
{
"name": "default",
"desc": "文字内容(姓名首字或简称)(规格 §23.2 text)"
},
{
"name": "badge",
"desc": "右下角角标位,挂在线状态或未读数(规格 §23.2 badge)"
}
]
},
"variantClasses": {
"size": {
"small": [
".kole-m-avatar--small"
],
"default": [],
"large": [
".kole-m-avatar--large"
]
},
"shape": {
"circle": [
".kole-m-avatar--circle"
],
"square": [
".kole-m-avatar--square"
]
},
"type": {
"text": [],
"image": [
".kole-m-avatar__image"
]
}
},
"demos": [
{
"id": "size",
"group": "01 组件类型",
"title": "三档尺寸",
"desc": "small 32 / default 40 / large 56,文字字号随尺寸放大,小尺寸下仍可读。",
"variant": "size=small|default|large"
},
{
"id": "shape",
"group": "01 组件类型",
"title": "两种形状",
"desc": "circle 用于人(成员、好友),square 用于企业或群组(标志、团队)。",
"variant": "shape=circle|square"
},
{
"id": "image",
"group": "01 组件类型",
"title": "图片头像",
"desc": "type=image 时图片铺满容器并按形状裁切,语义交给
。",
"variant": "type=image"
},
{
"id": "fallback",
"group": "02 组件状态",
"title": "图片兜底",
"desc": "状态 fallback:图片不可用时退回文字,底色转中性底,绝不出现裂图。",
"variant": "状态 fallback"
},
{
"id": "badge",
"group": "02 组件状态",
"title": "带角标",
"desc": "角标位挂在线状态或未读数,绝对定位不改变头像尺寸。",
"variant": "slots.badge"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "状态 disabled:置灰用于已离职或已停用的主体。",
"variant": "disabled=true"
}
],
"related": [
{
"slug": "mobile-list",
"why": "头像作为列表行的前缀位时用列表承载整行;只有单张头像时用本组件"
},
{
"slug": "mobile-badge",
"why": "需要数字角标时用徽标组件包裹头像;只表示在线状态用本组件自带的 badge 位"
},
{
"slug": "mobile-tag",
"why": "要用文字标注状态或分类时用标签;头像只承载主体身份"
}
]
}