海口市本地咨询网

vue使用h函数封装dialog组件(以命令的形式使用dialog组件)

2026-03-27 09:44:01 浏览次数:1
详细信息

1. 创建 Dialog 组件

src/components/CommandDialog.vue

<template>
  <transition name="fade">
    <div v-if="visible" class="command-dialog-overlay" @click.self="handleOverlayClick">
      <div class="command-dialog" :style="dialogStyle">
        <div class="command-dialog-header">
          <h3>{{ title }}</h3>
          <button class="close-btn" @click="handleClose">&times;</button>
        </div>
        <div class="command-dialog-body">
          <div v-if="typeof content === 'string'" v-html="content"></div>
          <component v-else-if="typeof content === 'object'" :is="content"></component>
          <div v-else>
            {{ content }}
          </div>
        </div>
        <div class="command-dialog-footer">
          <button v-for="btn in buttons" 
                  :key="btn.text"
                  :class="btn.className || 'default-btn'"
                  @click="() => handleButtonClick(btn)"
                  :style="btn.style">
            {{ btn.text }}
          </button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  title: {
    type: String,
    default: '提示'
  },
  content: {
    type: [String, Object],
    default: ''
  },
  width: {
    type: String,
    default: '400px'
  },
  buttons: {
    type: Array,
    default: () => [
      { text: '确定', className: 'primary-btn', handler: 'confirm' }
    ]
  },
  showClose: {
    type: Boolean,
    default: true
  },
  closeOnClickOverlay: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['close', 'confirm', 'cancel'])

const visible = ref(false)

const dialogStyle = computed(() => ({
  width: props.width
}))

const show = () => {
  visible.value = true
}

const hide = () => {
  visible.value = false
  emit('close')
}

const handleClose = () => {
  hide()
  emit('cancel')
}

const handleOverlayClick = () => {
  if (props.closeOnClickOverlay) {
    handleClose()
  }
}

const handleButtonClick = (btn) => {
  if (btn.handler === 'confirm') {
    emit('confirm')
    hide()
  } else if (btn.handler === 'cancel') {
    emit('cancel')
    hide()
  } else if (typeof btn.handler === 'function') {
    const result = btn.handler()
    if (result !== false) {
      hide()
    }
  } else {
    hide()
  }
}

defineExpose({
  show,
  hide
})
</script>

<style scoped>
.command-dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
}

.command-dialog {
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  overflow: hidden;
}

.command-dialog-header {
  padding: 16px 20px;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.command-dialog-header h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #999;
  padding: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.close-btn:hover {
  color: #333;
}

.command-dialog-body {
  padding: 20px;
  max-height: 60vh;
  overflow-y: auto;
}

.command-dialog-footer {
  padding: 16px 20px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.default-btn, .primary-btn {
  padding: 8px 16px;
  border-radius: 4px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  font-size: 14px;
}

.primary-btn {
  background: #409eff;
  border-color: #409eff;
  color: white;
}

.primary-btn:hover {
  background: #66b1ff;
}

.default-btn:hover {
  background: #f5f7fa;
}

/* 动画效果 */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

.fade-enter-active .command-dialog,
.fade-leave-active .command-dialog {
  transition: transform 0.3s, opacity 0.3s;
}

.fade-enter-from .command-dialog,
.fade-leave-to .command-dialog {
  transform: translateY(-20px);
  opacity: 0;
}
</style>

2. 创建命令式 Dialog 管理工具

src/utils/dialog.js

import { createApp, h } from 'vue'
import CommandDialog from '@/components/CommandDialog.vue'

class DialogManager {
  constructor() {
    this.instance = null
    this.container = null
    this.currentDialog = null
  }

  // 创建 Dialog 实例
  createDialog(options) {
    return new Promise((resolve, reject) => {
      if (this.currentDialog) {
        this.currentDialog.hide()
      }

      // 创建容器
      const container = document.createElement('div')
      document.body.appendChild(container)

      // 创建 Vue 应用
      const app = createApp({
        setup() {
          const dialogRef = ref(null)

          const onClose = () => {
            resolve({ action: 'close' })
            destroyDialog()
          }

          const onConfirm = () => {
            resolve({ action: 'confirm' })
            destroyDialog()
          }

          const onCancel = () => {
            resolve({ action: 'cancel' })
            destroyDialog()
          }

          const showDialog = () => {
            nextTick(() => {
              dialogRef.value?.show()
            })
          }

          onMounted(() => {
            showDialog()
          })

          // 销毁对话框
          const destroyDialog = () => {
            setTimeout(() => {
              app.unmount()
              if (container.parentNode) {
                container.parentNode.removeChild(container)
              }
              this.currentDialog = null
            }, 300)
          }

          return () => h(CommandDialog, {
            ref: dialogRef,
            ...options,
            onClose,
            onConfirm,
            onCancel
          })
        }
      })

      this.currentDialog = app.mount(container)
      this.instance = app
      this.container = container
    })
  }

  // 快捷方法:确认对话框
  async confirm(options) {
    const defaultOptions = {
      title: '确认',
      content: options.content || '确定执行此操作吗?',
      buttons: [
        { text: '取消', className: 'default-btn', handler: 'cancel' },
        { text: '确定', className: 'primary-btn', handler: 'confirm' }
      ]
    }

    const result = await this.createDialog({
      ...defaultOptions,
      ...options
    })

    return result.action === 'confirm'
  }

  // 快捷方法:提示对话框
  async alert(options) {
    const defaultOptions = {
      title: '提示',
      content: options.content || '',
      buttons: [
        { text: '确定', className: 'primary-btn', handler: 'confirm' }
      ]
    }

    await this.createDialog({
      ...defaultOptions,
      ...options
    })
  }

  // 快捷方法:自定义内容对话框
  async custom(options) {
    return await this.createDialog(options)
  }
}

// 创建单例
const dialogManager = new DialogManager()

// 导出命令式 API
export const $dialog = {
  /**
   * 确认对话框
   * @param {Object|String} options - 配置对象或内容字符串
   */
  confirm(options) {
    if (typeof options === 'string') {
      return dialogManager.confirm({ content: options })
    }
    return dialogManager.confirm(options)
  },

  /**
   * 提示对话框
   * @param {Object|String} options - 配置对象或内容字符串
   */
  alert(options) {
    if (typeof options === 'string') {
      return dialogManager.alert({ content: options })
    }
    return dialogManager.alert(options)
  },

  /**
   * 自定义对话框
   * @param {Object} options - 配置对象
   */
  custom(options) {
    return dialogManager.custom(options)
  },

  /**
   * 复杂对话框(使用组件)
   * @param {Object} options - 配置对象
   */
  component(options) {
    return dialogManager.custom({
      ...options,
      content: options.component
    })
  }
}

// 作为 Vue 插件安装
export default {
  install(app) {
    app.config.globalProperties.$dialog = $dialog
    app.provide('dialog', $dialog)
  }
}

3. 注册插件

main.js

import { createApp } from 'vue'
import App from './App.vue'
import dialogPlugin from './utils/dialog'

const app = createApp(App)

// 安装 Dialog 插件
app.use(dialogPlugin)

app.mount('#app')

4. 使用示例

4.1 在组件中使用

<template>
  <div>
    <button @click="showConfirmDialog">确认对话框</button>
    <button @click="showAlertDialog">提示对话框</button>
    <button @click="showCustomDialog">自定义对话框</button>
    <button @click="showComponentDialog">组件对话框</button>
  </div>
</template>

<script setup>
import { inject, h } from 'vue'

// 方法1:通过 inject
const dialog = inject('dialog')

// 方法2:通过全局属性(在模板中直接使用 $dialog)

const showConfirmDialog = async () => {
  const result = await dialog.confirm({
    title: '删除确认',
    content: '确定要删除这条数据吗?',
    width: '500px'
  })

  if (result) {
    console.log('用户点击了确定')
    // 执行删除操作
  } else {
    console.log('用户点击了取消')
  }
}

const showAlertDialog = async () => {
  await dialog.alert({
    title: '操作成功',
    content: '数据已保存成功!',
    buttons: [
      { text: '知道了', className: 'primary-btn' }
    ]
  })
}

const showCustomDialog = async () => {
  const result = await dialog.custom({
    title: '自定义操作',
    content: '<p>请选择操作:</p>',
    buttons: [
      { text: '选项一', className: 'default-btn' },
      { text: '选项二', className: 'default-btn' },
      { text: '选项三', className: 'primary-btn' }
    ]
  })

  console.log('用户操作:', result)
}

// 使用组件作为内容
const CustomContent = {
  setup() {
    const count = ref(0)

    return () => h('div', [
      h('p', '这是一个自定义组件'),
      h('p', `计数: ${count.value}`),
      h('button', {
        onClick: () => count.value++
      }, '增加计数')
    ])
  }
}

const showComponentDialog = async () => {
  await dialog.component({
    title: '组件对话框',
    component: CustomContent,
    width: '600px',
    buttons: [
      { text: '关闭', className: 'default-btn' }
    ]
  })
}
</script>

4.2 在 Options API 中使用

export default {
  methods: {
    async showDialog() {
      const result = await this.$dialog.confirm('确定要删除吗?')
      if (result) {
        // 处理确认逻辑
      }
    }
  }
}

5. TypeScript 支持

src/types/dialog.d.ts

declare module 'vue' {
  interface ComponentCustomProperties {
    $dialog: {
      confirm(options: DialogOptions | string): Promise<boolean>
      alert(options: DialogOptions | string): Promise<void>
      custom(options: DialogOptions): Promise<{ action: string }>
      component(options: ComponentDialogOptions): Promise<{ action: string }>
    }
  }
}

interface DialogOptions {
  title?: string
  content?: string | object
  width?: string
  buttons?: DialogButton[]
  showClose?: boolean
  closeOnClickOverlay?: boolean
}

interface ComponentDialogOptions extends DialogOptions {
  component: any
}

interface DialogButton {
  text: string
  className?: string
  style?: Record<string, string>
  handler?: 'confirm' | 'cancel' | Function
}

export {}

特性说明

命令式调用:无需在模板中声明,直接通过 API 调用 Promise 支持:所有方法返回 Promise,方便处理用户操作 灵活的按钮配置:支持自定义按钮和回调 组件支持:可以使用 Vue 组件作为对话框内容 动画效果:内置渐入渐出动画 响应式设计:支持移动端适配 Typescript 支持:完整的类型提示

这个实现使用了 Vue 3 的 Composition API 和 h 函数,提供了完整的命令式 Dialog 解决方案。

相关推荐