一、回忆一下 v-model 的本事
前面讲组件通信时我们拆过v-model,它是一个语法糖,本质是:value+@input的组合。在表单元素上,它能让数据和输入框双向绑定。
1.1 普通文本框
vue
<template> <div> <!-- v-model 绑定到 username 这个 ref 上 用户在输入框里打字,username 自动更新; 修改 username 的值,输入框也会自动变 --> <input v-model="username" placeholder="输入用户名" /> <p>你输入的是:{{ username }}</p> </div> </template> <script setup> import { ref } from 'vue' // 定义一个响应式变量存用户名 const username = ref('') </script>为什么方便?
如果不写v-model,你得手动写:value="username"和@input="e => username = e.target.value",麻烦又容易忘。
1.2 各种输入类型的绑定
vue
<template> <div> <!-- 文本框 --> <input v-model="text" placeholder="文本" /> <!-- 多行文本 --> <textarea v-model="textarea" placeholder="多行文本"></textarea> <!-- 复选框(单个布尔值) --> <input type="checkbox" v-model="checked" /> 同意协议 <p>是否同意:{{ checked }}</p> <!-- 多个复选框(绑定到数组) --> <input type="checkbox" v-model="hobbies" value="读书" /> 读书 <input type="checkbox" v-model="hobbies" value="跑步" /> 跑步 <input type="checkbox" v-model="hobbies" value="游泳" /> 游泳 <p>爱好:{{ hobbies }}</p> <!-- 单选框 --> <input type="radio" v-model="gender" value="男" /> 男 <input type="radio" v-model="gender" value="女" /> 女 <p>性别:{{ gender }}</p> <!-- 下拉选择框 --> <select v-model="city"> <option value="">请选择城市</option> <option value="北京">北京</option> <option value="上海">上海</option> <option value="广州">广州</option> </select> <p>城市:{{ city }}</p> </div> </template> <script setup> import { ref } from 'vue' const text = ref('') const textarea = ref('') const checked = ref(false) const hobbies = ref([]) // 多个复选框,绑数组 const gender = ref('') const city = ref('') </script>记住:
单个复选框 → 绑定布尔值。
多个复选框 → 绑定到数组,
value表示选中时的值。单选按钮 → 绑定到单个值。
下拉框 → 绑定到选项的
value。
二、修饰符:帮你在绑定过程中做点手脚
v-model有几个实用修饰符,用起来非常省事。
2.1.lazy:懒同步
默认情况下v-model在每次input事件后更新数据。.lazy会改成在change事件后更新(即输入框失去焦点时才同步)。
vue
<input v-model.lazy="msg" placeholder="失去焦点才更新" /> <p>{{ msg }}</p>什么时候用?不想每敲一个字就触发请求或校验,等用户输入完整再处理。
2.2.number:自动转数字
输入框里的值默认是字符串。如果你需要数字,可以用.number自动转换。
vue
<input v-model.number="age" type="number" /> <p>年龄 + 1:{{ age + 1 }}</p>不加.number时age是字符串"25","25" + 1会变成"251"。加了.number后age就是数字25。
2.3.trim:去除首尾空格
vue
<input v-model.trim="username" />
用户不小心在前面或后面打了空格,提交时自动去掉,非常贴心。
三、封装自定义表单组件
原生的<input>直接用没问题,但如果你要封装一个带标签、带校验样式、带错误提示的输入框,就需要自己写组件,并且让父组件能用v-model绑定。
3.1 自定义输入框组件
vue
<!-- MyInput.vue --> <template> <div class="my-input"> <!-- 标签文字 --> <label v-if="label">{{ label }}</label> <!-- 核心:用 :value + @input 实现 v-model --> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :type="type" :placeholder="placeholder" :class="{ error: hasError }" /> <!-- 错误提示 --> <span v-if="hasError" class="error-msg">{{ errorMsg }}</span> </div> </template> <script setup> // 接收 v-model 绑定值 defineProps({ modelValue: { type: String, default: '' }, // 其他配置项 label: String, // 标签文字 type: { type: String, default: 'text' }, placeholder: String, hasError: Boolean, // 是否显示错误状态 errorMsg: String // 错误提示文字 }) // 声明事件 defineEmits(['update:modelValue']) </script> <style scoped> .my-input { margin-bottom: 10px; } .my-input label { display: block; margin-bottom: 5px; font-weight: bold; } .my-input input { padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; width: 100%; box-sizing: border-box; } .my-input input.error { border-color: red; } .error-msg { color: red; font-size: 12px; } </style>父组件使用:
vue
<template> <div> <MyInput v-model="form.username" label="用户名" placeholder="请输入用户名" :has-error="errors.username" error-msg="用户名不能为空" /> <p>绑定值:{{ form.username }}</p> </div> </template> <script setup> import { reactive, ref } from 'vue' import MyInput from './MyInput.vue' const form = reactive({ username: '' }) const errors = reactive({ username: false }) // 可以结合 watch 做校验 </script>四、表单验证:最简单的校验方式
实际项目里,表单提交前必须校验。我们先从最基础的手动校验开始。
4.1 手动校验
vue
<template> <div> <form @submit.prevent="handleSubmit"> <!-- 用户名 --> <div> <label>用户名</label> <input v-model.trim="form.username" /> <span v-if="errors.username" style="color:red;">{{ errors.username }}</span> </div> <!-- 密码 --> <div> <label>密码</label> <input v-model="form.password" type="password" /> <span v-if="errors.password" style="color:red;">{{ errors.password }}</span> </div> <button type="submit">提交</button> </form> </div> </template> <script setup> import { reactive, ref } from 'vue' const form = reactive({ username: '', password: '' }) // 存放错误信息 const errors = reactive({ username: '', password: '' }) // 校验函数 function validate() { let isValid = true // 重置错误 errors.username = '' errors.password = '' // 校验用户名 if (!form.username) { errors.username = '用户名不能为空' isValid = false } else if (form.username.length < 3) { errors.username = '用户名至少3个字符' isValid = false } // 校验密码 if (!form.password) { errors.password = '密码不能为空' isValid = false } else if (form.password.length < 6) { errors.password = '密码至少6位' isValid = false } return isValid } function handleSubmit() { if (validate()) { alert('提交成功!' + JSON.stringify(form)) // 这里发请求 } } </script>流程很简单:定义校验函数,逐个检查字段,有错就放进errors对象里,页面显示对应的错误信息。提交时调用校验,通过了才发请求。
五、实战案例:完整的注册表单
来做一个稍微复杂的注册页面,包含用户名、邮箱、密码、确认密码、手机号。每个字段都有实时校验和提交时校验。
vue
<template> <div class="register"> <h2>用户注册</h2> <form @submit.prevent="handleSubmit"> <!-- 用户名 --> <div class="form-item"> <label>用户名</label> <input v-model.trim="form.username" @blur="validateField('username')" :class="{ error: errors.username }" placeholder="3-10位字符" /> <span class="error-msg" v-if="errors.username">{{ errors.username }}</span> </div> <!-- 邮箱 --> <div class="form-item"> <label>邮箱</label> <input v-model.trim="form.email" @blur="validateField('email')" :class="{ error: errors.email }" placeholder="example@mail.com" /> <span class="error-msg" v-if="errors.email">{{ errors.email }}</span> </div> <!-- 密码 --> <div class="form-item"> <label>密码</label> <input v-model="form.password" type="password" @blur="validateField('password')" :class="{ error: errors.password }" placeholder="至少6位" /> <span class="error-msg" v-if="errors.password">{{ errors.password }}</span> </div> <!-- 确认密码 --> <div class="form-item"> <label>确认密码</label> <input v-model="form.rePassword" type="password" @blur="validateField('rePassword')" :class="{ error: errors.rePassword }" placeholder="再次输入密码" /> <span class="error-msg" v-if="errors.rePassword">{{ errors.rePassword }}</span> </div> <!-- 手机号 --> <div class="form-item"> <label>手机号</label> <input v-model="form.phone" @blur="validateField('phone')" :class="{ error: errors.phone }" placeholder="11位手机号" /> <span class="error-msg" v-if="errors.phone">{{ errors.phone }}</span> </div> <!-- 同意协议 --> <div class="form-item"> <label> <input type="checkbox" v-model="form.agree" /> 我已阅读并同意《用户协议》 </label> <span class="error-msg" v-if="errors.agree">{{ errors.agree }}</span> </div> <button type="submit">注册</button> </form> </div> </template> <script setup> import { reactive } from 'vue' // 表单数据 const form = reactive({ username: '', email: '', password: '', rePassword: '', phone: '', agree: false }) // 错误对象 const errors = reactive({ username: '', email: '', password: '', rePassword: '', phone: '', agree: '' }) // 单个字段校验规则 function validateField(field) { switch (field) { case 'username': if (!form.username) { errors.username = '用户名不能为空' } else if (form.username.length < 3 || form.username.length > 10) { errors.username = '用户名需3-10位字符' } else { errors.username = '' } break case 'email': if (!form.email) { errors.email = '邮箱不能为空' } else if (!/^\S+@\S+\.\S+$/.test(form.email)) { errors.email = '邮箱格式不正确' } else { errors.email = '' } break case 'password': if (!form.password) { errors.password = '密码不能为空' } else if (form.password.length < 6) { errors.password = '密码至少6位' } else { errors.password = '' } // 如果确认密码已填,顺带校验一下是否一致 if (form.rePassword && form.password !== form.rePassword) { errors.rePassword = '两次密码不一致' } else if (form.rePassword) { errors.rePassword = '' } break case 'rePassword': if (!form.rePassword) { errors.rePassword = '请确认密码' } else if (form.rePassword !== form.password) { errors.rePassword = '两次密码不一致' } else { errors.rePassword = '' } break case 'phone': if (!form.phone) { errors.phone = '手机号不能为空' } else if (!/^1[3-9]\d{9}$/.test(form.phone)) { errors.phone = '手机号格式不正确' } else { errors.phone = '' } break } } // 校验全部字段 function validateAll() { validateField('username') validateField('email') validateField('password') validateField('rePassword') validateField('phone') // 协议单独校验 if (!form.agree) { errors.agree = '请同意用户协议' } else { errors.agree = '' } // 检查是否有错误 return Object.values(errors).every(msg => msg === '') } // 提交 function handleSubmit() { if (validateAll()) { alert('注册成功!') // 这里发送请求给后端 } } </script> <style scoped> .register { max-width: 400px; margin: 0 auto; } .form-item { margin-bottom: 15px; } .form-item label { display: block; margin-bottom: 5px; } .form-item input[type="text"], .form-item input[type="password"] { width: 100%; padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } .form-item input.error { border-color: red; } .error-msg { color: red; font-size: 12px; } button { width: 100%; padding: 8px; background: #409eff; color: white; border: none; border-radius: 4px; cursor: pointer; } </style>设计思路:
validateField负责单个字段校验,失去焦点时触发(@blur),实时给用户反馈。validateAll提交时校验所有字段,包括必须勾选的协议。错误信息统一存在
errors对象里,页面根据它来显示。
六、总结
今天我们学了:
v-model的基本绑定和各种输入类型。修饰符
.lazy、.number、.trim的用法。封装支持
v-model的自定义表单组件。手动校验的完整流程:字段校验、错误对象、实时反馈、提交检查。
这些知识足够你应对大部分项目中的表单需求。如果你想把校验逻辑抽出来复用,还可以结合之前学的组合式函数,封装一个useFormValidation,这个我们以后可以单独聊。
有问题评论区说,看到就回。下篇见!