基于react18+hooks通用全局手机端弹框组件

chatgpt/2023/10/4 7:36:33

RcPop 基于react18.x hooks自定义msg/alert/dialog/model/toast弹框组件

基于react18 hook开发全局通用mobile弹层组件。整合了msg/alert/dialog/toast及android/ios等弹窗效果。支持**20+**参数、组件式+函数式两种调用方式。

在这里插入图片描述

引入弹窗组件

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

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

在这里插入图片描述
RcPop支持标签/函数两种调用方式。

<RcPopvisible={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})}}]})
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

rcpop还支持自定义内容,被标签包裹的内容优先级高于content参数。

在这里插入图片描述

<RcPopvisible={visible}closeablexposition="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()}}]})
}

在这里插入图片描述

配置参数

// 弹窗默认参数
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}}><divref={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) returnopened.current = truesetOIndex(++index + options.zIndex)options.onOpen?.()// 右键/长按菜单if(options.follow) {setTimeout(() => {let rcpop = childRef.currentlet oW, oH, winW, winH, posoW = rcpop.clientWidthoH = rcpop.clientHeightwinW = window.innerWidthwinH = window.innerHeightpos = 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) returnsetClosed(true)setTimeout(() => {setClosed(false)opened.current = falseoptions.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 = {}.hasOwnPropertyvar classes = []for (var i = 0; i < arguments.length; i++) {var arg = arguments[i]if (!arg) continuevar argType = typeof argif (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() : getContainerreturn 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}}><divref={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())
})

将弹窗挂载到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) returnconst div = document.createElement('div')document.body.appendChild(div)const root = createRoot(div)root.render(<RcPopref={popRef}visible={true}{...options}onClose={() => {let node = document.querySelector(`#${options.id}`)if(!node) returnroot.unmount()document.body.removeChild(div)}}/>)return popRef
}

好了,以上就是react18 hooks自定义弹窗组件的一些分享。

最后附上两个最新实例项目

https://blog.csdn.net/yanxinyun1990/article/details/131734743

https://blog.csdn.net/yanxinyun1990/article/details/131408928

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-5313002.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

Arcgis通过模型构建器计算几何坐标

模型 模型中&#xff0c;先添加字段&#xff0c;再计算字段 计算字段 模型的计算字段中&#xff0c;表达式是类似这样写的&#xff0c;其中Xmin表示X坐标&#xff0c;Ymin表示Y坐标 !Shape.extent.Xmin!类似计算面积 !shape.area!

stm32 mpu6050 cubemx DMP法读取角度

文章目录 前言一、相关文件二、cubemx配置三、代码变量初始化主循环 总结 前言 文件 记录使用dmp库来读取mpu6050的角度。 这是参考文件 参考1–主要参考 github参考 参考2 参考三 一、相关文件 相关文件在这里下载&#xff08;未填&#xff0c;不过可以在上面的git中下载&a…

超前端相关的学习网站和一些靠谱的小工具

CSS相关 1. CSS Battle - 在线比拼 CSS https://cssbattle.dev 在线比拼 CSS &#xff0c;一个挺有趣的竞争性游戏&#xff0c;一共有12个级别&#xff0c;需要你用 HTML和 CSS 100%还原它给出的页面&#xff0c;然后再尽量减少代码&#xff0c;你也可以查看全球的排行榜&am…

windows端口占用

1.查看当前端口被哪个进程占用了&#xff08;进入到CMD中&#xff09; netstat -ano|findstr "8990"输出结果为&#xff1a; TCP 127.0.0.1:8990 0.0.0.0:0 LISTENING 2700 我们发现8990端口被2700进程占用了 2.基于进程号找进程名称 tasklist|findstr "2700&qu…

kubernetes错误汇总

title: “kubernetes错误汇总” categories: - “技术” tags: - “Kubernetes” - “错误汇总” toc: false original: true draft: false 1、增加 master etcd 报错 1.1、错误描述 由于创建的k8s集群&#xff0c;其中有一个master节点初始化失败&#xff0c;先删除了这个节…

Android跨进程传大图思考及实现——附上原理分析

1.抛一个问题 这一天&#xff0c;法海想锻炼小青的定力&#xff0c;由于Bitmap也是一个Parcelable类型的数据&#xff0c;法海想通过Intent给小青传个特别大的图片 intent.putExtra("myBitmap",fhBitmap)如果“法海”(Activity)使用Intent去传递一个大的Bitmap给“…

并发编程 - CompletableFuture

文章目录 Pre概述FutureFuture的缺陷类继承关系功能概述API提交任务的相关API结果转换的相关APIthenApplyhandlethenRunthenAcceptthenAcceptBoththenCombinethenCompose 回调方法的相关API异常处理的相关API获取结果的相关API DEMO实战注意事项 Pre 每日一博 - Java 异步编程…

SpringBoot Redis 多数据源集成支持哨兵模式和Cluster集群模式

Redis 从入门到精通【应用篇】之SpringBoot Redis 多数据源集成支持哨兵模式Cluster集群模式、单机模式 文章目录 Redis 从入门到精通【应用篇】之SpringBoot Redis 多数据源集成支持哨兵模式Cluster集群模式、单机模式0.前言说明项目结构Pom 依赖 1. 配置1.1 通用配置&#xf…
推荐文章