手把手教你用 Element Plus 实现动态阶梯表单校验

778 阅读2分钟

💻 背景介绍

在今天评审中,产品提出了一个输入“阶梯规则”的功能;用户可以动态新增或删除多个阶梯,每个阶梯包含两个字段:

  • 条件值(conditionValue):必填,且要求下一行的值小于上一行;
  • 比例值(ration):必填,且不能大于100%;

当点击提交按钮时,我们需要对每一个输入项做一下校验:

  • 所有输入项必须填写,不能为空;
  • 阶梯条件值不能重复;
  • 条件值必须按从大到小排序;

设计效果大致如下图所示:

image.png

🎯 实现思路

我们使用Element Plus<el-form>表单组件来实现校验逻辑,同时需要注意一下几点:

  • 多行表单结构:通过v-for渲染stepData数组,动态生成每一行阶梯s项;
  • 嵌套校验规则:每一行都嵌套一个el-form-item,设置自定义校验逻辑;
  • 清空标签label:由于多行为结构化展示,我们将主表单项的label置空;
  • 表单规则配置:包含是否必填、是否重复、是否递减校验等;

🧩 实现代码

<template>

<el-form :model="stepData" ref="formRef" class="config-form">
    <el-form-item
      v-for="(item, index) in stepData"
      :key="index"
      class="step-item"
      label=""
    >
      <div class="rule-row">
        <div class="input-group">
          <span class="label-text">当条件值大于</span>
          <el-form-item
            :prop="`[${index}].conditionValue`"
            :rules="[
              {
                required: true,
                message: '请输入',
                trigger: ['blur', 'change'],
              },
              {
                validator: validateDuplicate,
                trigger: ['blur', 'change']
              },
              {
                validator: validateDescending,
                trigger: ['blur', 'change']
              }
            ]"
            class="inline-form-item"
          >
            <el-input
              v-model="stepData[index].conditionValue"
              placeholder="请输入"
              class="custom-input"
              @input="
                stepData[index].conditionValue = stepData[index].conditionValue
                  .replace(/[^-0-9]/g, '')
                  .replace(/-+/g, '-')
                  .replace(/^(-?)0+(\d+)$/, '$1$2')
              "
            />
          </el-form-item>
          <span class="label-text">时,对应比例为</span>
          <el-form-item
            :prop="`[${index}].ratio`"
            :rules="[
              {
                required: true,
                message: '请输入',
                trigger: ['blur', 'change'],
              }
            ]"
            class="inline-form-item"
          >
            <el-input
              v-model="stepData[index].ratio"
              placeholder="请输入"
              class="custom-input"
              @input="
                stepData[index].ratio = stepData[index].ratio
                  .replace(/[^0-9]/g, '')
                  .replace(/^0+(\d+)/, '$1')
                  .replace(/^(\d{1,3}).*$/, (match, num) =>
                    Number(num) > 100 ? '' : num
                  )
              "
            />
          </el-form-item>
          <span class="label-text">%</span>
        </div>
        <el-button 
          v-if="index !== 0" 
          type="danger" 
          link
          @click="handleDelete(index)"
          class="delete-btn"
        >
          删除
        </el-button>
      </div>
    </el-form-item>
    <div class="action-bar">
      <el-button 
        type="primary" 
        @click="handleAdd"
        :disabled="stepData.length >= 5"
        class="action-btn"
      >
        新增阶梯
      </el-button>
      <el-button 
        type="primary" 
        @click="handleSubmit"
        class="action-btn submit-btn"
      >
        提交
      </el-button>
    </div>
  </el-form>
</template>


<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const formRef = ref(null)
const stepData = ref([
  { conditionValue: "", ratio: "" }
])

// 验证是否有重复的条件值
const validateDuplicate = (rule, value, callback) => {
  if (!value) {
    callback()
    return
  }
  
  const currentIndex = Number(rule.field.match(/\[(\d+)\]/)[1])
  const duplicateIndex = stepData.value.findIndex((item, index) => 
    index !== currentIndex && item.conditionValue === value
  )
  
  if (duplicateIndex !== -1) {
    callback(new Error('条件值不能重复'))
  } else {
    callback()
  }
}

// 验证是否按从大到小排列
const validateDescending = (rule, value, callback) => {
  if (!value) {
    callback()
    return
  }

  const currentIndex = Number(rule.field.match(/\[(\d+)\]/)[1])
  const currentValue = Number(value)
  
  // 检查与前一个值的关系(如果不是第一个)
  if (currentIndex > 0) {
    const prevValue = Number(stepData.value[currentIndex - 1].conditionValue)
    if (!isNaN(prevValue) && currentValue >= prevValue) {
      callback(new Error('条件值必须小于上一行的值'))
      return
    }
  }
  
  // 检查与后一个值的关系(如果不是最后一个)
  if (currentIndex < stepData.value.length - 1) {
    const nextValue = Number(stepData.value[currentIndex + 1].conditionValue)
    if (!isNaN(nextValue) && currentValue <= nextValue) {
      callback(new Error('条件值必须大于下一行的值'))
      return
    }
  }
  
  callback()
}

// 新增行
const handleAdd = () => {
  if (stepData.value.length >= 5) {
    ElMessage.warning('最多只能添加5个阶梯')
    return
  }
  stepData.value.push({ conditionValue: "", ratio: "" })
}

// 删除行
const handleDelete = (index) => {
  if (index === 0) {
    ElMessage.warning('第一行不能删除')
    return
  }
  stepData.value.splice(index, 1)
}

// 提交表单
const handleSubmit = async () => {
  try {
    await formRef.value.validate()
    // 验证通过,可以在这里处理提交逻辑
    ElMessage.success('验证通过,可以提交数据')
    console.log('提交的数据:', stepData.value)
  } catch (error) {
    ElMessage.error('请检查表单是否填写正确')
  }
}

</script>