Vue3+ElementPlus form表单动态生成校验实现

1. 背景

在使用Vue3和ElementPlus开发动态表单时,我们遇到了一些关于表单校验的挑战,特别是在动态添加表单项时如何正确应用校验规则而不触发全局校验。

2. 实现目标

  • 动态添加和删除考勤组(最多3组,至少1组)
  • 每组考勤时间包含开始时间、结束时间和休息时间
  • 新增考勤组时不触发全局校验
  • 确保新增的考勤组正确应用校验规则
  • 计算总工作时长

3. 核心实现

3.1 动态生成校验规则

const generateRules = () => {
  const baseRules = {
    name: [{ required: true, message: "请输入班次名称", trigger: "blur" }]
  };
  formModel.attendanceGroups.forEach((_, index) => {
    baseRules[`attendanceGroups.${index}.startTime`] = [
      { required: true, message: "请选择开始时间", trigger: "change" },
      { validator: validateAttendanceTime, trigger: "change" }
    ];
    // ... 其他字段的规则
  });
  return baseRules;
};

3.2 添加新考勤组

const handelAdd = async () => {
  if (formModel.attendanceGroups.length < 3) {
    // 暂时禁用表单的自动验证
    if (formRef.value) {
      formRef.value.validateDisabled = true;
    }

    const newIndex = formModel.attendanceGroups.length;
    formModel.attendanceGroups.push({
      // ... 新考勤组的初始数据
    });

    // 重新生成规则
    rules.value = generateRules();

    await nextTick();

    // 只对新添加的组应用校验规则
    if (formRef.value) {
      formRef.value.validateField([
        `attendanceGroups.${newIndex}.startTime`,
        `attendanceGroups.${newIndex}.endTime`
      ]);
    }

    // 重新启用表单的自动验证
    if (formRef.value) {
      formRef.value.validateDisabled = false;
    }
  }
};

3.3 html结构

    <div
          v-for="(group, index) in formModel.attendanceGroups"
          :key="index"
        >
          <span>第{{ index + 1 }}次</span>
          <el-row
            :gutter="20"
            style="width: 100%"
          >
            <el-col :span="18">
              <el-form-item
                :label="'开始时间'"
                :prop="`attendanceGroups.${index}.startTime`"
              >
                <el-time-picker
                  v-model="group.startTime"
                  clearable
                  format="HH:mm"
                  placeholder="开始时间"
                />
              </el-form-item>
              <el-form-item
                :label="'结束时间'"
                :prop="`attendanceGroups.${index}.endTime`"
              >
                <el-time-picker
                  v-model="group.endTime"
                  clearable
                  format="HH:mm"
                  placeholder="结束时间"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <div>
                <el-icon
                  v-if="index !== 0"
                  @click="handleClose(index)"
                >
                  <CircleClose />
                </el-icon>
                <el-checkbox
                  v-model="group.needClockIn"
                  :disabled="index === 0"
                  label="打卡"
                  size="large"
                />
                <el-checkbox
                  v-model="group.needClockOut"
                  label="打卡"
                  size="large"
                />
              </div>
            </el-col>
          </el-row>
          <el-form-item label="休息时间">
            <el-switch
              v-model="group.showRestTime"

              class="ml-2"
              style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
            />
          </el-form-item>
          <div v-if="group.showRestTime">
            <el-form-item label="休息开始">
              <el-time-picker
                v-model="group.restTime.startTime"
                :format="'HH:mm'"
                clearable
                placeholder="开始时间"
                value-format="HH:mm"
              />
            </el-form-item>
            <el-form-item label="休息结束">
              <el-time-picker
                v-model="group.restTime.endTime"
                :format="'HH:mm'"
                clearable
                placeholder="结束时间"
                value-format="HH:mm"
              />
            </el-form-item>
          </div>
        </div>

4. 关键点

  • 使用动态生成的校验规则,确保每个考勤组都有正确的校验规则
  • 在添加新考勤组时,暂时禁用表单的自动验证,避免触发全局校验
  • 只对新添加的考勤组应用校验规则,而不影响其他已存在的组
  • 使用nextTick确保DOM更新后再进行校验操作

5. 注意事项

  • 确保formRef正确绑定到el-form组件上
  • 在删除考勤组时也需要重新生成校验规则
  • 总工作时长的计算需要考虑休息时间

6. 总结

通过这种实现方式,我们成功解决了动态表单校验的问题,既保证了新增字段的校验规则生效,又避免了不必要的全局校验触发,提高了用户体验和表单的可用性。

完整代码如下:

<template>
  <el-pro-dialog
    :model-value="modelValue"
    :title="dialogType==='add'?'新增':'编辑'"
    class="completion-chart"
    width="600px"
    @closed="$emit('closed')"
    @update:model-value="(value:boolean) => $emit('update:modelValue',value)"
  >
    <div class="form-model">
      <span>注意:新增/修改班次规则对当天及历史考勤无影响!</span>

      <el-form
        ref="formRef"
        :model="formModel"
        :rules="rules"
        label-width="auto"
      >
        <el-form-item
          label="班次名称"
          prop="name"
        >
          <el-input v-model="formModel.name" />
        </el-form-item>
        <el-form-item label="上下班时间">
          <el-icon @click="handelAdd">
            <Plus />
          </el-icon>
        </el-form-item>
        <div
          v-for="(group, index) in formModel.attendanceGroups"
          :key="index"
        >
          <span>第{{ index + 1 }}次</span>
          <el-row
            :gutter="20"
            style="width: 100%"
          >
            <el-col :span="18">
              <el-form-item
                :label="'开始时间'"
                :prop="`attendanceGroups.${index}.startTime`"
              >
                <el-time-picker
                  v-model="group.startTime"
                  clearable
                  format="HH:mm"
                  placeholder="开始时间"
                />
              </el-form-item>
              <el-form-item
                :label="'结束时间'"
                :prop="`attendanceGroups.${index}.endTime`"
              >
                <el-time-picker
                  v-model="group.endTime"
                  clearable
                  format="HH:mm"
                  placeholder="结束时间"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <div>
                <el-icon
                  v-if="index !== 0"
                  @click="handleClose(index)"
                >
                  <CircleClose />
                </el-icon>
                <el-checkbox
                  v-model="group.needClockIn"
                  :disabled="index === 0"
                  label="打卡"
                  size="large"
                />
                <el-checkbox
                  v-model="group.needClockOut"
                  label="打卡"
                  size="large"
                />
              </div>
            </el-col>
          </el-row>
          <el-form-item label="休息时间">
            <el-switch
              v-model="group.showRestTime"

              class="ml-2"
              style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
            />
          </el-form-item>
          <div v-if="group.showRestTime">
            <el-form-item label="休息开始">
              <el-time-picker
                v-model="group.restTime.startTime"
                :format="'HH:mm'"
                clearable
                placeholder="开始时间"
                value-format="HH:mm"
              />
            </el-form-item>
            <el-form-item label="休息结束">
              <el-time-picker
                v-model="group.restTime.endTime"
                :format="'HH:mm'"
                clearable
                placeholder="结束时间"
                value-format="HH:mm"
              />
            </el-form-item>
          </div>
        </div>
        <el-form-item label="合计工作时长">
          {{ totalWorkDuration }}
        </el-form-item>
      </el-form>
    </div>
    <template #footer>
      <div
        style="text-align: center"
      >
        <el-button
          plain
          type="danger"
          @click="$emit('update:modelValue',false)"
        >
          取消
        </el-button>
        <el-button
          :loading="submitLoading"
          type="primary"
          @click="submit"
        >
          确认
        </el-button>
      </div>
    </template>
  </el-pro-dialog>
</template>

<script lang="ts" setup>
import {computed, nextTick, PropType, reactive, ref} from "vue";
import {FormInstance} from "element-plus";

const emit = defineEmits(["update:modelValue", "save", "closed"])
const props = defineProps({
  modelValue: {
    type: Boolean,
    required: true,
    default: false,
  },
  dialogType: {
    type: String as PropType<"add" | "detail">,
    required: true,
    default: "add",
  },
})
const formRef = ref<FormInstance>()
const formModel = reactive({
  name: "",
  attendanceGroups: [
    {
      startTime: "",
      endTime: "",
      needClockIn: true,
      needClockOut: true,
      showRestTime: false,
      restTime: {
        startTime: "",
        endTime: "",
      },
    }
  ],
})

const submitLoading = ref<boolean>(false)
const generateRules = () => {
  const baseRules: {} = {
    name: [{ required: true, message: "请输入班次名称", trigger: "blur", }],
  };
  formModel.attendanceGroups.forEach((_, index) => {
    baseRules[`attendanceGroups.${index}.startTime`] = [
      { required: true, message: "请选择开始时间", trigger: "change", },
      { validator: validateAttendanceTime, trigger: "change", }
    ];
    baseRules[`attendanceGroups.${index}.endTime`] = [
      { required: true, message: "请选择结束时间", trigger: "change", },
      { validator: validateAttendanceTime, trigger: "change", }
    ];
    baseRules[`attendanceGroups.${index}.restTime.startTime`] = [
      { validator: validateRestTime, trigger: "change", }
    ];
    baseRules[`attendanceGroups.${index}.restTime.endTime`] = [
      { validator: validateRestTime, trigger: "change", }
    ];
  });

  return baseRules;
};
const totalWorkDuration = computed(() => {
  let total = 0
  formModel.attendanceGroups.forEach(group => {
    if (group.startTime && group.endTime) {
      const start = new Date(`2000-01-01T${group.startTime}:00`)
      const end = new Date(`2000-01-01T${group.endTime}:00`)
      let duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60)

      if (group.showRestTime && group.restTime.startTime && group.restTime.endTime) {
        const restStart = new Date(`2000-01-01T${group.restTime.startTime}:00`)
        const restEnd = new Date(`2000-01-01T${group.restTime.endTime}:00`)
        const restDuration = (restEnd.getTime() - restStart.getTime()) / (1000 * 60 * 60)
        duration -= restDuration
      }

      total += duration
    }
  })
  return total.toFixed(2)
})

function validateAttendanceTime(rule: any, value: string, callback: Function) {
  console.log(rule, value,"中心")
  const index = parseInt(rule.field.split(".")[1])
  const currentGroup = formModel.attendanceGroups[index]
  const nextGroup = formModel.attendanceGroups[index + 1]

  if (currentGroup.startTime && currentGroup.endTime) {
    if (currentGroup.startTime >= currentGroup.endTime) {
      callback(new Error("结束时间必须大于开始时间"))
    } else if (nextGroup && currentGroup.endTime > nextGroup.startTime) {
      callback(new Error("考勤时间不可交叉覆盖"))
    } else {
      callback()
    }
  } else {
    callback()
  }
}
// const rules = computed(() => generateRules());
const rules = ref(generateRules());
function validateRestTime(rule: any, value: string, callback: Function) {
  const [groupIndex, field] = rule.field.split(".").slice(1, 3)
  const group = formModel.attendanceGroups[parseInt(groupIndex)]

  if (!group.startTime || !group.endTime) {
    callback(new Error("请先选择上下班时间"))
  } else if (group.showRestTime && group.restTime.startTime && group.restTime.endTime) {
    if (group.restTime.startTime < group.startTime || group.restTime.endTime > group.endTime) {
      callback(new Error("休息时间需在考勤时间内"))
    } else if (group.restTime.startTime >= group.restTime.endTime) {
      callback(new Error("休息结束时间必须大于开始时间"))
    } else {
      callback()
    }
  } else {
    callback()
  }
}
const handelAdd = async () => {
  if (formModel.attendanceGroups.length < 3) {
    // 暂时禁用表单的自动验证
    if (formRef.value) {
      formRef.value.validateDisabled = true;
    }

    const newIndex = formModel.attendanceGroups.length;
    formModel.attendanceGroups.push({
      startTime: "",
      endTime: "",
      needClockIn: false,
      needClockOut: false,
      showRestTime: false,
      restTime: {
        startTime: "",
        endTime: "",
      },
    });

    // 重新生成规则
    rules.value = generateRules();

    await nextTick();

    // 只对新添加的组应用校验规则
    if (formRef.value) {
      formRef.value.validateField([
        `attendanceGroups.${newIndex}.startTime`,
        `attendanceGroups.${newIndex}.endTime`
      ]);
    }

    // 重新启用表单的自动验证
    if (formRef.value) {
      formRef.value.validateDisabled = false;
    }
  }
};





const handleClose = (index: number) => {
  if (formModel.attendanceGroups.length > 1) {
    formModel.attendanceGroups.splice(index, 1)
    // 触发规则重新计算
    rules.value = generateRules();

  }
}

const submit = async () => {
  // 实现表单提交和校验逻辑
  if (!formRef.value) return

  try {
    formRef.value.validate((valid, fields) => {
      console.log("验证结果:", valid, fields)
      if (valid) {
        console.log("验证通过", formModel)
      } else {
        console.log("验证失败", fields)
      }
    })

  } catch (error) {
    console.error("表单验证失败", error)
  }
}
</script>

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3 Element Plus是一套基于Vue3的UI组件库提供了丰富的组件和功能,其中包括了Form表单组件。在Element Plus中,可以通过动态校验实现同一字段的表单动态校验。 在Vue3 Element Plus中,可以使用`el-form`组件来创建表单,通过`el-form-item`组件来包裹表单项。要实现同一字段的表单动态校验,可以使用`rules`属性来定义校验规则。 首先,需要在Vue组件中定义表单数据和校验规则。例如: ```javascript data() { return { form: { field1: '', field2: '' }, rules: { field1: [ { required: true, message: '字段1不能为空', trigger: 'blur' }, // 其他校验规则 ], field2: [ { required: true, message: '字段2不能为空', trigger: 'blur' }, // 其他校验规则 ] } } } ``` 然后,在模板中使用`el-form`和`el-form-item`组件来创建表单,并绑定数据和校验规则。例如: ```html <template> <el-form :model="form" :rules="rules" ref="form"> <el-form-item label="字段1" prop="field1"> <el-input v-model="form.field1"></el-input> </el-form-item> <el-form-item label="字段2" prop="field2"> <el-input v-model="form.field2"></el-input> </el-form-item> </el-form> </template> ``` 最后,可以通过调用`validate`方法来触发表单校验。例如: ```javascript methods: { submitForm() { this.$refs.form.validate((valid) => { if (valid) { // 校验通过,提交表单 } else { // 校验不通过,处理错误信息 } }); } } ``` 这样,就可以实现同一字段的表单动态校验了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值