前言
在做前端项目时,需要对基础组件进行一层封装,以方便自定义修改组件库如antd的组件属性和方法。
button组件
组件代码:
import { Button } from 'antd';
import { FC, memo, useRef } from 'react';
import styles from './index.less';
type ButtonType = 'black' | 'red' | 'gray';
const buttonTypeClass: any = {
black: styles.blackButton, // 黑色主题
red: styles.redButton, // 红色主题
gray: styles.grayButton // 灰色主题
}
interface PropsType {
onClick?: Function
disabled?: boolean // 是否可点击
repeatClick?: boolean // 是否允许重复提交(快速多次点击)
repeatLimit?: number // 重复点击时长限制
style?: object
className?: string | object
$ref?: any
type?: ButtonType // 几种类型
icon?: any
htmlType?: any
children?: any
}
const defaultOptions: PropsType = {
disabled: false,
repeatClick: false,
repeatLimit: 1000
}
const _Button: FC<PropsType> = (props: PropsType) => {
const { htmlType, icon, disabled = false, style = {}, repeatClick = false, className, onClick, repeatLimit = 1000, type = 'black' } = props const timestampRef = useRef<number>(Date.now())
const trigger = (e: any) => {
e.stopPropagation()
// 两次点击间隔小于repeatLimit视为重复点击
if (disabled || (Date.now() - timestampRef.current < repeatLimit && !repeatClick)) return
timestampRef.current = Date.now()
onClick && onClick(e)
}
return (
<Button
className={`${styles.button} ${type ? buttonTypeClass[type] : ''} ${className || ''}`}
style={{...style}}
icon={icon}
htmlType={htmlType}
disabled={disabled}
onClick={trigger}
>
{props.children ? props.children : ''}
</Button>
)
}
export const CustomButton = memo(_Button)
export default memo(_Button)
组件样式:
.button {
text-align: center;
padding: 13px 20px;
border-radius: 4px;
height: auto;
line-height: 1;
font-size: 14px;
font-weight: 400;
border: none;
box-shadow: none;
cursor: pointer;
}
.blackButton {
color: white;
background-color: @col0A0F16;
&:hover {
color: @white !important;
background-color: @col3B3F45 !important;
}
&:disabled {
color: @white !important;
background-color: @colCECFD0 !important;
}
}
.redButton {
color: @white;
background-color: @colD2001C;
&:hover {
color: @white !important;
background-color: @colDB3349 !important;
}
&:disabled {
color: @white !important;
background-color: @colF6CBD2 !important;
}
}
.grayButton {
color: @col0A0F16;
background-color: @colF2F3F8;
&:hover {
color: @col0A0F16 !important;
background-color: @colE6E7F1 !important;
}
&:disabled {
color: rgba(10, 15, 22, 0.40) !important;
background-color: @colF7F8FB !important;
}
}
全局toast组件
和button组件不一样的是,toast组件要求在js或ts代码中可以直接调用,思路:动态创建容器包裹toast组件并渲染。
import React, { useState, memo, useEffect } from 'react'
import { CSSTransition } from 'react-transition-group'
import styles from './index.module.less'
import { modernRender } from '../instance'
type positionType = 'top' | 'middle' | 'bottom'
interface PropsType {
text?: string
duration?: number
position?: positionType
customClass?: unknown
iconClass?: string
visible?: boolean
$ref?: unknown
}
const defaultOptions: PropsType = {
text: '',
duration: 3000,
position: 'middle',
iconClass: '', // 通过class制定icon
visible: false // 默认隐藏
}
const _Toast: React.FC<PropsType> = (props: PropsType) => {
const { duration = 3000, position, customClass, iconClass, text } = props
return (
<CSSTransition in={visible} timeout={10} classNames="slide-up">
<div
className={`${styles.toastContainer} flex-center ${styles[position || '']} ${customClass || ''}`}
style={{
display: `${visible ? 'block' : 'none'}`,
padding: `${iconClass ? '0' : '10px'}`,
animation: `${styles.opcity} ${duration / 1000}s linear`,
WebkitAnimation: `${styles.opcity} ${duration / 1000}s linear`
}}
>
{iconClass && <i className={`${styles.iconClass}`}></i>}
<div className={`${styles.toastText}`}>{text}</div>
</div>
</CSSTransition>
)
}
_Toast.defaultProps = defaultOptions
export const Toast = memo(_Toast)
/** * 全局公共组件(单例) */
const getInstance = () => {
let instance
return () => {
if (instance) {
return instance
}
const div = document.createElement('div')
document.body.appendChild(div)
return (instance = (props: string | object) => {
modernRender(
React.createElement(Toast, { ...(typeof props === 'object' ? props : { text: props }), visible: true, key: Date.now() }),
div
)
})
}
}
export const AToast = getInstance()()
export default AToast
instance.tsx:
import React from 'react'
import { createRoot, Root } from 'react-dom/client'
// 容器
type Container = Element | DocumentFragment
type ExtendedRoot = Root & { unmountPromise?: Promise<boolean> }
// 容器对应 root 的 map, 因为 Container 类型问题所以使用 weakMap
const containerMap = new WeakMap<Container, ExtendedRoot>()
export async function modernRender(node: React.ReactElement, container: Container) {
let root = containerMap.get(container)
root = containerMap.has(container) ? (containerMap.get(container) as ExtendedRoot) : createRoot(container)
containerMap.set(container, root)
root.render(node)
}
export function modernUnmount(container: Container) {
// Delay to unmount to avoid React 18 sync warning
if (containerMap.has(container)) {
const root = containerMap.get(container) as ExtendedRoot
root?.unmount()
containerMap.delete(container)
}
}
组件样式:
.toastContainer {
position: fixed;
max-width: 90%;
min-width: 60%;
border-radius: 5px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
box-sizing: border-box;
text-align: center;
z-index: -1;
opacity: 0;
&.top {
top: 50px;
left: 50%;
transform: translate(-50%, 0);
}
&.middle {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.bottom {
bottom: 50px;
left: 50%;
transform: translate(-50%, 0);
}
.toastText {
font-size: 28px;
line-height: 40px;
text-align: center;
min-width: 500px;
}
}
@keyframes opcity {
0% {
opacity: 0;
z-index: 100;
}
2% {
opacity: 1;
}
98% {
opacity: 1;
}
100% {
opacity: 0;
z-index: -1;
}
}