经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » React » 查看文章
react18 hooks自定义移动端Popup弹窗组件RcPop
来源:cnblogs  作者:xiaoyan2017  时间:2023/7/31 10:07:48  对本文有异议

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

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

引入组件

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

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

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

组件写法

  1. <RcPop
  2. visible={visible}
  3. title="标题"
  4. content="弹窗内容"
  5. type="android"
  6. shadeClose="false"
  7. closeable
  8. :btns="[
  9. {text: '取消', click: () => setVisible(false)},
  10. {text: '确认', style: {color: '#09f'}, click: handleOK},
  11. ]"
  12. @onOpen={handleOpen}
  13. @onClose={handleClose}
  14. />
  15. <div>这里是自定义弹窗内容,优先级高于content内容。</div>
  16. </RcPop>

函数写法

  1. function handlePopup() {
  2. rcpop({
  3. title: '标题',
  4. content: `<div style="padding:20px;">
  5. <p>函数式调用:<em style="color:#999;">rcpop({...})</em></p>
  6. </div>`,
  7. btns: [
  8. {
  9. text: '取消',
  10. click: () => {
  11. // 关闭弹窗
  12. rcpop.close()
  13. }
  14. },
  15. {
  16. text: '确认',
  17. style: {color: '#09f'},
  18. click: () => {
  19. rcpop({
  20. type: 'toast',
  21. icon: 'loading',
  22. content: '加载中...',
  23. opacity: .2,
  24. time: 2
  25. })
  26. }
  27. }
  28. ]
  29. })
  30. }
  • msg类型

  • 自定义多按钮

  1. rcpop({
  2. title: '标题',
  3. content: `<div style="color:#f90">
  4. <p>显示自定义弹窗内容</p>
  5. </div>`,
  6. btns: [
  7. { text: '稍后提示' },
  8. { text: '取消', click: () => rcpop.close() },
  9. {
  10. text: '立即更新',
  11. style: {color: '#09f'},
  12. click: () => {
  13. // ...
  14. }
  15. }
  16. ]
  17. })

  • ios弹窗类型

  • android弹窗类型

  • 长按/右键菜单

  • 自定义内容

  1. <RcPop
  2. visible={visible}
  3. closeable
  4. xposition="top"
  5. content="这里是内容信息"
  6. btns={[
  7. {text: '确认', style: {color: '#00d8ff'}, click: () => setVisible(false)},
  8. ]}
  9. onOpen={()=> {
  10. console.log('弹窗开启...')
  11. }}
  12. onClose={()=>{
  13. console.log('弹窗关闭...')
  14. setVisible(false)
  15. }}
  16. >
  17. <div style={{padding: '15px'}}>
  18. <img src={reactLogo} width="60" onClick={handleContextPopup} />
  19. <h3 style={{color:'#f60', 'paddingTop':'10px'}}>当 content 和 自定义插槽 内容同时存在,只显示插槽内容。</h3>
  20. </div>
  21. </RcPop>
  1. function handleContextPopup(e) {
  2. let points = [e.clientX, e.clientY]
  3. rcpop({
  4. type: 'contextmenu',
  5. follow: points,
  6. opacity: 0,
  7. btns: [
  8. {text: '标记备注信息'},
  9. {
  10. text: '删除',
  11. style: {color:'#f00'},
  12. click: () => {
  13. rcpop.close()
  14. }
  15. }
  16. ]
  17. })
  18. }

这次主打的是学习 React Hooks 开发自定义弹窗,之前也有开发过类似的弹层组件。

https://www.cnblogs.com/xiaoyan2017/p/14085142.html

https://www.cnblogs.com/xiaoyan2017/p/11589149.html

编码开发

在components目录下新建rcpop文件夹。

rcpop支持如下参数配置

  1. // 弹窗默认参数
  2. const defaultProps = {
  3. // 是否显示弹出层
  4. visible: false,
  5. // 弹窗唯一性标识
  6. id: null,
  7. // 弹窗标题
  8. title: '',
  9. // 弹窗内容
  10. content: '',
  11. // 弹窗类型(toast | footer | actionsheet | actionsheetPicker | ios | android | androidSheet | contextmenu)
  12. type: '',
  13. // toast图标(loading | success | fail)
  14. icon: '',
  15. // 是否显示遮罩层
  16. shade: true,
  17. // 点击遮罩层关闭
  18. shadeClose: true,
  19. // 遮罩透明度
  20. opacity: '',
  21. // 自定义遮罩层样式
  22. overlayStyle: {},
  23. // 是否圆角
  24. round: false,
  25. // 是否显示关闭图标
  26. closeable: false,
  27. // 关闭图标位置(left | right | top | bottom)
  28. closePosition: 'right',
  29. // 关闭图标颜色
  30. closeColor: '',
  31. // 动画类型(scaleIn | fadeIn | footer | fadeInUp | fadeInDown)
  32. anim: 'scaleIn',
  33. // 弹窗出现位置(top | right | bottom | left)
  34. position: '',
  35. // 长按/右键弹窗(坐标点)
  36. follow: null,
  37. // 弹窗关闭时长,单位秒
  38. time: 0,
  39. // 弹窗层级
  40. zIndex: 2023,
  41. // 弹窗按钮组(text | style | disabled | click)
  42. btns: null,
  43. // 指定挂载的节点(仅对标签组件有效)
  44. // teleport = () => document.body,
  45. teleport: null,
  46. // 弹窗打开回调
  47. onOpen: () => {},
  48. // 弹窗关闭回调
  49. onClose: () => {},
  50. // 点击遮罩层回调
  51. onClickOverlay: () => {},
  52. // 自定义样式
  53. customStyle: {},
  54. // 类名
  55. className: null,
  56. // 默认插槽内容
  57. children: null
  58. }

弹窗组件模板

  1. const renderNode = () => {
  2. return (
  3. <div ref={ref} className={classNames('rc__popup', options.className, {'rc__popup-closed': closed})} id={options.id} style={{'display': !opened.current ? 'none' : undefined}}>
  4. {/* 遮罩层 */}
  5. { isTrue(options.shade) && <div className="rcpopup__overlay" onClick={handleShadeClick} style={{'opacity': options.opacity, 'zIndex': oIndex-1, ...options.overlayStyle}}></div> }
  6. {/* 窗体 */}
  7. <div className="rcpopup__wrap" style={{'zIndex': oIndex}}>
  8. <div
  9. ref={childRef}
  10. className={classNames(
  11. 'rcpopup__child',
  12. {
  13. [`anim-${options.anim}`]: options.anim,
  14. [`popupui__${options.type}`]: options.type,
  15. 'round': options.round
  16. },
  17. options.position
  18. )}
  19. style={popStyles}
  20. >
  21. { options.title && <div className="rcpopup__title">{options.title}</div> }
  22. { (options.type == 'toast' && options.icon) && <div className={classNames('rcpopup__toast', options.icon)} dangerouslySetInnerHTML={{__html: ToastIcon[options.icon]}}></div> }
  23. {/* 内容 */}
  24. { options.children ? <div className="rcpopup__content">{options.children}</div> : options.content ? <div className="rcpopup__content" dangerouslySetInnerHTML={{__html: options.content}}></div> : null }
  25. {/* 按钮组 */}
  26. { options.btns &&
  27. <div className="rcpopup__actions">
  28. {
  29. options.btns.map((btn, index) => {
  30. return <span className={classNames('btn', {'btn-disabled': btn.disabled})} key={index} style={btn.style} dangerouslySetInnerHTML={{__html: btn.text}} onClick={e => handleActions(e, index)}></span>
  31. })
  32. }
  33. </div>
  34. }
  35. { isTrue(options.closeable) && <div className={classNames('rcpopup__xclose', options.closePosition)} style={{'color': options.closeColor}} onClick={close}></div> }
  36. </div>
  37. </div>
  38. </div>
  39. )
  40. }

完整代码块

  1. /**
  2. * @title 基于react18 hooks自定义移动端弹窗组件
  3. * @author YXY Q: 282310962
  4. * @date 2023/07/25
  5. */
  6. import { useState, useEffect, createRef, useRef, forwardRef, useImperativeHandle } from 'react'
  7. import { createPortal } from 'react-dom'
  8. import { createRoot } from 'react-dom/client'
  9.  
  10. // ...
  11. const RcPop = forwardRef((props, ref) => {
  12. const mergeProps = {
  13. ...defaultProps,
  14. ...props
  15. }
  16. const [options, setOptions] = useState(mergeProps)
  17. const [oIndex, setOIndex] = useState(options.zIndex)
  18. const [closed, setClosed] = useState(false)
  19. const [followStyle, setFollowStyle] = useState({
  20. position: 'absolute',
  21. left: '-999px',
  22. top: '-999px'
  23. })
  24. const opened = useRef(false)
  25. const childRef = useRef()
  26. const stopTimer = useRef(null)
  27. const popStyles = options.follow ? { ...followStyle, ...options.customStyle } : { ...options.customStyle }
  28. const isTrue = (str) => /^true$/i.test(str)
  29. const ToastIcon = {
  30. loading: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',
  31. 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>',
  32. 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>',
  33. 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>',
  34. 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>',
  35. }
  36. /**
  37. * 开启弹窗
  38. */
  39. function open(params) {
  40. params && setOptions({ ...options, ...params })
  41. if(options.type == 'toast') {
  42. options.time = options.time || 3
  43. }
  44. if(opened.current) return
  45. opened.current = true
  46. setOIndex(++index + options.zIndex)
  47. options.onOpen?.()
  48. // 右键/长按菜单
  49. if(options.follow) {
  50. setTimeout(() => {
  51. let rcpop = childRef.current
  52. let oW, oH, winW, winH, pos
  53. oW = rcpop.clientWidth
  54. oH = rcpop.clientHeight
  55. winW = window.innerWidth
  56. winH = window.innerHeight
  57. pos = getPos(options.follow[0], options.follow[1], oW, oH, winW, winH)
  58. setFollowStyle({
  59. ...followStyle,
  60. left: pos[0],
  61. top: pos[1]
  62. })
  63. })
  64. }
  65. if(options.time) {
  66. clearTimeout(stopTimer.current)
  67. stopTimer.current = setTimeout(() => {
  68. close()
  69. }, options.time * 1000)
  70. }
  71. }
  72. /**
  73. * 关闭弹窗
  74. */
  75. function close() {
  76. if(!opened.current) return
  77. setClosed(true)
  78. setTimeout(() => {
  79. setClosed(false)
  80. opened.current = false
  81. options.onClose?.()
  82. clearTimeout(stopTimer.current)
  83. }, 200)
  84. }
  85. // 点击遮罩层
  86. function handleShadeClick(e) {
  87. options.onClickOverlay?.(e)
  88. if(isTrue(options.shadeClose)) {
  89. close()
  90. }
  91. }
  92. // 点击按钮组
  93. function handleActions(e, index) {
  94. let btn = options.btns[index]
  95. if(!btn.disabled) {
  96. btn?.click?.(e)
  97. }
  98. }
  99. // 抽离的React的classnames操作类
  100. function classNames() {
  101. var hasOwn = {}.hasOwnProperty
  102. var classes = []
  103. for (var i = 0; i < arguments.length; i++) {
  104. var arg = arguments[i]
  105. if (!arg) continue
  106. var argType = typeof arg
  107. if (argType === 'string' || argType === 'number') {
  108. classes.push(arg)
  109. } else if (Array.isArray(arg) && arg.length) {
  110. var inner = classNames.apply(null, arg)
  111. if (inner) {
  112. classes.push(inner)
  113. }
  114. } else if (argType === 'object') {
  115. for (var key in arg) {
  116. if (hasOwn.call(arg, key) && arg[key]) {
  117. classes.push(key)
  118. }
  119. }
  120. }
  121. }
  122. return classes.join(' ')
  123. }
  124. // 获取挂载节点
  125. function getTeleport(getContainer) {
  126. const container = typeof getContainer == 'function' ? getContainer() : getContainer
  127. return container || document.body
  128. }
  129. // 设置挂载节点
  130. function renderTeleport(getContainer, node) {
  131. if(getContainer) {
  132. const container = getTeleport(getContainer)
  133. return createPortal(node, container)
  134. }
  135. return node
  136. }
  137. // 获取弹窗坐标点
  138. function getPos(x, y, ow, oh, winW, winH) {
  139. let l = (x + ow) > winW ? x - ow : x;
  140. let t = (y + oh) > winH ? y - oh : y;
  141. return [l, t];
  142. }
  143. const renderNode = () => {
  144. return (
  145. <div ref={ref} className={classNames('rc__popup', options.className, {'rc__popup-closed': closed})} id={options.id} style={{'display': !opened.current ? 'none' : undefined}}>
  146. {/* 遮罩层 */}
  147. { isTrue(options.shade) && <div className="rcpopup__overlay" onClick={handleShadeClick} style={{'opacity': options.opacity, 'zIndex': oIndex-1, ...options.overlayStyle}}></div> }
  148. {/* 窗体 */}
  149. <div className="rcpopup__wrap" style={{'zIndex': oIndex}}>
  150. <div
  151. ref={childRef}
  152. className={classNames(
  153. 'rcpopup__child',
  154. {
  155. [`anim-${options.anim}`]: options.anim,
  156. [`popupui__${options.type}`]: options.type,
  157. 'round': options.round
  158. },
  159. options.position
  160. )}
  161. style={popStyles}
  162. >
  163. { options.title && <div className="rcpopup__title">{options.title}</div> }
  164. { (options.type == 'toast' && options.icon) && <div className={classNames('rcpopup__toast', options.icon)} dangerouslySetInnerHTML={{__html: ToastIcon[options.icon]}}></div> }
  165. {/* 内容 */}
  166. {/*{ (options.children || options.content) && <div className="rcpopup__content">{options.children || options.content}</div> }*/}
  167. { options.children ? <div className="rcpopup__content">{options.children}</div> : options.content ? <div className="rcpopup__content" dangerouslySetInnerHTML={{__html: options.content}}></div> : null }
  168. {/* 按钮组 */}
  169. { options.btns &&
  170. <div className="rcpopup__actions">
  171. {
  172. options.btns.map((btn, index) => {
  173. return <span className={classNames('btn', {'btn-disabled': btn.disabled})} key={index} style={btn.style} dangerouslySetInnerHTML={{__html: btn.text}} onClick={e => handleActions(e, index)}></span>
  174. })
  175. }
  176. </div>
  177. }
  178. { isTrue(options.closeable) && <div className={classNames('rcpopup__xclose', options.closePosition)} style={{'color': options.closeColor}} onClick={close}></div> }
  179. </div>
  180. </div>
  181. </div>
  182. )
  183. }
  184. useEffect(() => {
  185. props.visible && open()
  186. !props.visible && close()
  187. }, [props.visible])
  188. // 暴露指定的方法给父组件调用
  189. useImperativeHandle(ref, () => ({
  190. open,
  191. close
  192. }))
  193. return renderTeleport(options.teleport || mergeProps.teleport, renderNode())
  194. })

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

  1. // 抽离的React的classnames操作类
  2. function classNames() {
  3. var hasOwn = {}.hasOwnProperty
  4. var classes = []
  5. for (var i = 0; i < arguments.length; i++) {
  6. var arg = arguments[i]
  7. if (!arg) continue
  8. var argType = typeof arg
  9. if (argType === 'string' || argType === 'number') {
  10. classes.push(arg)
  11. } else if (Array.isArray(arg) && arg.length) {
  12. var inner = classNames.apply(null, arg)
  13. if (inner) {
  14. classes.push(inner)
  15. }
  16. } else if (argType === 'object') {
  17. for (var key in arg) {
  18. if (hasOwn.call(arg, key) && arg[key]) {
  19. classes.push(key)
  20. }
  21. }
  22. }
  23. }
  24. return classes.join(' ')
  25. }

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

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

  1. /**
  2. * 函数式弹窗组件
  3. * rcpop({...}) | rcpop.close()
  4. */
  5. let popRef = createRef()
  6. function Popup(options = {}) {
  7. options.id = options.id || 'rcpopup-' + Math.floor(Math.random() * 10000)
  8. // 判断id唯一性
  9. let rnode = document.querySelector(`#${options.id}`)
  10. if(options.id && rnode) return
  11. const div = document.createElement('div')
  12. document.body.appendChild(div)
  13. const root = createRoot(div)
  14. root.render(
  15. <RcPop
  16. ref={popRef}
  17. visible={true}
  18. {...options}
  19. onClose={() => {
  20. let node = document.querySelector(`#${options.id}`)
  21. if(!node) return
  22. root.unmount()
  23. document.body.removeChild(div)
  24. }}
  25. />
  26. )
  27. return popRef
  28. }

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

 

原文链接:https://www.cnblogs.com/xiaoyan2017/p/17592708.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号