表单提交报错自动定位(antd+vue)

本文介绍了如何在表单提交时,处理必填项校验未通过的情况,通过绑定唯一ID并根据校验规则收集和排序错误项,实现页面滚动定位至第一个错误项的DOM。核心代码展示了如何结合Vue的AUI组件进行操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求:点击提交按钮,多项必填项未填写或者未通过校验,则自动定位至第一项

思路分析:对于这个功能,我们要明确报错项和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前端开发为什么注重用户体验感? 

  1. 提高用户满意度:用户体验感直接影响到用户的满意度和忠诚度。一个良好的用户体验感可以吸引用户再次访问网站,并推荐给其他人。

  2. 增加用户停留时间:用户体验感好的网站可以吸引用户停留更长的时间,从而增加网站的访问量和用户粘性。

  3. 提高转化率:用户体验感好的网站可以降低用户的操作难度,提高用户的转化率。例如,一个易于导航的网站可以减少用户的跳出率,提高用户的购买意愿。

  4. 提升品牌形象:用户体验感好的网站可以提升品牌的形象,增加用户的信任感。一个优秀的用户体验感可以提升用户的口碑,从而提高品牌的知名度和影响力。

  5. 满足用户需求:用户体验感好的网站可以更好地满足用户的需求,提供更加个性化的服务。例如,一个响应式的网站可以提供更好的移动端体验,满足用户的移动需求。

  6. 降低维护成本:用户体验感好的网站可以降低用户的操作难度,减少用户的错误操作,从而降低网站的维护成本。

  7. 提高网站性能:用户体验感好的网站可以优化网站的性能,提高网站的加载速度和响应速度。例如,一个优化过的网站可以减少用户的等待时间,提高用户的满意度。

总之,用户体验感是Web前端开发的重要目标之一,它直接影响到用户的满意度和忠诚度,从而影响到网站的访问量、转化率、品牌形象、用户粘性、维护成本和网站性能。因此,Web前端开发人员需要关注用户体验感,努力提供更好的用户体验。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

零凌林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值