Dialog 弹窗和抽屉组件

概述

Dialog 组件是一套基于 Element Plus 的弹窗和抽屉组件封装,提供统一的 API 和样式,支持对话框和抽屉两种模式。通过简单的配置即可快速创建弹窗或抽屉界面。

快速开始

1. 引入组件

import { useDialog } from '@/components/dialog'

2. 创建弹窗

const [Dialog, dialogApi] = useDialog({
  title: '弹窗标题',
  width: '50%',
  onConfirm: () => {
    // 确定按钮回调
  },
  onCancel: () => {
    // 取消按钮回调
  }
})

3. 在模板中使用

<template>
  <Dialog>
    <div>弹窗内容</div>
  </Dialog>
</template>

4. 控制弹窗显示

const show = () => {
  dialogApi.open()
}

const hide = () => {
  dialogApi.close()
}

核心概念

useDialog 配置参数

属性类型默认值说明
titlestring'操作'弹窗标题
customClassstring'm-dialog'自定义样式类名
destroyOnClosebooleantrue关闭时销毁弹窗内容
closeOnClickModalbooleanfalse点击遮罩层关闭弹窗
closeOnPressEscapebooleantrue按 ESC 键关闭弹窗
widthstring | number'60%'弹窗宽度
fullscreenbooleanfalse是否全屏
showClosebooleantrue是否显示关闭按钮
loadingbooleanfalse内容区域加载状态
confirmLoadingbooleanfalse确定按钮加载状态
showConfirmButtonbooleantrue是否显示确定按钮
showCancelButtonbooleantrue是否显示取消按钮
footerbooleantrue是否显示底部操作栏
onConfirm() => void-确定按钮回调函数
onCancel() => void-取消按钮回调函数
dialogType'dialog' | 'drawer''dialog'弹窗类型
direction'ltr' | 'rtl' | 'ttb' | 'btt''rtl'抽屉打开方向
modalbooleantrue是否需要遮罩层

dialogApi 方法

通过 useDialog返回的 dialogApi 提供以下方法:

方法名参数返回值说明
open-void打开弹窗
close-void关闭弹窗
setStatestate: MyDialogPropsPromise<void>动态设置弹窗属性
getState-Promise<MyDialogProps>获取弹窗当前属性

使用示例

基础弹窗

<template>
  <div>
    <el-button @click="showDialog">打开弹窗</el-button>
    <Dialog>
      <div style="padding: 20px">
        <p>这是弹窗内容</p>
      </div>
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { useDialog } from '@/components/dialog'

const [Dialog, dialogApi] = useDialog({
  title: '基础弹窗',
  width: '400px',
  onConfirm: () => {
    console.log('点击了确定')
    dialogApi.close()
  },
  onCancel: () => {
    console.log('点击了取消')
    dialogApi.close()
  }
})

const showDialog = () => {
  dialogApi.open()
}
</script>

抽屉模式

<template>
  <div>
    <el-button @click="showDrawer">打开抽屉</el-button>
    <Dialog>
      <div style="padding: 20px">
        <p>这是抽屉内容</p>
        <p>抽屉从右侧滑入</p>
      </div>
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { useDialog } from '@/components/dialog'

const [Dialog, dialogApi] = useDialog({
  title: '抽屉示例',
  dialogType: 'drawer',
  direction: 'rtl', // 从右侧滑入
  width: '400px',
  onConfirm: () => {
    console.log('点击了确定')
    dialogApi.close()
  }
})

const showDrawer = () => {
  dialogApi.open()
}
</script>

表单弹窗

<template>
  <div>
    <el-button @click="showFormDialog">打开表单弹窗</el-button>
    <Dialog>
      <BasicForm ref="formRef" />
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { useDialog } from '@/components/dialog'
import { useForm } from '@/components/form'

const formSchema = [
  {
    prop: 'name',
    label: '姓名',
    component: 'Input',
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
  },
  {
    prop: 'email',
    label: '邮箱',
    component: 'Input'
  }
]

const [BasicForm, formApi] = useForm(formSchema)
const [Dialog, dialogApi] = useDialog({
  title: '新增用户',
  width: '500px',
  confirmLoading: false,
  onConfirm: () => {
    handleSubmit()
  }
})

const showFormDialog = () => {
  dialogApi.open()
  // 重置表单
  nextTick(() => {
    formApi.resetFields()
  })
}

const handleSubmit = () => {
  dialogApi.setState({ confirmLoading: true })
  formApi
    .validate()
    .then(async () => {
      const values = await formApi.getValues()
      console.log('表单数据:', values)
      // 这里可以调用接口保存数据
      dialogApi.close()
    })
    .catch(() => {
      console.log('表单验证失败')
    })
    .finally(() => {
      dialogApi.setState({ confirmLoading: false })
    })
}
</script>

详情抽屉

<template>
  <div>
    <el-button @click="showDetailDrawer">查看详情</el-button>
    <Dialog>
      <DetailForm :model="detailData" />
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { useDialog } from '@/components/dialog'
import { useDetailForm } from '@/components/form'

const detailSchema = [
  {
    prop: 'name',
    label: '姓名',
    component: 'Input'
  },
  {
    prop: 'email',
    label: '邮箱',
    component: 'Input'
  }
]

const detailData = ref({
  name: '张三',
  email: 'zhangsan@example.com'
})

const [DetailForm] = useDetailForm(detailSchema)
const [Dialog, dialogApi] = useDialog({
  title: '用户详情',
  dialogType: 'drawer',
  direction: 'rtl',
  width: '400px',
  footer: false // 不显示底部操作栏
})

const showDetailDrawer = () => {
  dialogApi.open()
}
</script>

动态控制弹窗属性

<template>
  <div>
    <el-button @click="showDialog">打开弹窗</el-button>
    <Dialog>
      <div style="padding: 20px">
        <el-button @click="toggleFullscreen">切换全屏</el-button>
        <el-button @click="changeTitle">修改标题</el-button>
        <el-button @click="showLoading">显示加载</el-button>
      </div>
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { useDialog } from '@/components/dialog'

const [Dialog, dialogApi] = useDialog({
  title: '动态控制弹窗',
  width: '500px'
})

const showDialog = () => {
  dialogApi.open()
}

const toggleFullscreen = () => {
  dialogApi.getState().then((state) => {
    dialogApi.setState({
      fullscreen: !state.fullscreen
    })
  })
}

const changeTitle = () => {
  dialogApi.setState({
    title: '修改后的标题'
  })
}

const showLoading = () => {
  dialogApi.setState({
    loading: true
  })
  
  // 2秒后关闭加载状态
  setTimeout(() => {
    dialogApi.setState({
      loading: false
    })
  }, 2000)
}
</script>

插槽使用

Dialog 组件提供多个插槽来自定义底部操作栏:

<template>
  <Dialog>
    <div style="padding: 20px">
      <p>弹窗内容</p>
    </div>
    
    <!-- 自定义底部操作栏 -->
    <template #footer>
      <el-button @click="handleCustomAction">自定义操作</el-button>
      <el-button @click="dialogApi.close">取消</el-button>
      <el-button type="primary" @click="handleConfirm">确定</el-button>
    </template>
    
    <!-- 或者使用特定位置的插槽 -->
    <template #prepend-footer>
      <el-button>插入到最前面</el-button>
    </template>
    
    <template #center-footer>
      <el-button>插入到中间</el-button>
    </template>
    
    <template #append-footer>
      <el-button>插入到最后面</el-button>
    </template>
  </Dialog>
</template>

可用插槽列表:

插槽名说明
default弹窗主要内容区域
footer底部操作栏整体
prepend-footer底部操作栏前面
center-footer底部操作栏中间
append-footer底部操作栏后面

完整表单弹窗示例

<template>
  <Dialog>
    <BasicForm :model="formModel">
      <!-- 可以使用插槽自定义某些字段 -->
      <template #customField="{ model }">
        <el-input v-model="model.customField" placeholder="自定义字段" />
      </template>
    </BasicForm>
  </Dialog>
</template>

<script setup lang="ts">
import { useForm } from '@/components/form'
import { useDialog } from '@/components/dialog'

const record = ref({})
const updateFlag = ref(false)
const formModel = reactive<any>({})

const formSchema = [
  {
    prop: 'name',
    label: '姓名',
    component: 'Input',
    colSpan: 12,
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
  },
  {
    prop: 'email',
    label: '邮箱',
    component: 'Input',
    colSpan: 12
  }
]

const [BasicForm, formApi] = useForm(formSchema)
const [Dialog, dialogApi] = useDialog({
  title: '用户管理',
  destroyOnClose: true,
  onConfirm: () => {
    handleSubmit()
  }
})

const show = ({ data, isUpdate }: { data: any; isUpdate: boolean }) => {
  updateFlag.value = isUpdate === true
  record.value = data || {}
  dialogApi.open()
  dialogApi.setState({
    title: updateFlag.value ? '修改用户' : '新增用户'
  })
  nextTick(() => {
    formApi.setValues(record.value)
  })
}

const handleSubmit = () => {
  dialogApi.setState({ loading: true, confirmLoading: true })
  formApi
    .validate()
    .then(async () => {
      const values = await formApi.getValues()
      console.log(values)
      // 在这里处理提交逻辑
      dialogApi.close()
    })
    .finally(() => {
      dialogApi.setState({ loading: false, confirmLoading: false })
    })
}

defineExpose({
  show
})
</script>

注意事项

  1. 使用useDialog 创建的弹窗组件需要通过 dialogApi.open() 方法手动打开
  2. 弹窗关闭时默认会销毁内容,如需保留状态可设置 destroyOnClose: false
  3. 抽屉模式通过设置 dialogType: 'drawer' 启用
  4. 可以通过 setState 方法动态修改弹窗属性
  5. 确定和取消按钮的回调需要手动处理弹窗关闭逻辑
  6. 表单弹窗建议在打开时重置表单状态
  7. 加载状态可通过 loading(内容区域)和 confirmLoading(确定按钮)分别控制