vue3系列:vue3.0自定义弹框组件V3Popup|vue3.x手机端弹框组件

基于Vue3.0开发的轻量级手机端弹框组件V3Popup。

之前有分享一个vue2.x移动端弹框组件,今天给大家带来的是Vue3实现自定义弹框组件。

V3Popup 基于vue3.x实现的移动端弹出框组件,集合msg、alert、dialog、modal、actionSheet、toast等多种效果。支持20+种自定义参数配置,旨在通过极简的布局、精简的调用方式解决多样化的弹框场景。

v3popup 在开发之初参考借鉴了Vant3、ElementPlus等组件化思想。并且功能效果和之前vue2.0保持一致。

◆ 快速引入

在main.js中全局引入v3popup组件。

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 引入弹窗组件v3popup
import V3Popup from './components/v3popup'

app.use(V3Popup)
app.mount('#app')

v3popup同样支持标签式+函数式两种调用方式。

  • 标签写法
<v3-popup 
    v-model="showDialog"
    title="标题"
    content="<p style='color:#df6a16;padding:10px;'>这里是内容信息!</p>"
    type="android"
    shadeClose="false"
    xclose
    :btns="[
        {text: '取消', click: () => showDialog=false},
        {text: '确认', style: 'color:#f90;', click: handleOK},
    ]"
    @success="handleOpen"
    @end="handleClose"
/>
    <template #content>这里是自定义插槽内容信息!</template>
</v3-popup>
  • 函数写法
let $el = this.$v3popup({
    title: '标题',
    content: '<p style='color:#df6a16;padding:10px;'>这里是内容信息!</p>',
    type: 'android',
    shadeClose: false,
    xclose: true,
    btns: [
        {text: '取消', click: () => { $el.close(); }},
        {text: '确认', style: 'color:#f90;', click: () => handleOK},
    ],
    onSuccess: () => {},
    onEnd: () => {}
})

Vue3.0中挂载全局函数有2种方式  app.config.globalProperties  app.provide 

通过 app.config.globalProperties.$v3popup = V3Popup 方式挂载。

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

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

通过 app.provide('v3popup', V3Popup) 方式挂载。

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

// vue3.x中调用
setup() {
    const v3popup = inject('v3popup')
    
    const showDialog = () => {
        v3popup({...})
    }

    return {
        v3popup,
        showDialog
    }
}

不过vue.js作者是推荐使用 provide inject 方式来挂载原型链函数。

◆ 效果预览

◆ 参数配置

v3popup支持如下参数配置。

|props参数|
v-model         是否显示弹框
title           标题
content         内容(支持String、带标签内容、自定义插槽内容)***如果content内容比较复杂,推荐使用标签式写法
type            弹窗类型(toast | footer | actionsheet | actionsheetPicker | android | ios)
popupStyle      自定义弹窗样式
icon            toast图标(loading | success | fail)
shade           是否显示遮罩层
shadeClose      是否点击遮罩时关闭弹窗
opacity         遮罩层透明度
round           是否显示圆角
xclose          是否显示关闭图标
xposition       关闭图标位置(left | right | top | bottom)
xcolor          关闭图标颜色
anim            弹窗动画(scaleIn | fadeIn | footer | fadeInUp | fadeInDown)
position        弹出位置(top | right | bottom | left)
follow          长按/右键弹窗(坐标点)
time            弹窗自动关闭秒数(1、2、3)
zIndex          弹窗层叠(默认8080)
teleport        指定挂载节点(默认是挂载组件标签位置,可通过teleport自定义挂载位置) teleport="body | #xxx | .xxx"
btns            弹窗按钮(参数:text|style|disabled|click)
++++++++++++++++++++++++++++++++++++++++++++++
|emit事件触发|
success         层弹出后回调(@success="xxx")
end             层销毁后回调(@end="xxx"++++++++++++++++++++++++++++++++++++++++++++++
|event事件|
onSuccess       层打开回调事件
onEnd           层关闭回调事件

v3popup.vue模板

<template>
    <div ref="elRef" v-show="opened" class="vui__popup" :class="{'vui__popup-closed': closeCls}" :id="id">
        <!-- //蒙层 -->
        <div v-if="JSON.parse(shade)" class="vui__overlay" @click="shadeClicked" :style="{opacity}"></div>
        <div class="vui__wrap">
            <div class="vui__wrap-section">
                <div class="vui__wrap-child" :class="['anim-'+anim, type&&'popupui__'+type, round&&'round', position]" :style="[popupStyle]">
                    <div v-if="title" class="vui__wrap-tit" v-html="title"></div>
                    <div v-if="type=='toast'&&icon" class="vui__toast-icon" :class="['vui__toast-'+icon]" v-html="toastIcon[icon]"></div>
                    <!-- 判断插槽是否存在 -->
                    <template v-if="$slots.content">
                        <div class="vui__wrap-cnt"><slot name="content" /></div>
                    </template>
                    <template v-else>
                        <div v-if="content" class="vui__wrap-cnt" v-html="content"></div>
                    </template>
                    <slot />
                    <div v-if="btns" class="vui__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="vui__xclose" :class="xposition" :style="{'color': xcolor}" @click="close"></span>
                </div>
            </div>
        </div>
    </div>
</template>
/**
 * @Desc     Vue3.0自定义弹框组件V3Popup
 * @Time     andy by 2020-12
 * @About    Q:282310962  wx:xy190310
 */
<script>
    import { onMounted, ref, reactive, watch, toRefs, nextTick } from 'vue'
    let $index = 0, $locknum = 0, $timer = {}
    export default {
        props: {
            // 接收父组件v-model值,如果v-model:open,则这里需写open: {...}
            modelValue: { type: Boolean, default: false },
            // 标识符,相同ID共享一个实例
            id: {
                type: String, default: ''
            },
            title: String,
            content: String,
            type: String,
            popupStyle: String,
            icon: String,
            shade: { type: [Boolean, String], default: true },
            shadeClose: { type: [Boolean, String], default: true },
            opacity: { type: [Number, String], default: '' },
            round: Boolean,
            xclose: Boolean,
            xposition: { type: String, default: 'right' },
            xcolor: { type: String, default: '#333' },
            anim: { type: String, default: 'scaleIn' },
            position: String,
            follow: { type: Array, default: null },
            time: { type: [Number, String], default: 0 },
            zIndex: { type: [Number, String], default: '8080' },
            teleport: [String, Object],
            btns: {
                type: Array, default: null
            },
            onSuccess: { type: Function, default: null },
            onEnd: { type: Function, default: null },
        },
        emits: [
            'update:modelValue'
        ],
        setup(props, context) {
            const elRef = ref(null)

            const data = reactive({
                opened: false,
                closeCls: '',
                toastIcon: {
                    ...
                }
            })

            onMounted(() => {
                ...
            })

            // 监听弹层v-model
            watch(() => props.modelValue, (val) => {
                if(val) {
                    open()
                }else {
                    close()
                }
            })

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

                const dom = elRef.value
                dom.style.zIndex = getZIndex() + 1

                ...

                // 倒计时
                if(props.time) {
                    $index++
                    // 避免重复操作
                    if($timer[$index] !== null) clearTimeout($timer[$index])
                    $timer[$index] = setTimeout(() => {
                        close()
                    }, parseInt(props.time) * 1000)
                }

                // 长按|右键菜单
                if(props.follow) {
                    ...
                }
            }

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

                data.closeCls = true
                setTimeout(() => {
                    ...

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

            // 点击遮罩层
            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,
                shadeClicked,
                btnClicked,
            }
        }
    }
</script>

Vue3中可通过  createApp  或  createVNode | render  来挂载实例到body来实现函数式调用。

import { createApp } from 'vue'
import PopupConstructor from './popup.vue'

let $inst
// 创建挂载实例
let createMount = (opts) => {
    const mountNode = document.createElement('div')
    document.body.appendChild(mountNode)

    const app = createApp(PopupConstructor, {
        ...opts, modelValue: true,
        remove() {
            app.unmount(mountNode)
            document.body.removeChild(mountNode)
        }
    })
    return app.mount(mountNode)
}

function V3Popup(options = {}) {
    options.id = options.id || 'v3popup_' + generateId()
    $inst = createMount(options)
    
    return $inst
}

V3Popup.install = app => {
    app.component('v3-popup', PopupConstructor)
    // app.config.globalProperties.$v3popup = V3Popup
    app.provide('v3popup', V3Popup)
}

这样就实现了在vue3中注册原型链函数和v3-popup组件,就可以使用函数式调用了。

好了,基于Vue3开发自定义弹框组件就分享到这里。希望对大家有些帮助哈!💪

最后附上一个Next.js实例项目

Next+React.js桌面端聊天:https://www.cnblogs.com/xiaoyan2017/p/14195483.html

 

posted @ 2020-12-30 12:22  xiaoyan2017  阅读(8139)  评论(2编辑  收藏  举报
友情链接:web码农◆前端技术B站