基于React18 Hooks实现手机端弹框组件RcPop
react-popup 基于react18+hook自定义多功能弹框组件。整合了msg/alert/dialog/toast及android/ios弹窗效果。支持20+自定义参数、组件式+函数式调用方式,全方位满足各种弹窗场景需求。

引入组件
在需要使用弹窗的页面引入组件。
// 引入自定义组件 import RcPop, { rcpop } from './components/rcpop'
RcPop支持 组件式+函数式 两种调用方式。

组件写法
<RcPop visible={visible} title="标题" content="弹窗内容" type="android" shadeClose="false" closeable :btns="[ {text: '取消', click: () => setVisible(false)}, {text: '确认', style: {color: '#09f'}, click: handleOK}, ]" @onOpen={handleOpen} @onClose={handleClose} /> <div>这里是自定义弹窗内容,优先级高于content内容。div> RcPop>
函数写法
function handlePopup() { rcpop({ title: '标题', content: ``, btns: [ { text: '取消', click: () => { // 关闭弹窗 rcpop.close() } }, { text: '确认', style: {color: '#09f'}, click: () => { rcpop({ type: 'toast', icon: 'loading', content: '加载中...', opacity: .2, time: 2 }) } } ] }) }函数式调用:rcpop({...})
- msg类型

- 自定义多按钮

rcpop({ title: '标题', content: ``, btns: [ { text: '稍后提示' }, { text: '取消', click: () => rcpop.close() }, { text: '立即更新', style: {color: '#09f'}, click: () => { // ... } } ] })显示自定义弹窗内容




- ios弹窗类型


- android弹窗类型



- 长按/右键菜单

- 自定义内容

<RcPop visible={visible} closeable xposition="top" content="这里是内容信息" btns={[ {text: '确认', style: {color: '#00d8ff'}, click: () => setVisible(false)}, ]} onOpen={()=> { console.log('弹窗开启...') }} onClose={()=>{ console.log('弹窗关闭...') setVisible(false) }} > <div style={{padding: '15px'}}> <img src={reactLogo} width="60" onClick={handleContextPopup} /> <h3 style={{color:'#f60', 'paddingTop':'10px'}}>当 content 和 自定义插槽 内容同时存在,只显示插槽内容。h3> div> RcPop>
function handleContextPopup(e) { let points = [e.clientX, e.clientY] rcpop({ type: 'contextmenu', follow: points, opacity: 0, btns: [ {text: '标记备注信息'}, { text: '删除', style: {color:'#f00'}, click: () => { rcpop.close() } } ] }) }
这次主打的是学习 React Hooks 开发自定义弹窗,之前也有开发过类似的弹层组件。
https://www.cnblogs.com/xiaoyan2017/p/14085142.html
https://www.cnblogs.com/xiaoyan2017/p/11589149.html

编码开发

在components目录下新建rcpop文件夹。
rcpop支持如下参数配置
// 弹窗默认参数 const defaultProps = { // 是否显示弹出层 visible: false, // 弹窗唯一性标识 id: null, // 弹窗标题 title: '', // 弹窗内容 content: '', // 弹窗类型(toast | footer | actionsheet | actionsheetPicker | ios | android | androidSheet | contextmenu) type: '', // toast图标(loading | success | fail) icon: '', // 是否显示遮罩层 shade: true, // 点击遮罩层关闭 shadeClose: true, // 遮罩透明度 opacity: '', // 自定义遮罩层样式 overlayStyle: {}, // 是否圆角 round: false, // 是否显示关闭图标 closeable: false, // 关闭图标位置(left | right | top | bottom) closePosition: 'right', // 关闭图标颜色 closeColor: '', // 动画类型(scaleIn | fadeIn | footer | fadeInUp | fadeInDown) anim: 'scaleIn', // 弹窗出现位置(top | right | bottom | left) position: '', // 长按/右键弹窗(坐标点) follow: null, // 弹窗关闭时长,单位秒 time: 0, // 弹窗层级 zIndex: 2023, // 弹窗按钮组(text | style | disabled | click) btns: null, // 指定挂载的节点(仅对标签组件有效) // teleport = () => document.body, teleport: null, // 弹窗打开回调 onOpen: () => {}, // 弹窗关闭回调 onClose: () => {}, // 点击遮罩层回调 onClickOverlay: () => {}, // 自定义样式 customStyle: {}, // 类名 className: null, // 默认插槽内容 children: null }
弹窗组件模板
const renderNode = () => { return ( <div ref={ref} className={classNames('rc__popup', options.className, {'rc__popup-closed': closed})} id={options.id} style={{'display': !opened.current ? 'none' : undefined}}> {/* 遮罩层 */} { isTrue(options.shade) && <div className="rcpopup__overlay" onClick={handleShadeClick} style={{'opacity': options.opacity, 'zIndex': oIndex-1, ...options.overlayStyle}}>div> } {/* 窗体 */} <div className="rcpopup__wrap" style={{'zIndex': oIndex}}> <div ref={childRef} className={classNames( 'rcpopup__child', { [`anim-${options.anim}`]: options.anim, [`popupui__${options.type}`]: options.type, 'round': options.round }, options.position )} style={popStyles} > { options.title && <div className="rcpopup__title">{options.title}div> } { (options.type == 'toast' && options.icon) && <div className={classNames('rcpopup__toast', options.icon)} dangerouslySetInnerHTML={{__html: ToastIcon[options.icon]}}>div> } {/* 内容 */} { options.children ? <div className="rcpopup__content">{options.children}div> : options.content ? <div className="rcpopup__content" dangerouslySetInnerHTML={{__html: options.content}}>div> : null } {/* 按钮组 */} { options.btns && <div className="rcpopup__actions"> { options.btns.map((btn, index) => { return <span className={classNames('btn', {'btn-disabled': btn.disabled})} key={index} style={btn.style} dangerouslySetInnerHTML={{__html: btn.text}} onClick={e => handleActions(e, index)}>span> }) } div> } { isTrue(options.closeable) && <div className={classNames('rcpopup__xclose', options.closePosition)} style={{'color': options.closeColor}} onClick={close}>div> } div> div> div> ) }
完整代码块
/** * @title 基于react18 hooks自定义移动端弹窗组件 * @author YXY Q: 282310962 * @date 2023/07/25 */ import { useState, useEffect, createRef, useRef, forwardRef, useImperativeHandle } from 'react' import { createPortal } from 'react-dom' import { createRoot } from 'react-dom/client' // ... const RcPop = forwardRef((props, ref) => { const mergeProps = { ...defaultProps, ...props } const [options, setOptions] = useState(mergeProps) const [oIndex, setOIndex] = useState(options.zIndex) const [closed, setClosed] = useState(false) const [followStyle, setFollowStyle] = useState({ position: 'absolute', left: '-999px', top: '-999px' }) const opened = useRef(false) const childRef = useRef() const stopTimer = useRef(null) const popStyles = options.follow ? { ...followStyle, ...options.customStyle } : { ...options.customStyle } const isTrue = (str) => /^true$/i.test(str) const ToastIcon = { loading: '', success: '', error: '', warning: '', info: '', } /** * 开启弹窗 */ function open(params) { params && setOptions({ ...options, ...params }) if(options.type == 'toast') { options.time = options.time || 3 } if(opened.current) return opened.current = true setOIndex(++index + options.zIndex) options.onOpen?.() // 右键/长按菜单 if(options.follow) { setTimeout(() => { let rcpop = childRef.current let oW, oH, winW, winH, pos oW = rcpop.clientWidth oH = rcpop.clientHeight winW = window.innerWidth winH = window.innerHeight pos = getPos(options.follow[0], options.follow[1], oW, oH, winW, winH) setFollowStyle({ ...followStyle, left: pos[0], top: pos[1] }) }) } if(options.time) { clearTimeout(stopTimer.current) stopTimer.current = setTimeout(() => { close() }, options.time * 1000) } } /** * 关闭弹窗 */ function close() { if(!opened.current) return setClosed(true) setTimeout(() => { setClosed(false) opened.current = false options.onClose?.() clearTimeout(stopTimer.current) }, 200) } // 点击遮罩层 function handleShadeClick(e) { options.onClickOverlay?.(e) if(isTrue(options.shadeClose)) { close() } } // 点击按钮组 function handleActions(e, index) { let btn = options.btns[index] if(!btn.disabled) { btn?.click?.(e) } } // 抽离的React的classnames操作类 function classNames() { var hasOwn = {}.hasOwnProperty var classes = [] for (var i = 0; i < arguments.length; i++) { var arg = arguments[i] if (!arg) continue var argType = typeof arg if (argType === 'string' || argType === 'number') { classes.push(arg) } else if (Array.isArray(arg) && arg.length) { var inner = classNames.apply(null, arg) if (inner) { classes.push(inner) } } else if (argType === 'object') { for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) { classes.push(key) } } } } return classes.join(' ') } // 获取挂载节点 function getTeleport(getContainer) { const container = typeof getContainer == 'function' ? getContainer() : getContainer return container || document.body } // 设置挂载节点 function renderTeleport(getContainer, node) { if(getContainer) { const container = getTeleport(getContainer) return createPortal(node, container) } return node } // 获取弹窗坐标点 function getPos(x, y, ow, oh, winW, winH) { let l = (x + ow) > winW ? x - ow : x; let t = (y + oh) > winH ? y - oh : y; return [l, t]; } const renderNode = () => { return ({/* 遮罩层 */} { isTrue(options.shade) && } {/* 窗体 */}<div ref={childRef} className={classNames( 'rcpopup__child', { [`anim-${options.anim}`]: options.anim, [`popupui__${options.type}`]: options.type, 'round': options.round }, options.position )} style={popStyles} > { options.title &&{options.title}} { (options.type == 'toast' && options.icon) && } {/* 内容 */} {/*{ (options.children || options.content) &&{options.children || options.content}}*/} { options.children ?{options.children}: options.content ? : null } {/* 按钮组 */} { options.btns &&{ options.btns.map((btn, index) => { return handleActions(e, index)}> }) }} { isTrue(options.closeable) && }

