This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View 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;
}

View 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';

View 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',
},
],
};

View 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;

View 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';
}