共计 9343 个字符,预计需要花费 24 分钟才能阅读完成。
作者: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…