V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xiaoyan2017
V2EX  ›  推广

Vue 仿微信弹窗组件 | nuxt/vue 自定义弹出框

  •  
  •   xiaoyan2017 · 2020-10-08 09:44:01 +08:00 · 2383 次点击
    这是一个创建于 1536 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    最近在开发 Nuxt.js 项目,项目中有多个地方需要用到弹窗功能,于是就自己造了个Vue 自定义弹窗组件 VPopup

    介绍

    VPopup 一个基于 Vue.js 构建的多功能移动端弹出框组件。集合了 Vant 及 NutUI 中的 Popup 弹出层、Notify 通知信息、Dialog 对话框、ActionSheet 动作面板框及 Toast 弱提示框 等功能。

    快速开始

    在 main.js 中引入组件

    import Popup from './components/popup'
    Vue.use(Popup)
    

    支持标签式及函数式调用方式。

    <!-- 标签式调用 -->
    <template>
        <view id="root">
            ...
            
            <!-- VPopup 模板 -->
            <v-popup 
                v-model="showDialog" 
                anim="scaleIn" 
                title="标题内容"
                content="弹窗内容,告知当前状态、信息和解决方法,描述文字尽量控制在三行内!" 
                shadeClose="false" 
                xclose
                :btns="[
                    {...},
                    {...},
                ]"
            />
        </view>
    </template>
    
    <!-- 函数式调用 -->
    <script>
        export default {
            ...
            methods: {
                handleShowDialog() {
                    let $el = this.$vpopup({
                        title: '标题内容',
                        content: '弹窗内容,告知当前状态、信息和解决方法,描述文字尽量控制在三行内!',
                        anim: 'scaleIn',
                        shadeClose: false,
                        xclose: true,
                        onClose: () => {
                            console.log('vpopup is closed !')
                        },
                        btns: [
                            {text: '关闭'},
                            {
                                text: '确定',
                                style: 'color:#00e0a1',
                                click: () => {
                                    $el.close()
                                }
                            }
                        ]
                    });
                }
            }
        }
    </script>
    

    提供了基本的 Msg 信息框、ActionSheet 底部面板框、Android/IOS 弹窗风格、Toast 弱提示框。支持上 /下 /左 /右弹出层,右键 /长按弹窗。

    实现方式

    参数配置

    @@Props
    ------------------------------------------
    v-model     当前组件是否显示
    title       标题
    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 )
    btns        弹窗按钮(参数:text|style|disabled|click )
     
    @@$emit
    ------------------------------------------
    open        打开弹出层时触发(@open="xxx")
    close       关闭弹出层时触发(@close="xxx")
     
    @@Event
    ------------------------------------------
    onOpen      打开弹窗回调
    onClose     关闭弹窗回调
    

    弹窗模板 popup.vue

    <template>
      <div v-show="opened" class="nuxt__popup" :class="{'nuxt__popup-closed': closeCls}" :id="id">
        <div v-if="JSON.parse(shade)" class="nuxt__overlay" @click="shadeClicked" :style="{opacity}"></div>
        <div class="nuxt__wrap">
          <div class="nuxt__wrap-section">
            <div class="nuxt__wrap-child" :class="['anim-'+anim, type&&'popui__'+type, round&&'round', position]" :style="popupStyle">
              <div v-if="title" class="nuxt__wrap-tit" v-html="title"></div>
              <div v-if="type=='toast'&&icon" class="nuxt__toast-icon" :class="['nuxt__toast-'+icon]" v-html="toastIcon[icon]"></div>
              <template v-if="$slots.content"><div class="nuxt__wrap-cnt"><slot name="content" /></div></template>
              <template v-else><div v-if="content" class="nuxt__wrap-cnt" v-html="content"></div></template>
              <slot />
              <div v-if="btns" class="nuxt__wrap-btns">
                <span v-for="(btn,index) in btns" :key="index" class="btn" :style="btn.style" v-html="btn.text"></span>
              </div>
              <span v-if="xclose" class="nuxt__xclose" :class="xposition" :style="{'color': xcolor}" @click="close"></span>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    /**
     * @Desc     VueJs 自定义弹窗组件 VPopup
     * @Time     andy by 2020-10-06
     * @About    Q:282310962  wx:xy190310
     */
    <script>
      let $index = 0, $lockCount = 0, $timer = {};
      export default {
        props: {
          ...
        },
        data() {
          return {
            opened: false,
            closeCls: '',
            toastIcon: {
              ...
            }
          }
        },
        watch: {
          value(val) {
            const type = val ? 'open' : 'close';
            this[type]();
          },
        },
        methods: {
          // 打开弹窗
          open() {
            if(this.opened) return;
            this.opened = true;
            this.$emit('open');
            typeof this.onOpen === 'function' && this.onOpen();
            
            if(JSON.parse(this.shade)) {
              if(!$lockCount) {
                document.body.classList.add('nt-overflow-hidden');
              }
              $lockCount++;
            }
            
            // 倒计时关闭
            if(this.time) {
              $index++;
              if($timer[$index] !== null) clearTimeout($timer[$index])
              $timer[$index] = setTimeout(() => {
                this.close();
              }, parseInt(this.time) * 1000);
            }
            
            if(this.follow) {
              this.$nextTick(() => {
                let obj = this.$el.querySelector('.nuxt__wrap-child');
                let oW, oH, winW, winH, pos;
     
                oW = obj.clientWidth;
                oH = obj.clientHeight;
                winW = window.innerWidth;
                winH = window.innerHeight;
                pos = this.getPos(this.follow[0], this.follow[1], oW, oH, winW, winH);
     
                obj.style.left = pos[0] + 'px';
                obj.style.top = pos[1] + 'px';
              });
            }
          },
          // 关闭弹窗
          close() {
            if(!this.opened) return;
            
            this.closeCls = true;
            setTimeout(() => {
              this.opened = false;
              this.closeCls = false;
              if(JSON.parse(this.shade)) {
                $lockCount--;
                if(!$lockCount) {
                  document.body.classList.remove('nt-overflow-hidden');
                }
              }
              if(this.time) {
                $index--;
              }
              this.$emit('input', false);
              this.$emit('close');
              typeof this.onClose === 'function' && this.onClose();
            }, 200);
          },
          shadeClicked() {
            if(JSON.parse(this.shadeClose)) {
              this.close();
            }
          },
          btnClicked(e, index) {
            let btn = this.btns[index];
            if(!btn.disabled) {
              typeof btn.click === 'function' && btn.click(e)
            }
          },
          getZIndex() {
            for(var $idx = parseInt(this.zIndex), $el = document.getElementsByTagName('*'), i = 0, len = $el.length; i < len; i++)
              $idx = Math.max($idx, $el[i].style.zIndex)
            return $idx;
          },
          // 获取弹窗坐标点
          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];
          }
        },
      }
    </script>
    

    函数式实现

    通过 Vue.extend 扩展实例构造器来实现函数式调用方式。

    import Vue from 'vue';
    import VuePopup from './popup.vue';
     
    let PopupConstructor = Vue.extend(VuePopup);
     
    let $instance;
     
    let VPopup = function(options = {}) {
        // 同一个页面中,id 相同的 Popup 的 DOM 只会存在一个
        options.id = options.id || 'nuxt-popup-id';
        $instance = new PopupConstructor({
            propsData: options
        });
        $instance.vm = $instance.$mount();
        
        let popupDom = document.querySelector('#' + options.id);
        if(options.id && popupDom) {
            popupDom.parentNode.replaceChild($instance.$el, popupDom);
        } else {
            document.body.appendChild($instance.$el);
        }
     
        Vue.nextTick(() => {
            $instance.value = true;
        })
        
        return $instance;
    }
     
    VPopup.install = () => {
        Vue.prototype['$vpopup'] = VPopup;
        Vue.component('v-popup', VuePopup);
    }
     
    export default VPopup;
    

    这样就可以愉快的使用 this.$vpopup({...}) 函数来进行调用了。

    Ok,基于 Vue/Nuxt 自定义弹出层组件就介绍到这里。目前该弹窗已在 Nuxt.js 项目中使用,届时也会分享出来。💪✍

    作者:xiaoyan2017
    链接: https://www.cnblogs.com/xiaoyan2017/p/13776977.html
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    1 条回复    2020-10-08 12:57:01 +08:00
    justin2018
        1
    justin2018  
       2020-10-08 12:57:01 +08:00
    谢谢楼主分享~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2805 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 11:47 · PVG 19:47 · LAX 03:47 · JFK 06:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.