前言
对于面向企业的项目而言,前端开发中最常见的场景之一便是后台管理系统,而这类系统的核心页面多为基于表格表单的 CURD(增删改查)操作页面。随着同类页面开发量的增加,封装一套通用的 CURD 功能组件变得尤为必要——这不仅能显著提升开发效率,避免重复性工作,更能为后续的统一修改与维护提供便利。
一个标准的 CURD 页面通常包含以下核心功能模块:搜索条、操作工具条、数据表格、新增 / 编辑弹窗、详情弹窗等。
本方案将采用 “分而治之” 的策略:将每个功能模块封装为独立的基础组件,再通过组件组合的方式快速搭建完整的 CURD 页面。
在之前的文章中已经介绍了搜索条、数据表格功能模块组件的封装,参见文章:
本篇文章将继续介绍新增/编辑弹窗功能模块的封装实现细节。欢迎大家持续关注,并提出宝贵的建议与反馈。
新增/编辑弹窗
新增/编辑弹窗的的主体是一个表单,而表单也可以在非弹窗的场景下使用。因此我们最好也将表单封装为一个独立的组件,然后在弹窗中引用。
表单组件的实现与搜索条组件的实现方案一致,也是采用JSON 配置化方案:通过遍历配置项数组动态生成表单项,每个配置项需明确包含以下核心字段:后端映射字段名(prop)、表单标签(label)、表单类型(type)及更丰富的组件属性配置。
结合业务场景分析,新增/编辑表单中常用的表单控件类型主要有:文本输入框(input)、数字输入框(number)、选择器(select)、文本域(textarea)、时间选择器(time)以及文件上传组件(upload)。因此,组件内部先封装这几类表单控件,通过配置 type 参数指定显示类型。
对于未被满足的特殊表单控件,也可以通过配置插槽的方式实现。
实现
TsForm表单组件
1.props参数
表单组件对外的参数有三个:data(表单数据对象)、columns(表单配置项数组)、rules(表单校验规则)
2.原生属性透传
v-bind="$attrs"外部属性绑定在<el-form>标签上,调用者可以直接使用 Element Plus 表单的所有原生属性,无需在封装组件中重复声明这些属性。
3.动态表单控件渲染
表单组件最核心的功能是控件的遍历渲染。模板中通过v-for="item in columns"遍历配置数组,生成每个表单控件的容器<el-col>。
<el-col>容器占据的栅格宽度可以通过每个配置项中的span参数指定。也可以通过组件属性透传的cols属性整体设置表单的列数进行布局的控制。
容器内部分为两种类型,一种是配置了slot参数,通过插槽外部自定义表单控件内容;另一种是通过type参数判断渲染哪种已有表单控件类型。
<template>
<div class="ts-form">
<el-form ref="formRef" :model="data" :rules="rules" v-bind="$attrs">
<el-row>
<template v-for="item in columns" :key="item.prop">
<el-col :span="item.span || 24 / ($attrs.cols || 1)">
<!-- 插槽 -->
<slot v-if="item.slot" :name="item.slot" />
<el-form-item v-else :label="item.label + ':'" :prop="item.prop">
<!-- 输入框 -->
<el-input
v-if="!item.type || item.type === 'input'"
v-model.trim="data[item.prop]"
placeholder="请输入"
:clearable="item.clearable ?? true"
:disabled="item.disabled ?? false"
/>
<!-- 其他组件...... -->
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</div>
</template>
<script setup>
const props = defineProps({
data: {
type: Object,
required: true,
default: () => {}
},
columns: {
type: Array,
required: true,
default: () => []
},
rules: {
type: Object,
required: true,
default: () => {}
}
});
const { proxy } = getCurrentInstance();
defineOptions({
inheritAttrs: false
});
const formRef = ref();
// 表单校验方法
function validate() {
return formRef.value.validate();
}
// 重置表单
function resetFields() {
formRef.value.resetFields();
}
// ......
defineExpose({
formRef,
validate,
resetFields
});
</script>
新增/编辑弹窗
在弹窗组件中调用上面的表单组件。
<template>
<el-dialog v-model="model" :title="$attrs.title" :width="$attrs.width ?? '50%'">
<ts-form ref="tsFormRef" v-bind="$attrs">
<template v-for="item in $attrs.columns.filter((item) => item.slot)" #[item.slot]>
<slot :name="item.slot" />
</template>
</ts-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSure">确定</el-button>
</div>
</template>
</el-dialog>
</template>
1.多层级插槽透传
表单组件<ts-form>通过 item.slot 定义了自定义插槽入口,而弹窗组件需要作为「中间层」,将外部传入的插槽内容传递给表单组件。
外部传入的插槽内容对应插入到弹窗中的<slot>标签处,然后弹窗中的<template #[item.slot]>再插入到表单组件的<slot>标签处。
实现方式如下:
<template v-for="item in $attrs.columns.filter((item) => item.slot)" #[item.slot]>
<slot :name="item.slot" />
</template>
调用
以下为新增/编辑弹窗组件的完整调用示例,包含基础配置项与插槽用法。
editColumns为json表单控件配置项数组。
<template>
<!-- 编辑弹窗 -->
<ts-edit-dialog
v-model="editVisible"
:title="editType === 'add' ? '新增' : '编辑'"
:data="editData"
:columns="editColumns"
:rules="editRules"
label-width="120px"
label-position="right"
:width="900"
:cols="1"
@save-data="handleSaveData"
>
<!-- 编辑弹窗中插槽 -->
<template #age>
<el-form-item label="年龄:" prop="age">
<el-select v-model="editData.age" placeholder="请选择">
<el-option label="20-30岁" value="20" />
<el-option label="30-40岁" value="30" />
<el-option label="40-50岁" value="40" />
<el-option label="50-60岁" value="50" />
</el-select>
</el-form-item>
</template>
</ts-edit-dialog>
</template>
<script setup>
// start:新增编辑弹窗相关
const editVisible = ref(false);
const editType = ref('add');
const editData = ref({});
const editColumns = reactive([
{
prop: 'name',
label: '姓名'
},
{
prop: 'gender',
label: '性别',
type: 'select',
options: [
{ value: 0, label: '女' },
{ value: 1, label: '男' }
]
},
{
prop: 'age',
label: '年龄',
slot: 'age'
// type: 'number'
},
{
prop: 'date',
label: '出生日期',
type: 'time',
subType: 'date'
},
{
prop: 'address',
label: '地址',
type: 'textarea',
rows: 4,
span: 24
},
{
prop: 'files',
label: '文件上传',
type: 'upload',
fileTypes: ['zip', 'xlsx'],
template: 'appPlatformInforImport.xlsx',
multiple: true,
limit: 2
}
]);
const editRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
files: [{ required: true, message: '请上传文件', trigger: 'blur' }]
};
/**
* @description: 根据编辑表单配置项生成编辑数据初始值
* @return {*}
*/
function initEditData() {
const data = {};
for (const item of editColumns) {
data[item.prop] = '';
if (item.type == 'upload') {
data[item.prop] = [];
}
}
return data;
}
function handleAdd() {
editType.value = 'add';
editData.value = initEditData();
editVisible.value = true;
}
function handleEdit(row) {
editType.value = 'edit';
editData.value = row;
editVisible.value = true;
}
function handleSaveData() {
console.log('handleSaveData: ', editData.value);
const funcApi = editType.value == 'add' ? addItem : editItem;
funcApi(editData.value).then(() => {
editVisible.value = false;
proxy.$modal.msgSuccess('保存成功!');
});
}
</script>
总结
新增/编辑弹窗组件的核心是表单组件,而表单组件设计思路与搜索条组件一致,也是 “配置化 + 插槽扩展”。