react18 hooks自定义移动端Popup弹窗组件RcPop

  • react18 hooks自定义移动端Popup弹窗组件RcPop已关闭评论
  • 185 次浏览
  • A+
所属分类:Web前端
摘要

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

基于React18 Hooks实现手机端弹框组件RcPop

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

react18 hooks自定义移动端Popup弹窗组件RcPop

引入组件

在需要使用弹窗的页面引入组件。

// 引入自定义组件 import RcPop, { rcpop } from './components/rcpop'

RcPop支持 组件式+函数式 两种调用方式。

react18 hooks自定义移动端Popup弹窗组件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: `<div style="padding:20px;">             <p>函数式调用:<em style="color:#999;">rcpop({...})</em></p>         </div>`,         btns: [             {                 text: '取消',                 click: () => {                     // 关闭弹窗                     rcpop.close()                 }             },             {                 text: '确认',                 style: {color: '#09f'},                 click: () => {                     rcpop({                         type: 'toast',                         icon: 'loading',                         content: '加载中...',                         opacity: .2,                         time: 2                     })                 }             }         ]     }) }

  • msg类型

react18 hooks自定义移动端Popup弹窗组件RcPop

  • 自定义多按钮

react18 hooks自定义移动端Popup弹窗组件RcPop

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

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

  • ios弹窗类型

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

  • android弹窗类型

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

react18 hooks自定义移动端Popup弹窗组件RcPop

  • 长按/右键菜单

react18 hooks自定义移动端Popup弹窗组件RcPop

  • 自定义内容

react18 hooks自定义移动端Popup弹窗组件RcPop

<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

react18 hooks自定义移动端Popup弹窗组件RcPop

编码开发

react18 hooks自定义移动端Popup弹窗组件RcPop

在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: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',         success: '<svg viewBox="0 0 1024 1024"><path d="M512 85.333c235.648 0 426.667 191.019 426.667 426.667S747.648 938.667 512 938.667 85.333 747.648 85.333 512 276.352 85.333 512 85.333zm-74.965 550.4l-90.582-90.581a42.667 42.667 0 1 0-60.33 60.33l120.704 120.705a42.667 42.667 0 0 0 60.33 0L768.811 424.49a42.667 42.667 0 1 0-60.288-60.331L436.992 635.648z" /></svg>',         error: '<svg viewBox="0 0 1024 1024"><path d="M512 85.333C276.352 85.333 85.333 276.352 85.333 512S276.352 938.667 512 938.667 938.667 747.648 938.667 512 747.648 85.333 512 85.333zm128.427 606.72l-129.75-129.749-129.066 129.024a35.968 35.968 0 1 1-50.902-50.901L459.733 511.36 329.301 380.928a35.968 35.968 0 1 1 50.859-50.944l130.475 130.475 129.706-129.75a35.968 35.968 0 1 1 50.944 50.902L561.536 511.36l129.75 129.75a35.968 35.968 0 1 1-50.902 50.943z" /></svg>',         warning: '<svg viewBox="0 0 1024 1024"><path d="M512 941.12q-89.28 0-167.52-34.08t-136.32-92.16T116 678.08t-34.08-168T116 342.56t92.16-136.32 136.32-92.16T512 80t168 34.08 136.8 92.16 92.16 136.32 34.08 167.52-34.08 168-92.16 136.8T680 907.04t-168 34.08zM460.16 569.6q0 23.04 14.88 38.88T512 624.32t37.44-15.84 15.36-38.88V248q0-23.04-15.36-36.96T512 197.12t-37.44 14.4-15.36 37.44zM512 688.64q-27.84 0-47.52 19.68t-19.68 47.52 19.68 47.52T512 823.04t48-19.68 20.16-47.52T560 708.32t-48-19.68z"/></svg>',         info: '<svg viewBox="0 0 1024 1024"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm84 343.1l-87 301.4c-4.8 17.2-7.2 28.6-7.2 33.9 0 3.1 1.3 6 3.8 8.7s5.2 4 8.1 4c4.8 0 9.6-2.1 14.4-6.4 12.7-10.5 28-29.4 45.8-56.8l14.4 8.5c-42.7 74.4-88 111.6-136.1 111.6-18.4 0-33-5.2-43.9-15.5-10.9-10.3-16.3-23.4-16.3-39.2 0-10.5 2.4-23.7 7.2-39.9l58.9-202.7c5.7-19.5 8.5-34.2 8.5-44.1 0-6.2-2.7-11.7-8.1-16.5-5.4-4.8-12.7-7.2-22-7.2-4.2 0-9.3.1-15.3.4l5.5-17L570.4 407H596v.1zm17.8-88.7c-12.2 12.2-26.9 18.2-44.1 18.2-17 0-31.5-6.1-43.7-18.2-12.2-12.2-18.2-26.9-18.2-44.1s6-31.9 18-44.1c12-12.1 26.6-18.2 43.9-18.2 17.5 0 32.3 6.1 44.3 18.2 12 12.2 18 26.9 18 44.1s-6.1 31.9-18.2 44.1z"/></svg>',     }      /**      * 开启弹窗      */     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 (             <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 || options.content) && <div className="rcpopup__content">{options.children || options.content}</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>         )     }      useEffect(() => {         props.visible && open()         !props.visible && close()     }, [props.visible])      // 暴露指定的方法给父组件调用     useImperativeHandle(ref, () => ({         open,         close     }))          return renderTeleport(options.teleport || mergeProps.teleport, renderNode()) })

react动态设置className,于是抽离封装了classNames函数。

// 抽离的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(' ') }

非常方便的实现各种动态操作className类。

通过 createRoot 将弹窗组件挂载到body,实现函数式调用。

/**  * 函数式弹窗组件  * rcpop({...}) | rcpop.close()  */ let popRef = createRef() function Popup(options = {}) {     options.id = options.id || 'rcpopup-' + Math.floor(Math.random() * 10000)      // 判断id唯一性     let rnode = document.querySelector(`#${options.id}`)     if(options.id && rnode) return      const div = document.createElement('div')     document.body.appendChild(div)      const root = createRoot(div)     root.render(         <RcPop             ref={popRef}             visible={true}             {...options}             onClose={() => {                 let node = document.querySelector(`#${options.id}`)                 if(!node) return                 root.unmount()                 document.body.removeChild(div)             }}         />     )      return popRef }

OK,以上就是react18 hook实现自定义弹窗的一些小分享,希望对大家有所帮助~~?

react18 hooks自定义移动端Popup弹窗组件RcPop