1 实现效果
使用
<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>