常规管理系统中,一般都有大量的增删改查操作,而增和改两个操作,除了单独建立页面,很多时候都需要通过对话框+表单的形式进行,如下图:
常规解决方案,就是在需要使用的地方,通过dialog组件嵌套from表单组件实现,以element-ui为例:
// 省略参数配置等
<el-dialog>
<el-form>
<el-form-item></el-form-item>
</el-form>
</el-dialog>
这样使用的问题在于,如果使用的地方很多,那么需要在每个地方都复制同样的代码,并且配置所有的参数,显然更好的方式是做一个封装,新建一个DialogFrom组件:
// 省略参数配置等
<el-dialog>
<el-form>
<el-form-item v-for="item in formItems"
:key="item.key"
:label="item.label"
:prop="item.key">
<!-- 通过判断item的type展示不同的表单项 -->
<el-select v-if="item.type=='select'"
v-model="form[item.key]"
style="width: 100%">
<el-option v-for="op in item.options"
:key="op.value"
:label="op.label"
:value="op.value"></el-option>
</el-select>
...
<el-input v-else
v-model="form[item.key]"
:disabled="item.disabled || (itemData && item.editDisabled)"
:type="item.type || 'text'"
:placeholder="item.placeholder">
</el-input>
</el-form-item>
</el-form>
</el-dialog>
export default {
props: {
visible: {
type: Boolean,
required: true,
},
formItems: {
type: Array,
required: true,
},
itemData: {
type: Object,
},
propTitle: {
type: String,
},
},
emits: ['close', 'dialog-submit'],
setup(props, { emit }) {
const form = reactive({})
const title = ref('编辑')
watch(() => props.visible, () => {
if (props.visible == true) {
if (props.itemData) {
title.value = '编辑'
} else {
title.value = '添加'
}
props.formItems.map((item) => {
if (props.itemData && props.itemData[item.key] !== null) {
form[item.key] = props.itemData[item.key]
} else {
form[item.key] = ''
}
})
} else {
Object.keys(form).map((key) => {
delete form[key]
})
}
})
const close = () => {
emit('close')
}
const ruleForm = ref()
const saveEdit = async () => {
ruleForm.value.validate((validte) => {
if (validte) {
emit('dialog-submit', form)
} else {
ElMessage.warning('请完善表格')
}
})
}
return {
title,
close,
saveEdit,
form,
ruleForm,
}
},
具体实现没有太大难度,核心就在于几个参数:
- visible:在父组件使用时,控制弹框的展示
- formItems:配置表单中需要展示的表单项,这里可以通过传参,对表单项进行各种配置,具体看业务需求:
[
{ label: '用户姓名', key: 'realName', required: true },
{
label: '手机号(登录账号)',
key: 'phone',
required: true,
placeholder: '',
rule: phoneReg
},
]
- itemData: 主要用于编辑时,复现原数据,也需要根据这个参数是否有值,判断当前是编辑还是添加
- propTitle:定制化标题,比如编辑xxx
- 其他需要配置的参数,根据业务需求定制
实际使用中:
<template>
<div>
<DialogForm :visible="editVisible"
@close="editVisible = false"
:itemData="editItemData"
:formItems="formItems"
@dialog-submit="editSubmit"></DialogForm>
</div>
</template>
<script>
import DialogForm from './DialogForm.vue'
export default {
components: {
DialogForm,
},
setup() {
// 表格编辑时弹窗和保存
const editVisible = ref(false)
let editItemData = ref(null)
const handleEdit = (row) => {
editVisible.value = true
editItemData.value = row
}
const handleAdd = () => {
editVisible.value = true
editItemData.value = null
}
const editSubmit = async (row) => {
// 发送api等..
ElMessage.success('操作成功')
editVisible.value = false
}
return {
formItems: [
{ label: '用户姓名', key: 'realName', required: true },
{
label: '手机号(登录账号)',
key: 'phone',
required: true,
placeholder: ''
},
],
editVisible,
editItemData,
handleEdit,
handleAdd,
editSubmit
}
},
}
</script>
这样就完成了第一次封装,在使用时,HTML部分可以少写很多代码。
但是经过一段时间使用,发现如果页面比较多或者一个页面有多个对话框的时候,很多页面都需要去引入组件写HTML,并且维护大量变量,感觉也不是很方便,故继续封装。
这里可以想到elementUI的Message或者Confirm等组件,都是直接在JS中调用,比较方便,参考后思路如下:
- 利用vue3中的hooks的思想,希望在使用中直接通过useDialogForm的方式,获得dialog的vm实例,在各种情况下操作。
具体实现: 新建一个useDialogForm.js,这里建议放入components中
-- components
|-- DialogForm
|-- DialogForm.vue
|-- useDialogForm.js
useDialogForm.js:
// 这里的实现主要参考了elementUI中message等组件的实现
// 需要注意的是跟Vue2中写法略有区别,主要是因为一些api的改变
import {
createApp
} from 'vue'
import DialogForm from './DialogForm.vue'
import installElementPlus from '../plugins/element'
let instance
const useFormDialog = function(config) {
// 通过createApp可以得到一个vue的实例,这里的config约等于传入组件的props
instance = createApp(DialogForm, {
...config
})
// 这里需要注意的是通过createApp是新生成vue实例,跟我们之前挂载在#app上的应用已经没有关系
// 所以并没有注册过elementUI的组件,如果你想在这个组件中使用elementUI的组件,需要重新注册
installElementPlus(instance)
// 生成一个元素用于组件的挂载
let node = document.createElement('div')
let $vm = instance.mount(node)
document.body.appendChild(node)
// 这里注意,最后返回的是挂载到DOM元素之后的实例
// 这样组件中setup等方法才会被真正调用,其中返回的方法我们才能在外层使用
// 可以理解为图纸和真正产品的关系
return $vm
}
export default useFormDialog
另外仔细查看可以发现,这个文件的代码可以用于生成任何组件,其跟DialogForm其实是解耦的。
同时DialogForm.vue也需要做一些改造,这里只展示核心部分代码:
...
// props删除了visible和itemData,visible是因为可以组件自己内部控制,外界通过函数调用即可
// 同时itemData也可以组件内部维护,通过函数传入
// 增加了submitFunc,用于点击确认时的操作
props: {
formItems: {
type: Array,
required: true,
},
submitFunc: {
type: Function,
default: () => {console.log('no submit func')}
},
propTitle: {
type: String,
},
},
setup(props, { emit }) {
// 组件内部自己维护visible,外界通过调用show函数控制对话框的展示
// 同时传入itemData,通过判断是否有值来确认是编辑还是添加
const visible = ref(false)
const itemData = ref(null)
const show = (row) => {
itemData.value = row
visible.value = true
}
// 现在关闭对话框和对话框确认都不需要emit外部事件了,直接内部修改visible即可
const close = () => {
visible.value = false
}
// 点击确认直接调用传入的submitFunc即可,传入表单值
const saveEdit = async () => {
props.submitFunc(formData)
}
...
}
...
至此,改造完成,再来看看使用方式:
<template>
</template>
<script>
import useDialogForm from '@/components/DialogForm/DialogForm.js'
export default {
setup() {
const editDialog = useFormDialog({
formItems:[
{ label: '用户姓名', key: 'realName', required: true },
{
label: '手机号(登录账号)',
key: 'phone',
required: true,
placeholder: '',
rule: phoneReg
},
],
submitFunc: async (formData) => {
// 调用接口等进行其他确认操作
...
editDialog.close()
}
})
const handleEdit = (row) => {
// 传入数据代表是在编辑
editDialog.show(row)
}
const handleAdd = () => {
editDialog.show()
}
...
},
}
</script>
这样看上去就清爽多了,HTML代码可以直接省略,JS中不用再维护每个dialog的展示开关和需要编辑的editData,submitFunc直接写入,也不用定义好再返回出去。
至此,封装就暂时完成了。