如何在Vue中仅使用Zod将表单校验错误显示到对应HTML元素
给Vue表单添加Zod校验错误提示
核心思路
Zod校验失败时返回的errors数组中,每个错误对象包含path(字段路径数组)和message(错误文本)。我们可以通过字段名匹配path,提取对应的错误信息,渲染到对应字段下方的span中。
具体实现步骤
- 添加工具方法获取字段错误
在组件的methods中新增方法,根据字段名过滤错误数组:
getFieldError(fieldName) { if (!this.errors) return ''; const error = this.errors.find(err => err.path[0] === fieldName); return error ? error.message : ''; }
- 在模板中添加错误显示元素
给每个字段对应的容器下方添加错误提示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>
- 修复原代码中的潜在问题
- 原
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
相关产品推荐
相关产品推荐

