需求:点击提交按钮,多项必填项未填写或者未通过校验,则自动定位至第一项
思路分析:对于这个功能,我们要明确报错项和dom是相关联的,所以为了能定位到对应的dom位置,我们需要给每一个表单项都绑定唯一的id,然后根据校验规则,收集报错项,进行排序,然后计算第一个报错项的位置,scroll滚动到该dom所在的位置。
核心代码:
第一步:绑定唯一的id
<a-form-model-item ref="title" label="课程名称" prop="title">
<a-input
placeholder="请输入课程名称"
id="title"
style="width: 500px"
v-model.trim="form.title"
@blur="() => $refs.title.onFieldBlur()"
:maxLength="200"
/>
</a-form-model-item>
第二步,将prop的规则项和dom进行结合绑定,并且给每个dom自定义排序
tips:domains是动态表单增删校验的项,在下方我会在完整代码里进行展示
<script>
//positionData的key是表单的prop,ele是表单的dom的id名
const positionData = {
title: { ele: "title", sort: 1 },
time: { ele: "time", sort: 2 },
teacherId: { ele: "teacherId", sort: 3 },
date: { ele: "course-date", sort: 4 },
place: { ele: "place", sort: 5 },
// "domains.0.date": { ele: "domains.0.date", sort: 6 },
// "domains.1.date": { ele: "domains.1.date", sort: 7 },
// "domains.2.date": { ele: "domains.2.date", sort: 8 },
};
export default{
data(){
return{
positionData,
noValidate: [],//存放表单报错信息的数组
}
}
}
</script>
第三步:提交表单,收集报错项,进行排序,滚动定位
<script>
import { sortBy } from "lodash-es";
export default{
methods:{
handleSubmit() {
console.log("保存");
this.noValidate = [];
this.$refs.form.validate(async (valid, ...args) => {
if (!valid) {
this.handleErrorPosition(args);
return false;
}
});
},
}
}
</script>
//处理错误定位
handleErrorPosition(args) {
//获取错误的key
const errorDataKey = Object.keys(args[0]);
// 整理错误信息项
errorDataKey.forEach((key) => {
this.noValidate.push(this.positionData[key]);
});
//错误信息排序
this.noValidate = sortBy(this.noValidate, [(val) => val.sort]);
this.handleJump();
},
// 报错定位
handleJump() {
const box = document.getElementById(this.noValidate[0]["ele"]);
const container = document.getElementById("upgrad-offline-course-form");
const { top } = box.getBoundingClientRect();
const parentTop = container.getBoundingClientRect().top;
const scrollTop = container.scrollTop;
//realTop 每一项距离父级盒子顶部的距离
const realTop = top - parentTop;
const positionY = realTop + scrollTop;
container.scroll(0, positionY);
},
tips:这里的container是设置的滚动区域,十分重要,样式上一定要设置好,否则滚动不生效
核心css:
#upgrad-offline-course-form {
height: 83vh;
overflow-y: scroll;
padding-bottom: 30px;
}
完整代码:
tips:下面的代码使用了taiwindcss的类名样式,如果参考的小伙伴项目里有taiwindcss这个东西,就可以整个复制过去修改
表单比较全面:输入框,下拉框,时间选择器,数字输入框,动态增删表单
真正的项目代码过于复杂,所以我把这个源码优化成了一个demo,方便大家直接复制使用
<template>
<div id="upgrad-offline-course-form">
<a-form-model
ref="form"
:model="form"
:rules="rules"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 14 }"
:colon="false"
>
<a-form-model-item ref="title" label="课程名称" prop="title">
<a-input
placeholder="请输入课程名称"
id="title"
style="width: 500px"
v-model.trim="form.title"
@blur="() => $refs.title.onFieldBlur()"
:maxLength="200"
/>
</a-form-model-item>
<a-form-model-item ref="time" label="课程学时" prop="time">
<a-input-number
placeholder="请输入"
id="time"
v-model="form.time"
:min="0"
:max="99"
:precision="1"
/>
<span class="ml-12">小时</span>
<div class="gray-hint">支持0-99正的整数,1位小数</div>
</a-form-model-item>
<a-form-model-item ref="teacherId" label="课程讲师" prop="teacherId">
<a-select
id="teacherId"
show-search
v-model="form.teacherId"
placeholder="请选择课程讲师"
:default-active-first-option="false"
:filter-option="false"
@search="handleTeacherSearch"
@change="$refs.ownerId.onFieldChange()"
@blur="searchTeacher('')"
style="width: 400px"
>
<a-select-option
:value="item.id"
v-for="item in teacherList"
:key="item.id"
>
{{ item.truename }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="课程时间" prop="date">
<a-range-picker
valueFormat="YYYY-MM-DD HH:mm"
style="width: 400px"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
@change="onTimeChange"
v-model="form.date"
id="course-date"
:placeholder="['开始日期', '结束日期']"
/>
</a-form-model-item>
<a-form-model-item ref="place" label="课程地点" prop="place">
<a-input
placeholder="请输入课程地点"
id="place"
style="width: 400px"
v-model.trim="form.place"
@blur="() => $refs.place.onFieldBlur()"
:maxLength="60"
/>
</a-form-model-item>
<div
class="gray-area"
v-for="(domain, index) in form.domains"
:key="index"
:id="'domains.' + index + '.date'"
>
<div class="flex items-center justify-between mb-16">
<div class="flex items-center">
<div class="point"></div>
<div class="font-500">第 {{ index + 1 }} 次签到</div>
</div>
<a-tooltip>
<template slot="title"> 删除 </template>
<img
src="../assets/img/delete-bin-line.svg"
alt=""
@click="removeDomain(domain)"
/>
</a-tooltip>
</div>
<a-form-model-item
:prop="'domains.' + index + '.date'"
:rules="{
required: true,
message: '请输入签到时间',
trigger: 'change',
}"
label="签到时间"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 14 }"
style="margin-bottom: 0"
>
<a-range-picker
valueFormat="YYYY-MM-DD HH:mm"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-model="domain.date"
/>
</a-form-model-item>
</div>
<div
class="add-btn"
@click="addDomain"
:class="{ active: form.domains.length === 30 }"
>
<a-icon type="plus" class="mr-6" />
{{ `添加签到(${form.domains.length}/30)` }}
</div>
</a-form-model>
<div class="flex justify-end">
<a-button type="primary" ghost class="mr-4" @click="resetForm">
取消
</a-button>
<a-button type="primary" @click="handleSubmit">提交</a-button>
</div>
</div>
</template>
<script>
import { sortBy } from "lodash-es";
const positionData = {
title: { ele: "title", sort: 1 },
time: { ele: "time", sort: 2 },
teacherId: { ele: "teacherId", sort: 3 },
date: { ele: "course-date", sort: 4 },
place: { ele: "place", sort: 5 },
};
export default {
data() {
return {
positionData,
noValidate: [],
teacherList: [],
form: {
domains: [{ date: [] }],
startTime: "",
endTime: "",
date: undefined,
teacherId: undefined,
time: undefined,
title: undefined,
},
rules: {
place: [{ required: true, message: "请输入课程地点", trigger: "blur" }],
date: [
{ required: true, message: "请选择项目时间", trigger: "change" },
],
teacherId: [
{ required: true, message: "请选择课程讲师", trigger: "change" },
],
title: [{ required: true, message: "请输入课程名称", trigger: "blur" }],
time: [{ required: true, message: "请输入课程学时", trigger: "blur" }],
},
};
},
created() {
for (let i = 0; i < 30; i++) {
const check_key = `domains.${i}.date`;
this.positionData[check_key] = { ele: check_key, sort: 6 + i };
}
},
methods: {
//处理错误定位
handleErrorPosition(args) {
console.log(args[0]);
//获取错误的key
const errorDataKey = Object.keys(args[0]);
// 整理错误信息项
errorDataKey.forEach((key) => {
this.noValidate.push(this.positionData[key]);
});
//错误信息排序
this.noValidate = sortBy(this.noValidate, [(val) => val.sort]);
this.handleJump();
},
// 报错定位
handleJump() {
const box = document.getElementById(this.noValidate[0]["ele"]);
const container = document.getElementById("upgrad-offline-course-form");
const { top } = box.getBoundingClientRect();
const parentTop = container.getBoundingClientRect().top;
const scrollTop = container.scrollTop;
//realTop 每一项距离父级盒子顶部的距离
const realTop = top - parentTop;
const positionY = realTop + scrollTop;
container.scroll(0, positionY);
},
removeDomain(item) {
let index = this.form.domains.indexOf(item);
if (index !== -1) {
this.form.domains.splice(index, 1);
}
},
addDomain() {
if (this.form.domains.length === 30) {
return;
}
this.form.domains.push({
date: [],
});
},
//时间处理
onTimeChange(date, dateString) {
console.log(date, dateString);
if (!this.form.date.length) {
this.form.date = undefined;
}
this.form.startTime = dateString[0];
this.form.endTime = dateString[1];
},
handleSubmit() {
console.log("保存");
// this.isSumbit = true;
this.noValidate = [];
this.$refs.form.validate(async (valid, ...args) => {
// let isValid = valid;
if (!valid) {
this.handleErrorPosition(args);
return false;
}
});
},
//课程讲师搜索
handleTeacherSearch(value) {
this.searchUser(value);
},
searchTeacher(value) {
const requestForm = {};
if (value) {
requestForm.q = value;
}
},
resetForm() {
this.$refs.form.resetFields();
},
},
};
</script>
<style lang="less" scoped>
#upgrad-offline-course-form {
height: 83vh;
overflow-y: scroll;
padding-bottom: 30px;
.gray-hint {
font-size: 14px;
color: #919399;
line-height: normal;
}
.gray-area {
width: 498px;
background: #f7f8fa;
height: auto;
margin-bottom: 12px;
margin-left: 28px;
padding: 16px 20px 10px 16px;
}
.add-btn {
cursor: pointer;
margin-left: 34px;
color: #165dff;
&.active {
cursor: not-allowed;
color: #999;
opacity: 0.6;
}
}
.point {
margin-right: 8px;
width: 4px;
height: 4px;
background-color: #165dff;
}
}
</style>
效果图:
未校验表单(正常表单):
课程讲师未填写定位
动态表单第二次签到未填写定位
拓展:
getBoundingClientRect()的作用?
getBoundingClientRect()
是JavaScript中的一个方法,用于获取元素的大小及其相对于视口的位置。它返回一个DOMRect
对象,包含了元素的尺寸和位置信息。
getBoundingClientRect()
方法返回的DOMRect
对象包含以下属性:
x
:元素左上角的x坐标(相对于视口)y
:元素左上角的y坐标(相对于视口)top
:元素左上角的y坐标(相对于视口)right
:元素右下角的x坐标(相对于视口)bottom
:元素右下角的y坐标(相对于视口)width
:元素的宽度height
:元素的高度
使用getBoundingClientRect()
方法可以获取元素的位置和尺寸信息,从而实现各种布局和动画效果。例如,可以使用它来判断元素是否在视口内,或者计算元素相对于其他元素的位置。
web前端开发为什么注重用户体验感?
提高用户满意度:用户体验感直接影响到用户的满意度和忠诚度。一个良好的用户体验感可以吸引用户再次访问网站,并推荐给其他人。
增加用户停留时间:用户体验感好的网站可以吸引用户停留更长的时间,从而增加网站的访问量和用户粘性。
提高转化率:用户体验感好的网站可以降低用户的操作难度,提高用户的转化率。例如,一个易于导航的网站可以减少用户的跳出率,提高用户的购买意愿。
提升品牌形象:用户体验感好的网站可以提升品牌的形象,增加用户的信任感。一个优秀的用户体验感可以提升用户的口碑,从而提高品牌的知名度和影响力。
满足用户需求:用户体验感好的网站可以更好地满足用户的需求,提供更加个性化的服务。例如,一个响应式的网站可以提供更好的移动端体验,满足用户的移动需求。
降低维护成本:用户体验感好的网站可以降低用户的操作难度,减少用户的错误操作,从而降低网站的维护成本。
提高网站性能:用户体验感好的网站可以优化网站的性能,提高网站的加载速度和响应速度。例如,一个优化过的网站可以减少用户的等待时间,提高用户的满意度。
总之,用户体验感是Web前端开发的重要目标之一,它直接影响到用户的满意度和忠诚度,从而影响到网站的访问量、转化率、品牌形象、用户粘性、维护成本和网站性能。因此,Web前端开发人员需要关注用户体验感,努力提供更好的用户体验。