Skip to content

对话框弹窗组件

作者:Yuan Tang
更新于:5 个月前
字数统计:1.6k 字
阅读时长:6 分钟

回顾

组件封装四大要点详情可跳转 组件封装 查看。

样式

思路

样式方面只写基本样式,其余的放到拓展方法中。如设置预制类,部分样式的取舍由使用者来决定是否要使用。也可以通过 props 父组件传参,子组件获取类名覆盖原有样式,或者添加上某个类名。

设置样式时权重尽可能低,方便使用者在父组件中调整组件样式,无需设置过多 !important 来覆盖层级。

弹窗后面的浅色遮罩、弹窗的标题、内容、按钮、关闭按钮都是可以在组件内写好的基本样式。

可以通过父组件传递部分字段来渲染子组件,如组件库中最常见的宽高、外边距、全屏大小等。

代码示例

  • 父组件

    vue
    <MyDialog width="200" marginTop="50" fullDialog />
  • 子组件

    vue
    <script>
    export default {
      props: {
        width: String,
        marginTop: String,
        fullDialog: Boolean,
      },
      data() {
        return {
        }
      },
    }
    </script>
    
    <template>
      <div class="cover">
        <!-- fullDialog:传递该字段后让弹窗组件全屏显示 -->
        <div class="dialog" :class="{ fullDialog }" :style="{ width: `${width}px`, marginTop: `${marginTop}px` }">
          <!-- 标题部分 -->
          <div class="title">
            <div class="default-title">
              {{ title }}
            </div>
    
            <!-- 关闭按钮 -->
            <span class="close-icon">×</span>
          </div>
    
          <!-- 内容部分 -->
          <div class="content">
            <div class="default-content">
              {{ content }}
            </div>
          </div>
    
          <!-- 按钮 -->
          <div class="button">
            <div class="default-button">
              <button>取消</button>
              <button>确认</button>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <style scoped>
    .cover {
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      background-color: rgba(0, 0, 0, 0.5);
    }
    
    .dialog {
      min-width: 200px;
      background-color: #fff;
      width: 50%;
      margin: 50px auto;
    }
    
    .dialog.fullDialog {
      width: 100vw !important;
      height: 100vh !important;
      margin: 0 !important;
    }
    
    .title {
      position: relative;
      padding: 15px;
    }
    
    /* 预制类 */
    .center .title, .center .content {
      text-align: center;
    }
    .close-icon {
      position: absolute;
      right: 15px;
      top: 10px;
    }
    
    .content {
      padding: 15px;
    }
    
    .button {
      padding: 15px;
    }
    
    button {
      margin-right: 15px;
    }
    </style>

Slot

思路

内容和标题和按钮等部分不一定要写死,而是通过父组件动态传递,如果父组件不传递,则使用默认的内容。

代码示例

vue
<template>
  <div class="cover">
    <!-- fullDialog:传递该字段后让弹窗组件全屏显示 -->
    <div class="dialog" :class="{ fullDialog }" :style="{ width: `${width}px`, marginTop: `${marginTop}px` }">
      <!-- 标题部分 -->
      <div class="title">
        <slot name="title">
          <div class="default-title">
            {{ title }}
          </div>
        </slot> // [!code ++]

        <!-- 关闭按钮 -->
        <span class="close-icon">×</span>
      </div>

      <!-- 内容部分 -->
      <div class="content">
        <slot name="content">
          <div class="default-content">
            {{ content }}
          </div>
        </slot> // [!code ++]
      </div>
    </div>
  </div>
</template>

行为

思路

把行为一致的事件写在弹窗组件内,作为内置行为。如:

  1. 点击叉叉关闭弹窗
  2. 点击取消按钮控制组件显隐
  3. ......

而控制弹窗显隐则由父组件来决定,因此由父组件传递一个布尔值的变量,弹窗组件接收来 v-if 控制。

但是如果我想要子组件来修改值控制显示隐藏,而值是从父组件传递,使用父子组件传参又略显繁琐。如果使用 vue2 ,可以使用 .sync 修饰符来实现子组件修改 props 内的值。

点击取消按钮和关闭符号可能父组件想要处理其他事情,因此把变量变为 false 后再 $emit 提供一个方法给父组件使用。

代码示例

  • 父组件

    vue
    <script>
    import MyDialog from '@/components/MyDialog.vue'
    
    export default {
      components: {
        MyDialog
      },
      data() {
        return {
          show: true
        }
      },
      methods: {
        closeFn() {
          console.log(1)
        }
      },
    }
    </script>
    
    <template>
      <div id="app">
        <MyDialog v-model:show="show" width="200" margin-top="50" @close="closeFn" />
      </div>
    </template>
  • 子组件

    vue
    <script>
    export default {
      props: {
        show: {
          type: Boolean,
          required: true
        },
        content: {
          type: String,
          default: '默认内容'
        },
        title: {
          type: String,
          default: '默认标题'
        },
        width: String,
        marginTop: String,
        fullDialog: Boolean,
      },
      data() {
        return {
        }
      },
      methods: {
        close() {
          this.$emit('close')
          this.$emit('update:show', false)
        },
        cancel() {
          this.$emit('update:show', false)
          this.$emit('cancel')
        },
        comfirm() {
          this.$emit('comfirm')
        }
      }
    }
    </script>
    
    <template>
      <div v-if="show" class="cover">
        <div class="dialog" :class="{ fullDialog }" :style="{ width: `${width}px`, marginTop: `${marginTop}px` }">
          <!-- 标题部分 -->
          <div class="title">
            <!-- 插槽传入,有默认标题 -->
            <slot name="title">
              <div class="default-title">
                {{ title }}
              </div>
            </slot>
    
            <!-- 关闭按钮 -->
            <span class="close-icon" @click="close">×</span>
          </div>
    
          <!-- 内容部分 -->
          <div class="content">
            <slot name="content">
              <div class="default-content">
                {{ content }}
              </div>
            </slot>
          </div>
    
          <!-- 按钮 -->
          <div class="button">
            <slot name="button">
              <div class="default-button">
                <button @click="cancel">
                  取消
                </button>
                <button @click="comfirm">
                  确认
                </button>
              </div>
            </slot>
          </div>
        </div>
      </div>
    </template>

vue3

vue3 的写法中有部分代码需要修改,例如:

  1. 子组件的 propsemit 方法需要修改
  2. 父组件中不在通过 .sync 修饰符做语法糖处理,而是改为 v-model: ,如 v-model:show="show"

代码如下所示:

  • 子组件

    vue
    <script setup>
    import { ref } from 'vue'
    
    defineProps({
      show: {
        type: Boolean,
        required: true
      },
      content: {
        type: String,
        default: '默认内容'
      },
      title: {
        type: String,
        default: '默认标题'
      },
    })
    
    const emit = defineEmits(['update:show', 'cancel', 'close', 'comfirm'])
    
    function close() {
      emit('close')
      emit('update:show', false)
    }
    
    function cancel() {
      emit('update:show', false)
      emit('cancel')
    }
    
    function comfirm() {
      emit('comfirm')
    }
    </script>
  • 父组件

    vue
    <MyDialog v-model:show="show" @close="closeFn" />

Prop

前面三个方法已经描述了在何种场景何种需求父组件传递什么参数给子组件,这里做个总结。

  • 样式类

    可以通过传递类名、部分模块的显隐、部分样式的值等入手

  • 插槽类

    通过传递内容来渲染到子组件的特定插槽处

  • 行为类

    传递一些变量来操控组件的显示隐藏等

拓展

阻止滚动

当弹窗弹出时阻止底部的滚动,有两种方法

  1. window 添加禁止滚动的事件

  2. 通过 css 在弹窗显示时把超出部分隐藏,取消后恢复

    js
    beforeDestory() {
        this.enableScroll()
    },
    methods: {
        disableScroll() {
            const scrollbarWidth = window.innerWidth - document.bodu.clientWidth
            document.body.style.paddingRight = scrollbarWidth + 'px'
            
            document.body.style.overflow = 'hidden'
        },
        enableScroll() {
            this.body.style.paddingRight = 0
            document.body.style.overflow = 'auto'
        },
    }

    当页面隐藏超出部分时,右侧滚动条会消失,此时页面整体内容布局会改变,因此可以通过页面宽度减去内容宽度得出的滚动条宽度,然后在隐藏超出部分时作为右边距。

    如果用户未取消就直接去往其他页面,页面超出隐藏的样式不会被清除,因此也要在页面销毁事件中绑定。

其他

这个组件可以不用做的那么大而全,已经有很多类似 ant desing 的组件库已经实现这些功能了,因此不要考虑大而全,而是从小而美的方向考虑。

比如点击确定按钮可能会有调用接口的可能,因此可以传递一个对象,子组件中该对象默认为空对象。通过判断该对象是否有 url 字段,如果有则调用接口,最后通过 $parent 把参数返回即可。

Contributors

Yuan Tang