优化
This commit is contained in:
59
client/packages/design/components/Avatar/combined.css
Normal file
59
client/packages/design/components/Avatar/combined.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.td-combined-avatar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.td-combined-avatar-2 > .line1 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: white;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.td-combined-avatar-3 > .line2 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 1px;
|
||||
background-color: white;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.td-combined-avatar-3 > .line1 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: white;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.td-combined-avatar-4 > .line2 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: white;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.td-combined-avatar-4 > .line1 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: white;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.td-combined-avatar__item {
|
||||
position: absolute;
|
||||
border-radius: 0;
|
||||
}
|
||||
106
client/packages/design/components/Avatar/combined.tsx
Normal file
106
client/packages/design/components/Avatar/combined.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Avatar, AvatarProps } from '.';
|
||||
import React from 'react';
|
||||
import _take from 'lodash/take';
|
||||
import { px2rem } from './utils';
|
||||
import './combined.css';
|
||||
|
||||
interface CombinedAvatarProps {
|
||||
shape?: 'circle' | 'square';
|
||||
size?: number;
|
||||
items: Pick<AvatarProps, 'name' | 'src'>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装式头像
|
||||
*/
|
||||
export const CombinedAvatar: React.FC<CombinedAvatarProps> = React.memo(
|
||||
(props) => {
|
||||
const { size = 32, shape = 'circle' } = props;
|
||||
const items = _take(props.items, 4);
|
||||
|
||||
const length = items.length;
|
||||
const getCellStyle = (i: number): React.CSSProperties => {
|
||||
if (length === 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (length === 2) {
|
||||
if (i === 0) {
|
||||
return {
|
||||
width: '50%',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
};
|
||||
}
|
||||
if (i === 1) {
|
||||
return {
|
||||
width: '50%',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (length === 3) {
|
||||
if (i === 0) {
|
||||
return {
|
||||
width: '50%',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
};
|
||||
}
|
||||
if (i === 1) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '100% 0 0' };
|
||||
}
|
||||
if (i === 2) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '100% 100% 0' };
|
||||
}
|
||||
}
|
||||
|
||||
if (length === 4) {
|
||||
if (i === 0) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '0 0 0' };
|
||||
}
|
||||
if (i === 1) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '100% 0 0' };
|
||||
}
|
||||
if (i === 2) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '0 100% 0' };
|
||||
}
|
||||
if (i === 3) {
|
||||
return { transform: 'scale(50%)', transformOrigin: '100% 100% 0' };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`td-combined-avatar td-combined-avatar-${length}`}
|
||||
style={{
|
||||
width: px2rem(size),
|
||||
height: px2rem(size),
|
||||
borderRadius: shape === 'circle' ? '50%' : 3,
|
||||
}}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<Avatar
|
||||
key={i}
|
||||
className="td-combined-avatar__item"
|
||||
style={getCellStyle(i)}
|
||||
size={size}
|
||||
{...item}
|
||||
/>
|
||||
))}
|
||||
|
||||
{items.length >= 2 && <div className="line1" />}
|
||||
{items.length >= 3 && <div className="line2" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
CombinedAvatar.displayName = 'CombinedAvatar';
|
||||
120
client/packages/design/components/Avatar/index.stories.tsx
Normal file
120
client/packages/design/components/Avatar/index.stories.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { Avatar } from '.';
|
||||
import { CombinedAvatar } from './combined';
|
||||
|
||||
export default {
|
||||
title: 'Tailchat/Avatar',
|
||||
component: Avatar,
|
||||
argTypes: {
|
||||
name: {
|
||||
description: '显示名称,用于无图片下的展示',
|
||||
},
|
||||
isOnline: {
|
||||
description: '是否在线, 可不传',
|
||||
},
|
||||
size: {
|
||||
description: '图标大小',
|
||||
type: 'number',
|
||||
},
|
||||
src: {
|
||||
description: '头像图片地址',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Avatar>;
|
||||
|
||||
const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />;
|
||||
|
||||
export const normal = Template.bind({});
|
||||
normal.args = {
|
||||
name: 'Anonymous',
|
||||
};
|
||||
|
||||
export const withSize = Template.bind({});
|
||||
withSize.args = {
|
||||
name: 'Anonymous',
|
||||
size: 48,
|
||||
};
|
||||
|
||||
export const withOnline = Template.bind({});
|
||||
withOnline.args = {
|
||||
name: 'Anonymous',
|
||||
isOnline: true,
|
||||
};
|
||||
|
||||
export const withImage = Template.bind({});
|
||||
withImage.args = {
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
};
|
||||
|
||||
const CombinedTemplate: ComponentStory<typeof CombinedAvatar> = (args) => (
|
||||
<div>
|
||||
<CombinedAvatar {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const combined1 = CombinedTemplate.bind({});
|
||||
combined1.args = {
|
||||
size: 48,
|
||||
items: [
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const combined2 = CombinedTemplate.bind({});
|
||||
combined2.args = {
|
||||
size: 48,
|
||||
items: [
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const combined3 = CombinedTemplate.bind({});
|
||||
combined3.args = {
|
||||
size: 48,
|
||||
items: [
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const combined4 = CombinedTemplate.bind({});
|
||||
combined4.args = {
|
||||
size: 48,
|
||||
items: [
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
src: 'http://dummyimage.com/50x50',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
},
|
||||
{
|
||||
name: 'Anonymous',
|
||||
},
|
||||
],
|
||||
};
|
||||
106
client/packages/design/components/Avatar/index.tsx
Normal file
106
client/packages/design/components/Avatar/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { ForwardRefExoticComponent, useMemo } from 'react';
|
||||
import { Avatar as AntdAvatar, Badge } from 'antd';
|
||||
import _head from 'lodash/head';
|
||||
import _upperCase from 'lodash/upperCase';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import _isEmpty from 'lodash/isEmpty';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
import _omit from 'lodash/omit';
|
||||
import type { AvatarProps as AntdAvatarProps } from 'antd/lib/avatar';
|
||||
import { getTextColorHex, px2rem } from './utils';
|
||||
import { isValidStr } from '../utils';
|
||||
import { imageUrlParser } from '../Image';
|
||||
|
||||
export { getTextColorHex };
|
||||
|
||||
export interface AvatarProps extends AntdAvatarProps {
|
||||
name?: string;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
const _Avatar: React.FC<AvatarProps> = React.memo((_props) => {
|
||||
const { isOnline, ...props } = _props;
|
||||
const src = isValidStr(props.src) ? imageUrlParser(props.src) : undefined;
|
||||
|
||||
const name = useMemo(() => _upperCase(_head(props.name)), [props.name]);
|
||||
|
||||
const color = useMemo(
|
||||
() =>
|
||||
// 如果src为空 且 icon为空 则给个固定颜色
|
||||
_isEmpty(src) && _isNil(props.icon)
|
||||
? getTextColorHex(props.name)
|
||||
: undefined,
|
||||
[src, props.icon, props.name]
|
||||
);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const _style: React.CSSProperties = {
|
||||
userSelect: 'none',
|
||||
...props.style,
|
||||
backgroundColor: color,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
if (_isNumber(props.size)) {
|
||||
// 为了支持rem统一管理宽度,将size转换为样式宽度(size类型上不支持rem单位)
|
||||
if (!_style.width) {
|
||||
_style.width = px2rem(props.size);
|
||||
}
|
||||
if (!_style.height) {
|
||||
_style.height = px2rem(props.size);
|
||||
}
|
||||
|
||||
if (typeof _style.fontSize === 'undefined') {
|
||||
// 如果props.size是数字且没有指定文字大小
|
||||
// 则自动增加fontSize大小
|
||||
_style.fontSize = px2rem(props.size * 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
return _style;
|
||||
}, [props.style, props.size, color]);
|
||||
|
||||
const inner = (
|
||||
<AntdAvatar
|
||||
draggable={false}
|
||||
{..._omit(props, ['size'])}
|
||||
src={src}
|
||||
style={style}
|
||||
>
|
||||
{name}
|
||||
</AntdAvatar>
|
||||
);
|
||||
|
||||
if (typeof isOnline === 'boolean') {
|
||||
const style = {
|
||||
bottom: 0,
|
||||
top: 'auto',
|
||||
};
|
||||
|
||||
if (isOnline === true) {
|
||||
return (
|
||||
<Badge dot={true} color="green" style={style}>
|
||||
{inner}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Badge dot={true} color="#999" style={style}>
|
||||
{inner}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return inner;
|
||||
});
|
||||
_Avatar.displayName = 'Avatar';
|
||||
|
||||
type CompoundedComponent = ForwardRefExoticComponent<AvatarProps> & {
|
||||
Group: typeof AntdAvatar.Group;
|
||||
};
|
||||
|
||||
export const Avatar: CompoundedComponent = _Avatar as any;
|
||||
Avatar.Group = AntdAvatar.Group;
|
||||
41
client/packages/design/components/Avatar/utils.ts
Normal file
41
client/packages/design/components/Avatar/utils.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import _isString from 'lodash/isString';
|
||||
import str2int from 'str2int';
|
||||
|
||||
const colors = [
|
||||
'#333333',
|
||||
'#2c3e50',
|
||||
'#8e44ad',
|
||||
'#2980b9',
|
||||
'#27ae60',
|
||||
'#16a085',
|
||||
'#f39c12',
|
||||
'#d35400',
|
||||
'#c0392b',
|
||||
'#3498db',
|
||||
'#9b59b6',
|
||||
'#2ecc71',
|
||||
'#1abc9c',
|
||||
'#f1c40f',
|
||||
'#e74c3c',
|
||||
'#e67e22',
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据文本内容返回一个内置色卡的颜色
|
||||
* @param text 文本
|
||||
*/
|
||||
export function getTextColorHex(text: unknown): string {
|
||||
if (!text || !_isString(text)) {
|
||||
return '#ffffff'; // 如果获取不到文本,则返回白色
|
||||
}
|
||||
|
||||
const id = str2int(text);
|
||||
return colors[id % colors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将像素转换为rem单位
|
||||
*/
|
||||
export function px2rem(size: number): string {
|
||||
return size * (1 / 16) + 'rem';
|
||||
}
|
||||
Reference in New Issue
Block a user