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>