Based on react18+hooks universal global mobile phone pop-up component

RcPop customizes the msg/alert/dialog/model/toast pop-up component based on react18.x hooks

Based on react18 hookthe development of global universal mobile elastic layer components. Integrated pop-up window effects such as msg/alert/dialog/toast and android/ios . Supports **20+** parameters and component + functional calling methods.

Insert image description here

Introduce pop-up component

Introduce components into pages that require pop-up windows.

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

Insert image description here
RcPop supports two calling methods: label/function.

<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
                    })
                }
            }
        ]
    })
}

Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here

rcpop also supports custom content, and content wrapped in tags has priority over the content parameter.

Insert image description here

<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()
                }
            }
        ]
    })
}

Insert image description here

Configuration parameters

// 弹窗默认参数
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
}

component template

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>
    )
}

Complete pop-up code block

/**
 * @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())
})

Mount the pop-up window to the body to implement function calls.

/**
 * 函数式弹窗组件
 * 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
}

Okay, the above is some sharing of react18 hooks custom pop-up components.

Finally, two latest example projects are attached

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

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

Guess you like

Origin blog.csdn.net/yanxinyun1990/article/details/132019347