【vue3】嵌套对象数组结构-element实现动态表单点击按钮新增/删除行&列功能

310 阅读3分钟

一、需求背景

之前开发过的动态表单,都是单个按钮点击后在数组中push一个新内容实现尾部插入,这次的需求需要横向和纵向都实现数据插入,即 提供两个push按钮:

按钮一实现纵向插入内容(插入行)

按钮二实现横向插入内容(插入列)

还有一个删除整行的按钮;

每行之间是或的关系,行中每一列是且的关系

出现两行后左侧出现“或”字样,出现两列后用“且”连接

二、期望效果

image.png

三、数据准备

后端提供的数据结构如下所示:

{
    checkItemHolds: [
      {
        fatherItem: [
          {
            name: 'aaaa',
            id: 1
          },
          {
            name: 'bbbb',
            id: 2
          }
        ]
      },
      {
        fatherItem: [
          {
            name: '磁盘只读',
            id: 3
          }
        ]
      }
    ]
  };

四、编码实现

step1:前端数据结构定义

const formData = ref({
  checkItemHolds: []
});

step2:vue中渲染,如果要兼容回显,发接口拿到后端的checkItemHolds进行填充即可,否则是formData里定义的空数组;其实是根据formData.checkItemHolds来渲染数据,然后根据每一项具体的fatherItem渲染el-select的个数,所以需要两个v-for循环;

 <el-form-item
        label="xxxx:"
        prop="checkItemHoldIds"
        :rules="[
          {
            validator: checkItemHoldIdsValidator,
            trigger: 'change'
          }
        ]"
      >
        <el-link @click="addFatherAlertRules" class="holds-text-button">增加行规则</el-link>

        <!-- 若需动态控制左边框 :class="{ 'border-left': formData.checkItemHolds.length > 1 } -->
        <div class="holds-container">
          <div v-for="(item, index) in formData.checkItemHolds" :key="item.id" class="holds-line">
            <p class="label">
              <span>行{{ index + 1 }}:</span>
            </p>
            <div class="center">
              <div v-for="(selectItem, selectIndex) in item.fatherItem" :key="selectIndex" class="center-selects">
                <el-select
                  v-model="item.fatherItem[selectIndex].id"
                  placeholder="请选择"
                  class="select-container"
                  filterable
                >
                  <el-option v-for="item in holdOptions" :key="item.id" :value="item.id" :label="item.name"></el-option>
                </el-select>
                <span v-if="selectIndex !== item.fatherItem.length - 1" class="and-text">且</span>
              </div>
            </div>
            <p class="right-buttons">
              <el-icon @click="addSonAlertRules(index)">
                <CirclePlusFilled />
              </el-icon>
              <el-icon @click="deleteAlertRules(index)">
                <DeleteFilled />
              </el-icon>
            </p>
          </div>
          <span v-if="formData.checkItemHolds.length > 1" class="or-text">或</span>
        </div>
      </el-form-item>

step3:点击“增加行规则”按钮新增行

const addFatherAlertRules = () => {
  // 用户点击新增行规则后拿下拉列表数据
  if (holdOptions.value.length === 0) {
    getHoldOptions(); // 这里是根据业务需求写的代码,因为编辑侧进入时就发请求了,所以这里不需要再次拿el-select的接口,但是新建弹框在点击增加行规则的时候才会去拿option的数据;
  }
  formData.value.checkItemHolds.push({
    fatherItem: [
      {
        name: '',
        id: null
      }
    ]
  });
};

step4:点击“+”号新增列,注意传入的index是当前行的行数,来自第一层for循环

const addSonAlertRules = (index) => {
  formData.value.checkItemHolds[index].fatherItem.push({
    name: '',
    id: null
  });
};

step5:点击“删除”按钮,删除整行,每次删除需要触发该表单的校验,否则编辑态的时候,可能存在为空的select触发了校验,但是该项删除后,校验提示依然存在的情况!!

const deleteAlertRules = (index) => {
  formData.value.checkItemHolds.splice(index, 1);
  // 每次删除触发单条校验 fix校验出现后一直存在的问题
  formRef.value.validateField('checkItemHoldIds', () => {});
};

另外还有自定义校验的代码:

// 自定义校验checkItemHoldIds不为空
const checkItemHoldIdsValidator = (rule, value, callback) => {
  if (formData.value.checkItemHolds.length > 0) {
    const hasNullId = formData.value.checkItemHolds.some((hold) => hold.fatherItem.some((item) => item.id == null));
    if (hasNullId) {
      callback(new Error('存在空值'));
      return;
    }
  }
  callback();
};

scss样式代码:

.holds-container {
  width: 100%;
  border-left: 3px solid rgb(55, 136, 184);
  padding-left: 10px;
  position: relative;
  .holds-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex: 1;
    margin-top: 5px;
    margin-bottom: 12px;
    .label {
      // margin-right: 8px;
      align-self: flex-start;
    }
    .center {
      display: flex;
      flex-wrap: wrap;
      flex: 1;
      // margin-right: 8px;
      .center-selects {
        display: flex;
        align-items: center;
        margin-right: 2px;
        margin-bottom: 5px;
        .select-container {
          flex: 1 1 auto;
          margin-right: 2px;
          width: 238px;
        }
        .and-text {
          flex: 0 0 auto;
          color: rgb(55, 136, 184);
          font-weight: bold;
        }
      }
    }
    .right-buttons {
      display: flex;
      padding-top: 4px; // icon对齐
      gap: 5px;
      font-size: 22px;
      cursor: pointer;
      color: rgb(55, 136, 184);
      align-self: flex-start;
    }
  }
  .or-text {
    position: absolute;
    left: -6px;
    top: 50%;
    transform: translateY(-50%);
    color: rgb(55, 136, 184);
    font-size: 14px;
    font-weight: bold;
    white-space: nowrap;
    background-color: white;
  }
}

中间还遇到了el-select匹配不到值的问题,是因为el-option中,后端把之前的值删了,那el-select就会默认展示id,那肯定是不行的!!!--优化,拿到el-option后,匹配一下formData.value.checkItemHolds也就是后端给的数据中,是不是id和name都在option中存在,不然就把id设置为null,不影响编辑态的展示:

const filterCheckItemHolds = () => {
  // 去除select中匹配不到的情况-case1 id值null
  formData.value.checkItemHolds.forEach((checkItemHold) => {
    checkItemHold.fatherItem = checkItemHold.fatherItem.map((fatherItem) => {
      const hasMatch = holdOptions.value.some((option) => option.id === fatherItem.id);
      return hasMatch ? fatherItem : { ...fatherItem, id: null };
    });
  });
};

五、最终效果

只有一行规则:

image.png

两行规则出现“或”样式

image.png

两列后出现“且”样式

image.png

存在空值校验

image.png