From 2168c39110337022e9e8408842279708355f0e7c Mon Sep 17 00:00:00 2001 From: North <1227379879@qq.com> Date: Fri, 8 Aug 2025 01:58:45 +0800 Subject: [PATCH] feat(avatar): add Avatar component with styles, types, and tests --- .../vant/src/avatar-group/AvatarGroup.tsx | 127 ++++ packages/vant/src/avatar-group/README.md | 0 .../vant/src/avatar-group/README.zh-CN.md | 254 ++++++++ packages/vant/src/avatar-group/index.less | 45 ++ packages/vant/src/avatar-group/index.ts | 14 + .../test/__snapshots__/index.spec.ts.snap | 81 +++ .../vant/src/avatar-group/test/index.spec.ts | 296 +++++++++ packages/vant/src/avatar-group/types.ts | 5 + packages/vant/src/avatar/Avatar.tsx | 99 +++ packages/vant/src/avatar/README.md | 254 ++++++++ packages/vant/src/avatar/README.zh-CN.md | 254 ++++++++ packages/vant/src/avatar/demo/index.vue | 172 +++++ packages/vant/src/avatar/index.less | 75 +++ packages/vant/src/avatar/index.ts | 14 + .../test/__snapshots__/demo-ssr.spec.ts.snap | 597 ++++++++++++++++++ .../test/__snapshots__/demo.spec.ts.snap | 501 +++++++++++++++ .../test/__snapshots__/index.spec.ts.snap | 32 + .../vant/src/avatar/test/demo-ssr.spec.ts | 7 + packages/vant/src/avatar/test/demo.spec.ts | 4 + packages/vant/src/avatar/test/index.spec.ts | 157 +++++ packages/vant/src/avatar/types.ts | 19 + packages/vant/vant.config.mjs | 8 + 22 files changed, 3015 insertions(+) create mode 100644 packages/vant/src/avatar-group/AvatarGroup.tsx create mode 100644 packages/vant/src/avatar-group/README.md create mode 100644 packages/vant/src/avatar-group/README.zh-CN.md create mode 100644 packages/vant/src/avatar-group/index.less create mode 100644 packages/vant/src/avatar-group/index.ts create mode 100644 packages/vant/src/avatar-group/test/__snapshots__/index.spec.ts.snap create mode 100644 packages/vant/src/avatar-group/test/index.spec.ts create mode 100644 packages/vant/src/avatar-group/types.ts create mode 100644 packages/vant/src/avatar/Avatar.tsx create mode 100644 packages/vant/src/avatar/README.md create mode 100644 packages/vant/src/avatar/README.zh-CN.md create mode 100644 packages/vant/src/avatar/demo/index.vue create mode 100644 packages/vant/src/avatar/index.less create mode 100644 packages/vant/src/avatar/index.ts create mode 100644 packages/vant/src/avatar/test/__snapshots__/demo-ssr.spec.ts.snap create mode 100644 packages/vant/src/avatar/test/__snapshots__/demo.spec.ts.snap create mode 100644 packages/vant/src/avatar/test/__snapshots__/index.spec.ts.snap create mode 100644 packages/vant/src/avatar/test/demo-ssr.spec.ts create mode 100644 packages/vant/src/avatar/test/demo.spec.ts create mode 100644 packages/vant/src/avatar/test/index.spec.ts create mode 100644 packages/vant/src/avatar/types.ts diff --git a/packages/vant/src/avatar-group/AvatarGroup.tsx b/packages/vant/src/avatar-group/AvatarGroup.tsx new file mode 100644 index 00000000000..5902dfccc0a --- /dev/null +++ b/packages/vant/src/avatar-group/AvatarGroup.tsx @@ -0,0 +1,127 @@ +import { + defineComponent, + computed, + cloneVNode, + type PropType, + type CSSProperties, + type ExtractPropTypes, +} from 'vue'; +import { numericProp, makeStringProp, createNamespace } from '../utils'; +import type { AvatarSize, AvatarShape } from '../avatar'; +import VanAvatar from '../avatar'; +import type { AvatarGroupCascadingValue } from './types'; + +const [name, bem] = createNamespace('avatar-group'); + +export const avatarGroupProps = { + maxCount: numericProp, + cascading: makeStringProp('left-up'), + shape: String as PropType, + size: String as PropType, + collapseAvatar: String, +}; + +export type AvatarGroupProps = ExtractPropTypes; + +export default defineComponent({ + name, + + props: avatarGroupProps, + + setup(props, { slots }) { + const groupClass = computed(() => { + const classes = [bem()]; + classes.push(bem(props.cascading as string)); + return classes; + }); + + const renderCollapseAvatar = (restCount: number) => { + if (slots.collapseAvatar) { + const collapseContent = slots.collapseAvatar({ restCount }); + + const collapseNode = Array.isArray(collapseContent) + ? collapseContent[0] + : collapseContent; + + if (Array.isArray(collapseContent) && collapseContent.length > 1) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn( + '[Vant] Multiple elements found in collapseAvatar slot, only the first element will be rendered.', + ); + } + } + if (collapseNode && typeof collapseNode === 'object') { + const childProps = collapseNode.props || {}; + return cloneVNode(collapseNode, { + shape: props.shape || childProps.shape, + size: props.size || childProps.size, + }); + } + return collapseContent; + } + + const content = props.collapseAvatar || `+${restCount}`; + + return ( + + ); + }; + + return () => { + const children = slots.default?.() || []; + const rawMaxCount = props.maxCount + ? Number(props.maxCount) + : children.length; + const maxCount = rawMaxCount <= 0 ? children.length : rawMaxCount; + const displayChildren = children.slice(0, maxCount); + const restCount = Math.max(0, children.length - maxCount); + + return ( +
+ {displayChildren.map((child, index) => { + const itemStyle: CSSProperties = {}; + + if (props.cascading === 'left-up') { + itemStyle.zIndex = index + 1; + } else { + itemStyle.zIndex = displayChildren.length - index; + } + + const childProps = child.props || {}; + const clonedChild = cloneVNode(child, { + shape: childProps.shape || props.shape, + size: childProps.size || props.size, + }); + + return ( +
+ {clonedChild} +
+ ); + })} + {restCount > 0 && ( +
+ {renderCollapseAvatar(restCount)} +
+ )} +
+ ); + }; + }, +}); diff --git a/packages/vant/src/avatar-group/README.md b/packages/vant/src/avatar-group/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/vant/src/avatar-group/README.zh-CN.md b/packages/vant/src/avatar-group/README.zh-CN.md new file mode 100644 index 00000000000..7375695ca1c --- /dev/null +++ b/packages/vant/src/avatar-group/README.zh-CN.md @@ -0,0 +1,254 @@ +# Avatar 头像 + +### 介绍 + +用来代表用户或事物,支持图片、文本或图标展示。 + +### 引入 + +通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。 + +```js +import { createApp } from 'vue'; +import { Avatar } from 'vant'; + +const app = createApp(); +app.use(Avatar); +``` + +## 代码演示 + +### 基础用法 + +头像支持三种类型:图片、文本和图标。 + +```html + + + +``` + +### 头像形状 + +通过 `shape` 属性可以设置头像形状,支持 `round` 圆形和 `square` 方形,默认为圆形。 + +```html + + +``` + +### 头像尺寸 + +通过 `size` 属性可以设置头像尺寸,支持预设尺寸和自定义数值。 + +```html + + + + + + + + + +``` + +### 自定义颜色 + +通过 `bg-color` 和 `color` 属性可以自定义背景色和文字颜色。 + +```html + + + + + +``` + +### 带徽记的头像 + +结合 Badge 组件使用,可以在头像右上角显示徽记。 + +```html + + + + + + + + + + + + + + + +``` + +### 头像组 + +使用 AvatarGroup 组件可以将多个头像组合展示,支持层叠效果。 + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 组尺寸和组形状 + +通过 AvatarGroup 的 `size` 和 `shape` 属性可以统一设置组内头像的尺寸和形状,子头像的属性优先级更高。 + +```html + + + + + + + + + + + + + + + + + +``` + +### 自定义折叠头像 + +当头像数量超出限制时,可以通过 `collapse-avatar` 插槽自定义折叠头像的展示。 + +```html + + + + + + + + + +``` + +## API + +### Avatar Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| src | 头像图片链接 | _string_ | - | +| text | 头像文字内容 | _string_ | - | +| icon | 头像图标名称 | _string_ | - | +| shape | 头像形状,可选值为 `round` `square` | _string_ | `round` | +| size | 头像尺寸,可选值为 `large` `medium` `normal` `small`,也支持数值 | _number \| string_ | `normal` | +| bg-color | 背景颜色 | _string_ | - | +| color | 文字颜色 | _string_ | `#fff` | +| alt | 图片加载失败替代文本 | _string_ | - | + +### AvatarGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| max-count | 显示的最大头像个数 | _number \| string_ | - | +| cascading | 图片之间的层叠关系,可选值为 `left-up` `right-up` | _string_ | `left-up` | +| shape | 形状,优先级低于 Avatar.shape | _string_ | - | +| size | 尺寸,优先级低于 Avatar.size | _number \| string_ | - | +| collapse-avatar | 头像数量超出时的折叠元素内容文本 | _string_ | - | + +### Avatar Events + +| 事件名 | 说明 | 回调参数 | +| ------ | ------------------ | -------------- | +| error | 图片加载失败时触发 | _event: Event_ | +| click | 点击头像时触发 | - | + +### Avatar Slots + +| 名称 | 描述 | +| ------- | -------------- | +| default | 自定义头像内容 | + +### AvatarGroup Slots + +| 名称 | 描述 | 参数 | +| --------------- | ------------------ | ----------------------- | +| default | 头像组内容 | - | +| collapse-avatar | 自定义折叠头像内容 | _{ restCount: number }_ | + +### 类型定义 + +组件导出以下类型定义: + +```ts +import type { + AvatarProps, + AvatarSize, + AvatarShape, + AvatarGroupProps, +} from 'vant'; +``` + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。 + +#### Avatar 样式变量 + +| 名称 | 默认值 | 描述 | +| ----------------------------- | ---------------------- | ------------ | +| --van-avatar-size | _54px_ | 普通尺寸 | +| --van-avatar-size-large | _76px_ | 大号尺寸 | +| --van-avatar-size-medium | _64px_ | 中号尺寸 | +| --van-avatar-size-small | _48px_ | 小号尺寸 | +| --van-avatar-bg-color | _var(--van-gray-5)_ | 默认背景色 | +| --van-avatar-text-color | _var(--van-white)_ | 默认文字颜色 | +| --van-avatar-font-size | _20px_ | 普通字体大小 | +| --van-avatar-font-size-large | _28px_ | 大号字体大小 | +| --van-avatar-font-size-medium | _24px_ | 中号字体大小 | +| --van-avatar-font-size-small | _18px_ | 小号字体大小 | +| --van-avatar-font-weight | _var(--van-font-bold)_ | 文字粗细 | +| --van-avatar-line-height | _1.2_ | 行高 | +| --van-avatar-border-radius | _var(--van-radius-md)_ | 头像圆角 | + +#### AvatarGroup 样式变量 + +| 名称 | 默认值 | 描述 | +| -------------------------- | ------- | ------------ | +| --van-avatar-group-overlap | _-12px_ | 头像重叠距离 | diff --git a/packages/vant/src/avatar-group/index.less b/packages/vant/src/avatar-group/index.less new file mode 100644 index 00000000000..8873e1b80f4 --- /dev/null +++ b/packages/vant/src/avatar-group/index.less @@ -0,0 +1,45 @@ +:root, +:host { + --van-avatar-group-overlap: -12px; +} + +.van-avatar-group { + display: inline-flex; + align-items: center; + + &__right-up { + .van-avatar-group__item { + position: relative; + + + .van-avatar-group__item { + margin-left: var(--van-avatar-group-overlap); + } + } + } + + &__left-up { + .van-avatar-group__item { + position: relative; + + + .van-avatar-group__item { + margin-left: var(--van-avatar-group-overlap); + } + } + } + + &__item { + position: relative; + border-radius: 50%; + + .van-avatar { + position: relative; + z-index: 1; + } + } + + &__collapse { + background-color: var(--van-gray-3); + color: var(--van-gray-6); + font-size: 12px; + } +} diff --git a/packages/vant/src/avatar-group/index.ts b/packages/vant/src/avatar-group/index.ts new file mode 100644 index 00000000000..1a28b3cf405 --- /dev/null +++ b/packages/vant/src/avatar-group/index.ts @@ -0,0 +1,14 @@ +import { withInstall } from '../utils'; +import _AvatarGroup from './AvatarGroup'; + +export const AvatarGroup = withInstall(_AvatarGroup); +export default AvatarGroup; +export { avatarGroupProps } from './AvatarGroup'; +export type { AvatarGroupProps } from './AvatarGroup'; +export type { AvatarGroupThemeVars, AvatarGroupCascadingValue } from './types'; + +declare module 'vue' { + export interface GlobalComponents { + VanAvatarGroup: typeof AvatarGroup; + } +} diff --git a/packages/vant/src/avatar-group/test/__snapshots__/index.spec.ts.snap b/packages/vant/src/avatar-group/test/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000000..63d6e14e63c --- /dev/null +++ b/packages/vant/src/avatar-group/test/__snapshots__/index.spec.ts.snap @@ -0,0 +1,81 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should limit avatars when maxCount is set 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+ + +2 + +
+
+
+`; + +exports[`should render avatar group correctly 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+`; + +exports[`should render empty group when no children provided 1`] = ` +
+
+`; diff --git a/packages/vant/src/avatar-group/test/index.spec.ts b/packages/vant/src/avatar-group/test/index.spec.ts new file mode 100644 index 00000000000..9f464b62f91 --- /dev/null +++ b/packages/vant/src/avatar-group/test/index.spec.ts @@ -0,0 +1,296 @@ +import { h } from 'vue'; +import { AvatarGroup } from '..'; +import { Avatar } from '../../avatar'; +import { mount } from '../../../test'; + +test('should render avatar group correctly', () => { + const wrapper = mount(AvatarGroup, { + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + h(Avatar, { src: 'avatar3.jpg' }), + ], + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.findAll('.van-avatar')).toHaveLength(3); +}); + +test('should limit avatars when maxCount is set', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: 2, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + h(Avatar, { src: 'avatar3.jpg' }), + h(Avatar, { src: 'avatar4.jpg' }), + ], + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + + // Should show 2 avatars + 1 collapse avatar + const avatars = wrapper.findAll('.van-avatar'); + expect(avatars).toHaveLength(3); + + // The last avatar should show the count + expect(wrapper.text()).toContain('+2'); +}); + +test('should not show collapse avatar when total equals maxCount', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: 3, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + h(Avatar, { src: 'avatar3.jpg' }), + ], + }, + }); + + const avatars = wrapper.findAll('.van-avatar'); + expect(avatars).toHaveLength(3); + expect(wrapper.text()).not.toContain('+'); +}); + +test('should apply cascading prop correctly', () => { + const wrapper = mount(AvatarGroup, { + props: { + cascading: 'right-up', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + expect(wrapper.classes()).toContain('van-avatar-group__right-up'); + + // Test left-up cascading + const wrapper2 = mount(AvatarGroup, { + props: { + cascading: 'left-up', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + expect(wrapper2.classes()).toContain('van-avatar-group__left-up'); +}); + +test('should pass size prop to child avatars', () => { + const wrapper = mount(AvatarGroup, { + props: { + size: 'large', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + const avatars = wrapper.findAll('.van-avatar'); + avatars.forEach((avatar) => { + expect(avatar.classes()).toContain('van-avatar--large'); + }); +}); + +test('should pass shape prop to child avatars', () => { + const wrapper = mount(AvatarGroup, { + props: { + shape: 'square', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + const avatars = wrapper.findAll('.van-avatar'); + avatars.forEach((avatar) => { + expect(avatar.classes()).toContain('van-avatar--square'); + }); +}); + +test('should pass numeric size prop to child avatars', () => { + const wrapper = mount(AvatarGroup, { + props: { + size: 60, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + const avatars = wrapper.findAll('.van-avatar'); + avatars.forEach((avatar) => { + expect(avatar.element.style.width).toBe('60px'); + expect(avatar.element.style.height).toBe('60px'); + }); +}); + +test('should prioritize child avatar props over group props', () => { + const wrapper = mount(AvatarGroup, { + props: { + size: 'large', + shape: 'square', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg', size: 'small', shape: 'round' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + const avatars = wrapper.findAll('.van-avatar'); + + // First avatar should keep its own props (small, round) + expect(avatars[0].classes()).toContain('van-avatar--small'); + expect(avatars[0].classes()).not.toContain('van-avatar--square'); + expect(avatars[0].classes()).not.toContain('van-avatar--large'); + + // Second avatar should use group props (large, square) + expect(avatars[1].classes()).toContain('van-avatar--large'); + expect(avatars[1].classes()).toContain('van-avatar--square'); +}); + +test('should render empty group when no children provided', () => { + const wrapper = mount(AvatarGroup); + + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.findAll('.van-avatar')).toHaveLength(0); +}); + +test('should handle single avatar correctly', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: 5, + }, + slots: { + default: () => [h(Avatar, { src: 'avatar1.jpg' })], + }, + }); + + expect(wrapper.findAll('.van-avatar')).toHaveLength(1); + expect(wrapper.text()).not.toContain('+'); +}); + +test('should handle non-avatar children gracefully', () => { + const wrapper = mount(AvatarGroup, { + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h('div', 'Not an avatar'), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + // Should still render all children + expect(wrapper.findAll('.van-avatar')).toHaveLength(2); + expect(wrapper.text()).toContain('Not an avatar'); +}); + +test('should calculate collapse count correctly with non-avatar children', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: 2, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h('div', 'Not an avatar'), + h(Avatar, { src: 'avatar2.jpg' }), + h(Avatar, { src: 'avatar3.jpg' }), + h(Avatar, { src: 'avatar4.jpg' }), + ], + }, + }); + + // Should show 2 children (1 avatar + 1 div) + collapse avatar showing +3 (remaining children) + const avatars = wrapper.findAll('.van-avatar'); + expect(avatars).toHaveLength(2); // 1 displayed avatar + 1 collapse avatar + expect(wrapper.text()).toContain('+3'); +}); + +test('should apply z-index correctly for cascading effect', () => { + const wrapper = mount(AvatarGroup, { + props: { + cascading: 'right-up', + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + h(Avatar, { src: 'avatar3.jpg' }), + ], + }, + }); + + const items = wrapper.findAll('.van-avatar-group__item'); + + // First item should have highest z-index for right-up + expect((items[0].element as HTMLElement).style.zIndex).toBe('3'); + expect((items[1].element as HTMLElement).style.zIndex).toBe('2'); + expect((items[2].element as HTMLElement).style.zIndex).toBe('1'); +}); + +test('should handle maxCount of 0', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: 0, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + // Should show all avatars when maxCount is 0 (same as negative values) + const avatars = wrapper.findAll('.van-avatar'); + expect(avatars).toHaveLength(2); + expect(wrapper.text()).not.toContain('+'); +}); + +test('should handle negative maxCount', () => { + const wrapper = mount(AvatarGroup, { + props: { + maxCount: -1, + }, + slots: { + default: () => [ + h(Avatar, { src: 'avatar1.jpg' }), + h(Avatar, { src: 'avatar2.jpg' }), + ], + }, + }); + + // Should show all avatars when maxCount is negative + expect(wrapper.findAll('.van-avatar')).toHaveLength(2); + expect(wrapper.text()).not.toContain('+'); +}); diff --git a/packages/vant/src/avatar-group/types.ts b/packages/vant/src/avatar-group/types.ts new file mode 100644 index 00000000000..29bac1a3549 --- /dev/null +++ b/packages/vant/src/avatar-group/types.ts @@ -0,0 +1,5 @@ +export type AvatarGroupCascadingValue = 'left-up' | 'right-up'; + +export type AvatarGroupThemeVars = { + avatarGroupOverlap?: string; +}; diff --git a/packages/vant/src/avatar/Avatar.tsx b/packages/vant/src/avatar/Avatar.tsx new file mode 100644 index 00000000000..b6536f48edf --- /dev/null +++ b/packages/vant/src/avatar/Avatar.tsx @@ -0,0 +1,99 @@ +import { + defineComponent, + type PropType, + type CSSProperties, + type ExtractPropTypes, +} from 'vue'; +import { isDef, addUnit, makeStringProp, createNamespace } from '../utils'; +import { AvatarShape, AvatarSize } from './types'; + +const [name, bem] = createNamespace('avatar'); + +export const avatarProps = { + src: String, + text: String, + size: [Number, String] as PropType, + shape: makeStringProp('round'), + bgColor: String, + color: String, + icon: String, + alt: String, +}; + +export type AvatarProps = ExtractPropTypes; + +export default defineComponent({ + name, + + props: avatarProps, + + emits: ['error', 'click'], + + setup(props, { emit, slots }) { + const style = (): CSSProperties => { + const style: CSSProperties = {}; + + if (isDef(props.size)) { + const size = addUnit(props.size); + style.width = size; + style.height = size; + style.fontSize = `calc(${size} * 0.45)`; + } + + if (props.color) { + style.color = props.color; + } + + if (props.bgColor) { + style.backgroundColor = props.bgColor; + if (!props.color) { + style.color = '#fff'; + } + } + + return style; + }; + + const onImageError = (event: Event) => { + emit('error', event); + }; + + const onImageClick = () => { + emit('click'); + }; + + const renderContent = () => { + if (slots.default) { + return slots.default(); + } + + if (props.src) { + return ( + {props.alt} + ); + } + + if (props.text) { + return {props.text}; + } + + if (props.icon) { + return ; + } + + return null; + }; + + return () => ( +
+ {renderContent()} +
+ ); + }, +}); diff --git a/packages/vant/src/avatar/README.md b/packages/vant/src/avatar/README.md new file mode 100644 index 00000000000..68edfa101ce --- /dev/null +++ b/packages/vant/src/avatar/README.md @@ -0,0 +1,254 @@ +# Avatar + +### Intro + +Used to represent users or things, supporting image, text, or icon display. + +### Install + +Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways. + +```js +import { createApp } from 'vue'; +import { Avatar } from 'vant'; + +const app = createApp(); +app.use(Avatar); +``` + +## Usage + +### Basic Usage + +Avatar supports three types: image, text, and icon. + +```html + + + +``` + +### Avatar Shape + +Use the `shape` prop to set the avatar shape. Supports `round` and `square`, default is `round`. + +```html + + +``` + +### Avatar Size + +Use the `size` prop to set the avatar size. Supports preset sizes and custom numeric values. + +```html + + + + + + + + + +``` + +### Custom Colors + +Use the `bg-color` and `color` props to customize background and text colors. + +```html + + + + + +``` + +### Avatar with Badge + +Use with Badge component to display badges on the top-right corner of the avatar. + +```html + + + + + + + + + + + + + + + +``` + +### Avatar Group + +Use AvatarGroup component to display multiple avatars with cascading effects. + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Group Size and Shape + +Use AvatarGroup's `size` and `shape` props to set the size and shape of avatars in the group uniformly. Individual avatar props have higher priority. + +```html + + + + + + + + + + + + + + + + + +``` + +### Custom Collapse Avatar + +When the number of avatars exceeds the limit, you can customize the collapsed avatar display through the `collapse-avatar` slot. + +```html + + + + + + + + + +``` + +## API + +### Avatar Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| src | Avatar image URL | _string_ | - | +| text | Avatar text content | _string_ | - | +| icon | Avatar icon name | _string_ | - | +| shape | Avatar shape, can be set to `round` `square` | _string_ | `round` | +| size | Avatar size, can be set to `large` `medium` `normal` `small`, also supports numeric values | _number \| string_ | `normal` | +| bg-color | Background color | _string_ | - | +| color | Text color | _string_ | `#fff` | +| alt | Alternative text for failed image loading | _string_ | - | + +### AvatarGroup Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| max-count | Maximum number of avatars to display | _number \| string_ | - | +| cascading | Cascading relationship between images, can be set to `left-up` `right-up` | _string_ | `left-up` | +| shape | Shape, lower priority than Avatar.shape | _string_ | - | +| size | Size, lower priority than Avatar.size | _number \| string_ | - | +| collapse-avatar | Text content for collapsed element when avatar count exceeds limit | _string_ | - | + +### Avatar Events + +| Event | Description | Arguments | +| ----- | -------------------------------- | -------------- | +| error | Emitted when image fails to load | _event: Event_ | +| click | Emitted when avatar is clicked | - | + +### Avatar Slots + +| Name | Description | +| ------- | --------------------- | +| default | Custom avatar content | + +### AvatarGroup Slots + +| Name | Description | SlotProps | +| --------------- | ------------------------------- | ----------------------- | +| default | Avatar group content | - | +| collapse-avatar | Custom collapsed avatar content | _{ restCount: number }_ | + +### Types + +The component exports the following type definitions: + +```ts +import type { + AvatarProps, + AvatarSize, + AvatarShape, + AvatarGroupProps, +} from 'vant'; +``` + +## Theming + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/config-provider). + +#### Avatar CSS Variables + +| Name | Default Value | Description | +| --- | --- | --- | +| --van-avatar-size | _54px_ | Normal size | +| --van-avatar-size-large | _76px_ | Large size | +| --van-avatar-size-medium | _64px_ | Medium size | +| --van-avatar-size-small | _48px_ | Small size | +| --van-avatar-bg-color | _var(--van-gray-5)_ | Default background color | +| --van-avatar-text-color | _var(--van-white)_ | Default text color | +| --van-avatar-font-size | _20px_ | Normal font size | +| --van-avatar-font-size-large | _28px_ | Large font size | +| --van-avatar-font-size-medium | _24px_ | Medium font size | +| --van-avatar-font-size-small | _18px_ | Small font size | +| --van-avatar-font-weight | _var(--van-font-bold)_ | Font weight | +| --van-avatar-line-height | _1.2_ | Line height | +| --van-avatar-border-radius | _var(--van-radius-md)_ | Avatar border radius | + +#### AvatarGroup CSS Variables + +| Name | Default Value | Description | +| -------------------------- | ------------- | ----------------------- | +| --van-avatar-group-overlap | _-12px_ | Avatar overlap distance | diff --git a/packages/vant/src/avatar/README.zh-CN.md b/packages/vant/src/avatar/README.zh-CN.md new file mode 100644 index 00000000000..7375695ca1c --- /dev/null +++ b/packages/vant/src/avatar/README.zh-CN.md @@ -0,0 +1,254 @@ +# Avatar 头像 + +### 介绍 + +用来代表用户或事物,支持图片、文本或图标展示。 + +### 引入 + +通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。 + +```js +import { createApp } from 'vue'; +import { Avatar } from 'vant'; + +const app = createApp(); +app.use(Avatar); +``` + +## 代码演示 + +### 基础用法 + +头像支持三种类型:图片、文本和图标。 + +```html + + + +``` + +### 头像形状 + +通过 `shape` 属性可以设置头像形状,支持 `round` 圆形和 `square` 方形,默认为圆形。 + +```html + + +``` + +### 头像尺寸 + +通过 `size` 属性可以设置头像尺寸,支持预设尺寸和自定义数值。 + +```html + + + + + + + + + +``` + +### 自定义颜色 + +通过 `bg-color` 和 `color` 属性可以自定义背景色和文字颜色。 + +```html + + + + + +``` + +### 带徽记的头像 + +结合 Badge 组件使用,可以在头像右上角显示徽记。 + +```html + + + + + + + + + + + + + + + +``` + +### 头像组 + +使用 AvatarGroup 组件可以将多个头像组合展示,支持层叠效果。 + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 组尺寸和组形状 + +通过 AvatarGroup 的 `size` 和 `shape` 属性可以统一设置组内头像的尺寸和形状,子头像的属性优先级更高。 + +```html + + + + + + + + + + + + + + + + + +``` + +### 自定义折叠头像 + +当头像数量超出限制时,可以通过 `collapse-avatar` 插槽自定义折叠头像的展示。 + +```html + + + + + + + + + +``` + +## API + +### Avatar Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| src | 头像图片链接 | _string_ | - | +| text | 头像文字内容 | _string_ | - | +| icon | 头像图标名称 | _string_ | - | +| shape | 头像形状,可选值为 `round` `square` | _string_ | `round` | +| size | 头像尺寸,可选值为 `large` `medium` `normal` `small`,也支持数值 | _number \| string_ | `normal` | +| bg-color | 背景颜色 | _string_ | - | +| color | 文字颜色 | _string_ | `#fff` | +| alt | 图片加载失败替代文本 | _string_ | - | + +### AvatarGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| max-count | 显示的最大头像个数 | _number \| string_ | - | +| cascading | 图片之间的层叠关系,可选值为 `left-up` `right-up` | _string_ | `left-up` | +| shape | 形状,优先级低于 Avatar.shape | _string_ | - | +| size | 尺寸,优先级低于 Avatar.size | _number \| string_ | - | +| collapse-avatar | 头像数量超出时的折叠元素内容文本 | _string_ | - | + +### Avatar Events + +| 事件名 | 说明 | 回调参数 | +| ------ | ------------------ | -------------- | +| error | 图片加载失败时触发 | _event: Event_ | +| click | 点击头像时触发 | - | + +### Avatar Slots + +| 名称 | 描述 | +| ------- | -------------- | +| default | 自定义头像内容 | + +### AvatarGroup Slots + +| 名称 | 描述 | 参数 | +| --------------- | ------------------ | ----------------------- | +| default | 头像组内容 | - | +| collapse-avatar | 自定义折叠头像内容 | _{ restCount: number }_ | + +### 类型定义 + +组件导出以下类型定义: + +```ts +import type { + AvatarProps, + AvatarSize, + AvatarShape, + AvatarGroupProps, +} from 'vant'; +``` + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。 + +#### Avatar 样式变量 + +| 名称 | 默认值 | 描述 | +| ----------------------------- | ---------------------- | ------------ | +| --van-avatar-size | _54px_ | 普通尺寸 | +| --van-avatar-size-large | _76px_ | 大号尺寸 | +| --van-avatar-size-medium | _64px_ | 中号尺寸 | +| --van-avatar-size-small | _48px_ | 小号尺寸 | +| --van-avatar-bg-color | _var(--van-gray-5)_ | 默认背景色 | +| --van-avatar-text-color | _var(--van-white)_ | 默认文字颜色 | +| --van-avatar-font-size | _20px_ | 普通字体大小 | +| --van-avatar-font-size-large | _28px_ | 大号字体大小 | +| --van-avatar-font-size-medium | _24px_ | 中号字体大小 | +| --van-avatar-font-size-small | _18px_ | 小号字体大小 | +| --van-avatar-font-weight | _var(--van-font-bold)_ | 文字粗细 | +| --van-avatar-line-height | _1.2_ | 行高 | +| --van-avatar-border-radius | _var(--van-radius-md)_ | 头像圆角 | + +#### AvatarGroup 样式变量 + +| 名称 | 默认值 | 描述 | +| -------------------------- | ------- | ------------ | +| --van-avatar-group-overlap | _-12px_ | 头像重叠距离 | diff --git a/packages/vant/src/avatar/demo/index.vue b/packages/vant/src/avatar/demo/index.vue new file mode 100644 index 00000000000..64b48e2b3fa --- /dev/null +++ b/packages/vant/src/avatar/demo/index.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/packages/vant/src/avatar/index.less b/packages/vant/src/avatar/index.less new file mode 100644 index 00000000000..0dad84a4ecb --- /dev/null +++ b/packages/vant/src/avatar/index.less @@ -0,0 +1,75 @@ +:root, +:host { + --van-avatar-size: 54px; + --van-avatar-size-large: 76px; + --van-avatar-size-medium: 64px; + --van-avatar-size-small: 48px; + --van-avatar-bg-color: var(--van-gray-5); + --van-avatar-text-color: var(--van-white); + --van-avatar-font-size: 20px; + --van-avatar-font-size-large: 28px; + --van-avatar-font-size-medium: 24px; + --van-avatar-font-size-small: 18px; + --van-avatar-font-weight: var(--van-font-bold); + --van-avatar-line-height: 1.2; + --van-avatar-border-radius: var(--van-radius-md); +} + +.van-avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--van-avatar-size); + height: var(--van-avatar-size); + color: var(--van-avatar-text-color); + font-weight: var(--van-avatar-font-weight); + font-size: var(--van-avatar-font-size); + line-height: var(--van-avatar-line-height); + text-align: center; + background-color: var(--van-avatar-bg-color); + border-radius: var(--van-avatar-border-radius); + overflow: hidden; + user-select: none; + vertical-align: middle; + + &--round { + border-radius: 50%; + } + + &--square { + border-radius: var(--van-avatar-border-radius); + } + + &--large { + width: var(--van-avatar-size-large); + height: var(--van-avatar-size-large); + font-size: var(--van-avatar-font-size-large); + } + + &--medium { + width: var(--van-avatar-size-medium); + height: var(--van-avatar-size-medium); + font-size: var(--van-avatar-font-size-medium); + } + + &--small { + width: var(--van-avatar-size-small); + height: var(--van-avatar-size-small); + font-size: var(--van-avatar-font-size-small); + } + + &__img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + &__text { + white-space: nowrap; + } + + &__icon { + font-size: inherit; + } +} diff --git a/packages/vant/src/avatar/index.ts b/packages/vant/src/avatar/index.ts new file mode 100644 index 00000000000..49ee0233b67 --- /dev/null +++ b/packages/vant/src/avatar/index.ts @@ -0,0 +1,14 @@ +import { withInstall } from '../utils'; +import _Avatar from './Avatar'; + +export const Avatar = withInstall(_Avatar); +export default Avatar; +export { avatarProps } from './Avatar'; +export type { AvatarProps } from './Avatar'; +export type { AvatarSize, AvatarShape, AvatarThemeVars } from './types'; + +declare module 'vue' { + export interface GlobalComponents { + VanAvatar: typeof Avatar; + } +} diff --git a/packages/vant/src/avatar/test/__snapshots__/demo-ssr.spec.ts.snap b/packages/vant/src/avatar/test/__snapshots__/demo-ssr.spec.ts.snap new file mode 100644 index 00000000000..2dfd2b3fc1e --- /dev/null +++ b/packages/vant/src/avatar/test/__snapshots__/demo-ssr.spec.ts.snap @@ -0,0 +1,597 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should render demo and match snapshot 1`] = ` + +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ slot +
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ + +
+ +
+ + V + +
+
+
+ +
+ + A + +
+
+
+ +
+ + N + +
+
+
+ +
+ + T + +
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+ +
+
+ 10 +
+
+
+ +
+ +
+
+ 20 +
+
+
+ +
+ +
+
+
+
+
+ +
+ + A + +
+
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +2 + +
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +2 + +
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +1 + +
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + Custom + +
+
+
+
+`; diff --git a/packages/vant/src/avatar/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/avatar/test/__snapshots__/demo.spec.ts.snap new file mode 100644 index 00000000000..8ed3440cd8b --- /dev/null +++ b/packages/vant/src/avatar/test/__snapshots__/demo.spec.ts.snap @@ -0,0 +1,501 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should render demo and match snapshot 1`] = ` +
+
+
+
+ +
+
+
+
+
+ slot +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + V + +
+
+
+
+ + A + +
+
+
+
+ + N + +
+
+
+
+ + T + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+ 10 +
+
+
+
+ +
+
+ 20 +
+
+
+
+ +
+
+
+
+
+
+ + A + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +2 + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +2 + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + +1 + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + A + +
+
+
+
+ + B + +
+
+
+
+ + Custom + +
+
+
+
+`; diff --git a/packages/vant/src/avatar/test/__snapshots__/index.spec.ts.snap b/packages/vant/src/avatar/test/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000000..dfe4aa1b321 --- /dev/null +++ b/packages/vant/src/avatar/test/__snapshots__/index.spec.ts.snap @@ -0,0 +1,32 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should render default slot when no props provided 1`] = ` +
+ Custom Content +
+`; + +exports[`should render icon avatar correctly 1`] = ` +
+ + +
+`; + +exports[`should render image avatar correctly 1`] = ` +
+ User Avatar +
+`; + +exports[`should render text avatar correctly when no src provided 1`] = ` +
+ + AB + +
+`; diff --git a/packages/vant/src/avatar/test/demo-ssr.spec.ts b/packages/vant/src/avatar/test/demo-ssr.spec.ts new file mode 100644 index 00000000000..e00f7b09342 --- /dev/null +++ b/packages/vant/src/avatar/test/demo-ssr.spec.ts @@ -0,0 +1,7 @@ +/** + * @vitest-environment node + */ +import Demo from '../demo/index.vue'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo, { ssr: true }); diff --git a/packages/vant/src/avatar/test/demo.spec.ts b/packages/vant/src/avatar/test/demo.spec.ts new file mode 100644 index 00000000000..c0e0c95b9a2 --- /dev/null +++ b/packages/vant/src/avatar/test/demo.spec.ts @@ -0,0 +1,4 @@ +import Demo from '../demo/index.vue'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/packages/vant/src/avatar/test/index.spec.ts b/packages/vant/src/avatar/test/index.spec.ts new file mode 100644 index 00000000000..83a514ef301 --- /dev/null +++ b/packages/vant/src/avatar/test/index.spec.ts @@ -0,0 +1,157 @@ +import { Avatar } from '..'; +import { mount } from '../../../test'; + +test('should render image avatar correctly', () => { + const wrapper = mount(Avatar, { + props: { + src: 'https://example.com/avatar.jpg', + alt: 'User Avatar', + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + const img = wrapper.find('img'); + expect(img.attributes('src')).toBe('https://example.com/avatar.jpg'); + expect(img.attributes('alt')).toBe('User Avatar'); +}); + +test('should render text avatar correctly when no src provided', () => { + const wrapper = mount(Avatar, { + props: { + text: 'AB', + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.text()).toBe('AB'); +}); + +test('should render icon avatar correctly', () => { + const wrapper = mount(Avatar, { + props: { + icon: 'user-o', + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + const icon = wrapper.find('.van-icon-user-o'); + expect(icon.exists()).toBe(true); +}); + +test('should render default slot when no props provided', () => { + const wrapper = mount(Avatar, { + slots: { + default: () => 'Custom Content', + }, + }); + + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.text()).toBe('Custom Content'); +}); + +test('should apply different sizes correctly', () => { + const wrapper = mount(Avatar, { + props: { + size: 'large', + text: 'A', + }, + }); + + expect(wrapper.classes()).toContain('van-avatar--large'); + + // Test number size + const wrapper2 = mount(Avatar, { + props: { + size: 60, + text: 'B', + }, + }); + + expect(wrapper2.style.width).toBe('60px'); + expect(wrapper2.style.height).toBe('60px'); + + // Test string size + const wrapper3 = mount(Avatar, { + props: { + size: '80px', + text: 'C', + }, + }); + + expect(wrapper3.style.width).toBe('80px'); + expect(wrapper3.style.height).toBe('80px'); +}); + +test('should apply different shapes correctly', () => { + const wrapper = mount(Avatar, { + props: { + shape: 'square', + text: 'A', + }, + }); + + expect(wrapper.classes()).toContain('van-avatar--square'); +}); + +test('should apply background color and text color', () => { + const wrapper = mount(Avatar, { + props: { + bgColor: '#ff0000', + color: '#ffffff', + text: 'A', + }, + }); + + expect(wrapper.style.backgroundColor).toBe('rgb(255, 0, 0)'); + expect(wrapper.style.color).toBe('rgb(255, 255, 255)'); +}); + +test('should emit error event when image load fails', async () => { + const wrapper = mount(Avatar, { + props: { + src: 'invalid-url.jpg', + }, + }); + + const img = wrapper.find('img'); + await img.trigger('error'); + + expect(wrapper.emitted('error')).toHaveLength(1); +}); + +test('should prioritize src over text and icon', () => { + const wrapper = mount(Avatar, { + props: { + src: 'https://example.com/avatar.jpg', + text: 'AB', + icon: 'user-o', + }, + }); + + expect(wrapper.find('img').exists()).toBe(true); + expect(wrapper.text()).toBe(''); + expect(wrapper.find('.van-icon-user-o').exists()).toBe(false); +}); + +test('should prioritize text over icon when no src', () => { + const wrapper = mount(Avatar, { + props: { + text: 'AB', + icon: 'user-o', + }, + }); + + expect(wrapper.text()).toBe('AB'); + expect(wrapper.find('.van-icon-user-o').exists()).toBe(false); +}); + +test('should handle empty text correctly', () => { + const wrapper = mount(Avatar, { + props: { + text: '', + icon: 'user-o', + }, + }); + + expect(wrapper.find('.van-icon-user-o').exists()).toBe(true); +}); diff --git a/packages/vant/src/avatar/types.ts b/packages/vant/src/avatar/types.ts new file mode 100644 index 00000000000..7706b88654c --- /dev/null +++ b/packages/vant/src/avatar/types.ts @@ -0,0 +1,19 @@ +// Types +export type AvatarSize = 'large' | 'medium' | 'normal' | 'small'; +export type AvatarShape = 'square' | 'round'; + +export type AvatarThemeVars = { + avatarSize?: string; + avatarSizeLarge?: string; + avatarSizeMedium?: string; + avatarSizeSmall?: string; + avatarBgColor?: string; + avatarTextColor?: string; + avatarFontSize?: string; + avatarFontSizeLarge?: string; + avatarFontSizeMedium?: string; + avatarFontSizeSmall?: string; + avatarFontWeight?: string; + avatarLineHeight?: string; + avatarBorderRadius?: string; +}; diff --git a/packages/vant/vant.config.mjs b/packages/vant/vant.config.mjs index 571896a700f..4cf05df2a05 100644 --- a/packages/vant/vant.config.mjs +++ b/packages/vant/vant.config.mjs @@ -292,6 +292,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io'); { title: '展示组件', items: [ + { + path: 'avatar', + title: 'Avatar 头像', + }, { path: 'badge', title: 'Badge 徽标', @@ -764,6 +768,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io'); { title: 'Display Components', items: [ + { + path: 'avatar', + title: 'Avatar', + }, { path: 'badge', title: 'Badge',