CURD页面组件封装——新增/编辑弹窗组件

88 阅读3分钟

前言

对于面向企业的项目而言,前端开发中最常见的场景之一便是后台管理系统,而这类系统的核心页面多为基于表格表单的 CURD(增删改查)操作页面。随着同类页面开发量的增加,封装一套通用的 CURD 功能组件变得尤为必要——这不仅能显著提升开发效率,避免重复性工作,更能为后续的统一修改与维护提供便利。

一个标准的 CURD 页面通常包含以下核心功能模块:搜索条、操作工具条、数据表格、新增 / 编辑弹窗、详情弹窗等。

本方案将采用 “分而治之” 的策略:将每个功能模块封装为独立的基础组件,再通过组件组合的方式快速搭建完整的 CURD 页面。

在之前的文章中已经介绍了搜索条、数据表格功能模块组件的封装,参见文章:

CURD页面组件封装——搜索组件

CURD页面组件封装——数据表格组件

本篇文章将继续介绍新增/编辑弹窗功能模块的封装实现细节。欢迎大家持续关注,并提出宝贵的建议与反馈。

新增/编辑弹窗

新增/编辑弹窗的的主体是一个表单,而表单也可以在非弹窗的场景下使用。因此我们最好也将表单封装为一个独立的组件,然后在弹窗中引用。

表单组件的实现与搜索条组件的实现方案一致,也是采用JSON 配置化方案:通过遍历配置项数组动态生成表单项,每个配置项需明确包含以下核心字段:后端映射字段名(prop)、表单标签(label)、表单类型(type)及更丰富的组件属性配置。

结合业务场景分析,新增/编辑表单中常用的表单控件类型主要有:文本输入框(input)、数字输入框(number)、选择器(select)、文本域(textarea)、时间选择器(time)以及文件上传组件(upload)。因此,组件内部先封装这几类表单控件,通过配置 type 参数指定显示类型。

对于未被满足的特殊表单控件,也可以通过配置插槽的方式实现。

image.png

实现

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>

总结

新增/编辑弹窗组件的核心是表单组件,而表单组件设计思路与搜索条组件一致,也是 “配置化 + 插槽扩展”。