react.js自定义pc桌面端对话框|react仿layer弹窗

简介

RLayer.js 基于react.js开发的一款轻量级仿layer.js自定义弹窗组件。支持流畅拖拽、缩放、全屏及最大化等功能。拥有精美UI及简易API,让复杂的弹窗交互变得简单化。

rlayer支持Alert、Dialog、Message、Notification、ActionSheet、Toast、Popover、Popconfirm等多种弹窗类型。

开发灵感来自之前分享的一个Vue.js弹框组件。
Vue.js PC桌面端弹出框组件|vue自定义对话框组件

快速引入

// 引入弹窗组件RLayer
import rlayer from './components/rlayer'

通过rlayer({...})即可快速调用一个弹窗实例。

const showConfirm = () => {
    let $el = rlayer({
        title: '标题内容',
        content: "<div style='color:#0956f0;padding:30px;'>弹窗内容</div>",
        shadeClose: true,
        zIndex: 1111,
        lockScroll: true,
        resize: false,
        dragOut: true,
        btns: [
            {
                text: '关闭',
                click: () => {
                    $el.close()
                }
            },
            {
                text: '确定',
                style: {color: '#09f'},
                click: () => {
                    // ...
                }
            }
        ]
    })
}

一睹效果

提供了非常丰富的弹窗应用场景,满足前端多样化需求。








弹窗配置

rlayer.js提供了超过30+种参数任意搭配组合使用。

/**
 * 参数配置
 */
static defaultProps = {
    // 参数
    id: '',                       // {string} 控制弹层唯一标识,相同id共享一个实例
    title: '',                    // {string} 标题
    content: '',                  // {string|element} 内容(支持字符串或组件)
    type: '',                     // {string} 弹框类型(toast|footer|actionsheet|actionsheetPicker|android|ios|contextmenu|drawer|iframe)
    layerStyle: '',               // {object} 自定义弹框样式
    icon: '',                     // {string} Toast图标(loading|success|fail)
    shade: true,                  // {bool} 是否显示遮罩层
    shadeClose: true,             // {bool} 是否点击遮罩层关闭弹框
    lockScroll: true,             // {bool} 是否弹框显示时将body滚动锁定
    opacity: '',                  // {number|string} 遮罩层透明度
    xclose: true,                 // {bool} 是否显示关闭图标
    xposition: 'right',           // {string} 关闭图标位置(top|right|bottom|left)
    xcolor: '#333',               // {string} 关闭图标颜色
    anim: 'scaleIn',              // {string} 弹框动画(scaleIn|fadeIn|footer|fadeInUp|fadeInDown|fadeInLeft|fadeInRight)
    position: 'auto',             // {string|array} 弹框位置(auto|['150px','100px']|t|r|b|l|lt|rt|lb|rb)
    drawer: '',                   // {string} 抽屉弹框(top|right|bottom|left)
    follow: null,                 // {string|array} 跟随定位弹框(支持.xxx #xxx 或 [e.clientX,e.clientY])
    time: 0,                      // {number} 弹框自动关闭秒数(1|2|3...)
    zIndex: 8090,                 // {number} 弹框层叠
    topmost: false,               // {bool} 是否置顶当前弹框
    area: 'auto',                 // {string|array} 弹框宽高(auto|'250px'|['','200px']|['650px','300px'])
    maxWidth: 375,                // {number} 弹框最大宽度(只有当area:'auto'时设定才有效)
    maximize: false,              // {bool} 是否显示最大化按钮
    fullscreen: false,            // {bool} 是否全屏弹框
    fixed: true,                  // {bool} 是否固定弹框
    drag: '.rlayer__wrap-tit',    // {string|bool} 拖拽元素(可自定义拖动元素drag:'#xxx' 禁止拖拽drag:false)
    dragOut: false,               // {bool} 是否允许拖拽到浏览器外
    lockAxis: null,               // {string} 限制拖拽方向可选: v 垂直、h 水平,默认不限制
    resize: false,                // {bool} 是否允许拉伸弹框
    btns: null,                   // {array} 弹框按钮(参数:text|style|disabled|click)

    // 事件
    success: null,                // {func} 层弹出后回调
    end: null,                    // {func} 层销毁后回调
}

分享一些代码片段供大家学习参考。

render() {
    let opt = this.state

    return (
        <>
        <div className={domUtils.classNames('rui__layer', {'rui__layer-closed': opt.closeCls})} id={opt.id} style={{display: opt.opened?'block':'none'}}>
            {/* 遮罩 */}
            { opt.shade && <div className="rlayer__overlay" onClick={this.shadeClicked} style={{opacity: opt.opacity}}></div> }
            <div className={domUtils.classNames('rlayer__wrap', opt.anim&&'anim-'+opt.anim, opt.type&&'popui__'+opt.type)} style={{...opt.layerStyle}}>
            { opt.title && <div className='rlayer__wrap-tit' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
            <div className='rlayer__wrap-cntbox'>
                { opt.content ? 
                <>
                    {
                    opt.type == 'iframe' ? 
                    (
                        <iframe scrolling='auto' allowtransparency='true' frameBorder='0' src={opt.content}></iframe>
                    )
                    : 
                    (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? 
                    (
                        <div className='rlayer__wrap-cnt'>
                        { opt.icon && <i className={domUtils.classNames('rlayer-msg__icon', opt.icon)} dangerouslySetInnerHTML={{__html: opt.messageIcon[opt.icon]}}></i> }
                        <div className='rlayer-msg__group'>
                            { opt.title && <div className='rlayer-msg__title' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                            { typeof opt.content == 'string' ? 
                            <div className='rlayer-msg__content' dangerouslySetInnerHTML={{__html: opt.content}}></div>
                            :
                            <div className='rlayer-msg__content'>{opt.content}</div>
                            }
                        </div>
                        </div>
                    )
                    : 
                    (
                        typeof opt.content == 'string' ? 
                        (<div className='rlayer__wrap-cnt' dangerouslySetInnerHTML={{__html: opt.content}}></div>)
                        :
                        opt.content
                    )
                    }
                </>
                :
                null
                }
            </div>
            { opt.btns && <div className='rlayer__wrap-btns'>
                {
                    opt.btns.map((btn, index) => {
                        return <span className={domUtils.classNames('btn')} key={index} style={{...btn.style}} dangerouslySetInnerHTML={{__html: btn.text}}></span>
                    })
                }
                </div>
            }
            { opt.xclose && <span className={domUtils.classNames('rlayer__xclose')}></span> }
            { opt.maximize && <span className='rlayer__maximize'></span> }
            { opt.resize && <span className='rlayer__resize'></span> }
            </div>
            <div className='rlayer__dragfix'></div>
        </div>
        </>
    )
}

/**
 * @Desc     ReactJs|Next.js自定义弹窗组件RLayer
 * @Time     andy by 2020-12-04
 * @About    Q:282310962  wx:xy190310
 */
import React from 'react'
import ReactDOM from 'react-dom'

// 引入操作类
import domUtils from './utils/dom'

let $index = 0, $lockCount = 0, $timer = {}

class RLayerComponent extends React.Component {
    static defaultProps = {
        // ...
    }

    constructor(props) {
        super(props)
        this.state = {
            opened: false,
            closeCls: '',
            toastIcon: {
                // ...
            },
            messageIcon: {
                // ...
            },
            rlayerOpts: {},
            tipArrow: null,
        }

        this.closeTimer = null
    }

    componentDidMount() {
        window.addEventListener('resize', this.autopos, false)
    }
    componentWillUnmount() {
        window.removeEventListener('resize', this.autopos, false)
        clearTimeout(this.closeTimer)
    }

    /**
     * 打开弹框
     */
    open = (options) => {
        options.id = options.id || `rlayer-${domUtils.generateId()}`

        this.setState({
            ...this.props, ...options, opened: true,
        }, () => {
            const { success } = this.state
            typeof success === 'function' && success.call(this)

            this.auto()
            this.callback()
        })
    }

    /**
     * 关闭弹框
     */
    close = () => {
        const { opened, time, end, remove, rlayerOpts, action } = this.state
        if(!opened) return

        this.setState({ closeCls: true })
        clearTimeout(this.closeTimer)
        this.closeTimer = setTimeout(() => {
            this.setState({
                closeCls: false,
                opened: false,
            })
            if(rlayerOpts.lockScroll) {
                $lockCount--
                if(!$lockCount) {
                    document.body.style.paddingRight = ''
                    document.body.classList.remove('rc-overflow-hidden')
                }
            }
            if(time) {
                $index--
            }
            if(action == 'update') {
                document.body.style.paddingRight = ''
                document.body.classList.remove('rc-overflow-hidden')
            }
            rlayerOpts.isBodyOverflow && (document.body.style.overflow = '')
            remove()
            typeof end === 'function' && end.call(this)
        }, 200);
    }

    // 弹框位置
    auto = () => {
        // ...

        this.autopos()

        // 全屏弹框
        if(fullscreen) {
            this.full()
        }

        // 弹框拖拽|缩放
        this.move()
    }

    autopos = () => {
        const { opened, id, fixed, follow, position } = this.state
        if(!opened) return
        let oL, oT
        let dom = document.querySelector('#' + id)
        let rlayero = dom.querySelector('.rlayer__wrap')

        if(!fixed || follow) {
            rlayero.style.position = 'absolute'
        }

        let area = [domUtils.client('width'), domUtils.client('height'), rlayero.offsetWidth, rlayero.offsetHeight]

        oL = (area[0] - area[2]) / 2
        oT = (area[1] - area[3]) / 2

        if(follow) {
            this.offset()
        } else {
            typeof position === 'object' ? (
                oL = parseFloat(position[0]) || 0, oT = parseFloat(position[1]) || 0
            ) : (
                position == 't' ? oT = 0 : 
                position == 'r' ? oL = area[0] - area[2] : 
                position == 'b' ? oT = area[1] - area[3] : 
                position == 'l' ? oL = 0 : 
                position == 'lt' ? (oL = 0, oT = 0) : 
                position == 'rt' ? (oL = area[0] - area[2], oT = 0) : 
                position == 'lb' ? (oL = 0, oT = area[1] - area[3]) :
                position == 'rb' ? (oL = area[0] - area[2], oT = area[1] - area[3]) : 
                null
            )

            rlayero.style.left = parseFloat(fixed ? oL : domUtils.scroll('left') + oL) + 'px'
            rlayero.style.top = parseFloat(fixed ? oT : domUtils.scroll('top') + oT) + 'px'
        }
    }

    // 跟随元素定位
    offset = () => {
        const { id, follow } = this.state
        let oW, oH, pS
        let dom = document.querySelector('#' + id)
        let rlayero = dom.querySelector('.rlayer__wrap')

        oW = rlayero.offsetWidth
        oH = rlayero.offsetHeight
        pS = domUtils.getFollowRect(follow, oW, oH)

        rlayero.style.left = pS[0] + 'px'
        rlayero.style.top = pS[1] + 'px'
    }

    // 最大化弹框
    full = () => {
        // ...
    }

    // 恢复弹框
    restore = () => {
        // ...
    }

    // 拖拽|缩放弹框
    move = () => {
        // ...
    }

    // 事件处理
    callback = () => {
        const { time } = this.state
        // 倒计时关闭弹框
        if(time) {
            $index++
            // 防止重复计数
            if($timer[$index] != null) clearTimeout($timer[$index])
            $timer[$index] = setTimeout(() => {
                this.close()
            }, parseInt(time) * 1000);
        }
    }

    // 点击最大化按钮
    maximizeClicked = (e) => {
        let o = e.target
        if(o.classList.contains('maximized')) {
            // 恢复
            this.restore()
        } else {
            // 最大化
            this.full()
        }
    }

    // 点击遮罩层
    shadeClicked = () => {
        if(this.state.shadeClose) {
            this.close()
        }
    }

    // 按钮事件
    btnClicked = (index, e) => {
        let btn = this.state.btns[index]
        if(!btn.disabled) {
            typeof btn.click === 'function' && btn.click(e)
        }
    }

    render() {
        let opt = this.state
        return (
            <>
            <div className={domUtils.classNames('rui__layer', {'rui__layer-closed': opt.closeCls})} id={opt.id} style={{display: opt.opened?'block':'none'}}>
                { opt.shade && <div className="rlayer__overlay" onClick={this.shadeClicked} style={{opacity: opt.opacity}}></div> }
                <div className={domUtils.classNames('rlayer__wrap', opt.anim&&'anim-'+opt.anim, opt.type&&'popui__'+opt.type, opt.drawer&&'popui__drawer-'+opt.drawer, opt.xclose&&'rlayer-closable', opt.tipArrow)} style={{...opt.layerStyle}}>
                { opt.title && <div className='rlayer__wrap-tit' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                { opt.type == 'toast' && opt.icon ? <div className={domUtils.classNames('rlayer__toast-icon', 'rlayer__toast-'+opt.icon)} dangerouslySetInnerHTML={{__html: opt.toastIcon[opt.icon]}}></div> : null }
                <div className='rlayer__wrap-cntbox'>
                    { opt.content ? 
                    <>
                        {
                        opt.type == 'iframe' ? 
                        (
                            <iframe scrolling='auto' allowtransparency='true' frameBorder='0' src={opt.content}></iframe>
                        )
                        : 
                        (opt.type == 'message' || opt.type == 'notify' || opt.type == 'popover') ? 
                        (
                            <div className='rlayer__wrap-cnt'>
                            { opt.icon && <i className={domUtils.classNames('rlayer-msg__icon', opt.icon)} dangerouslySetInnerHTML={{__html: opt.messageIcon[opt.icon]}}></i> }
                            <div className='rlayer-msg__group'>
                                { opt.title && <div className='rlayer-msg__title' dangerouslySetInnerHTML={{__html: opt.title}}></div> }
                                { typeof opt.content == 'string' ? 
                                <div className='rlayer-msg__content' dangerouslySetInnerHTML={{__html: opt.content}}></div>
                                :
                                <div className='rlayer-msg__content'>{opt.content}</div>
                                }
                            </div>
                            </div>
                        )
                        : 
                        (
                            typeof opt.content == 'string' ? 
                            (<div className='rlayer__wrap-cnt' dangerouslySetInnerHTML={{__html: opt.content}}></div>)
                            :
                            opt.content
                        )
                        }
                    </>
                    :
                    null
                    }
                </div>
                {/* btns */}
                { opt.btns && <div className='rlayer__wrap-btns'>
                    {
                        opt.btns.map((btn, index) => {
                            return <span className={domUtils.classNames('btn')} key={index} style={{...btn.style}} dangerouslySetInnerHTML={{__html: btn.text}}></span>
                        })
                    }
                    </div>
                }
                { opt.xclose && <span className={domUtils.classNames('rlayer__xclose')} style={{color: opt.xcolor}}></span> }
                { opt.maximize && <span className='rlayer__maximize'></span> }
                { opt.resize && <span className='rlayer__resize'></span> }
                </div>
                <div className='rlayer__dragfix'></div>
            </div>
            </>
        )
    }
}

由于在react.js种动态操作class不是很方便,这里就抽离了classnames类库中的方法。

classNames: function() {
    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(' ');
}

调用上面的方法就可以很容易的动态操作类了。

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

rlayer.js默认标题区是可以拖拽的,不过也可以根据需要自定义拖拽手柄drag: '#xxxx',或者设置drag: false来禁止弹窗拖拽功能。设置dragOut: true还可以将弹窗拖拽到浏览器外部。
设置fullscreen: true默认显示全屏弹窗。maximize: true 开启最大化按钮。

好了,基于react.js开发桌面端弹窗的分享就到这里。希望大家能喜欢哈~~ ✍💪

最后附上两个示例
vue.js自定义桌面端虚拟滚动条组件
vue.js+Electron仿微信客户端聊天示例

本作品采用《CC 协议》,转载必须注明作者和本文链接
本文为原创文章,未经作者允许不得转载,欢迎大家一起交流 QQ(282310962) wx(xy190310)
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
文章
47
粉丝
45
喜欢
101
收藏
54
排名:367
访问:2.6 万
私信
所有博文
博客标签
react
1
angular
1
react仿微信
2
react实战开发
2
react+redux
1
react网页聊天
2
angular仿微信
1
angular聊天室
1
angular+node聊天
1
h5仿微信
1
仿微信语音
1
仿微信界面
1
RN弹窗
1
react-native自定义弹窗
1
react-native弹窗
1
React Native
1
reactNative仿微信
1
RN仿微信聊天
1
ReactNative朋友圈
1
uniapp仿微信
1
uniapp聊天室
2
uniapp聊天App
1
uni-app+vue实例
1
uni-app即时通讯
1
uniapp直播
1
uni-app仿抖音
1
uniapp仿陌陌
1
uni-app小视频
1
taro仿微信
1
taro聊天室
1
taro仿微信界面
1
taro+react聊天APP
1
taro即时通讯
1
electron+vue
1
electron-vue仿微信
1
electron聊天
1
electron实例
1
flutter实例
1
flutter仿微信
1
flutter+dart聊天
1
flutter聊天室
1
flutter聊天界面
1
vue自定义弹窗
1
vue全局对话框
1
vue长按弹出框
1
vue右键弹层
1
nuxt对话框
1
vue仿微信弹窗
1
vue仿探探
1
vue仿Tinder
1
vue卡片堆叠
1
vue翻牌滑动
1
nuxt仿探探
1
nuxt聊天室
1
nuxt仿微信
1
nuxt即时聊天
1
vue+nuxt聊天实例
1
nuxt.js朋友圈
1
vue.js自定义对话框
1
vue pc端弹窗
1
vue.js桌面端模态框
1
vue弹窗组件
1
vue仿layer
1
vue.js自定义滚动条
1
vue虚拟滚动条
1
vue美化滚动条
1
vue仿饿了么滚动条
1
Vue-Scrollbar
1
react.js弹窗示例
1
react桌面端弹框
1
react.js自定义对话框
1
react pc自定义弹窗
1
react全局弹框
1
vue3.0自定义组件
1
vue3弹框
1
vue3.x对话框
1
vue3.0弹窗
1
vue3.0实例
1
vue3.0聊天室
1
vue3.0仿微信
2
vue3聊天模板
1
vue3+vant3实战开发
1
vue3.x案例
1
vue3聊天实例
1
vue3.0仿QQ
1
vue3.x实战聊天
1
vue3网页聊天
1
vue3.0仿抖音app
1
vue3短视频
1
vue3.x小视频
1
vue3.0直播实例
1
vue3+vite2+vant3实战
1
vue3跨端开发
1
electron仿QQ
1
electron打包
1
electron聊天室
1
electron仿微信
1
electron仿抖音
1
electron短视频
1
electron直播
1
vite2+electron12
1
vite2+vue3.x+swiper
1
vue3+vite.js+vant3
1
vue3后台系统
1
Electron管理系统
1
vite2+electron后台
1
electron12权限管理
1
electron桌面端后台
1
vue3桌面管理
1
vite2+electron13
1
electron仿mac桌面
1
electron桌面管理
1
vite2桌面应用
1
uniapp短视频
1
uniapp仿抖音
1
uni-app直播
1
uniapp后台
1
uni-app+uview后台系统
1
svelte.js实战开发
1
svelte3仿微信
1
svelte+svelteKit聊天室
1
svelte聊天实例
2
svelte朋友圈
1
svelte.js仿微信
1
svelte.js网页聊天
1
svelte-ui-admin
1
svelte-admin后台管理
1
svelte管理系统
1
tauri桌面应用
1
tauri+vue3
1
vite3+tauri
1
tauri聊天程序
1
tauri仿微信
1
vue3后台管理
1
vite.js管理系统
1
vue3+vite4
1
vite4+pinia
1
vue3+pinia2
1
vue3-chatgpt
2
vite-chatgpt
1
chatgpt-mobile
1
electron-chatgpt
1
electron+vite4+vue3
1
electron25-vue3
1
chatgpt-vite
1
uni-chatgpt
1
uniapp+vue3+pinia
2
vite+uniapp
1
chatgpt-uniapp
1
tauri-admin
1
tauri+rust+vue3
1
tauri后台管理
1
tauri-vite
1
tauri+vue3桌面端后台
1
react18 hooks
2
react18+arco
2
react18+zustand
1
react18-webchat
1
react18-admin
1
react-arco-admin
1
react-vite-admin
1
react18后台管理
1
electron-mateos
1
electron27+react18
1
electron-react-macos
1
electron桌面os
1
react-macos
1
uniapp+vue3直播
1
flutter3-chat
2
flutter3仿微信
2
flutter3聊天
2
flutter3+dart3
1
flutter3桌面端开发
1
flutter3-douyin
1
flutter3仿抖音
1
flutter3短视频
1
flutter3直播
1
flutter3-macos
1
flutter3-osx
1
flutter3桌面os
1
flutter3仿macOS
1
社区赞助商