You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Vue中仅使用Zod将表单校验错误显示到对应HTML元素

给Vue表单添加Zod校验错误提示

核心思路

Zod校验失败时返回的errors数组中,每个错误对象包含path(字段路径数组)和message(错误文本)。我们可以通过字段名匹配path,提取对应的错误信息,渲染到对应字段下方的span中。

具体实现步骤

  1. 添加工具方法获取字段错误
    在组件的methods中新增方法,根据字段名过滤错误数组:
getFieldError(fieldName) {
  if (!this.errors) return '';
  const error = this.errors.find(err => err.path[0] === fieldName);
  return error ? error.message : '';
}
  1. 在模板中添加错误显示元素
    给每个字段对应的容器下方添加错误提示span,使用v-if控制显示,样式用红色文本突出:
  • Rank选择框下方:
<span v-if="getFieldError('rank_name')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('rank_name') }}</span>
  • Ranking输入框下方:
<span v-if="getFieldError('rank')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('rank') }}</span>
  • Research Output输入框下方:
<span v-if="getFieldError('research')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('research') }}</span>
  • Year选择框下方:
<span v-if="getFieldError('year')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('year') }}</span>
  1. 修复原代码中的潜在问题
  • 原selectedRank初始值为'selecte rank',会导致Zod校验误判为已填写,改为空字符串:
data(){
  return{
    // ...其他数据
    selectedRank: '',
    // ...其他数据
  }
}
  • 原addRank方法存在冗余判断和逻辑错误,调整后仅在校验通过时发起请求:
addRank(){
  const formElem = document.getElementById("rank-form");
  const formData = new FormData(formElem);
  const csrfToken = document.head.querySelector('meta[name="csrf-token"]').content;
  formData.append('uni_id', this.university_id);
  formData.append('_token', csrfToken);

  const formDataObject = Object.fromEntries(formData.entries());

  try{
    schema.parse(formDataObject); 
    fetch("/admin/university-rank/save", {
      method: "POST",
      body: formData,
    })
    .then(response => response.json())
    .then(data => {
      if(data.result){
        // 校验成功后重置表单
        this.selectedRank = '';
        this.rank = '';
        this.research = '';
        this.selectedYear = '';
        this.errors = null;
      }
      if(data.error){
        console.log("error--->" + data.error);
      }
    })
    .catch(error => {
      console.error('请求失败:', error);
    });
  }catch (error) {
    this.errors = error.errors;
  }
}

完整修改后的组件代码

<template>
  <div id="basic" class="w-full details-model  relative hidden rounded-md mx-auto  mt-2 py-3 px-3 bg-white border border-gray-300  opend-src" style="animation-name: animat-top;animation-duration: 0.4s;box-shadow: rgb(82 63 104 / 12%) 0px 0px 10px 0px;">
    <span class="absolute inline-block w-4 h-4 bg-white top-[-.5rem] sm:left-20 sm:mr-4 sm:rotate-45"></span>
    <form id="rank-form" class="mainpower-form flex flex-wrap gap-y-3 gap-x-4 pt-4 justify-between" enctype="multipart/form-data" >
      <div class="flex-100 addrs-ctrl relative rounded-md mb-6">
        <button  class="inline-flex openDisBtn group relative w-full border !border-gray-300 items-center px-3 py-2.7 text-sm font-medium text-gray-400 rounded-md focus:!border-blue-500 focus:outline-none peer-focus:!text-blue-500 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
          <span v-if="selectedRank" class="font-normal" >{{selectedRank}} </span>
          <span v-else class="text-sm text-gray-400 font-normal">Select Rank</span>
          <svg class="w-2.5 h-2.5 ml-auto" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
          </svg>
          <label  class="px-2 bg-white ml-2 z-20 font-medium absolute text-md text-gray-600  -translate-y-6 scale-76 top-[0.74rem]  origin-[0] left-0 peer-focus:text-blue-500">Ranks</label>
        </button>
        <div class="z-50 dtls-menu absolute top-12 hidden bg-white rounded-md border !border-gray-300 shadow w-full mt-2 dark:bg-gray-700" style="box-shadow: rgb(0 0 0 / 3%) 0px 12.5px 10px, rgb(0 0 0 / 6%) 0px 100px 80px">
          <div v-if="ranks.length" class="h-48 px-2 pb-3 pt-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200" >
            <div v-for="rank in ranks" :key="rank.id" class="flex py-2.5 px-2 border-b items-center border-b-gray-300  bg-gry-100 hover:bg-gray-200 dark:hover:bg-gray-600">
              <input :id="rank.id" type="radio" name="rank_name" v-model="selectedRank"  :value="rank.rank_name" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
              <label :for="rank.id" class="w-full ml-2 mb-0 text-sm font-medium text-gray-900 capitalize rounded dark:text-gray-300">{{rank.rank_name}}</label>
            </div>
          </div>
          <div v-else>
            Loading disciplines...
          </div>
        </div>
        <!-- 错误提示 -->
        <span v-if="getFieldError('rank_name')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('rank_name') }}</span>
      </div>
      <div class="flex-48">
        <div class="relative z-0  mb-6 border border-gray-200 group rounded-md focus-within:!border-blue-400 ">
          <input type="text" name="rank"  id="rank" v-model="rank" class="placeholder:font-normal placeholder:text-sm block py-2.7 font-normal pl-3 w-full text-md text-gray-900 bg-transparent appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:!pl-4  focus:border-blue-600 peer" placeholder="ranking in selected year" required />
          <label for="rank" class="px-2 bg-white ml-2 font-medium absolute text-sm text-gray-700  duration-300 transform -translate-y-6 scale-76 top-[0.74rem] -z-10 origin-[0] left-0 peer-focus:text-blue-500">Ranking</label>
        </div>
        <!-- 错误提示 -->
        <span v-if="getFieldError('rank')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('rank') }}</span>
      </div>
      <div class="flex-48">
        <div class="relative z-0 mb-6 border border-gray-200 group rounded-md focus-within:!border-blue-400 ">
          <input type="text" name="research" id="research" v-model="research" class="block py-2.7 font-normal pl-3 w-full text-md text-gray-900 bg-transparent appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:!pl-4  focus:border-blue-600 peer" placeholder="university research number .... " required />
          <label for="research" class="px-2 bg-white ml-2 font-medium absolute text-sm text-gray-700  duration-300 transform -translate-y-6 scale-76 top-[0.74rem] -z-10 origin-[0] left-0 peer-focus:text-blue-500">Research Output</label>
        </div>
        <!-- 错误提示 -->
        <span v-if="getFieldError('research')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('research') }}</span>
      </div>
      <div class="flex-100 addrs-ctrl relative rounded-md mb-6">
        <button  class="inline-flex openDisBtn group relative w-full border !border-gray-300 items-center px-3 py-2.7 text-sm font-medium text-gray-400 rounded-md focus:!border-blue-500 focus:outline-none peer-focus:!text-blue-500 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
          <span v-if="selectedYear" class="font-normal" >{{selectedYear}} </span>
          <span v-else class="text-sm text-gray-400 font-normal">Select Year</span>
          <svg class="w-2.5 h-2.5 ml-auto" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
          </svg>
          <label  class="px-2 bg-white ml-2 z-20 font-medium absolute text-sm text-gray-600  -translate-y-6 scale-76 top-[0.74rem]  origin-[0] left-0 peer-focus:text-blue-500">Disciplines</label>
        </button>
        <div class="z-10 dtls-menu absolute top-12 hidden bg-white rounded-md border !border-gray-300 shadow w-full mt-2 dark:bg-gray-700" style="box-shadow: rgb(0 0 0 / 3%) 0px 12.5px 10px, rgb(0 0 0 / 6%) 0px 100px 80px">
          <div v-if="rankingYears.length" class="h-48 px-2 pb-3 pt-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200" >
            <div v-for="year in rankingYears" :key="year" class="flex py-2.5 px-2 border-b items-center border-b-gray-300  bg-gry-100 hover:bg-gray-200 dark:hover:bg-gray-600">
              <input :id="year" type="radio" name="year" v-model="selectedYear"  :value="year" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
              <label :for="year" class="w-full ml-2 mb-0 text-sm font-medium text-gray-900 capitalize rounded dark:text-gray-300">{{year}}</label>
            </div>
          </div>
          <div v-else>
            Loading disciplines...
          </div>
        </div>
        <!-- 错误提示 -->
        <span v-if="getFieldError('year')" class="text-red-500 text-xs mt-1 block">{{ getFieldError('year') }}</span>
      </div>
      <div class="form-group flex-100">
        <button  type="button" id="basicId" class="w-full mt-4 text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center" @click="addRank">Save Basic Info</button >
      </div>
    </form>
  </div>
</template>
<script>
import { object, string, array } from "zod";
const schema = object({
  rank_name: string().min(1, '请选择排名').max(255),
  rank: string().min(1, '请填写排名数值').max(255),
  research: string().min(1, '请填写研究成果编号').max(255),
  year: string().min(1, '请选择年份').max(4, '年份格式不正确'),
});
export default {
  name:'RanksInfo',
  props:{
    ranks :{
      type: Array,
      required: true
    },
    university_id:String
  },
  mounted() {
  },
  data(){
    return{
      selectedYear : '' ,
      selectedLevel : [],
      scholarship :{},
      rankingYears : ['2020','2021'],
      selectedRank : '',
      id:'',
      errors: null,
      rank: '',
      research: '',
    }
  },
  methods:{  
    getFieldError(fieldName) {
      if (!this.errors) return '';
      const error = this.errors.find(err => err.path[0] === fieldName);
      return error ? error.message : '';
    },
    addRank(){
      const formElem = document.getElementById("rank-form");
      const formData = new FormData(formElem);
      const csrfToken = document.head.querySelector('meta[name="csrf-token"]').content;
      formData.append('uni_id', this.university_id);
      formData.append('_token', csrfToken);

      const formDataObject = Object.fromEntries(formData.entries());

      try{
        schema.parse(formDataObject); 
        fetch("/admin/university-rank/save", {
          method: "POST",
          body: formData,
        })
        .then(response => response.json())
        .then(data => {
          if(data.result){
            // 重置表单
            this.selectedRank = '';
            this.rank = '';
            this.research = '';
            this.selectedYear = '';
            this.errors = null;
          }
          if(data.error){
            console.log("error--->" + data.error);
          }
        })
        .catch(error => {
          console.error('请求失败:', error);
        });
      }catch (error) {
        this.errors = error.errors;
      }
    },
  },
};
</script>

额外优化说明

  • 给Zod校验规则添加了自定义中文错误提示,用户体验更友好
  • 修复了原表单中research输入框未绑定v-model的问题
  • 简化了按钮点击事件的写法,去掉冗余的箭头函数包装

内容的提问来源于stack exchange,提问作者ahmed alsumairi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 16:35:56