作者: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 有点匆忙,还有一些不尽意的中央,还待改善,以及做的过程中可能遇到的一些问题:
- 这里只是作为 demo,并未应用 module 化的 less 款式。
- 我在想如果 AvatarGroup 蕴含一个带 Badge 的 Avatar 的话,款式可能会乱掉。不过我感觉不应该这么应用吧,AvatarGroup 的子组件必须是 Avatar 组件,如果不是则能够加个报错提醒。
- 一开始发现自己不会查看 Material UI 上的涟漪 @keyframes 动画成果,起初我用 safari 浏览能够查到 @keyframes。
参考:
https://v4.mui.com/components…