diff --git a/src/avatar/__test__/__snapshots__/demo.test.jsx.snap b/src/avatar/__test__/__snapshots__/demo.test.jsx.snap index e50f969d6..44138474c 100644 --- a/src/avatar/__test__/__snapshots__/demo.test.jsx.snap +++ b/src/avatar/__test__/__snapshots__/demo.test.jsx.snap @@ -343,8 +343,50 @@ exports[`Avatar > Avatar actionVue demo works fine 1`] = ` +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+
- `; @@ -927,8 +969,36 @@ exports[`Avatar > Avatar exhibitionVue demo works fine 1`] = ` +
+
+
+
+ +
+ + + +5 + + +
+ +
+ + +
+
+
- `; @@ -1903,8 +1973,36 @@ exports[`Avatar > Avatar mobileVue demo works fine 1`] = ` +
+
+
+
+ +
+ + + +5 + + +
+ +
+ + +
+
+
- @@ -2271,8 +2369,50 @@ exports[`Avatar > Avatar mobileVue demo works fine 1`] = ` +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+
- diff --git a/src/avatar/__test__/index.test.jsx b/src/avatar/__test__/index.test.jsx index b23dbfc9a..1724326b0 100644 --- a/src/avatar/__test__/index.test.jsx +++ b/src/avatar/__test__/index.test.jsx @@ -2,8 +2,8 @@ import { h, nextTick } from 'vue'; import { mount } from '@vue/test-utils'; import { describe, it, expect, vi } from 'vitest'; import { UserIcon } from 'tdesign-icons-vue-next'; -import Avatar from '../avatar.vue'; -import AvatarGroup from '../avatar-group.vue'; +import Avatar from '../avatar'; +import AvatarGroup from '../avatar-group'; import Badge from '../../badge/badge'; const IMAGE = 'https://tdesign.gtimg.com/site/avatar.jpg'; @@ -121,7 +121,7 @@ describe('avatar-group', async () => { it(': max', async () => { const wrapper = mount(() => ( - + @@ -130,6 +130,7 @@ describe('avatar-group', async () => { )); const avatarList = wrapper.findAllComponents(Avatar); - expect(avatarList.length).toBe(1); + expect(avatarList.length).toBe(3); + expect(avatarList[avatarList.length-1].text()).toBe('+3'); }); }); diff --git a/src/avatar/avatar-group.tsx b/src/avatar/avatar-group.tsx new file mode 100644 index 000000000..366e4641f --- /dev/null +++ b/src/avatar/avatar-group.tsx @@ -0,0 +1,73 @@ +import { computed, defineComponent, Fragment, provide, RendererNode } from 'vue'; + +import AvatarGroupProps from './avatar-group-props'; +import config from '../config'; +import TAvatar from './avatar'; + +import { useTNodeJSX } from '../hooks/tnode'; +import { usePrefixClass } from '../hooks/useClass'; + +const { prefix } = config; +const name = `${prefix}-avatar-group`; + +export default defineComponent({ + name, + props: AvatarGroupProps, + setup(props) { + const renderTNodeJSX = useTNodeJSX(); + const avatarGroupClass = usePrefixClass('avatar-group'); + + provide('avatarGroup', { ...props }); + + const direction = props.cascading ? props.cascading.split('-')[0] : 'right'; + + const avatarGroupClasses = computed(() => [ + `${avatarGroupClass.value}`, + `${avatarGroupClass.value}-offset-${direction}-${props.size.indexOf('px') > -1 ? 'medium' : props.size}`, + ]); + + const readerAvatar = () => { + const children: Array = renderTNodeJSX('default'); + const allChildren: Array = []; + + children.forEach((child) => { + if (child.type === Fragment) { + allChildren.push(...child.children); + } else { + allChildren.push(child); + } + }); + + let isShowCollapse = false; + let avatarList: Array = []; + if (allChildren.length > props.max) { + avatarList = allChildren.slice(0, props.max); + isShowCollapse = true; + } else { + avatarList = allChildren; + } + + if (props.cascading === 'left-up') { + const defaultZIndex = 100; + for (let index = 0; index < avatarList.length; index++) { + avatarList[index].props.style = `z-index: ${defaultZIndex - index * 10}`; + } + } + + if (isShowCollapse) { + const collapseAvatar = renderTNodeJSX('collapseAvatar'); + avatarList.push( + + {collapseAvatar || `+${allChildren.length - props.max}`} + , + ); + } + + return avatarList; + }; + + return () => { + return
{readerAvatar()}
; + }; + }, +}); diff --git a/src/avatar/avatar-group.vue b/src/avatar/avatar-group.vue deleted file mode 100644 index 1c1608d8b..000000000 --- a/src/avatar/avatar-group.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/avatar/avatar.en-US.md b/src/avatar/avatar.en-US.md index 1235d74b3..12b815505 100644 --- a/src/avatar/avatar.en-US.md +++ b/src/avatar/avatar.en-US.md @@ -1,6 +1,7 @@ :: BASE_DOC :: ## API + ### Avatar Props name | type | default | description | required @@ -11,7 +12,7 @@ hideOnLoadFailed | Boolean | false | hide image when loading image failed | N icon | Slot / Function | - | use icon to fill。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N image | String | - | images url | N imageProps | Object | - | Typescript:`ImageProps`,[Image API Documents](./image?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N -shape | String | circle | shape。options:circle/round。Typescript:`ShapeEnum ` `type ShapeEnum = 'circle' \| 'round'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N +shape | String | circle | shape。options: circle/round。Typescript:`ShapeEnum ` `type ShapeEnum = 'circle' \| 'round'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N size | String | medium | size | N onError | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image load failed | N @@ -21,17 +22,18 @@ name | params | description -- | -- | -- error | `(context: { e: ImageEvent })` | trigger on image load failed + ### AvatarGroup Props name | type | default | description | required -- | -- | -- | -- | -- -cascading | String | 'right-up' | multiple images cascading。options:left-up/right-up。Typescript:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N +cascading | String | 'right-up' | multiple images cascading。options: left-up/right-up。Typescript:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N collapseAvatar | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N max | Number | - | \- | N size | String | medium | size | N +### CSS 变量 -### CSS Variables The component provides the following CSS variables, which can be used to customize styles. Name | Default Value | Description -- | -- | -- @@ -55,4 +57,4 @@ Name | Default Value | Description --td-avatar-small-width | 40px | - --td-avatar-text-large-font-size | 16px | - --td-avatar-text-medium-font-size | @font-size-base | - ---td-avatar-text-small-font-size | @font-size-s | - +--td-avatar-text-small-font-size | @font-size-s | - \ No newline at end of file diff --git a/src/avatar/avatar.md b/src/avatar/avatar.md index d41830551..69b7371c0 100644 --- a/src/avatar/avatar.md +++ b/src/avatar/avatar.md @@ -1,9 +1,10 @@ :: BASE_DOC :: ## API + ### Avatar Props -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- alt | String | - | 头像替换文本,仅当图片加载失败时有效 | N badgeProps | Object | - | 头像右上角提示信息,继承 Badge 组件的全部特性。如:小红点,或者数字。TS 类型:`BadgeProps`,[Badge API Documents](./badge?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N @@ -21,17 +22,18 @@ onError | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
-- | -- | -- error | `(context: { e: ImageEvent })` | 图片加载失败时触发 + ### AvatarGroup Props -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- cascading | String | 'right-up' | 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上。可选项:left-up/right-up。TS 类型:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/avatar/type.ts) | N collapseAvatar | String / Slot / Function | - | 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5`,`...`, `更多`。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N max | Number | - | 能够同时显示的最多头像数量 | N size | String | medium | 尺寸,示例值:small/medium/large/24px/38px 等。优先级低于 Avatar.size | N - ### CSS 变量 + 组件提供了下列 CSS 变量,可用于自定义样式。 名称 | 默认值 | 描述 -- | -- | -- @@ -55,4 +57,4 @@ size | String | medium | 尺寸,示例值:small/medium/large/24px/38px 等 --td-avatar-small-width | 40px | - --td-avatar-text-large-font-size | 16px | - --td-avatar-text-medium-font-size | @font-size-base | - ---td-avatar-text-small-font-size | @font-size-s | - +--td-avatar-text-small-font-size | @font-size-s | - \ No newline at end of file diff --git a/src/avatar/avatar.tsx b/src/avatar/avatar.tsx new file mode 100644 index 000000000..03eb3579a --- /dev/null +++ b/src/avatar/avatar.tsx @@ -0,0 +1,82 @@ +import { computed, defineComponent, getCurrentInstance, inject, ref } from 'vue'; +import TBadge from '../badge'; +import TImage from '../image'; +import config from '../config'; +import AvatarProps from './props'; +import { TdAvatarGroupProps, TdAvatarProps } from './type'; +import CLASSNAMES from '../shared/constants'; +import { useContent, useTNodeJSX } from '../hooks/tnode'; +import { usePrefixClass } from '../hooks/useClass'; + +const { prefix } = config; +const name = `${prefix}-avatar`; + +export default defineComponent({ + name, + props: AvatarProps, + setup(props) { + const renderTNodeJSX = useTNodeJSX(); + const renderTNodeContent = useContent(); + const avatarClass = usePrefixClass('avatar'); + + const avatarGroupProps = inject('avatarGroup', {}); + const hasAvatarGroupProps = Object.keys(avatarGroupProps).length > 0; + + const sizeValue = ref(props.size || (avatarGroupProps && avatarGroupProps.size)); + const sizeReValue = ref((avatarGroupProps && avatarGroupProps.size) || props.size); + const sizeClass = `${sizeReValue.value.indexOf('px') > -1 ? 'medium' : sizeReValue.value}`; + const avatarClasses = computed(() => [ + `${avatarClass.value}`, + `${avatarClass.value}--${sizeClass}`, + { + [`${avatarClass.value}--${props.shape}`]: props.shape, + }, + hasAvatarGroupProps ? `${avatarClass.value}--border ${avatarClass.value}--border-${sizeClass}` : '', + ]); + + const isCustomSize = computed(() => sizeValue.value && !CLASSNAMES.SIZE[sizeValue.value]); + const customSize = computed(() => { + return isCustomSize.value + ? { + height: sizeValue.value, + width: sizeValue.value, + } + : {}; + }); + + const handleImgLoadError = (e: any) => { + props.onError?.(e); + }; + return () => { + const icon = renderTNodeJSX('icon'); + const TNodeContent = renderTNodeContent('default', 'content'); + + const readerAvatar = () => { + if (props.image && !props.hideOnLoadFailed) { + return ( + + ); + } + if (icon) { + return
{icon}
; + } + return {TNodeContent}; + }; + return ( +
+
+ +
{readerAvatar()}
+
+
+
+ ); + }; + }, +}); diff --git a/src/avatar/avatar.vue b/src/avatar/avatar.vue deleted file mode 100644 index e1824b444..000000000 --- a/src/avatar/avatar.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/src/avatar/index.ts b/src/avatar/index.ts index 0f72d86a9..d0d020b6d 100644 --- a/src/avatar/index.ts +++ b/src/avatar/index.ts @@ -1,5 +1,5 @@ -import _Avatar from './avatar.vue'; -import _AvatarGroup from './avatar-group.vue'; +import _Avatar from './avatar'; +import _AvatarGroup from './avatar-group'; import { withInstall, WithInstallType } from '../shared'; import './style'; diff --git a/src/cell/__test__/index.test.jsx b/src/cell/__test__/index.test.jsx index ba0275d39..772d84a30 100644 --- a/src/cell/__test__/index.test.jsx +++ b/src/cell/__test__/index.test.jsx @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { ChevronRightIcon, AppIcon } from 'tdesign-icons-vue-next'; import Cell from '../cell'; import CellGroup from '../cell-group'; -import Avatar from '../../avatar/avatar.vue'; +import Avatar from '../../avatar/avatar'; const appIcon = () => h(AppIcon); const chevronRightIcon = () => h(ChevronRightIcon);