element 在表格中使用表单校验

1,058 阅读4分钟

先上个图

image.png

上图说明

  1. 看起来是表格包含了很多个表单,其实是表单包含了表格;
  2. 表头是通过插槽自定义的,也可以直接使用表格的 label 属性;
  3. 外层是一个大表格,里面是一个小表格,然后小表格的每一列是根据大表格的主表列里的选项不同,数据不同自动循环出来的,有的是必填,有的是非必填,截图上都是必填;
  4. 小表格可以进行动态添加数据,小表格跟大表格是两个表单,并且分开校验;

element 表单校验理解

  1. el-form 组件中的 model 属性,即绑定校验的外层对象,我们都知道对象里第一层数据的校验,而这个对象里面可能还有对象、数组什么的,怎么去校验被层层包裹住的数据呢,这就是表格校验要过的第一个关卡;
  2. el-form 组件中的 rules 属性,对数据的校验规则对象,它通过 el-form-item 的 prop 属性去找该字段匹配 rules 属性绑定对象的哪一个字段,嵌套层级的话,可以通过 prop 属性体现出来,但是如果嵌套比较烦,我们对 rules 对象的书写又比较麻烦,el-form-item 组件中 也有一个 rules 属性,可以直接传递该字段的校验数组,清晰明了;
  3. 此处即用到了表格单行校验,也用到了全表格校验,全表格校验和正常的表单校验一样,但是单行校验就相对复杂了;
  4. 此处还有一个表格的展开行的坑,就是当我们要默认展开行或者要根据需求去做一些特殊的表格展开的时候,el-table 组件的 row-key 属性一定要在数据中是字符串类型的,不然不会出效果;

上代码

标签部分

    <!-- iq 是自定义的,放到对象里时,一定要转成字符串 -->
    <el-button @click="iq=iq+1,tableForm.tableData.unshift({iq:iq.toString(),manageCom:'',gradeCode:[],isEdit: true,list:[],add:1})"
      :loading="isLoading" type="primary">新增配置
    </el-button>
    <el-form :model="tableForm" ref="tableForm" size="small">
      <el-table :data="tableForm.tableData" ref="tableData" v-loading="isLoading" @expand-change="expandChange"
        :expand-row-keys="expandRowKeys" row-key="iq">
        <el-table-column type="expand">
          <template slot-scope="props">
            <!-- 给每一个小表格动态赋值 ref 属性 -->
            <el-form v-if="props.row.asalaryCode" :model="tableForm.tableData[props.$index]"
              :ref="`tableForm${props.$index}`" size="mini">
              <el-table :data="props.row.list" border default-expand-all>
                <div v-for="(col, index) in salaryArr" :key="index">
                  <!-- 因为是根据 asalaryCode 字段来生成小表格的,所以得先判断 -->
                  <div v-if="col.value == props.row.asalaryCode">
                    <!-- 循环生成小表格表头  :fixed="indexF == 0" 不知为什么在渲染的时候 顺序不对,就加了这段 -->
                    <el-table-column v-for="(fileByte, indexF) in col.fileBytes" :key="indexF" align="center"
                      :fixed="indexF == 0">
                      <!-- 自定义表头部 -->
                      <template slot="header">
                        <span>
                          <span v-if="!fileByte.notRequired" style="color: red; font-size: 16px">*</span>子表列{{indexF++}}
                        </span>
                      </template>

                      <template slot-scope="scope">
                        <!-- prop 属性是组件用来找到指定校验数据的关键,此处比较模糊,放在下面解释 -->
                        <el-form-item :prop="`list.${scope.$index}.${fileByte.model}`"
                          :rules="rules[fileByte.rule || fileByte.model]" style="margin-top:18px">

                          <el-select v-if="fileByte.type == 'select'" v-model="scope.row[fileByte.model]"
                            :disabled="!props.row.isEdit" placeholder="请选择" filterable class="item-width">
                            <!-- 动态获取枚举 -->
                            <el-option v-for="item in getNacosOne('parameterName' + props.row.asalaryCode)"
                              :key="item.value" :label="item.value" :value="item.value">
                            </el-option>
                          </el-select>

                          <el-input v-else-if="fileByte.type == 'input'" v-model.trim="scope.row[fileByte.model]"
                            placeholder="请输入" clearable class="item-width" />
                        </el-form-item>
                      </template>
                    </el-table-column>

                    <el-table-column v-if="props.row.isEdit && tableForm.tableData[props.$index].asalaryCode"
                      width="100" label="操作" align="center">
                      <template slot-scope="scope">
                        <el-popconfirm title="该行将被删除,请确认" @confirm="deleteOne(props.row.list,scope.$index,1)">
                          <el-button slot="reference" type="text">删除</el-button>
                        </el-popconfirm>
                      </template>
                    </el-table-column>
                  </div>
                </div>
              </el-table>
              <div @click="childrenAdd(props.row.list" class="addBtn"
                style="border: 1px solid rgba(24, 144, 255, 1);color: rgba(24, 144, 255, 1);">
                <i class="el-icon-plus"></i>添加
              </div>
            </el-form>
          </template>
        </el-table-column>

        <el-table-column align="center">
          <template slot="header">
            <span>
              <span style="color: red; font-size: 16px">*</span>主表列1
            </span>
          </template>
          <template slot-scope="scope">
            <!-- prop 属性是组件用来找到指定校验数据的关键,用此处的 prop 来举个栗子:tableForm['tableData'][scope.$index]['manageCom'],
            这就是该字段在这个表格中的定位,即通过对 prop 值的拆解,再跟 form 表单的 model 属性结合,就能找到指定数据,再加上 rules,就能进行数据校验 -->
            <!-- 此处 el-form-item 组件的 rules 属性也是为了定位相应的校验规则,直接传,才能对动态表格进行操作 -->
            <el-form-item :prop="`tableData.${scope.$index}.manageCom`" :rules="rules.manageCom"
              style="margin-top:18px">
              <el-cascader v-model="scope.row.manageCom" @change="edit(scope.row,'isEdit')" :options="manageComArr"
                :props="{label:'label',value:'manageComCode',emitPath:false,checkStrictly: true}"
                :show-all-levels="false" placeholder="请选择" filterable class="item-width" />
            </el-form-item>
          </template>
        </el-table-column>

        <el-table-column align="center">
          <template slot="header">
            <span>
              <span style="color: red; font-size: 16px">*</span>主表列2
            </span>
          </template>
          <template slot-scope="scope">
            <el-form-item :prop="`tableData.${scope.$index}.gradeCode`" :rules="rules.gradeCode"
              style="margin-top:18px">
              <el-select v-model="scope.row.gradeCode" placeholder="请选择" collapse-tags multiple clearable filterable
                class="item-width">
                <el-option v-for="item in getNacosOne('agentRank')" :key="item.value"
                  :label="item.value + '-' + item.label" disabled :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
          </template>
        </el-table-column>
        <el-table-column align="center">
          <template slot="header">
            <span>
              <span style="color: red; font-size: 16px">*</span>主表列3
            </span>
          </template>
          <template slot-scope="scope">
            <el-form-item :prop="`tableData.${scope.$index}.asalaryCode`" :rules="rules.asalaryCode"
              style="margin-top:18px">
              <el-select v-model="scope.row.asalaryCode" @change="salaryChange(scope)" clearable placeholder="请选择"
                filterable class="item-width">
                <el-option v-for="item in salaryArrDis" :key="item.value" :label="item.value" :value="item.value">
                </el-option>
              </el-select>
            </el-form-item>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" align="center">
          <template slot-scope="scope">
            <el-button v-if="(scope.row.isEdit || scope.row.isConf) && scope.row.list.length > 0" @click="save(scope)"
              :disabled="scope.row.isLoading" type="text">保存
            </el-button>
            <el-button v-else-if="scope.row.list.length > 0"
              @click="expandChange(scope.row,[1,2]),edit(scope.row,'isEdit')" type="text">配置
            </el-button>
            <!-- 删除大项 -->
            <el-popconfirm title="该薪资项系数配置信息将被删除,请确认" @confirm="deleteOne(tableForm.tableData,scope.$index)"
              :disabled="scope.row.isLoading" style="margin: 0 10px;">
              <el-button slot="reference" type="text">删除</el-button>
            </el-popconfirm>
          </template>
        </el-table-column>
      </el-table>
    </el-form>

js部分

export default {
  data() {
    return {
      manageComArr: [],
      iq: 0,
      // el-form 组件需要用一层对象包裹起来
      tableForm: {
        tableData: [],
      },
      expandRowKeys: [], // 用于展开默认项
      // 校验规则对象
      rules: {
        manageCom: [{ required: true, message: "请选择", trigger: "change" }],
        gradeCode: [
          {
            type: "array",
            required: true,
            message: "请选择",
            trigger: "change",
          },
        ],
        asalaryCode: [{ required: true, message: "请选择", trigger: "change" }],

        stdKey: [{ required: true, message: "请选择", trigger: "change" }],
        minStd: [
          {
            required: true,
            message: "请输入",
            trigger: "blur",
          },
          { validator: checkStd, trigger: "blur" },
        ],
        maxStd: [
          { required: true, message: "请输入", trigger: "blur" },
          { validator: checkStd, trigger: "blur" },
        ],

        stdRatio: [
          { required: true, message: "请输入", trigger: "blur" },
          {
            validator: (rule, value, callback) =>
              value >= 0 ? callback() : callback(new Error("范围大于等于0!")),
            trigger: "blur",
          },
        ],
        secondRatio: [
          { required: true, message: "请输入", trigger: "blur" },
          {
            validator: (rule, value, callback) =>
              value >= 0 ? callback() : callback(new Error("范围大于等于0!")),
            trigger: "blur",
          },
        ],
        stdValue: [
          { required: true, message: "请输入", trigger: "blur" },
          {
            validator: (rule, value, callback) =>
              value >= 0 ? callback() : callback(new Error("请输入整数!")),
            trigger: "blur",
          },
        ],
      },
      // 用于生成小表格的数据
      salaryArr: [
        {
          value: "03",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
        {
          value: "04",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
        {
          value: "05",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
        {
          value: "06",
          fileBytes: [
            { model: "stdKey", type: "select" },
            {
              model: "stdRatio",
              type: "input",
            },
            { model: "stdValue", type: "input" },
          ],
        },
        {
          value: "07",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
        {
          value: "08",
          fileBytes: [
            { model: "stdKey", type: "select" },
            {
              model: "grownDate",
              type: "text",
              notRequired: true,
            },
            { model: "stdRatio", type: "input" },
            {
              model: "secondRatio",
              type: "input",
            },
          ],
        },
        {
          value: "09",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
        {
          value: "10",
          fileBytes: [
            { model: "stdKey", type: "select" },
            { model: "minStd", type: "input" },
            { model: "maxStd", type: "input" },
            { model: "stdRatio", type: "input" },
          ],
        },
      ],
    };
  },
  methods: {
    // 结构为题,我们要保存的是主表中的一行(三个字段)和子表整个表格,所以主表的那三个单独字段的校验就需要做特殊处理
    save(scope) {
      let that = this;
      let validateFieldList = [];
      // 主表格单条数据提交校验 三个字段
      this.$refs.tableForm.validateField(
        [          `tableData.${scope.$index}.manageCom`,          `tableData.${scope.$index}.gradeCode`,          `tableData.${scope.$index}.asalaryCode`,        ],
        async (valid) => {
          validateFieldList.push(valid);
          if (validateFieldList.length == 3) {
            // 校验子表
            that.$refs[`tableForm${scope.$index}`].validate((validI) => {
              // 都成功才成功
              if (validateFieldList.every((item) => item === "") && validI) {
                that.saveReq();
              } else that.$message.error(VALID_ERR_TABLE);
            });
          }
        }
      );
    },
    // 展开行触发事件,主动展开的是哪一行,就把哪一行的 row-key 放到 expandRowKeys 数组中,只要 expandRowKeys 只有一项,那就只展开一项,别的项就会收起
    expandChange(row, expandedRows) {
      this.$set(
        this,
        "expandRowKeys",
        expandedRows.length == 0 ? [] : [row.iq]
      );
    },
    // 子表添加一行
    childrenAdd(tableData) {
      let obj = { isEdit: true, add: 1, stdKey: "" };
      tableData.push(obj);
    },
    // 获取数据后将 iq 放到每一项数据中
    tableThen(res) {
      this.iq = 0;
      res.data.list.forEach((item) => {
        this.iq++;
        item.iq = this.iq.toString(); // row-key="iq" 这个必须是字符串,蛋疼
        if (item.list) item.list.forEach((itemC) => (itemC.iq = this.iq++));
      });
      this.$set(this.tableForm, "tableData", res.data.list);
    },
  },
};

总结

  1. 表格单行保存、编辑呢,一般可以使用 el-table-column 组件插槽中的 $index 就可以判断出要编辑的是哪一行了,不需要在获取到数据之后再手动给每一条数据添加 key 去判断,此处是因为有别的用处;
  2. 表格的单个数据的编辑也可以使用 el-table-column 组件插槽中的 $index 加上字段名称就可定位到修改哪一行的哪个数据;
  3. 此功能的难点在表格单行数据的保存时的数据校验,在校验对象中,也可以加自定义校验规则,不冲突,特别要注意的就是 prop 属性,只要 prop 属性是对的,组件能找到该数据,就能进行规则校验;