diff --git a/site/sidebar.config.ts b/site/sidebar.config.ts index 25c1b67..70de4ce 100644 --- a/site/sidebar.config.ts +++ b/site/sidebar.config.ts @@ -124,6 +124,12 @@ export default [ name: 'Data', type: 'component', // 组件文档 children: [ + { + title: 'Avatar 头像', + name: 'avatar', + path: '/components/avatar', + component: () => import('tdesign-web-components/avatar/README.md'), + }, { title: 'Calendar 日历', name: 'calendar', diff --git a/src/avatar/README.md b/src/avatar/README.md new file mode 100644 index 0000000..673c7c7 --- /dev/null +++ b/src/avatar/README.md @@ -0,0 +1,73 @@ +--- +title: Avatar 头像 +description: 用图标、图片、字符的形式展示用户或事物信息 +isComponent: true +usage: { title: '', description: '' } +spline: base +--- + +### 头像类型 + +头像提供了 3 种不同类型的头像:图标头像、图片头像、字符头像 +{{ base }} + +### 头像形状 + +头像默认支持两种形状:round、circle,用户也可自定义设置头像形状 +{{ shape }} + +### 头像大小 + +头像默认支持三种大小:small、medium、large,用户可自定义设置大小 +{{ size }} + +### 字符头像大小自适应 + +头像支持字符自适应,即字符长度过长时,头像可自动调整字符以便呈现完整内容 +{{ adjust }} + +### 组合头像 + +组合头像展现 +{{ group }} + +### 组合头像偏移方向 + +组合头像可控制层叠方向 +{{ group-cascading }} + +### 组合头像个数 + +组合头像可设置最大展示个数,超过则隐藏显示 +{{ group-max }} + +## API + +### Avatar Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +alt | String | - | 头像替换文本,仅当图片加载失败时有效 | N +children | TNode | - | 子元素内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +content | TNode | - | 子元素内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +hideOnLoadFailed | Boolean | false | 加载失败时隐藏图片 | N +icon | TElement | - | 图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +image | String | - | 图片地址 | N +imageProps | Object | - | 透传至 Image 组件。TS 类型:`ImageProps`,[Image API Documents](./image?tab=api)。[详细类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/avatar/type.ts) | N +shape | String | circle | 形状。可选项:circle/round。TS 类型:`ShapeEnum ` `type ShapeEnum = 'circle' \| 'round'`。[详细类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/avatar/type.ts) | N +size | String | - | 尺寸,示例值:small/medium/large/24px/38px 等。优先级高于 AvatarGroup.size 。Avatar 单独存在时,默认值为 medium。如果父组件存在 AvatarGroup,默认值便由 AvatarGroup.size 决定 | N +onError | Function | | TS 类型:`(context: { e: ImageEvent }) => void`
图片加载失败时触发 | N + +### AvatarGroup Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +cascading | String | 'right-up' | 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上。可选项:left-up/right-up。TS 类型:`CascadingValue` `type CascadingValue = 'left-up' \| 'right-up'`。[详细类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/avatar/type.ts) | N +collapseAvatar | TNode | - | 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5`,`...`, `更多`。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +max | Number | - | 能够同时显示的最多头像数量 | N +popupProps | Object | - | 头像右上角提示信息。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/avatar/type.ts) | N +size | String | - | 尺寸,示例值:small/medium/large/24px/38px 等。优先级低于 Avatar.size | N \ No newline at end of file diff --git a/src/avatar/_example/adjust.tsx b/src/avatar/_example/adjust.tsx new file mode 100644 index 0000000..d22fc03 --- /dev/null +++ b/src/avatar/_example/adjust.tsx @@ -0,0 +1,18 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class Avatar extends Component { + static css = 't-avatar{}'; + + render() { + return ( + + + 王亿 + 王亿亿 + + ); + } +} diff --git a/src/avatar/_example/base.tsx b/src/avatar/_example/base.tsx new file mode 100644 index 0000000..f9eda84 --- /dev/null +++ b/src/avatar/_example/base.tsx @@ -0,0 +1,25 @@ +import 'tdesign-web-components/image'; +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; +import 'tdesign-web-components/icon'; + +// import 'tdesign-icons-omi/user' +import { Component } from 'omi'; + +export default class Avatar extends Component { + static css = 't-avatar{}'; + + render() { + return ( + + } style={{ marginRight: '40px' }} /> + + W + + ); + } +} diff --git a/src/avatar/_example/group-cascading.tsx b/src/avatar/_example/group-cascading.tsx new file mode 100644 index 0000000..c6e10d1 --- /dev/null +++ b/src/avatar/_example/group-cascading.tsx @@ -0,0 +1,28 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; +import 'tdesign-web-components/avatar/avatar-group'; +import 'tdesign-web-components/icon'; + +import { Component } from 'omi'; + +export default class AvatarGroupCascading extends Component { + static css = 't-avatar{}'; + + render() { + return ( + + + + W + }> + + + + + W + }> + + + ); + } +} diff --git a/src/avatar/_example/group-max.tsx b/src/avatar/_example/group-max.tsx new file mode 100644 index 0000000..b61ac6e --- /dev/null +++ b/src/avatar/_example/group-max.tsx @@ -0,0 +1,35 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; +import 'tdesign-web-components/avatar/avatar-group'; +import 'tdesign-web-components/icon'; +import 'tdesign-web-components/image'; + +import { Component } from 'omi'; + +export default class AvatarGroupMax extends Component { + static css = 't-avatar{}'; + + render() { + return ( + + + + Avatar + + + + }> + + Avatar + }> + + + + + Avatar + }> + + + ); + } +} diff --git a/src/avatar/_example/group.tsx b/src/avatar/_example/group.tsx new file mode 100644 index 0000000..d105e4f --- /dev/null +++ b/src/avatar/_example/group.tsx @@ -0,0 +1,29 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; +import 'tdesign-web-components/avatar/avatar-group'; +import 'tdesign-web-components/icon'; + +// import 'tdesign-icons-omi/user' +import { Component } from 'omi'; + +export default class AvatarGroup extends Component { + static css = 't-avatar{}'; + + render() { + return ( + + + + W + }> + + + + + W + }> + + + ); + } +} diff --git a/src/avatar/_example/shape.tsx b/src/avatar/_example/shape.tsx new file mode 100644 index 0000000..3c34032 --- /dev/null +++ b/src/avatar/_example/shape.tsx @@ -0,0 +1,17 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class AvatarShape extends Component { + render() { + return ( + + W + + W + + + ); + } +} diff --git a/src/avatar/_example/size.tsx b/src/avatar/_example/size.tsx new file mode 100644 index 0000000..474184e --- /dev/null +++ b/src/avatar/_example/size.tsx @@ -0,0 +1,68 @@ +import 'tdesign-web-components/avatar'; +import 'tdesign-web-components/space'; + +import { Component } from 'omi'; + +export default class AvatarSize extends Component { + render() { + return ( + + + + W + + + W + + + W + + + W + + + + + W + + + W + + + W + + + W + + + + + + + + + + ); + } +} diff --git a/src/avatar/avatar-group.tsx b/src/avatar/avatar-group.tsx new file mode 100644 index 0000000..aff2583 --- /dev/null +++ b/src/avatar/avatar-group.tsx @@ -0,0 +1,84 @@ +import './avatar'; + +import { toArray } from 'lodash'; +import { classNames, cloneElement, Component, OmiProps, tag } from 'omi'; + +import { getClassPrefix } from '../_util/classname'; +import parseTNode from '../_util/parseTNode'; +import { StyledProps } from '../common'; +import { styleSheet } from './style/index.ts'; +import { TdAvatarGroupProps } from './type'; + +import borderCss from './style/border.less'; +import offsetLeftCss from './style/offset_left.less'; +import offsetLeftZIndexCss from './style/offset_left_zIndex.less'; +import offsetRightCss from './style/offset_right.less'; + +export interface AvatarGroupProps extends TdAvatarGroupProps, StyledProps {} + +@tag('t-avatar-group') +export default class AvatarGroup extends Component { + static css = styleSheet; + + static defaultProps = { cascading: 'right-up' }; + + static propTypes = { + cascading: String, + max: Number, + size: String, + collapseAvatar: Object, + children: Object, + }; + + preClass = `${getClassPrefix()}-avatar`; + + allChildrenList: any; + + provide = { groupSize: undefined as any }; + + install() { + this.provide = { groupSize: this.props.size }; + } + + render(props: OmiProps) { + const { preClass } = this; + const { children, max, cascading, collapseAvatar } = props; + const childrenList = toArray(children); + if (childrenList.length > 0) { + this.allChildrenList = childrenList.map((child, index) => { + let childrenCss = borderCss; + if (cascading === 'right-up' && index !== childrenList.length - 1) { + childrenCss += offsetRightCss; + } else if (cascading === 'left-up' && index !== 0) { + childrenCss += offsetLeftCss + offsetLeftZIndexCss; + } else if (cascading === 'left-up') { + childrenCss += offsetLeftZIndexCss; + } + + return cloneElement(child, { + key: `avatar-group-item-${index}`, + css: childrenCss, + }); + }); + } + const groupClass = classNames(`${preClass}-group`, this.className, { + [`${preClass}--offset-right`]: cascading === 'right-up', + [`${preClass}--offset-left`]: cascading === 'left-up', + }); + + const childrenCount = childrenList.length; + if (props.max && childrenCount > max) { + const showList = this.allChildrenList.slice(0, max); + let childrenCss = borderCss; + if (cascading === 'left-up') { + childrenCss += offsetLeftCss + offsetLeftZIndexCss; + } + const ellipsisAvatar = ( + {parseTNode(collapseAvatar) || `+${childrenCount - max}`} + ); + showList.push(
{ellipsisAvatar}
); + return
{showList}
; + } + return
{this.allChildrenList}
; + } +} diff --git a/src/avatar/avatar.en-US.md b/src/avatar/avatar.en-US.md new file mode 100644 index 0000000..16c72e9 --- /dev/null +++ b/src/avatar/avatar.en-US.md @@ -0,0 +1,32 @@ +:: BASE_DOC :: + +## API + +### Avatar Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | N +alt | String | - | show it when url is not valid | N +children | TNode | - | children, same as `content`。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +content | TNode | - | content slot or props.content。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/omi/blob/master/tdesign/desktop/src/common.ts) | N +hideOnLoadFailed | Boolean | false | hide image when loading image failed | N +icon | TElement | - | use icon to fill。Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/omi/blob/master/tdesign/desktop/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/omi/blob/master/tdesign/desktop/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/omi/blob/master/tdesign/desktop/src/avatar/type.ts) | N +size | String | - | size | N +onError | Function | | Typescript:`(context: { e: ImageEvent }) => void`
trigger on image load failed | N + +### AvatarGroup Props + +name | type | default | description | required +-- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,Typescript:`React.CSSProperties` | 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-react/blob/develop/src/avatar/type.ts) | N +collapseAvatar | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +max | Number | - | \- | N +popupProps | Object | - | Typescript:`PopupProps`,[Popup API Documents](./popup?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/avatar/type.ts) | N +size | String | - | size | N diff --git a/src/avatar/avatar.tsx b/src/avatar/avatar.tsx new file mode 100644 index 0000000..ae0da63 --- /dev/null +++ b/src/avatar/avatar.tsx @@ -0,0 +1,136 @@ +import { classNames, Component, createRef, OmiProps, tag } from 'omi'; + +import { getClassPrefix, getCommonClassName } from '../_util/classname'; +import { StyledProps } from '../common'; +import { ImageProps } from '../image'; +import { styleSheet } from './style/index.ts'; +import { TdAvatarProps } from './type'; + +export interface AvatarProps extends TdAvatarProps, StyledProps {} + +@tag('t-avatar') +export default class Avatar extends Component { + static css = styleSheet; + + static defaultProps = { hideOnLoadFailed: false, shape: 'circle' }; + + static propsType = { + alt: String, + hideOnLoadFailed: Boolean, + icon: Object, + image: String, + shape: String, + size: String, + onError: Function, + children: Object, + content: Object, + style: Object, + imageProps: Object, + }; + + scale = 1; + + gap = 4; + + isImgExist = true; + + inject = ['groupSize']; + + groupSize: any; + + avatarRef = createRef(); + + avatarChildrenRef = createRef(); + + componentName = `${getClassPrefix()}-avatar`; + + handleScale = () => { + const { avatarChildrenRef, avatarRef, gap } = this; + if (!avatarChildrenRef.current || !avatarRef.current) { + return; + } + const avatar = avatarRef.current as HTMLElement; + const children = avatarChildrenRef.current as HTMLElement; + const avatarWidth = avatar.offsetWidth; + const childrenWidth = children.offsetWidth; + + if (childrenWidth !== 0 && avatarWidth !== 0) { + if (gap * 2 < avatarWidth) { + this.scale = avatarWidth - gap * 2 < childrenWidth ? (avatarWidth - gap * 2) / childrenWidth : 1; + } + } + }; + + handleImgLoadError: ImageProps['onError'] = (ctx) => { + const { hideOnLoadFailed, onError } = this.props; + onError?.(ctx); + if (!hideOnLoadFailed) { + this.isImgExist = false; + this.update(); + } + }; + + // resizeObserver + beforeRender(): void { + this.groupSize = this.injection ? this.injection.groupSize : null; + } + + installed() { + this.handleScale(); + this.update(); + } + + render(props: OmiProps) { + const { SIZE } = getCommonClassName(); + const { componentName, isImgExist, groupSize, avatarRef, avatarChildrenRef, handleImgLoadError } = this; + const { alt, icon, image, shape, size: avatarSize, children, content, style, imageProps, ...avatarProps } = props; + // console.log('this.injection.groupSize: ', this.injection.groupSize) + const size = avatarSize === undefined ? groupSize : avatarSize; + + const numSizeStyle = + size && !SIZE[size] + ? { + width: size, + height: size, + fontSize: `${Number.parseInt(size, 10) / 2}px`, + } + : {}; + + const imageStyle = + size && !SIZE[size] + ? { + width: size, + height: size, + } + : {}; + + const avatarClass = classNames(componentName, this.className, { + [SIZE[size]]: !!SIZE[size], + [`${componentName}--${shape}`]: !!shape, + [`${componentName}__icon`]: !!icon, + }); + let renderChildren: string | number | boolean | object; + + if (image && isImgExist) { + renderChildren = ( + + ); + } else if (icon) { + renderChildren = icon; + } else { + const childrenStyle = { + transform: `scale(${this.scale})`, + }; + renderChildren = ( + + {children || content} + + ); + } + return ( +
+ {renderChildren} +
+ ); + } +} diff --git a/src/avatar/index.tsx b/src/avatar/index.tsx new file mode 100644 index 0000000..0d51d43 --- /dev/null +++ b/src/avatar/index.tsx @@ -0,0 +1,9 @@ +import _Avatar from './avatar'; +import _AvatarGroup from './avatar-group'; + +export type { TdAvatarGroupProps } from './type'; +export type { AvatarProps } from './avatar'; +export const Avatar = _Avatar; +export const AvatarGroup = _AvatarGroup; + +export default Avatar; diff --git a/src/avatar/style/border.less b/src/avatar/style/border.less new file mode 100644 index 0000000..08bbf63 --- /dev/null +++ b/src/avatar/style/border.less @@ -0,0 +1,11 @@ +@import '../../_common/style/web/base.less'; + +@import '../../_common/style/web/components/avatar/_var.less'; + +@import '../../_common/style/web/components/avatar/_mixin.less'; + +@import '../../_common/style/web/mixins/_reset.less'; + +.@{prefix}-avatar { + border: 2px solid @avatar-border-color; +} diff --git a/src/avatar/style/css.js b/src/avatar/style/css.js new file mode 100644 index 0000000..6a9a4b1 --- /dev/null +++ b/src/avatar/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/avatar/style/index.ts b/src/avatar/style/index.ts new file mode 100644 index 0000000..6126e3e --- /dev/null +++ b/src/avatar/style/index.ts @@ -0,0 +1,10 @@ +import { css, globalCSS } from 'omi'; + +import avatarStyle from '../../_common/style/web/components/avatar/_index.less'; +import theme from '../../_common/style/web/theme/_index.less'; + +export const styleSheet = css` + ${avatarStyle} + ${theme} +`; + +globalCSS(styleSheet); diff --git a/src/avatar/style/offset_left.less b/src/avatar/style/offset_left.less new file mode 100644 index 0000000..ffe7b11 --- /dev/null +++ b/src/avatar/style/offset_left.less @@ -0,0 +1,11 @@ +@import '../../_common/style/web/base.less'; + +@import '../../_common/style/web/components/avatar/_var.less'; + +@import '../../_common/style/web/components/avatar/_mixin.less'; + +@import '../../_common/style/web/mixins/_reset.less'; + +.@{prefix}-avatar { + .avatar-group-size--left() !important; +} diff --git a/src/avatar/style/offset_left_zIndex.less b/src/avatar/style/offset_left_zIndex.less new file mode 100644 index 0000000..f2b8c2e --- /dev/null +++ b/src/avatar/style/offset_left_zIndex.less @@ -0,0 +1,9 @@ +@import '../../_common/style/web/base.less'; + +@import '../../_common/style/web/components/avatar/_var.less'; + +@import '../../_common/style/web/components/avatar/_mixin.less'; + +@import '../../_common/style/web/mixins/_reset.less'; + +.generate-z-index(@avatar-group-init-zIndex); diff --git a/src/avatar/style/offset_right.less b/src/avatar/style/offset_right.less new file mode 100644 index 0000000..de71fbb --- /dev/null +++ b/src/avatar/style/offset_right.less @@ -0,0 +1,19 @@ +@import '../../_common/style/web/base.less'; + +@import '../../_common/style/web/components/avatar/_var.less'; + +@import '../../_common/style/web/components/avatar/_mixin.less'; + +@import '../../_common/style/web/mixins/_reset.less'; + +.@{prefix}-avatar { + .avatar-group-offset-right(@avatar-group-offset-medium) !important; + + &.@{prefix}-size-s { + .avatar-group-offset-right(@avatar-group-offset-small); + } + + &.@{prefix}-size-l { + .avatar-group-offset-right(@avatar-group-offset-large); + } +} diff --git a/src/avatar/type.ts b/src/avatar/type.ts new file mode 100644 index 0000000..f2527aa --- /dev/null +++ b/src/avatar/type.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { ImageProps } from '../image'; +import { PopupProps } from '../popup'; +import { TNode, TElement } from '../common'; + +export interface TdAvatarProps { + /** + * 头像替换文本,仅当图片加载失败时有效 + * @default '' + */ + alt?: string; + /** + * 子元素内容,同 content + */ + children?: TNode; + /** + * 子元素内容 + */ + content?: TNode; + /** + * 加载失败时隐藏图片 + * @default false + */ + hideOnLoadFailed?: boolean; + /** + * 图标 + */ + icon?: TElement; + /** + * 图片地址 + * @default '' + */ + image?: string; + /** + * 透传至 Image 组件 + */ + imageProps?: ImageProps; + /** + * 形状 + * @default circle + */ + shape?: ShapeEnum; + /** + * 尺寸,示例值:small/medium/large/24px/38px 等。优先级高于 AvatarGroup.size 。Avatar 单独存在时,默认值为 medium。如果父组件存在 AvatarGroup,默认值便由 AvatarGroup.size 决定 + * @default '' + */ + size?: string; + /** + * 图片加载失败时触发: TODO ImageEvent ? + */ + onError?: (context: { e: Event }) => void; +} + +export interface TdAvatarGroupProps { + /** + * 图片之间的层叠关系,可选值:左侧图片在上和右侧图片在上 + * @default 'right-up' + */ + cascading?: CascadingValue; + /** + * 头像数量超出时,会出现一个头像折叠元素。该元素内容可自定义。默认为 `+N`。示例:`+5`,`...`, `更多` + */ + collapseAvatar?: TNode; + /** + * 能够同时显示的最多头像数量 + */ + max?: number; + /** + * 头像右上角提示信息 + */ + popupProps?: PopupProps; + /** + * 尺寸,示例值:small/medium/large/24px/38px 等。优先级低于 Avatar.size + * @default '' + */ + size?: string; +} + +export type ShapeEnum = 'circle' | 'round'; + +export type CascadingValue = 'left-up' | 'right-up'; diff --git a/src/index.ts b/src/index.ts index 5fe7556..8789882 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from './avatar'; export * from './button'; export * from './common'; export * from './divider';