项目组大佬封装的弹窗组件,可以省略大量的标签代码,表单重置,visible管理等重复性工作,特别好用,膜拜大佬,学习记录分享一下:
调用方法的形式来调用:
- 导入方法;
- 将弹窗内的表单组件和个性化的弹窗配置参数传入;
- 在beforeResolve中进行业务请求。
import showOdinDialog from '@/components/OdinDialog'
import DetailForm from './detail-form'
// 新增,在onclick方法中:
showOdinDialog(DetailForm, {
title: '新增',
data: {
// 你希望回显的数据
},
disabled: true,
buttons: {
submit: {
show: false
}
},
beforeResolve: async (formData) => {
// 新增的业务请求
},
beforeClose: async (oldData, getValue) => {
// 关闭弹窗前的回调
}
})
首先,准备一个公用dialog组件:
<template>
<el-dialog
v-model="visible"
title="提示"
:width="width"
:top="top"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="beforeClose"
:custom-class="`odin-dialog ${customClass}`"
destroy-on-close
>
<template #default>
<div
class="odin-dialog__content"
:style="{ height: contentHeight, maxHeight: contentMaxHeight }"
>
<component
:is="formComponent"
ref="formComponentRef"
:disabled="disabled"
:data="data"
@cancel="onCancelCalled"
@submit="onSubmitCalled"
/>
</div>
</template>
<template v-if="buttonsShow" #footer>
<div>
<el-button
v-if="submitButtonShow"
type="primary"
v-bind="{ icon: parseElIcon(submitIcon).name }"
:loading="loading"
@click="onSubmitClick"
>
<svg-icon
v-if="parseSvgIcon(submitIcon).show"
:icon-class="parseSvgIcon(submitIcon).name"
/>
{{ submitButtonLabel }}
</el-button>
<template v-if="customButtons">
<template v-for="(customButton, index) in customButtons" :key="index">
<el-button
v-bind="customButton"
:icon="parseElIcon(customButton.icon).name"
:type="customButton.primary ?? 'primary'"
@click="onCustomButtonClick(customButton)"
>
<svg-icon
v-if="parseSvgIcon(customButton.icon).show"
:icon-class="parseSvgIcon(customButton.icon).name"
/>
{{ customButton.label }}</el-button
>
</template>
</template>
<el-button
v-if="cancelButtonShow"
v-bind="{ icon: parseElIcon(cancelIcon).name }"
@click="close()"
>
<svg-icon
v-if="parseSvgIcon(cancelIcon).show"
:icon-class="parseSvgIcon(cancelIcon).name"
/>
{{ cancelButtonLabel }}
</el-button>
</div>
</template>
</el-dialog>
</template>
弹窗的宽高样式等通过传参调整,内容部分用元组件等待传入子组件,按钮内置了两个通用按钮,提交submit和取消cancel,考虑有其他个性化需求,预留自定义按钮的button。
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
formComponent: {
type: [Object, String],
default: () => 'div'
},
width: {
type: String
},
top: {
type: String
},
bottom: {
type: String
},
height: {
type: String
},
buttonsShow: {
type: Boolean
},
submitButtonShow: {
type: Boolean
},
submitButtonLabel: {
type: String
},
submitIcon: {
type: String
},
cancelButtonShow: {
type: Boolean
},
cancelButtonLabel: {
type: String
},
cancelIcon: {
type: String
},
disabled: {
type: Boolean
},
data: {
type: Object
},
customButtons: {
type: Array
},
customClass: {
type: String
},
beforeClose: {
type: Function,
default: null
}
})
const FOOTER_HEIGHT = '70px'
// 显示双绑
const visible = ref(true)
const emits = defineEmits(['cancel', 'submit', 'customSubmit'])
const loading = ref(false)
const contentHeight = computed(() => {
if (props.height != null) {
return props.height
}
if (props.top == null) {
return 'auto'
}
if (props.bottom != null) {
return `calc(100vh - ${props.top} - ${props.bottom} - ${FOOTER_HEIGHT} - 40px)`
}
return 'auto'
})
const contentMaxHeight = computed(() => {
if (props.height == null || props.top == null || props.bottom == null) {
return `calc(80vh - 110px)`
}
return `calc(100vh - ${props.top} - ${props.bottom} - ${FOOTER_HEIGHT})`
})
const formComponentRef = ref(null)
const close = async (bySubmit = false) => {
if (!bySubmit) {
await props.beforeClose?.(props.data, () => getValue())
emits('cancel')
}
visible.value = false
}
const parseSvgIcon = (str) => {
const [type, name] = (str ?? '').split(':')
const isSvg = type === 'svg'
return { show: isSvg, name: isSvg ? name : null }
}
const parseElIcon = (str) => {
const [type, name] = (str ?? '').split(':')
return { show: type === 'el', name }
}
const validate = async () => {
if (formComponentRef.value.validate) {
await formComponentRef.value.validate()
}
}
const getValue = async () => {
let formData = null
if (
formComponentRef.value.getValue &&
typeof formComponentRef.value.getValue === 'function'
) {
formData = await formComponentRef.value.getValue()
}
return formData
}
const closeBySubmit = () => {
close(true)
}
const showLoading = () => {
loading.value = true
}
const hideLoading = () => {
loading.value = false
}
const onSubmitClick = async () => {
await validate()
const formData = await getValue()
emits('submit', formData, { close: closeBySubmit, showLoading, hideLoading })
}
const onCustomButtonClick = async (customButton) => {
const onButtonClick = customButton.onButtonClick
if (!onButtonClick || typeof onButtonClick !== 'function') {
console.error(
'[OdinDialog] you should set a "onButtonClick" function for customButton'
)
return
}
await onButtonClick({ validate, getValue, close })
}
const onCancelCalled = () => {
close()
}
const onSubmitCalled = () => {
onSubmitClick()
}
const beforeClose = async (done) => {
await props.beforeClose?.(props.data, () => getValue())
emits('cancel')
done()
}
</script>
弹窗组件封装好后,在业务组件中需要能够以调用函数的方式来调用
import { createVNode, render } from 'vue'
import OdinDialog from './odin-dialog'
import { getApp } from '@/utils/app-instance'
import { get } from 'lodash'
const containers = {}
let seed = 1
const parseOptions = (options = {}) => {
const title = get(options, 'title', '')
const width = get(options, 'size.width', '40%')
const height = get(options, 'size.height', null)
const top = get(options, 'position.top', null)
const bottom = get(options, 'position.bottom', null)
const buttonsShow = get(options, 'buttons.show', true)
const submitButtonShow = get(options, 'buttons.submit.show', true)
const submitButtonLabel = get(options, 'buttons.submit.label', '确定')
const submitIcon = get(options, 'buttons.submit.icon', 'el:el-icon-check')
const cancelButtonShow = get(options, 'buttons.cancel.show', true)
const cancelButtonLabel = get(options, 'buttons.cancel.label', '取消')
const cancelIcon = get(options, 'buttons.cancel.icon', 'el:el-icon-close')
const disabled = get(options, 'disabled', false)
const beforeResolve = get(options, 'beforeResolve', null)
const data = get(options, 'data', {})
const customButtons = get(options, 'custom.buttons', null)
const customClass = get(options, 'customClass')
const beforeClose = get(options, 'beforeClose', null)
return {
title,
width,
height,
top,
bottom,
buttonsShow,
submitButtonShow,
submitButtonLabel,
submitIcon,
cancelButtonShow,
cancelButtonLabel,
cancelIcon,
disabled,
beforeResolve,
data,
customButtons,
customClass,
beforeClose
}
}
const getFather = () => {
const fullScreen = document.querySelector(':not(:root):fullscreen')
if (fullScreen) {
return fullScreen
}
return document.querySelector('body')
}
/**
* 显示一个为增删改查专门设计的弹窗组件
* @param {*} component 表单组件
* @param {*} options 配置
* @returns Promise
* @description 更多使用介绍,请移步 ./README.md
*/
export default function showOdinDialog(component, options = {}) {
const { beforeResolve, ...propsOptions } = parseOptions(options)
return new Promise((resolve, reject) => {
const id = 'dialog_' + seed++
const container = document.createElement('div')
containers[id] = container
container.className = `container_${id}`
const father = getFather()
father.appendChild(container)
const clearContainer = () => {
render(null, container)
delete containers[id]
}
const onCancel = () => {
clearContainer()
reject()
}
const onSubmit = async (formData, { close, showLoading, hideLoading }) => {
if (beforeResolve && typeof beforeResolve === 'function') {
showLoading()
try {
const res = await beforeResolve(formData)
close()
clearContainer()
resolve(res)
} catch (err) {
console.error(err)
hideLoading()
}
} else {
close()
clearContainer()
resolve(formData)
}
}
const onCustomSubmit = async () => {
close()
resolve()
}
const vm = createVNode(OdinDialog, {
formComponent: component,
...propsOptions,
onCancel,
onSubmit,
onCustomSubmit
})
vm.appContext = getApp()._context
render(vm, container)
})
}
表单组件结构,对外暴露表单校验规则和取值的方法,取值从props.data中取值
<template>
<el-form>
// 业务代码
</el-form>
<template>
<script setup>
// 定义props,其中data就是默认传入的参数
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// 定义表单结构
const formData = reactive({
name: null
})
//建议按此方式给把prop.data中的值给自持的表单数据
onMounted(() => {
Object.assign(formData, props.data)
})
// 对外暴露的取值方法
const getValue = async () => {
return formData
}
// 对外暴露的校验方法
const validate = async () => {
}
defineExpose({
getValue,
validate
})
</script>
细节待完善...