作者:Stony Chen

先看看最终的成果,如下图:

组件成果以及款式咱们参考了https://v4.mui.com/components...,但组件属性和Material-UI有些不一样。另外咱们还多做了一个Avatar组件盘绕的涟漪成果。

须要实现的组件以及属性列表:

Step 1 咱们用上面的命令初始化一个React我的项目

应用 yarn 创立React我的项目

$ yarn create react-app avatar-demo --template typescript

而后咱们进入我的项目并启动

$ cd avatar-demo$ yarn start

Step 2 高级配置

装置 craco , craco-less , react-icons 和 classnames

$ yarn add @craco/craco$ yarn add craco-less$ yarn add react-icons$ yarn add classnames

批改 package.json 里的 scripts 属性

/* package.json */"scripts": {-   "start": "react-scripts start",-   "build": "react-scripts build",-   "test": "react-scripts test",-   "eject": "react-scripts eject"+   "start": "craco start",+   "build": "craco build",+   "test": "craco test",+   "eject": "craco eject"}

根目录下新增 craco.config.js

const CracoLessPlugin = require("craco-less")module.exports = {    plugins: [        {            plugin:CracoLessPlugin,            options:{                lessLoaderOptions:{                    lessOptions:{                        modifyVars :{                            // "@prmary-color":"#0073d5"                        },                        javascriptEnabled:true                    }                }            }        }    ]}

而后重新启动我的项目,能够看到我的项目运行如下图

Step 3 文件目录筹备

筹备示例头像文件,并搁置在public目录下

筹备如下代码文件构造,此处咱们没有应用less的module构造

Step 4 创立Avatar组件

简略粗犷一点,说什么都不如间接上代码,此处咱们应用ripple属性作为开关来管制外围涟漪成果,以及能够配置涟漪成果色彩。另外size应用number,能够随便批改,不再应用large, small之类,更加灵便

\src\components\avatar\index.tsx

import classNames from  "classnames"import "./index.less"export type AvatarProps = {    alt?: string    src?: string    size?: number    icon?: any    ripple?: boolean    rippleColor?: string    [key:string]: any}const Avatar = (props:AvatarProps) => {    const { children, alt, src, size, icon, ripple, rippleColor, className, style, ...others } = props    const classes = classNames({        avatar: true,        [className!]: className    })    const finalSize = size || 40    const finalStyle = {        ...style,        width: `${finalSize}px`,        height: `${finalSize}px`    }    const rippleStyle = {        border: `2px solid ${rippleColor}`,        width: `${finalSize}px`,        height: `${finalSize}px`    }    if(ripple){        return (            <div className="ripple-container" style={{width:`${finalSize}px`,height:`${finalSize}px`}}>                <div className={classes} style={finalStyle} {...others}>                    {src ? <img alt={alt} src={src}></img>:children}                    {icon}                </div>                <span className="ripple" style={rippleStyle}></span>            </div>        )    } else {        return (            <div className={classes} style={finalStyle} {...others}>                {src ? <img alt={alt} src={src}></img>:children}                {icon}            </div>        )    }}export default Avatar

以及款式, 此处咱们应用了动画成果来做涟漪成果,并参考了Material UI的成果

\src\components\avatar\index.less

.avatar {    height:40px;    width: 40px;    border-radius: 50%;    font-size: 1.25rem;    line-height: 1;    overflow: hidden;    display: flex;    align-items: center;    justify-content: center;    color: rgb(18, 18, 18);    background: rgb(117, 117, 117);    z-index: 1;    img {        width: 100%;        height: 100%;        text-align: center;        object-fit: cover;        color: transparent;        text-indent: 10000px;    }}@-webkit-keyframes rippleFrames {    0% {        -webkit-transform: scale(.8);        -moz-transform: scale(.8);        -ms-transform: scale(.8);        transform: scale(.8);        opacity: 1;    }    100% {        -webkit-transform: scale(1.4);        -moz-transform: scale(1.4);        -ms-transform: scale(1.4);        transform: scale(1.4);        opacity: 0;    }}@keyframes rippleFrames {    0% {        -webkit-transform: scale(.8);        -moz-transform: scale(.8);        -ms-transform: scale(.8);        transform: scale(.8);        opacity: 1;    }    100% {        -webkit-transform: scale(1.4);        -moz-transform: scale(1.4);        -ms-transform: scale(1.4);        transform: scale(1.4);        opacity: 0;    }}.ripple-container {    position: relative;    border-radius: 50%;    width: 40px;    height: 40px;    box-sizing: border-box;    .avatar {        position: absolute;    }    .ripple {        position: absolute;        top: 0;        left: 0;        width: 100%;        height: 100%;        border-radius: 50%;        animation: 1.2s ease-in-out 0s infinite normal none running rippleFrames;        border: 2px solid red;        z-index: 0;        box-sizing: border-box;    }}

Step 5 创立AvatarGroup组件

代码如下,此处须留神如果total大于以后的子组件,咱们须要再多创立一个Avatar组件显示 +N 的成果, 并且大小须要和后面的Avatar大小统一

\src\components\avatar-group\index.tsx

import classNames from  "classnames"import Avatar from "../avatar"import "./index.less"export type AvatarGroupProps = {    total?: number    [key:string]: any}const AvatarGroup = (props:AvatarGroupProps) => {    const { children, total, className, ...others } = props    const classes = classNames({        "avatar-group": true,        [className!]: className    })    let finalSize = 40    if(children && children.type && children.type.name === 'Avatar') {        finalSize = children.props.size || finalSize    } else if (children && children.length > 0){        if(children[0].type && children[0].type.name === 'Avatar'){            finalSize = children[0].props.size || finalSize        }    }    return (        <div className={classes} {...others}>            {children}            {total && total - children.length ? <Avatar size={finalSize} style={{fontSize:"0.85rem"}}>{`+${total - children.length}`}</Avatar> : null}        </div>    )}export default AvatarGroup

以及款式,AvatarGroup组件款式绝对就比较简单一点

\src\components\avatar-group\index.less

.avatar-group {    display: flex;    >* {        margin-left: -8px;        border: 1px solid #fff;        box-sizing: border-box;    }    > :first-child {        margin-left: 0;            }}

Step 6 创立Badge组件

代码如下,同样咱们还是用ripple属性来管制Badge组件的涟漪成果。另外这里留神的是地位,咱们裸露了left,top, 然而如同在不同的电脑和浏览器,地位仿佛有点点偏差

\src\components\badge\index.tsx

import classNames from  "classnames"import React from "react"import "./index.less"export type BadgeProps = {    ripple?: boolean    size?: number    color?: string    left?: string    top?: string    content?: React.ReactDOM | string | number    [key:string]: any}const Badge = (props:BadgeProps) => {    const { children, size, color, ripple, top, left, content, className, ...others } = props    const classes = classNames({        "badge-container": true,        ripple: ripple,        [className!]: className    })    let finalSize = size || 8    let location = { left, top}    if(!top) {        location.top ="30px"    }    if(!left) {        location.left ="30px"    }        const style = {        ...location,        background: color,        color: ripple ? color : "#fff",        width: content ? "auto" : finalSize,        height: finalSize,        padding: content ? "0 3px" : 0,        borderRadius: Math.floor(finalSize! / 2),        fontSize: finalSize! - 2 > 12 ? 12 : finalSize! - 2,    }    return (        <div className={classes} {...others}>            {children}            <span className="badge" style={style}>{content}</span>        </div>    )}export default Badge

以及款式, 用了和后面相似的涟漪成果

\src\components\badge\index.less

@-webkit-keyframes badgeRippleFrames {    0% {        -webkit-transform: scale(.8);        -moz-transform: scale(.8);        -ms-transform: scale(.8);        transform: scale(.8);        opacity: 1;    }    100% {        -webkit-transform: scale(2.4);        -moz-transform: scale(2.4);        -ms-transform: scale(2.4);        transform: scale(2.4);        opacity: 0;    }}@keyframes badgeRippleFrames {    0% {        -webkit-transform: scale(.8);        -moz-transform: scale(.8);        -ms-transform: scale(.8);        transform: scale(.8);        opacity: 1;    }    100% {        -webkit-transform: scale(2.4);        -moz-transform: scale(2.4);        -ms-transform: scale(2.4);        transform: scale(2.4);        opacity: 0;    }}.badge-container {    position: relative;    .badge {        position: absolute;        width: 8px;        height: 8px;        background: rgb(68, 183, 0);        color: rgb(68, 183, 0);        box-shadow: #fff 0 0 0 2px;        z-index: 1;        box-sizing: border-box;        border-radius: 50%;        line-height: 1;        display: flex;        align-items: center;        justify-content: center;    }    &.ripple {        .badge {            &::after {                position: absolute;                top: 0;                left: 0;                width: 100%;                height: 100%;                border-radius: 50%;                border: 1px solid currentColor;                animation: 1.2s ease-in-out 0s infinite normal none running badgeRippleFrames;                content: '';                box-sizing: border-box;            }        }    }}

Step 7 最初一步,批改App.tsx和App.less文件,展现demo

代码如下,咱们一排一排地展现不同类型的成果

\src\App.tsx

import logo from './logo.svg';import Avatar from './components/avatar';import Badge from './components/badge';import AvatarGroup from './components/avatar-group';import { MdAirplay, MdAlarm, MdShare } from 'react-icons/md'import './App.less';function App() {  return (    <div className="App">      <div className='row'>        <Avatar alt="Peter Pan" ripple rippleColor='#44b700' src="samples/1.jpg"></Avatar>        <Avatar alt="Peter Pan" ripple src="/samples/2.jpg"></Avatar>        <Avatar alt="Peter Pan" ripple rippleColor='green' src="/samples/3.jpg"></Avatar>        <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        <Avatar alt="Peter Pan" src={logo}></Avatar>      </div>      <div className='row'>        <Badge>          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </Badge>        <Badge color="yellow">          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </Badge>        <Badge color="grey">          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </Badge>        <Badge color="red" size={10} ripple>          <Avatar alt="Peter Pan" src="samples/1.jpg"></Avatar>        </Badge>        <Badge color="green" size={10} ripple>          <Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>        </Badge>        <Badge color="blue" size={10} ripple top="2px">          <Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>        </Badge>        <Badge ripple>          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </Badge>        <Badge content={20} size={12} color="red">          <Avatar alt="Peter Pan" src={logo}></Avatar>        </Badge>      </div>      <div className='row'>        <Avatar alt="Peter Pan" size={24} src="/samples/1.jpg"></Avatar>        <Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>        <Avatar alt="Peter Pan" size={56} src="/samples/3.jpg"></Avatar>        <Avatar alt="Peter Pan" size={80} src="/samples/4.jpg"></Avatar>      </div>      <div className='row'>        <Avatar>K</Avatar>        <Avatar style={{background:"yellow"}}>S</Avatar>        <Avatar style={{background:"blue"}}>J</Avatar>        <Avatar style={{background:"red"}}>N</Avatar>        <Avatar icon={<MdAlarm/>}></Avatar>        <Avatar icon={<MdAirplay/>}></Avatar>        <Avatar icon={<MdShare/>}></Avatar>      </div>      <div className='row'>        <AvatarGroup total={20}>          <Avatar alt="Peter Pan" src="/samples/1.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </AvatarGroup>      </div>      <div className='row'>        <AvatarGroup total={20}>          <Avatar alt="Peter Pan" ripple rippleColor='red' src="/samples/1.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/2.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/3.jpg"></Avatar>          <Avatar alt="Peter Pan" src="/samples/4.jpg"></Avatar>        </AvatarGroup>      </div>    </div>  );}export default App;

以及款式

\src\App.less

.App {  text-align: center;  padding: 30px;}.row {  margin-bottom: 50px;  display: flex;  flex-wrap: wrap;  >* {    margin: 0 20px 20px 0;  }}

总结

通过以上的辛苦致力,咱们终于可能失去后面效果图所示的完满成绩。做这个demo有点匆忙,还有一些不尽意的中央,还待改善,以及做的过程中可能遇到的一些问题:

  1. 这里只是作为demo,并未应用module化的less款式。
  2. 我在想如果AvatarGroup蕴含一个带Badge的Avatar的话,款式可能会乱掉。不过我感觉不应该这么应用吧,AvatarGroup的子组件必须是Avatar组件,如果不是则能够加个报错提醒。
  3. 一开始发现自己不会查看Material UI上的涟漪@keyframes动画成果,起初我用safari浏览能够查到@keyframes。

参考:
https://v4.mui.com/components...