x***@126.com
x***@126.com
  • 发布:2021-01-04 10:58
  • 更新:2021-10-28 16:58
  • 阅读:6463

Vue3.0仿layer全局对话框组件|vue3自定义弹层

分类:uni-app

介绍

v3layer弹层 基于vue3.0开发的仿layer.js桌面端对话框组件。支持7+弹窗动画、10+弹窗类型、30+参数配置,拥有流畅的拖拽、缩放、最大化及全屏等功能。轻松实现各种弹窗效果。

vue3.0 mobile移动端自定义弹层组件v3popup

img

V3Layer支持 Msg、Modal、Dialog、Message、Notification、ActionSheet、Toast、Popover 等多种弹窗类型。

img

引入组件

// 在main.js中全局引入组件  
import { createApp } from 'vue'  
import App from './App.vue'  

const app = createApp(App)  

// 引入饿了么vue3组件库  
import ElementPlus from 'element-plus'  
import 'element-plus/lib/theme-chalk/index.css'  

// 引入弹窗组件  
import Vue3Layer from './components/v3layer'  

app.use(ElementPlus)  
app.use(Vue3Layer)  

app.mount('#app')

支持如下两种调用形式。

标签式

<v3-layer   
    v-model="showDialog"  
    title="标题内容"  
    content="<div style='color:#f57b16;padding:30px;'>这里是内容信息!</div>"  
    z-index="1011"  
    lockScroll="false"  
    xclose  
    resize  
    dragOut  
    :btns="[  
        {text: '取消', click: () => showDialog=false},  
        {text: '确认', style: 'color:#f90;', click: handleSure},  
    ]"  
>  
    <template v-slot:content>这里是自定义插槽内容信息!</template>  
</v3-layer>

函数式

let $el = v3layer({  
    title: '标题内容',  
    content: '<div style='color:#f57b16;padding:30px;'>这里是内容信息!</div>',   
    shadeClose: false,  
    zIndex: 1011,  
    lockScroll: false,  
    xclose: true,  
    resize: true,  
    dragOut: true,  
    btns: [  
        {text: '取消', click: () => { $el.close() }},  
        {text: '确认', click: () => handleSure},  
    ]  
});

img

img

如上图:还支持 message popover nofity 三种弹窗类型。

vue2中可以通过prototype来实现挂载全局方法。
vue3中提供了两种全新的挂载全局方法 app.config.globalPropertiesapp.provide

如果使用第一种方式:

// vue2.x中调用  
methods: {  
    showDialog() {  
        this.$v3layer({...})  
    }  
}  

// vue3.x中调用  
setup() {  
    // 获取上下文  
    const { ctx } = getCurrentInstance()  
    ctx.$v3layer({...})  
}

如果使用第二种方式:

// vue2.x中调用  
methods: {  
    showDialog() {  
        this.v3layer({...})  
    }  
}  

// vue3.x中调用  
setup() {  
    const v3layer = inject('v3layer')  

    const showDialog = () => {  
        v3layer({...})  
    }  

    return {  
        v3layer,  
        showDialog  
    }  
}

img

大家感兴趣可以去官网看看文档说明。
https://v3.cn.vuejs.org/api/application-config.html#globalproperties
https://v3.cn.vuejs.org/guide/component-provide-inject.html

img

img

img

img

img

img

img

编码实现

v3layer支持如下参数自定义配置。

|props参数|  
v-model         是否显示弹框  
id              弹窗唯一标识  
title           标题  
content         内容(支持String、带标签内容、自定义插槽内容)***如果content内容比较复杂,推荐使用标签式写法  
type            弹框类型(toast|footer|actionsheet|actionsheetPicker|android|ios|contextmenu|drawer|iframe)  
layerStyle      自定义弹窗样式  
icon            toast图标(loading | success | fail)  
shade           是否显示遮罩层  
shadeClose      是否点击遮罩时关闭弹窗  
lockScroll      是否弹窗出现时将body滚动锁定  
opacity         遮罩层透明度  
xclose          是否显示关闭图标  
xposition       关闭图标位置(left | right | top | bottom)  
xcolor          关闭图标颜色  
anim            弹窗动画(scaleIn | fadeIn | footer | fadeInUp | fadeInDown | fadeInLeft | fadeInRight)  
position        弹出位置(auto | ['100px','50px'] | t | r | b | l | lt | rt | lb | rb)  
drawer          抽屉弹窗(top | right | bottom | left)  
follow          跟随元素定位弹窗(支持元素.kk #kk 或 [e.clientX, e.clientY])  
time            弹窗自动关闭秒数(1、2、3)  
zIndex          弹窗层叠(默认8080)  
teleport        指定挂载节点(默认是挂载组件标签位置,可通过teleport自定义挂载位置) teleport="body | #xxx | .xxx"  
topmost         置顶当前窗口(默认false)  
area            弹窗宽高(默认auto)设置宽度area: '300px' 设置高度area:['', '200px'] 设置宽高area:['350px', '150px']  
maxWidth        弹窗最大宽度(只有当area:'auto'时,maxWidth的设定才有效)  
maximize        是否显示最大化按钮(默认false)  
fullscreen      全屏弹窗(默认false)  
fixed           弹窗是否固定  
drag            拖拽元素(可定义选择器drag:'.xxx' | 禁止拖拽drag:false)  
dragOut         是否允许拖拽到窗口外(默认false)  
lockAxis        限制拖拽方向可选: v 垂直、h 水平,默认不限制  
resize          是否允许拉伸尺寸(默认false)  
btns            弹窗按钮(参数:text|style|disabled|click)  
++++++++++++++++++++++++++++++++++++++++++++++  
|emit事件触发|  
success         层弹出后回调(@success="xxx")  
end             层销毁后回调(@end="xxx")  
++++++++++++++++++++++++++++++++++++++++++++++  
|event事件|  
onSuccess       层打开回调事件  
onEnd           层关闭回调事件

v3layer组件模板

<template>  
    <div ref="elRef" v-show="opened" class="vui__layer" :class="{'vui__layer-closed': closeCls}" :id="id">  
        <!-- //蒙版 -->  
        <div v-if="JSON.parse(shade)" class="vlayer__overlay" @click="shadeClicked" :style="{opacity}"></div>  
        <div class="vlayer__wrap" :class="[''+anim, type&&'popui__'+type, tipArrow]" :style="[layerStyle]">  
            <div v-if="title" class="vlayer__wrap-tit" v-html="title"></div>  
            <div v-if="type=='toast'&&icon" class="vlayer__toast-icon" :class="['vlayer'+icon]" v-html="toastIcon[icon]"></div>  
            <div class="vlayer__wrap-cntbox">  
                <!-- 判断插槽是否存在 -->  
                <template v-if="$slots.content">  
                    <div class="vlayer__wrap-cnt"><slot name="content" /></div>  
                </template>  
                <template v-else>  
                    <template v-if="content">  
                        <iframe v-if="type=='iframe'" scrolling="auto" allowtransparency="true" frameborder="0" :src="content"></iframe>  
                        <!-- message|notify|popover -->  
                        <div v-else-if="type=='message' || type=='notify' || type=='popover'" class="vlayer__wrap-cnt">  
                            <i v-if="icon" class="vlayer-msg__icon" :class="icon" v-html="messageIcon[icon]"></i>  
                            <div class="vlayer-msg__group"><div v-if="title" class="vlayer-msg__title" v-html="title"></div><div v-html="content"></div></div>  
                        </div>  
                        <div v-else class="vlayer__wrap-cnt" v-html="content"></div>  
                    </template>  
                </template>  
                <slot />  
            </div>  
            <div v-if="btns" class="vlayer__wrap-btns">  
                <span v-for="(btn,index) in btns" :key="index" class="btn" :style="btn.style" @click="btnClicked($event,index)" v-html="btn.text"></span>  
            </div>  
            <span v-if="xclose" class="vlayer__xclose" :class="!maximize&&xposition" :style="{'color': xcolor}" @click="close"></span>  
            <span v-if="maximize" class="vlayer__maximize" @click="maximizeClicked($event)"></span>  
            <span v-if="resize" class="vlayer__resize"></span>  
        </div>  
        <!-- 优化拖拽卡顿 -->  
        <div class="vlayer__dragfix"></div>  
    </div>  
</template>

v3layer逻辑处理

<script>  
    import { onMounted, onUnmounted, ref, reactive, watch, toRefs, nextTick } from 'vue'  
    import domUtils from './utils/dom.js'  
    // 索引,蒙层控制,定时器  
    let $index = 0, $locknum = 0, $timer = {}, $closeTimer = null  
    export default {  
        props: {  
            // ...  
        },  
        emits: [  
            'update:modelValue'  
        ],  
        setup(props, context) {  
            const elRef = ref(null);  

            const data = reactive({  
                opened: false,  
                closeCls: '',  
                toastIcon: {  
                    // ...  
                },  
                messageIcon: {  
                    // ...  
                },  
                vlayerOpts: {},  
                tipArrow: null,  
            })  

            onMounted(() => {  
                if(props.modelValue) {  
                    open();  
                }  
                window.addEventListener('resize', autopos, false);  
            })  

            onUnmounted(() => {  
                window.removeEventListener('resize', autopos, false);  
                clearTimeout($closeTimer);  
            })  

            // 监听弹层v-model  
            watch(() => props.modelValue, (val) => {  
                // console.log('V3Layer is now [%s]', val ? 'show' : 'hide')  
                if(val) {  
                    open();  
                }else {  
                    close();  
                }  
            })  

            // 打开弹窗  
            const open = () => {  
                if(data.opened) return;  
                data.opened = true;  
                typeof props.onSuccess === 'function' && props.onSuccess();  

                const dom = elRef.value;  
                // 弹层挂载位置  
                if(props.teleport) {  
                    nextTick(() => {  
                        let teleportNode = document.querySelector(props.teleport);  
                        teleportNode.appendChild(dom);  

                        auto();  
                    })  
                }  

                callback();  
            }  

            // 关闭弹窗  
            const close = () => {  
                if(!data.opened) return;  

                let dom = elRef.value;  
                let vlayero = dom.querySelector('.vlayer__wrap');  
                let ocnt = dom.querySelector('.vlayer__wrap-cntbox');  
                let omax = dom.querySelector('.vlayer__maximize');  

                data.closeCls = true;  
                clearTimeout($closeTimer);  
                $closeTimer = setTimeout(() => {  
                    data.opened = false;  
                    data.closeCls = false;  
                    if(data.vlayerOpts.lockScroll) {  
                        $locknum--;  
                        if(!$locknum) {  
                            document.body.style.paddingRight = '';  
                            document.body.classList.remove('vui__body-hidden');  
                        }  
                    }  
                    if(props.time) {  
                        $index--;  
                    }  
                    // 清除弹窗样式  
                    vlayero.style.width = vlayero.style.height = vlayero.style.top = vlayero.style.left = '';  
                    ocnt.style.height = '';  
                    omax && omax.classList.contains('maximized') && omax.classList.remove('maximized');  

                    data.vlayerOpts.isBodyOverflow && (document.body.style.overflow = '');  

                    context.emit('update:modelValue', false);  
                    typeof props.onEnd === 'function' && props.onEnd();  
                }, 200)  
            }  

            // 弹窗位置  
            const auto = () => {  
                // ...  

                autopos();  

                // 全屏弹窗  
                if(props.fullscreen) {  
                    full();  
                }  

                // 弹窗拖动|缩放  
                move();  
            }  

            const autopos = () => {  
                if(!data.opened) return;  
                let oL, oT  
                let pos = props.position;  
                let isFixed = JSON.parse(props.fixed);  
                let dom = elRef.value;  
                let vlayero = dom.querySelector('.vlayer__wrap');  

                if(!isFixed || props.follow) {  
                    vlayero.style.position = 'absolute';  
                }  

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

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

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

                    vlayero.style.left = parseFloat(isFixed ? oL : domUtils.scroll('left') + oL) + 'px';  
                    vlayero.style.top = parseFloat(isFixed ? oT : domUtils.scroll('top') + oT) + 'px';  
                }  
            }  

            // 元素跟随定位  
            const offset = () => {  
                let oW, oH, pS  
                let dom = elRef.value  
                let vlayero = dom.querySelector('.vlayer__wrap');  

                oW = vlayero.offsetWidth;  
                oH = vlayero.offsetHeight;  
                pS = domUtils.getFollowRect(props.follow, oW, oH);  
                data.tipArrow = pS[2];  

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

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

            // 恢复弹窗  
            const restore = () => {  
                let dom = elRef.value;  
                let vlayero = dom.querySelector('.vlayer__wrap');  
                let otit = dom.querySelector('.vlayer__wrap-tit');  
                let ocnt = dom.querySelector('.vlayer__wrap-cntbox');  
                let obtn = dom.querySelector('.vlayer__wrap-btns');  
                let omax = dom.querySelector('.vlayer__maximize');  

                let t = otit ? otit.offsetHeight : 0  
                let b = obtn ? obtn.offsetHeight : 0  

                if(!data.vlayerOpts.lockScroll) {  
                    data.vlayerOpts.isBodyOverflow = false;  
                    document.body.style.overflow = '';  
                }  

                props.maximize && omax.classList.remove('maximized')  

                vlayero.style.left = parseFloat(data.vlayerOpts.rect[0]) + 'px';  
                vlayero.style.top = parseFloat(data.vlayerOpts.rect[1]) + 'px';  
                vlayero.style.width = parseFloat(data.vlayerOpts.rect[2]) + 'px';  
                vlayero.style.height = parseFloat(data.vlayerOpts.rect[3]) + 'px';  
            }  

            // 拖动|缩放弹窗  
            const move = () => {  
                // ...  
            }  

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

            // 点击最大化按钮  
            const maximizeClicked = (e) => {  
                let o = e.target  
                if(o.classList.contains('maximized')) {  
                    // 恢复  
                    restore();  
                } else {  
                    // 最大化  
                    full();  
                }  
            }  
            // 点击遮罩层  
            const shadeClicked = () => {  
                if(JSON.parse(props.shadeClose)) {  
                    close();  
                }  
            }  
            // 按钮事件  
            const btnClicked = (e, index) => {  
                let btn = props.btns[index]  
                if(!btn.disabled) {  
                    typeof btn.click === 'function' && btn.click(e)  
                }  
            }  

            return {  
                ...toRefs(data),  
                elRef,  
                close,  
                maximizeClicked,  
                shadeClicked,  
                btnClicked,  
            }  
        }  
    }  
</script>

大家可以在此逻辑基础上自行开发一些新功能。

okay,基于vue3开发自定义pc端弹窗组件就分享这么多,希望以上分享对大家有所帮助哈~~

React+Hooks自定义pc端弹窗组件|react.js自定义对话框

链接:https://juejin.cn/post/6913710656428457991/
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 关注 分享

要回复文章请先登录注册

5***@qq.com

5***@qq.com

请问dom.js在哪里下载?
2021-10-28 16:58
7***@qq.com

7***@qq.com

[Vue warn]: A plugin must either be a function or an object with an "install" function.
[Vue warn]: injection "v3layer" not found.
跑不起来,能不能给个完整的例子,谢谢
2021-05-17 15:33
7***@qq.com

7***@qq.com

dom.js哪里下载?
2021-05-17 15:28
x***@126.com

x***@126.com (作者)

回复 9***@qq.com :
多谢支持,还没的。
2021-03-01 09:02
9***@qq.com

9***@qq.com

不错不错,v3layer的官网有吗?
2021-02-19 18:34