1. 需求描述
之前,在写后台模板新增、修改时发现这两个组件内容大致是相似的,恰好最近又在写这种,于是萌生了将这两个组件抽离出去,之后直接调用这个组件就可以使用了。
2. 代码
2.1 公共组件封装
目前使用的技术栈是 vue3 + TS。
大致思路就是将需要的数据以 formData 保存,然后将需要展示的参数内容以 formItems 的参数形式传递过来,然后使用 v-for 遍历展示。
// utils.ts
export const removeExtraCharacterOfNumber = (value: string) => {
return value
.replace(/[^-0-9.]/g, '')
.replace(/^\./g, '')
.replace('.', '$#$')
.replace(/\./g, '')
.replace('$#$', '.')
}
types/index.ts
export interface IFormItem {
type: string
kind?: string
field: string
label: string
width?: number
isNumber?: boolean
placeholder: string
options?: IOptionsItem[]
slotName?: string
}
export interface IPassedDialogProps {
title: string
rules: FormRules
formItems: IFormItem[]
}
export interface INewAndEditedBasic<T> {
new: T
edited: T
}
export interface INewAndEditedRouteParams {
title: string
rules: INewAndEditedBasic<FormRules>
formItems: INewAndEditedBasic<IFormItem[]>
}
<template>
<el-dialog
:model-value="visible"
:title="`${isEdited ? '修改' : '新增'}${title}`"
@close="closeDialog"
width="520px"
:close-on-press-escape="false"
:close-on-click-modal="false"
destroy-on-close
>
<el-form ref="ruleFormRef" :model="formData" :rules="rules" label-width="100px">
<template v-for="(item, index) in formItems" :key="index">
<el-form-item :label="item.label" :label-width="item.width ? item.width : 100" class="form-item" :prop="item.field">
<template v-if="item.type === 'input'">
<el-input
v-model="formData[`${item.field}`]"
:placeholder="item.placeholder"
:type="item.kind ? item.kind : 'text'"
clearable
@input="
item.isNumber ? (formData[`${item.field}`] = handleNumberInput($event)) : null
"
></el-input>
</template>
<template v-else-if="item.type === 'select'">
<el-select
v-model="formData[`${item.field}`]"
:placeholder="item.placeholder"
style="width: 100%"
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</el-select>
</template>
<template v-else-if="item.type === 'slot'">
<slot :name="item.slotName" v-bind="item" />
</template>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
<el-button type="primary" @click="submitForm(ruleFormRef)">确认</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { defineExpose, defineEmits, defineProps, PropType, ref, watch } from 'vue'
import { FormRules, FormInstance } from 'element-plus'
import { removeExtraCharacterOfNumber } from '@/utils/toolFns'
import { IFormItem } from '@/types'
// submit 提交
// 修改弹窗开关 updata:visible
// 修改表单数据 update:formData
const emits = defineEmits(['submit', 'update:visible', 'update:formData'])
const props = defineProps({
// 是否修改
isEdited: { type: Boolean, default: false },
// 弹窗开关
visible: { type: Boolean, default: false },
// 弹窗标题
title: { type: String, default: '' },
// 表单验证规则
rules: {
type: Object as PropType<FormRules>,
default: () => {
return {}
}
},
// 表单信息
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => []
},
// 表单绑定的内容,最后就是把这个提交给后台
formData: {
type: Object,
default: () => ({})
}
})
const ruleFormRef = ref<FormInstance>()
let formData = ref(props.formData)
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emits('update:formData', formData.value)
emits('submit')
// 如果需要在对话框中添加其他判断逻辑时,那么以下代码不能添加,
// 否则不管怎么样,下面的事件执行顺序总是会先于对话框中点击确定里面的判断逻辑(需要向后端发送请求)
// 如果想要使用 Promise 异步操作,在上述 submit 执行结束后再根据条件是否选择执行下述代码,
// 会发现第一次触发逻辑后不管成功与否还是会关闭对话框,这种方式暂未找到最优解
// 目前采用的方式是在执行完对话框中的确认逻辑后手动关闭
// emits('update:visible', false)
}
})
}
// 重置表单
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
// 关闭弹窗
const closeDialog = () => {
emits('update:visible', false)
resetForm(ruleFormRef.value)
}
watch(
() => props.formData,
(val) => {
formData.value = val
}
)
// 由于输入时可能会有数字,所以需要对数字做处理
const handleNumberInput = (inputValue: string) => {
return removeExtraCharacterOfNumber(inputValue)
}
</script>
<style lang="scss" scoped></style>
2.2 父组件的使用
// index.ts
const newPreApplicationFormItems = [
{
type: 'input',
field: 'code',
label: '编号',
placeholder: '请输入编号'
},
{
type: 'input',
field: 'name',
label: '名称',
placeholder: '请输入名称'
}
]
const editedPreApplicationFormItems = [
{ type: 'input', field: 'name', label: '名称', placeholder: '请输入名称' }
]
const newAndEditedFormData: INewAndEditedRouteParams = {
title: '预申请类型',
rules: {
new: newPreApplicationTypeRule,
edited: editedPreApplicationTypeRule
},
formItems: {
new: [
{
type: 'input',
field: 'code',
label: '编号',
placeholder: '请输入编号'
},
{
type: 'input',
field: 'name',
label: '名称',
placeholder: '请输入名称'
}
],
edited: [{ type: 'input', field: 'name', label: '名称', placeholder: '请输入名称' }]
}
}
<template>
...
<el-table-column
v-if="rightsBtn.modify || rightsBtn.delete"
fixed="right"
label="操作"
align="center"
width="180"
>
<template #default="scope">
<tool-tip v-if="rightsBtn.modify" content="修改">
<template #cmp>
<el-button
class="primary-bg-btn"
type="primary"
icon="Edit"
size="small"
@click="editType(scope.row)"
/>
</template>
</tool-tip>
<tool-tip v-if="rightsBtn.delete" content="删除">
<template #cmp>
<el-button
class="danger-bg-btn"
type="danger"
icon="Delete"
size="small"
@click="deleteType(scope.row)"
/>
</template>
</tool-tip>
</template>
</el-table-column>
...
<PageModel
ref="pageContentRef"
v-bind="modelConfig"
:is-edited="isEdited"
:visible="addFormDialogVisible"
:form-data="addSignFormData"
@submit="confirmSubmit"
@update:visible="updateDialogVisible"
@update:form-data="updateFormData"
></PageModel>
</template>
<script setup lang="ts">
import newAndEditedFormData from './index'
import PageModel from '@/components/ComposedPage/PageModel.vue'
const isEdited = ref(false)
const addSignFormDefaultData = ref<INewDefaultForm>({
name: '',
code: ''
})
const addSignFormData = ref<INewDefaultForm>({
name: '',
code: ''
})
// 配置参数
const modelConfig = reactive<IPassedDialogProps>({
title: '',
rules: {},
formItems: []
})
const addFormDialogVisible = ref(false)
const addType = () => {
isEdited.value = false
formDialogVisible(false)
}
const editType = (row: ICommonNameId) => {
isEdited.value = true
const { id, name } = row
formDialogVisible(true, { id, name })
}
// 由于我的项目中是5、6中类型页面共用此组件,需要根据路由跳转来判断当前页面是哪种类型,
// 从而在 activated 周期中需要重置,因此将此代码抽离出来,如果只要一个页面用来此组件,可以不将代码抽离出来
const resetModelConfig = (isEdited: boolean, newAndEditedFormData: INewAndEditedRouteParams) => {
modelConfig.title = newAndEditedFormData!.title
modelConfig.formItems = isEdited ? newAndEditedFormData!.formItems['edited'] : newAndEditedFormData!.formItems['new']
modelConfig.rules = isEdited ? newAndEditedFormData!.rules['edited'] : newAndEditedFormData!.rules['new']
}
// 根据条件的不同从而渲染不同的数据
const formDialogVisible = (isEdited: boolean, item?: any) => {
if (isEdited) {
addSignFormData.value = { ...item }
} else {
addSignFormData.value = { ...addSignFormDefaultData.value }
}
resetModelConfig(isEdited, newAndEditedFormData)
addFormDialogVisible.value = true
}
const updateDialogVisible = (payload: boolean) => {
addFormDialogVisible.value = payload
}
const updateFormData = (submitData: any) => {
addSignFormData.value = submitData
}
const confirmSubmit = async () => {
let result: IDataType<null>
if (isEdited.value) {
result = await editTypeManagement(addSignFormData.value as any)
} else {
result = await createTypeManagement(addSignFormData.value)
}
if (result.code !== 200) return error(result.message)
success(result.message)
queryList()
}
</script>
代码大致就是这样,后续还会继续优化此代码,加入其他的一些边界判断。此外,表格内容也可以抽成组件,后续有时间会将 el-table 进行二次封装。