on-dialog 对话框组件

218 阅读1分钟

1 实现效果

image.png 使用

    <on-dialog title="提示" v-model:visible="visible">
      <span>这是一段内容</span>
      <template v-slot:footer>
        <on-button type="primary" class="btn">确定</on-button>
        <on-button class="btn" @click="visible = false">取消</on-button>
      </template>
    </on-dialog>

2 涉及知识点

  • 组件间通信(sync)
  • 插槽的使用
  • v-show
  • 阻止冒泡

3 功能实现

本文将对对话框组件的功能从简单到复杂拆分讲解,中间穿插涉及的知识点,基本骨架如下

<template>
  <div class="on-dialog_wrapper" v-show="visible" @click.self="handleClose">
    <div class="on-dialog" :style="{width:width,marginTop:top}">
      <div class="on-dialog_header">
        <slot name="title">
          <span class="on-dialog_title">{{title}}</span>
        </slot>
        <button class="on-dialog_headerbtn" @click="handleClose">
          <i class="on-icon-close"></i>
        </button>
      </div>
      <div class="on-dialog_body">
        <!-- 默认插槽 -->
        <slot></slot>
      </div>
      <div class="on-dialog_footer" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>

3.1 对话框标题

// template部分
       <slot name="title">
         <span class="on-dialog_title">{{title}}</span>
       </slot>
// props部分
      props: {
        title: {
          type: String,
          default: '提示'
        },
      },
 //  父组件基本使用
     <on-dialog title="温馨提示"></on-dialog>
 // 父组件自定义标题内容
     <on-dialog v-model:visible="visible">
      <template #title>
        <h3>我是标题</h3>
      </template>
    </on-dialog>

在这部分,父组件可以通过title属性将标题传递给dialog组件,也可以使用插槽自定义标题。这一部分中值得注意的是插槽如上写法,如果父组件没有传入插槽则使用插槽内的内容,如果父组件传值则将插槽内容覆盖。

3.2 对话框内容以及按钮部分

        <button class="on-dialog_headerbtn" @click="handleClose">
          <i class="on-icon-close"></i>
        </button>
      </div>
      <div class="on-dialog_body">
        <!-- 默认插槽 -->
        <slot></slot>
      </div>
      <div class="on-dialog_footer" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>

这部分同样是通过具名插槽的形式实现,在骨架完成后开始对样式进行完善

3.3 对话框的显示和隐藏

为便于说明,将该组件的所有代码展示

<template>
  <div class="on-dialog_wrapper" v-show="visible" @click.self="handleClose">
    <div class="on-dialog" :style="{width:width,marginTop:top}">
      <div class="on-dialog_header">
        <slot name="title">
          <span class="on-dialog_title">{{title}}</span>
        </slot>
        <button class="on-dialog_headerbtn" @click="handleClose">
          <i class="on-icon-close"></i>
        </button>
      </div>
      <div class="on-dialog_body">
        <!-- 默认插槽 -->
        <slot></slot>
      </div>
      <div class="on-dialog_footer" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'OnDialog',
  props: {
    title: {
      type: String,
      default: '提示'
    },
    width: {
      type: String,
      default: '30%'
    },
    top: {
      type: String,
      default: '15vh'
    },
    visible: {
      type: Boolean,
      default: false
    }
  },
  setup (props, { emit }) {
    const handleClose = () => {
      emit('update:visible', false)
    }
    return {
      handleClose
    }
  }
})

3.3.1 v-show="visible"控制显示和隐藏

  • 主要使用v-show="visible"来控制显示和隐藏,由于对话框组件需要频繁的显示和隐藏所以这里使用v-show优于v-if,因为v-if会将元素从dom中删除,从而引起浏览器回流。
    3.3.2 当点击x号和对话框外的元素时,关闭对话框
  • 由于点击的是组件,所以需要在组件相应的地方添加点击事件,当被点击时就将visible改成false。值得注意的是,按照普通的写法,当点击后将visible的值通过this.$emit注册一个事件,然后父组件在调用该事件。
  • 但是这样写的话,作为组件的使用者就会很麻烦,每次都需要在组件上注册一个事件来接收子组件传过来的一个数据。所以这里就使用vue提供的一个语法糖sync,由于vue3.0对sync进行了修改,所以这里主要讲vue3.0中的用法。
// 父组件使用
    <on-dialog title="提示" v-model:visible="visible">
      <span>这是一段内容</span>
      <template v-slot:footer>
        <on-button type="primary" class="btn">确定</on-button>
        <on-button class="btn" @click="visible = false">取消</on-button>
    </on-dialog>
  // 对话框组件
  <div class="on-dialog_wrapper" v-show="visible" @click.self="handleClose">
    <div class="on-dialog" :style="{width:width,marginTop:top}">
      <div class="on-dialog_header">
        <slot name="title">
          <span class="on-dialog_title">{{title}}</span>
        </slot>
        <button class="on-dialog_headerbtn" @click="handleClose">
          <i class="on-icon-close"></i>
        </button>
      </div>
     setup (props, { emit }) {
    const handleClose = () => {
      emit('update:visible', false)
    }
    return {
      handleClose
    }
  }

当用户点击时触发handleClose事件,在通过update:visible触发事件就可以将事件传递给父组件,父组件中通过v-model:visible="visible"就可以实现双向数据绑定的效果 vue3.0 sync使用

4 完整代码

<template>
  <div class="on-dialog_wrapper" v-show="visible" @click.self="handleClose">
    <div class="on-dialog" :style="{width:width,marginTop:top}">
      <div class="on-dialog_header">
        <slot name="title">
          <span class="on-dialog_title">{{title}}</span>
        </slot>
        <button class="on-dialog_headerbtn" @click="handleClose">
          <i class="on-icon-close"></i>
        </button>
      </div>
      <div class="on-dialog_body">
        <!-- 默认插槽 -->
        <slot></slot>
      </div>
      <div class="on-dialog_footer" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'OnDialog',
  props: {
    title: {
      type: String,
      default: '提示'
    },
    width: {
      type: String,
      default: '30%'
    },
    top: {
      type: String,
      default: '15vh'
    },
    visible: {
      type: Boolean,
      default: false
    }
  },
  setup (props, { emit }) {
    const handleClose = () => {
      emit('update:visible', false)
    }
    return {
      handleClose
    }
  }
})

</script>
<style lang="scss" scoped>
.on-dialog_wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow: auto;
  margin: 0;
  z-index: 2001;
  background-color: rgba(0, 0, 0, 0.5);
  .on-dialog {
    position: relative;
    margin: 15vh auto 50px;
    background: #fff;
    border-radius: 2px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    box-sizing: border-box;
    width: 30%;
    &_header {
      padding: 20px 20px 10px;
      .on-dialog_title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
      }
      .on-dialog_headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: transparent;
        border: none;
        outline: none;
        cursor: pointer;
        font-size: 16px;
        .one-icon-close {
          color: 909399;
        }
      }
    }
    &_body {
      padding: 30px 20px;
      color: #606266;
      font-size: 14px;
      word-break: break-all;
    }
    &_footer {
      padding: 10px 20px 20px;
      text-align: right;
      box-sizing: border-box;
      ::v-deep .on-button:first-child {
        margin-right: 20px;
      }
    }
  }
}
.dialog-fade-enter-active {
  animation: fade 0.3s;
}
.dialog-fade-leave-active {
  animation: fade 0.3s reverse;
}
@keyframes fade {
  0% {
    opacity: 0;
    transform: translateY(-20px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
</style>