feat:avatar头像组件 (#3175)

* fix:inputImage 裁剪模式可配置可旋转,但是功能无效

* feat:组件头像avatar

* feat:组件头像avatar完善

* feat:组件头像avatar根据评审完善

* feat:组件头像avatar根据评审完善

* feat:组件头像avatar根据评审完善

Co-authored-by: sqzhou <zhoushengqiang01@baidu.com>
This commit is contained in:
zhou999 2021-12-28 16:32:11 +08:00 committed by GitHub
parent b0541d3bfb
commit 2032de2ad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 491 additions and 115 deletions

View File

@ -43,7 +43,10 @@ order: 27
## 动态图片或文字
src、text 都支持变量,可以从上下文中动态获取图片或文字,下面的例子中第一个获取到了,而第二个没获取到,因此降级为显示 icon
src、text 都支持变量,可以从上下文中动态获取图片或文字,下面的例子中:
- 第一个获取到了,显示正常
- 第二个没获取到,因此降级为显示 icon
- 第三个图片没获取到,由于 text 优先级比 icon 高,所以显示 text
```schema
{
@ -61,6 +64,12 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
"type": "avatar",
"icon": "fa fa-user",
"src": "$other"
},
{
"type": "avatar",
"src": "$other",
"icon": "fa fa-user",
"text": "avatar"
}
]
}
@ -97,13 +106,52 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
[
{
"type": "avatar",
"size": 20,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
"size": 'large',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 'default',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 'small',
"icon": "fa fa-user"
},
{
"type": "avatar",
"size": 60,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
{
"type": "avatar",
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
{
"type": "avatar",
"size": 20,
"src": "https://suda.cdn.bcebos.com/images/amis/ai-fake-face.jpg"
},
]
```
## 控制字符类型距离左右两侧边界单位像素
通过 gap 可以控制字符类型距离左右两侧边界单位像素
```schema: scope="body"
[
{
"type": "avatar",
"text": 'ejson',
"gap": 2
},
{
"type": "avatar",
"text": "ejson",
"gap": 7
}
]
@ -111,7 +159,7 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
## 图片拉伸方式
通过 `fit` 可以控制图片拉伸方式,默认是 `cover`,具体细节可以参考 MDN [文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit)
通过 `fit` 可以控制图片拉伸方式,默认是 `'cover'`
```schema: scope="body"
[
@ -143,6 +191,39 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
]
```
## 控制图片是否允许拖动
通过 draggable 可以控制图片是否允许拖动
```schema: scope="body"
[
{
"type": "avatar",
"fit": "cover",
"src": "https://suda.cdn.bcebos.com/images/amis/plumeria.jpeg",
"draggable": false
},
{
"type": "avatar",
"fit": "cover",
"src": "https://suda.cdn.bcebos.com/images/amis/plumeria.jpeg",
"draggable": true
}
]
```
## 图片加载失败后,通过 onError 控制是否进行 text、icon 置换
> 如果同时存在 text 和 icon会优先用 text、接着 icon
```schema: scope="body"
{
"type": "avatar",
"src": "empty",
"text": "avatar",
"onError": "return true;"
},
```
## 样式
可以通过 style 来控制背景及文字颜色
@ -161,12 +242,17 @@ src、text 都支持变量,可以从上下文中动态获取图片或文字,
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | -------- | ------ | --------------------- |
| className | `string` | | 外层 dom 的类名 |
| fit | `string` | cover | 图片缩放类型 |
| src | `string` | | 图片地址 |
| text | `string` | | 文字 |
| icon | `string` | | 图标 |
| shape | `string` | circle | 形状,也可以是 square |
| size | `number` | 40 | 大小 |
| style | `object` | | 外层 dom 的样式 |
| --------- | ----------- | ------ | --------------------- |
| className | `string` | | 外层 dom 的类名 |
| style | `object` | | 外层 dom 的样式 |
| fit |`'contain'` \| `'cover'` \| `'fill'` \| `'none'` \| `'scale-down'` | `'cover'` | 具体细节可以参考 MDN [文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit) |
| src | `string` | | 图片地址 |
| text | `string` | | 文字 |
| icon | `string` | `'fa fa-user'` | 图标 |
| shape | `'circle'` \| `'square'` \| `'rounded'` | `'circle'` | 形状,有三种 `'circle'` (圆形)、`'square'`(正方形)、`'rounded'`(圆角) |
| size | `number` \| `'default'` \| `'normal'` \| `'small'` | `'default'` | `'default' \| 'normal' \| 'small'`三种字符串类型代表不同大小分别是48、40、32也可以直接数字表示 |
| gap | `number` | 4 | 控制字符类型距离左右两侧边界单位像素 |
| alt | `number` | | 图像无法显示时的替代文本 |
| draggable | `boolean` | | 图片是否允许拖动 |
| crossOrigin | `'anonymous'` \| `'use-credentials'` \| `''` | | 图片的 `CORS` 属性设置 |
| onError | `string` | | 图片加载失败的字符串这个字符串是一个New Function内部执行的字符串参数是event使用event.nativeEvent获取原生dom事件这个字符串需要返回boolean值。设置 `"return ture;"` 会在图片加载失败后,使用 `text` 或者 `icon` 代表的信息来进行替换。目前图片加载失败默认是不进行置换。注意:图片加载失败,不包括$获取数据为空情况 |

View File

@ -164,6 +164,14 @@
--Audio-volumeControl-width: #{px2rem(110px)};
--Avatar-bg: #{$gray300};
--Avatar-width: #{px2rem(40px)};
--Avatar-size-large: #{px2rem(48px)};
// 兼容旧的size大小写法
--Avatar-size-default: var(--Avatar-width);
--Avatar-size-small: #{px2rem(32px)};
--Avatar-icon-size-large: #{px2rem(20px)};
// 兼容旧的icon大小写法
--Avatar-icon-size-default: var(--fontSizeLg);
--Avatar-icon-size-small: #{px2rem(12px)};
--Badge-size: var(--gap-md);
--Badge-color: var(--white);

View File

@ -1,14 +1,37 @@
@mixin avatar-size($size, $fontSize) {
width: $size;
height: $size;
line-height: $size;
i {
font-size: $fontSize;
}
}
.#{$ns}Avatar {
background: var(--Avatar-bg);
width: var(--Avatar-width);
height: var(--Avatar-width);
line-height: var(--Avatar-width);
@include avatar-size(var(--Avatar-size-default), var(--Avatar-icon-size-default));
position: relative;
display: inline-block;
overflow: hidden;
flex-shrink: 0;
border-radius: 50%;
text-align: center;
&--lg {
@include avatar-size(var(--Avatar-size-large), var(--Avatar-icon-size-large));
}
&--sm {
@include avatar-size(var(--Avatar-size-small), var(--Avatar-icon-size-small));
}
&--text {
position: absolute;
left: 50%;
transform-origin: 0 center;
}
&--square {
border-radius: 0%;
}
@ -17,18 +40,13 @@
border-radius: 10%;
}
i {
font-size: var(--fontSizeLg);
}
img {
color: transparent;
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover {
img,
i {
transform: scale(1.1);

253
src/components/Avatar.tsx Normal file
View File

@ -0,0 +1,253 @@
import * as React from 'react';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
/**
* Avatar
*/
interface AvatarCmptProps extends ThemeProps {
style?: {
[prop: string]: any
};
className?: string;
classnames: ClassNamesFn;
/**
*
*/
src?: string | React.ReactNode;
/**
*
*/
icon?: string | React.ReactNode;
/**
*
*/
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
/**
*
*/
shape?: 'circle' | 'square' | 'rounded';
/**
*
*/
size?: number | 'small' | 'default' | 'large';
/**
*
*/
text?: string;
/**
*
*/
gap?: number;
/**
*
*/
alt?: string;
/**
*
*/
draggable?: boolean;
/**
* CORS属性
*/
crossOrigin?: 'anonymous' | 'use-credentials' | '';
/**
* false
*/
onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => boolean;
/**
*
*/
children?: JSX.Element | ((props?: any) => JSX.Element)
}
const prefix = 'Avatar--';
const childPrefix = prefix + 'text';
export interface AvatarState {
scale: number;
hasImg: boolean;
}
export class Avatar extends React.Component<AvatarCmptProps, AvatarState> {
static defaultProps: Partial<AvatarCmptProps> = {
shape: 'circle',
size: 'default',
fit: 'cover',
gap: 4
};
state: AvatarState = {
scale: 1,
hasImg: true
};
avatarChildrenRef: React.RefObject<HTMLElement>;
avatarRef: React.RefObject<HTMLElement>;
constructor(props: AvatarCmptProps) {
super(props);
this.avatarChildrenRef = React.createRef();
this.avatarRef = React.createRef();
this.handleImageLoadError = this.handleImageLoadError.bind(this);
}
componentDidMount() {
this.setScaleByGap();
}
componentDidUpdate(prevProps: AvatarCmptProps, prevState: AvatarState) {
const {src, gap, text, children} = this.props;
const {hasImg} = this.state;
if (prevProps.src !== src) {
this.setState({
hasImg: !!src
});
}
if ((prevState.hasImg && !hasImg)
|| (prevProps.text !== text)
|| (prevProps.children !== children)
|| (prevProps.gap !== gap)) {
this.setScaleByGap();
}
}
handleImageLoadError(event: React.SyntheticEvent<HTMLImageElement, Event>) {
const {onError} = this.props;
this.setState({
hasImg: onError ? !onError(event) : false
});
}
setScaleByGap() {
const {gap = 4} = this.props;
if (!this.avatarChildrenRef.current || !this.avatarRef.current) {
return;
}
const childrenWidth = this.avatarChildrenRef.current.offsetWidth;
const nodeWidth = this.avatarRef.current.offsetWidth;
if (childrenWidth && nodeWidth) {
if (gap * 2 < nodeWidth) {
const diff = nodeWidth - gap * 2;
this.setState({
scale: diff < childrenWidth ? diff / childrenWidth : 1
});
}
}
};
render() {
let {
style = {},
className,
shape,
size,
src,
icon,
alt,
draggable,
crossOrigin,
fit,
text,
children,
classnames: cx
} = this.props;
const {scale, hasImg} = this.state;
const isImgRender = React.isValidElement(src);
const isIconRender = React.isValidElement(icon);
let childrenRender;
let sizeStyle = {};
let sizeClass = '';
if (typeof size === 'number') {
sizeStyle = {
height: size,
width: size,
lineHeight: size + 'px'
};
}
else if (typeof size === 'string') {
sizeClass = size === 'large'
? `${prefix}lg`
: size === 'small' ? `${prefix}sm` : '';
}
const scaleX = `scale(${scale}) translateX(-50%)`;
const scaleStyle = {
msTransform: scaleX,
WebkitTransform: scaleX,
transform: scaleX
};
if (typeof src === 'string' && hasImg) {
const imgStyle = fit ? {objectFit: fit} : {};
childrenRender = (
<img
style={imgStyle}
src={src}
alt={alt}
draggable={draggable}
onError={this.handleImageLoadError}
crossOrigin={crossOrigin}
/>
);
}
else if (isImgRender) {
childrenRender = src;
}
else if (typeof text === 'string' || typeof text === 'number') {
childrenRender = (
<span
className={cx(childPrefix)}
ref={this.avatarChildrenRef}
style={scaleStyle}>
{text}
</span>
);
}
else if (typeof icon === 'string') {
childrenRender = (<i className={icon} />);
}
else if (isIconRender) {
childrenRender = icon;
}
else {
childrenRender = (
<span
className={cx(childPrefix)}
ref={this.avatarChildrenRef}
style={scaleStyle}>
{children}
</span>
);
}
return (
<span
className={cx(`Avatar`, className, prefix + shape, sizeClass)}
style={{...sizeStyle, ...style}}
ref={this.avatarRef}>
{childrenRender}
</span>
);
}
}
export default themeable(Avatar);

View File

@ -8,6 +8,7 @@ import NotFound from './404';
import {default as Alert, alert, confirm, prompt} from './Alert';
import {default as ContextMenu, openContextMenus} from './ContextMenu';
import AsideNav from './AsideNav';
import Avatar from './Avatar';
import Button from './Button';
import Checkbox from './Checkbox';
import Checkboxes from './Selection';
@ -60,6 +61,7 @@ export {
NotFound,
Alert as AlertComponent,
alert,
Avatar,
confirm,
prompt,
ContextMenu,

View File

@ -3,64 +3,15 @@
*/
import React from 'react';
import {Renderer, RendererProps} from '../factory';
import {
BaseSchema,
SchemaClassName,
SchemaIcon,
SchemaUrlPath
} from '../Schema';
import Avatar from '../components/Avatar';
import {BadgeSchema, withBadge} from '../components/Badge';
import {
isPureVariable,
resolveVariable,
resolveVariableAndFilter
} from '../utils/tpl-builtin';
import {BaseSchema, SchemaClassName} from '../Schema';
import {isPureVariable, resolveVariableAndFilter} from '../utils/tpl-builtin';
/**
* Avatar
* https://baidu.gitee.io/amis/docs/components/avatar
*/
export interface AvatarSchema extends BaseSchema {
/**
*
*/
// 指定类型
type: 'avatar';
/**
*
*/
size?: number;
/**
*
*/
shape?: 'circle' | 'square';
/**
*
*/
icon?: string;
/**
*
*/
text?: string;
/**
*
*/
src?: string;
/**
*
*/
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
/**
*
*/
alt?: string;
/**
*
*/
@ -77,67 +28,125 @@ export interface AvatarSchema extends BaseSchema {
*
*/
badge?: BadgeSchema;
/**
*
*/
src?: string;
/**
*
*/
icon?: string;
/**
*
*/
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
/**
*
*/
shape?: 'circle' | 'square' | 'rounded';
/**
*
*/
size?: number | 'small' | 'default' | 'large';
/**
*
*/
text?: string;
/**
*
*/
gap?: number;
/**
*
*/
alt?: string;
/**
*
*/
draggable?: boolean;
/**
* CORS属性
*/
crossOrigin: 'anonymous' | 'use-credentials' | '';
/**
*
*/
onError?: string
}
export interface AvatarProps
extends RendererProps,
Omit<AvatarSchema, 'type' | 'className'> {}
export interface AvatarProps extends RendererProps, Omit<AvatarSchema, 'type' | 'className'> {}
export class AvatarField extends React.Component<AvatarProps> {
export class AvatarField extends React.Component<AvatarProps, object> {
render() {
let {
style = {},
className,
icon = 'fa fa-user',
text,
src,
fit = 'cover',
data,
shape = 'circle',
size = 40,
style,
classnames: cx,
props
src,
icon = 'fa fa-user',
fit,
shape,
size,
text,
gap,
alt,
draggable,
crossOrigin,
onError,
data
} = this.props;
let sizeStyle = {
height: size,
width: size,
lineHeight: size + 'px'
};
let errHandler = () => false;
if (isPureVariable(text)) {
text = resolveVariableAndFilter(text, data);
if (typeof onError === 'string') {
try {
errHandler = new Function('event', onError) as () => boolean;
} catch (e) {
console.warn(onError, e);
}
}
if (isPureVariable(src)) {
src = resolveVariableAndFilter(src, data, '| raw');
}
if (isPureVariable(text)) {
text = resolveVariableAndFilter(text, data);
}
if (isPureVariable(icon)) {
icon = resolveVariableAndFilter(icon, data);
}
let avatar = <i className={icon} />;
if (text) {
if (text.length > 2) {
text = text.substring(0, 2).toUpperCase();
}
avatar = <span>{text}</span>;
}
if (src) {
avatar = <img src={src} style={{objectFit: fit}} />;
}
return (
<div
className={cx('Avatar', className, `Avatar--${shape}`)}
style={{...sizeStyle, ...style}}
{...props}
>
{avatar}
</div>
<Avatar
style={style}
className={className}
classnames={cx}
src={src}
icon={icon}
fit={fit}
shape={shape}
size={size}
text={text}
gap={gap}
alt={alt}
draggable={draggable}
crossOrigin={crossOrigin}
onError={errHandler}
/>
);
}
}