实现类似van-dialog自定义弹框

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 插槽自定义内容

这个实现方式确保了遮罩层和内容动画的同步性,用户体验更加流畅。

相关推荐
KLW752 小时前
vue3中操作样式的变化
前端·javascript·vue.js
天天讯通2 小时前
BI 报表:呼叫中心的伪刚需
大数据·前端·数据库
WebInfra2 小时前
Midscene v1.0 发布 - 视觉驱动,UI 自动化体验跃迁
javascript·人工智能·测试
自由与自然2 小时前
栅格布局常用用法
开发语言·前端·javascript
Violet_YSWY2 小时前
讲一下ruoyi-vue3的前端项目目录结构
前端·javascript·vue.js
这是你的玩具车吗2 小时前
转型成为AI研发工程师之路
前端·ai编程
Drift_Dream3 小时前
在Vue样式中使用JavaScript 变量(CSS 变量注入)
前端
C_心欲无痕3 小时前
vue3 - toRaw获取响应式对象(如由reactive创建的)的原始对象
前端·javascript·vue.js
PlankBevelen3 小时前
手搓实现简易版 Vue2 响应式系统
前端