1. 创建自定义弹框组件
sql
<template>
<!-- 遮罩层 -->
<transition name="fade">
<div
v-if="visible"
class="custom-dialog-overlay"
:class="{ 'custom-dialog-overlay--mask': showOverlay }"
@click="onOverlayClick"
></div>
</transition>
<!-- 对话框内容 -->
<transition name="dialog">
<div
v-if="visible"
class="custom-dialog"
:style="dialogStyle"
>
<!-- 标题 -->
<div v-if="title" class="custom-dialog__header">
<slot name="title">
<div class="custom-dialog__title">{{ title }}</div>
</slot>
</div>
<!-- 内容 -->
<div class="custom-dialog__content">
<slot></slot>
</div>
<!-- 底部按钮 -->
<div v-if="showButtons" class="custom-dialog__footer">
<slot name="footer">
<button
v-if="showCancelButton"
class="custom-dialog__button custom-dialog__button--cancel"
@click="onCancel"
>
{{ cancelButtonText }}
</button>
<button
v-if="showConfirmButton"
class="custom-dialog__button custom-dialog__button--confirm"
@click="onConfirm"
>
{{ confirmButtonText }}
</button>
</slot>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'CustomDialog',
props: {
// 是否显示
value: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ''
},
// 是否显示遮罩层
showOverlay: {
type: Boolean,
default: true
},
// 点击遮罩层是否关闭
closeOnClickOverlay: {
type: Boolean,
default: true
},
// 是否显示取消按钮
showCancelButton: {
type: Boolean,
default: true
},
// 是否显示确认按钮
showConfirmButton: {
type: Boolean,
default: true
},
// 取消按钮文本
cancelButtonText: {
type: String,
default: '取消'
},
// 确认按钮文本
confirmButtonText: {
type: String,
default: '确认'
},
// 对话框宽度
width: {
type: [String, Number],
default: '320px'
},
// 是否显示圆角
round: {
type: Boolean,
default: true
}
},
data() {
return {
visible: this.value
}
},
computed: {
dialogStyle() {
const style = {}
if (this.width) {
style.width = typeof this.width === 'number' ? `${this.width}px` : this.width
}
if (this.round) {
style.borderRadius = '8px'
}
return style
},
showButtons() {
return this.showCancelButton || this.showConfirmButton || this.$slots.footer
}
},
watch: {
value(val) {
if (val !== this.visible) {
this.visible = val
}
},
visible(val) {
// 控制 body 滚动
if (val) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
this.$emit('input', val)
if (val) {
this.$emit('open')
} else {
this.$emit('close')
}
}
},
methods: {
// 打开对话框
open() {
this.visible = true
},
// 关闭对话框
close() {
this.visible = false
},
// 确认
onConfirm() {
this.$emit('confirm')
this.close()
},
// 取消
onCancel() {
this.$emit('cancel')
this.close()
},
// 点击遮罩层
onOverlayClick() {
if (this.closeOnClickOverlay) {
this.close()
}
this.$emit('click-overlay')
}
},
beforeDestroy() {
// 组件销毁时恢复滚动
document.body.style.overflow = ''
}
}
</script>
<style scoped>
/* 遮罩层动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* 对话框内容动画 */
.dialog-enter-active, .dialog-leave-active {
transition: all 0.3s ease;
}
.dialog-enter-from {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(0.7);
}
.dialog-leave-to {
opacity: 0;
transform: translate3d(-50%, -50%, 0) scale(0.9);
}
/* 遮罩层样式 */
.custom-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.custom-dialog-overlay--mask {
background-color: rgba(0, 0, 0, 0.7);
}
/* 对话框样式 */
.custom-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
background-color: #fff;
overflow: hidden;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 标题 */
.custom-dialog__header {
padding: 20px 16px 0;
text-align: center;
font-size: 18px;
font-weight: 600;
line-height: 1.4;
}
/* 内容 */
.custom-dialog__content {
padding: 16px;
font-size: 15px;
line-height: 1.5;
color: #333;
max-height: 60vh;
overflow-y: auto;
}
/* 底部按钮 */
.custom-dialog__footer {
display: flex;
padding: 0;
border-top: 1px solid #f0f0f0;
}
.custom-dialog__button {
flex: 1;
height: 48px;
border: none;
background: none;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.custom-dialog__button--cancel {
color: #666;
border-right: 1px solid #f0f0f0;
}
.custom-dialog__button--cancel:active {
background-color: #f7f7f7;
}
.custom-dialog__button--confirm {
color: #1989fa;
font-weight: 500;
}
.custom-dialog__button--confirm:active {
background-color: #f2f2f2;
}
</style>
2. 使用示例
sql
<template>
<div class="container">
<h1>自定义弹框演示</h1>
<button @click="showDialog = true">打开弹框</button>
<button @click="showAsyncDialog">打开异步弹框</button>
<button @click="showCustomDialog">自定义内容弹框</button>
<!-- 基本用法 -->
<CustomDialog
v-model="showDialog"
title="提示"
@confirm="onConfirm"
@cancel="onCancel"
>
这是一个自定义弹框
</CustomDialog>
<!-- 异步关闭 -->
<CustomDialog
v-model="showAsyncDialogVisible"
title="请确认"
:closeOnClickOverlay="false"
@confirm="handleAsyncConfirm"
>
确定要执行此操作吗?
</CustomDialog>
<!-- 自定义内容 -->
<CustomDialog
v-model="showCustomContentDialog"
:showCancelButton="false"
:showConfirmButton="false"
>
<div class="custom-content">
<h3>自定义标题</h3>
<p>这是自定义内容区域</p>
<input v-model="inputValue" placeholder="请输入内容" />
</div>
<template #footer>
<button class="custom-btn" @click="showCustomContentDialog = false">关闭</button>
</template>
</CustomDialog>
</div>
</template>
<script>
import CustomDialog from './CustomDialog.vue'
export default {
components: {
CustomDialog
},
data() {
return {
showDialog: false,
showAsyncDialogVisible: false,
showCustomContentDialog: false,
inputValue: ''
}
},
methods: {
onConfirm() {
console.log('确认')
},
onCancel() {
console.log('取消')
},
showAsyncDialog() {
this.showAsyncDialogVisible = true
},
handleAsyncConfirm() {
// 模拟异步操作
setTimeout(() => {
console.log('异步操作完成')
this.showAsyncDialogVisible = false
}, 1000)
},
showCustomDialog() {
this.showCustomContentDialog = true
}
}
}
</script>
<style scoped>
.container {
padding: 20px;
}
button {
margin: 10px;
padding: 10px 20px;
background-color: #1989fa;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.custom-content {
padding: 20px;
text-align: center;
}
.custom-content input {
width: 100%;
padding: 8px;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.custom-btn {
width: 100%;
height: 48px;
background-color: #07c160;
color: white;
border: none;
border-radius: 0;
font-size: 16px;
}
</style>
3. 函数式调用(可选)
如果你想要像 vant 一样支持函数式调用,可以创建一个插件:
sql
javascript
// dialog.js
import Vue from 'vue'
import CustomDialog from './CustomDialog.vue'
const DialogConstructor = Vue.extend(CustomDialog)
let instance
const Dialog = (options = {}) => {
if (typeof options === 'string') {
options = {
message: options
}
}
// 合并默认配置
options = {
title: '',
message: '',
showCancelButton: true,
showConfirmButton: true,
cancelButtonText: '取消',
confirmButtonText: '确认',
closeOnClickOverlay: false,
...options
}
// 创建实例
instance = new DialogConstructor({
propsData: options
})
// 监听事件
instance.$on('confirm', () => {
options.onConfirm && options.onConfirm()
})
instance.$on('cancel', () => {
options.onCancel && options.onCancel()
})
// 挂载
instance.$mount()
document.body.appendChild(instance.$el)
// 显示
Vue.nextTick(() => {
instance.visible = true
})
// 返回关闭方法
return {
close: () => {
instance.visible = false
}
}
}
// 快捷方法
Dialog.alert = (message, options = {}) => {
return Dialog({
message,
showCancelButton: false,
...options
})
}
Dialog.confirm = (message, options = {}) => {
return Dialog({
message,
...options
})
}
// 安装插件
Dialog.install = (Vue) => {
Vue.prototype.$dialog = Dialog
}
export default Dialog
主要特点
同时动画效果:使用 Vue 的 组件,让遮罩层和内容同时执行动画
灵活配置:支持标题、按钮、遮罩层等配置
事件监听:提供 open、close、confirm、cancel 等事件
阻止背景滚动:显示弹框时自动阻止背景滚动
自定义插槽:支持 title 和 footer 插槽自定义内容
这个实现方式确保了遮罩层和内容动画的同步性,用户体验更加流畅。