前言

RLayer.js 一个react构建的桌面PC端自定义Dialog组件内置30+参数配置、10+弹框类型、7+动画成果,提供极简的接口及清新的皮肤。领有顺滑般最大化/缩放/拖拽体验!


RLayer在设计及开发上参考了之前的VLayer弹出框组件。在成果上放弃一致性。
vlayer一款vue2.x开发的网页弹框组件,感兴趣的能够去看看这篇文章。
https://segmentfault.com/a/11...

引入应用

在须要用到弹出框的页面引入rlayer组件即可。

// 引入RLayerimport rlayer from './components/rlayer';

提供了十分繁难的调用写法 rlayer({...})

showConfirm = () => {    let $rlayer = rlayer({        title: '题目信息',        content: "<div style='color:#0070f3;padding:50px;'>显示弹窗内容。</div>",        shadeClose: true,        zIndex: 2021,        lockScroll: true,        resize: true,        dragOut: false,        btns: [            {                text: '勾销',                click: () => {                    $rlayer.close()                }            },            {                text: '确定',                style: {color: '#09f'},                click: () => {                    // ...                }            }        ]    })}

留神:如果弹框类型为 message|notify|popover,则须要应用如下调用形式。

rlayer.message({...})rlayer.notify({...})rlayer.popover({...})

一睹成果















编码实现

rlayer反对如下丰盛的参数配置。

/** * 弹出框参数配置 */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} 层销毁后回调}

rlayer弹框模板

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

动静className

在react.js中动静绑定class类名。有如下几种罕用办法。

// 字符串拼接<i className={["rlayer"+" "+item.icon]} ></i>// 判断<i className={["rlayer ",isOK ? item.icon : '' ].join('')} ></i>// ES6模板字符串<i className={`rlayer ${isOK ? item.icon : '' }`} ></i>

这种办法简略的还行,简单的拼接就麻烦,而且会生成很多莫名的空格。
这里采纳了React classnames库。
看看如下的应用办法,就晓得有多不便。

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 typesclassNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'// other falsy values are just ignoredclassNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

rlayer组件反对自定义拖拽把手 drag:'#aaa',是否能够拖动到窗口内部 dragOut:true
另外还反对iframe弹框,只需设置 type:'iframe',content传入网址就行。
配置 fullscreen:true 即可关上弹框就显示全屏。

好了,基于React.js开发pc端弹窗组件就分享到这里。心愿对大家有些帮忙哈!✍✍

ending,附上两个vue.js示例我的项目
vue|nuxt.js仿微信app聊天实例:https://segmentfault.com/a/11...
vue.js自定义虚拟化滚动条组件:https://segmentfault.com/a/11...