头像 avatar
通用组件。三层同源:无头内核给出解剖与状态机,Vue 组件与自定义元素只是它的两层外壳,行为完全一致。
示例
基础用法
图片加载失败或未提供时落到 fallback
曦<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
</script>
<template>
<XhAvatarRoot src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot>
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
</template>加载失败回退
图片地址取不到时切到 fallback,切换由状态机决定而不是 CSS
回退<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
</script>
<template>
<XhAvatarRoot src="/images/does-not-exist.png" alt="取不到的图">
<XhAvatarImage />
<XhAvatarFallback>回退</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot>
<XhAvatarImage />
<XhAvatarFallback>无图</XhAvatarFallback>
</XhAvatarRoot>
</template>排成一列
头像本身不管布局,叠放与间距由外层容器决定
<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
const members = ["曦", "寒", "懿", "XH"];
</script>
<template>
<div style="display: flex">
<XhAvatarRoot
v-for="(m, i) in members"
:key="m"
:style="{ marginLeft: i ? '-8px' : '0', outline: '2px solid var(--vp-c-bg)', borderRadius: '999px' }"
>
<XhAvatarImage />
<XhAvatarFallback>{{ m }}</XhAvatarFallback>
</XhAvatarRoot>
</div>
</template>尺寸
size 三档只换直径,回退字的字号跟着一起缩放;缺省档不输出 data-size
曦
曦
曦sm / 缺省 / lg<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
</script>
<template>
<!-- 有图的一行:图片铺满 root,跟着三档一起缩放 -->
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot size="sm" src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot size="lg" src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">sm / 缺省 / lg</span>
</div>
<!-- 落回退态的一行:小头像里的字不撑出去,大头像里的字也不显小 -->
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot size="sm">
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot>
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot size="lg">
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">回退字随档位缩放</span>
</div>
</template>形状
圆角是一个组件令牌,整圆、圆角方、直角都是同一个槽位换值;图片的圆角从根继承,不用另设
曦
曦
曦整圆(缺省)/ 圆角方 / 直角<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
</script>
<template>
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot
src="/images/logo.png"
alt="曦寒"
style="--xh-avatar-radius: var(--xh-radius-md)"
>
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot
src="/images/logo.png"
alt="曦寒"
style="--xh-avatar-radius: var(--xh-radius-none)"
>
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">整圆(缺省)/ 圆角方 / 直角</span>
</div>
<!-- 落回退态时形状一样成立 -->
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot>
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot style="--xh-avatar-radius: var(--xh-radius-md)">
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot style="--xh-avatar-radius: var(--xh-radius-none)">
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
</XhAvatarRoot>
</div>
</template>图标当回退
fallback 是普通插槽,放图标和放缩写字一样;没有名字可写时用图标表示「某位用户」
<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot, XhIcon } from "@xihan-ui/vue";
const UserIcon = {
name: "user",
viewBox: "0 0 24 24",
attrs: {
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
},
nodes: [
{ tag: "circle", attrs: { cx: "12", cy: "8", r: "3.5" } },
{ tag: "path", attrs: { d: "M5 20C5 16.5 8.1 14.5 12 14.5C15.9 14.5 19 16.5 19 20" } },
],
} as const;
</script>
<template>
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot size="sm">
<XhAvatarImage />
<XhAvatarFallback>
<XhIcon :icon="UserIcon" size="sm" />
</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot>
<XhAvatarImage />
<XhAvatarFallback>
<XhIcon :icon="UserIcon" />
</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot size="lg">
<XhAvatarImage />
<XhAvatarFallback>
<XhIcon :icon="UserIcon" size="lg" />
</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">图元跟着档位一起换,取的是根流下来的前景色</span>
</div>
</template>自定义直径与配色
三档之外的直径、底色、字色各是一个组件令牌;按人名分配颜色就是逐个实例覆盖
曦<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
const people = [
{ text: "曦", bg: "#fee2e2", fg: "#b91c1c" },
{ text: "寒", bg: "#dcfce7", fg: "#15803d" },
{ text: "懿", bg: "#e0e7ff", fg: "#4338ca" },
{ text: "XH", bg: "#fef3c7", fg: "#b45309" },
];
</script>
<template>
<!-- 直径与字号一起给,回退字才不会在大头像里显小 -->
<div style="display: flex; align-items: center; gap: 12px">
<XhAvatarRoot
src="/images/logo.png"
alt="曦寒"
style="--xh-avatar-size: 56px; --xh-avatar-font-size: 20px"
>
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
</XhAvatarRoot>
<XhAvatarRoot style="--xh-avatar-size: 56px; --xh-avatar-font-size: 20px">
<XhAvatarImage />
<XhAvatarFallback>曦寒</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">直径 56px</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<XhAvatarRoot
v-for="p in people"
:key="p.text"
:style="{ '--xh-avatar-bg': p.bg, '--xh-avatar-fg': p.fg }"
>
<XhAvatarImage />
<XhAvatarFallback>{{ p.text }}</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">底色与字色逐个给</span>
</div>
</template>加载状态
status-change 在状态落位时通知,过渡态 idle 不通知;没给地址等同于取不到,直接落 error 让回退接管
曦地址有效 → 等待中
回退地址取不到 → 等待中<script setup lang="ts">
import { ref } from "vue";
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
const cases = [
{ key: "ok", src: "/images/logo.png", alt: "曦寒", text: "曦", note: "地址有效" },
{ key: "bad", src: "/images/does-not-exist.png", alt: "取不到的图", text: "回退", note: "地址取不到" },
{ key: "none", src: undefined, alt: undefined, text: "无图", note: "没给地址" },
];
const status = ref<Record<string, string>>({});
function record(key: string, details: { status: string }) {
status.value[key] = details.status;
}
</script>
<template>
<div style="display: grid; gap: 10px">
<div
v-for="c in cases"
:key="c.key"
style="display: flex; align-items: center; gap: 10px"
>
<XhAvatarRoot :src="c.src" :alt="c.alt" @status-change="record(c.key, $event)">
<XhAvatarImage />
<XhAvatarFallback>{{ c.text }}</XhAvatarFallback>
</XhAvatarRoot>
<span style="font-size: 13px">{{ c.note }} → {{ status[c.key] ?? "等待中" }}</span>
</div>
</div>
</template>成组与溢出计数
组内共用的直径、字号、形状在容器上写一次,自定义属性沿继承流给每一枚;超出上限的收成一枚「+N」,它只是又一枚落回退态的头像
<script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot } from "@xihan-ui/vue";
const members = ["曦", "寒", "懿", "承", "临", "旭"];
const max = 4;
const shown = members.slice(0, max);
const rest = members.length - shown.length;
// 两组只差容器上的这几个槽位,组内的写法完全一样
const groups: { key: string; tokens: Record<string, string> }[] = [
{
key: "圆",
tokens: { "--xh-avatar-size": "36px", "--xh-avatar-font-size": "14px" },
},
{
key: "方",
tokens: {
"--xh-avatar-size": "26px",
"--xh-avatar-font-size": "11px",
"--xh-avatar-radius": "var(--xh-radius-md)",
},
},
];
</script>
<template>
<div style="display: grid; gap: 16px">
<div
v-for="g in groups"
:key="g.key"
:style="{ display: 'flex', alignItems: 'center', ...g.tokens }"
>
<!-- 叠放是外层的事:后一枚往回挪一段,再描一圈底色把压住的边分开 -->
<XhAvatarRoot
v-for="(m, i) in shown"
:key="m"
:style="{ marginInlineStart: i ? '-10px' : '0', outline: '2px solid var(--vp-c-bg)' }"
>
<XhAvatarImage />
<XhAvatarFallback>{{ m }}</XhAvatarFallback>
</XhAvatarRoot>
<!-- 计数格没有图,只写回退内容 -->
<XhAvatarRoot
v-if="rest > 0"
style="
margin-inline-start: -10px;
outline: 2px solid var(--vp-c-bg);
--xh-avatar-bg: var(--xh-bg-muted);
--xh-avatar-fg: var(--xh-fg-muted);
"
>
<XhAvatarFallback>+{{ rest }}</XhAvatarFallback>
</XhAvatarRoot>
</div>
</div>
</template>挂状态点与角标
根自己就是定位上下文,角标直接写进默认插槽;要挂到圆外就把根的裁剪打开,图片的圆角取自自身,不靠根裁
曦
曦 12 <script setup lang="ts">
import { XhAvatarFallback, XhAvatarImage, XhAvatarRoot, XhBadge } from "@xihan-ui/vue";
</script>
<template>
<div style="display: flex; align-items: center; gap: 24px">
<!-- 状态点落在圆内,裁剪不用动 -->
<XhAvatarRoot size="lg" src="/images/logo.png" alt="曦寒">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
<span
role="img"
aria-label="在线"
style="
position: absolute;
inset-block-end: 2px;
inset-inline-end: 2px;
inline-size: 10px;
block-size: 10px;
border-radius: var(--xh-shape-pill);
background: var(--xh-fg-success);
"
/>
</XhAvatarRoot>
<!-- 计数挂到圆外:根的裁剪打开,徽标绝对定位到右上角 -->
<XhAvatarRoot size="lg" src="/images/logo.png" alt="曦寒" style="overflow: visible">
<XhAvatarImage />
<XhAvatarFallback>曦</XhAvatarFallback>
<XhBadge
variant="solid"
tone="danger"
size="sm"
style="position: absolute; inset-block-start: -4px; inset-inline-end: -10px"
>
12
</XhBadge>
</XhAvatarRoot>
<!-- 落回退态时一样成立;点描一圈底色,压在头像边上也分得开 -->
<XhAvatarRoot size="lg" style="overflow: visible">
<XhAvatarImage />
<XhAvatarFallback>XH</XhAvatarFallback>
<span
role="img"
aria-label="离线"
style="
position: absolute;
inset-block-end: 0;
inset-inline-end: 0;
inline-size: 12px;
block-size: 12px;
border: 2px solid var(--vp-c-bg);
border-radius: var(--xh-shape-pill);
background: var(--xh-fg-disabled);
"
/>
</XhAvatarRoot>
</div>
</template>产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-avatar> |
| Vue 组件 | XhAvatarFallback XhAvatarImage XhAvatarRoot |
| 组合式函数 | useAvatar |
| 状态机 | 无,connect 直接由 props 算属性 |
| 皮肤 | @xihan-ui/styles/avatar.css |
解剖
部件名即 data-part 属性值,也是皮肤的选择器。加粗的是必备部件,不渲染它组件不工作(Web Components 适配器会在诊断通道上报 wc.missing-part)。
data-scope="avatar":root · image · fallback
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
src | string | ||
alt | string | ||
size | Size | 尺寸:sm / md / lg,缺省 md;缺省档不输出 data-size | |
onStatusChange | (details: AvatarStatusChangeDetails) => void | 状态落位时通知,过渡态 idle 不通知。 |
状态机
事件:SRC.CHANGE · IMAGE.LOAD · IMAGE.ERROR
判据:hasSrc
connect API
useAvatar 产出的对象。getXxxProps() 铺到对应部件的宿主元素上,其余是可读状态与操作入口。
| 成员 | 类型 | 说明 |
|---|---|---|
status | AvatarStatus | |
loaded | boolean | |
getRootProps | () => T['element'] | |
getImageProps | () => T['img'] | |
getFallbackProps | () => T['element'] |
键盘
规格出处:W3C APG
无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。