Appearance
✨ 表单处理 👌
本章聚焦 v-model 在表单中的双向绑定用法,覆盖文本输入、复选框、单选框、下拉列表以及常用修饰符,并配合 VuePress 的 ::: 容器突出重点与注意事项。
本章目标
- 掌握
v-model在常见表单元素上的绑定与取值方式 - 正确使用修饰符
.lazy、.number、.trim - 理解复选框组与多选下拉的数组绑定与取值
- 避免常见坑:
selected/checked与v-model冲突、选项值类型不一致等
核心概念:v-model 的工作方式
v-model是“值 + 事件”组合的语法糖:- 文本类(
input[type=text]、textarea):值映射到value,事件为input - 单个复选框(
checkbox):布尔值映射为checked,事件为change - 复选框组:值映射为数组(根据每个选项的
value),事件为change - 单选框(
radio):值映射为字符串(选中项的value),事件为change - 下拉列表(
select):值映射为选中项的value(多选为数组),事件为change
- 文本类(
常见误区
- 将
selected/checked等静态属性与v-model混用,会互相覆盖造成状态不一致 - 复选框组和多选下拉的绑定对象必须是数组;单选框和单选下拉则是“单值”
- 选项值需要用
:value绑定非字符串类型(如数字、对象);否则都会被当作字符串 - 单个复选框的
true-value/false-value仅用于布尔型复选框,不适用于复选组数组绑定
文本输入与文本域
文本框(input[type=text])
推荐直接使用 v-model 简化双向绑定:
vue
<template>
<input type="text" v-model="text" placeholder="请输入内容" />
<p>当前输入:{{ text }}</p>
</template>
<script setup>
import { ref } from "vue";
const text = ref("");
</script>如需了解语法糖背后的原理,可以手动组合 :value 与 @input:
vue
<template>
<input :value="text" @input="text = $event.target.value" />
<p>当前输入:{{ text }}</p>
<button @click="text = ''">清空</button>
<button @click="text = 'Hello Vue'">赋值</button>
<button @click="text += '!'">追加</button>
<button @click="text = text.toUpperCase()">大写</button>
<button @click="text = text.toLowerCase()">小写</button>
<button @click="text = text.trim()">去空格</button>
<button @click="text = text.replace(/\s+/g, '-')">空格转连字符</button>
<button @click="text = text.replace(/[aeiou]/gi, '*')">元音替换为 *</button>
<p>长度:{{ text.length }}</p>
<p>包含空格:{{ /\s/.test(text) ? "是" : "否" }}</p>
<p>是否英文:{{ /^[a-zA-Z]*$/.test(text) ? "是" : "否" }}</p>
<p>是否纯数字:{{ /^\d*$/.test(text) ? "是" : "否" }}</p>
<p>包含中文:{{ /[\u4e00-\u9fa5]/.test(text) ? "是" : "否" }}</p>
</template>
<script setup>
import { ref } from "vue";
const text = ref("");
</script>文本域(textarea)
多行文本同样使用 v-model:
vue
<template>
<textarea
v-model="desc"
cols="30"
rows="4"
placeholder="请输入描述"
></textarea>
<p>描述:{{ desc }}</p>
<p>字数:{{ desc.length }}</p>
</template>
<script setup>
import { ref } from "vue";
const desc = ref("");
</script>复选框(checkbox)
单个复选框(布尔值)
vue
<template>
<label> <input type="checkbox" v-model="checked" /> 同意协议 </label>
<p>选中状态:{{ checked }}</p>
<button @click="checked = !checked">切换选中</button>
<button @click="checked = true">设为选中</button>
<button @click="checked = false">设为未选中</button>
</template>
<script setup>
import { ref } from "vue";
const checked = ref(true);
</script>手动实现
vue
<template>
<input type="checkbox" :checked="isChecked" @change="handleChange" /> 同意协议
<p>选中状态: {{ isChecked }}</p>
</template>
<script setup>
import { ref } from "vue";
const isChecked = ref(false);
const handleChange = (event) => {
isChecked.value = event.target.checked;
};
</script>自定义选中与未选中的实际取值(而非布尔):
vue
<template>
<input
type="checkbox"
v-model="agreement"
:true-value="agree"
:false-value="disagree"
/>
<p>当前值:{{ agreement }}</p>
<button @click="toggle">切换</button>
</template>
<script setup>
import { ref } from "vue";
const agree = ref("yes");
const disagree = ref("no");
const agreement = ref("yes");
const toggle = () => {
agreement.value =
agreement.value === agree.value ? disagree.value : agree.value;
};
</script>适用场景
true-value / false-value 适用于“单个复选框”的业务语义(如 yes/no)。对于“复选框组”,应使用数组绑定,不要用自定义真假值。
复选框组(数组绑定)
将多个复选框绑定到同一个数组,选中项的 value 会加入数组:
vue
<template>
<fieldset>
<legend>请选择爱好</legend>
<label v-for="item in hobbiesOptions" :key="item.id">
<input type="checkbox" v-model="hobbies" :value="item.value" />
{{ item.title }}
</label>
</fieldset>
<p v-if="hobbies.length === 0">请选择至少一个爱好</p>
<p v-else>您选择了:{{ hobbies.join("、") }}</p>
</template>
<script setup>
import { ref } from "vue";
const hobbies = ref([]);
const hobbiesOptions = ref([
{ id: "swim", title: "游泳", value: "游泳" },
{ id: "run", title: "跑步", value: "跑步" },
{ id: "game", title: "游戏", value: "游戏" },
{ id: "music", title: "音乐", value: "音乐" },
{ id: "movie", title: "电影", value: "电影" },
]);
</script>关键点
- 组绑定的是数组,是否选中取决于“该复选框的
value是否在数组中” - 请使用
:value(而不是纯字符串value="...")以便传递非字符串类型的值
单选框(radio)
v-model 绑定单个“当前值”,与选中项的 value 保持一致:
vue
<template>
<fieldset>
<legend>性别</legend>
<!-- 为语义化与可及性,建议添加相同 name;v-model 不依赖它 -->
<label>
<input type="radio" name="gender" v-model="gender" value="男" />男
</label>
<label>
<input type="radio" name="gender" v-model="gender" value="女" />女
</label>
<label>
<input type="radio" name="gender" v-model="gender" value="保密" />保密
</label>
</fieldset>
<p>当前选择:{{ gender }}</p>
<button @click="gender = '男'">设为男</button>
<button @click="gender = '女'">设为女</button>
<button @click="gender = '保密'">设为保密</button>
</template>
<script setup>
import { ref } from "vue";
const gender = ref("保密");
</script>手动实现(不使用 v-model)
通过手动组合“状态判断 + change 事件”,可以复刻 v-model 的行为:
vue
<template>
<fieldset>
<legend>性别(手动实现)</legend>
<label>
<input
type="radio"
name="gender2"
value="男"
:checked="gender2 === '男'"
@change="onGenderChange"
/>
男
</label>
<label>
<input
type="radio"
name="gender2"
value="女"
:checked="gender2 === '女'"
@change="onGenderChange"
/>
女
</label>
<label>
<input
type="radio"
name="gender2"
value="保密"
:checked="gender2 === '保密'"
@change="onGenderChange"
/>
保密
</label>
</fieldset>
<p>当前选择(手动):{{ gender2 }}</p>
</template>
<script setup>
import { ref } from "vue";
const gender2 = ref("保密");
const onGenderChange = (e) => {
gender2.value = e.target.value;
};
</script>手动实现要点
- 通过
:checked="current === value"控制选中状态;通过@change同步数据 - 手动实现与
v-model等价,便于理解“值 + 事件”的底层机制
下拉列表(select)
单选下拉
v-model 绑定的是选中项的 value:
vue
<template>
<select v-model="city">
<option value="" disabled>请选择城市</option>
<option v-for="item in cityOptions" :key="item.key" :value="item.key">
{{ item.label }}
</option>
</select>
<p>选择的城市:{{ city || "未选择" }}</p>
</template>
<script setup>
import { ref } from "vue";
const city = ref("");
const cityOptions = ref([
{ key: "成都", label: "成都" },
{ key: "北京", label: "北京" },
{ key: "上海", label: "上海" },
{ key: "广州", label: "广州" },
{ key: "重庆", label: "重庆" },
]);
</script>手动实现(不使用 v-model)
通过 :value 结合 @change 手动同步数据,可复刻 v-model 的行为:
vue
<template>
<select :value="city2" @change="onCityChange">
<option value="" disabled>请选择城市</option>
<option v-for="item in cityOptions" :key="item.key" :value="item.key">
{{ item.label }}
</option>
</select>
<p>选择的城市(手动):{{ city2 || "未选择" }}</p>
<button @click="city2 = '北京'">设为北京</button>
<button @click="city2 = ''">清空选择</button>
<button @click="city2 = '上海'">设为上海</button>
<button @click="city2 = '成都'">设为成都</button>
<button @click="city2 = '广州'">设为广州</button>
<button @click="city2 = '重庆'">设为重庆</button>
</template>
<script setup>
import { ref } from "vue";
const city2 = ref("");
const cityOptions = ref([
{ key: "成都", label: "成都" },
{ key: "北京", label: "北京" },
{ key: "上海", label: "上海" },
{ key: "广州", label: "广州" },
{ key: "重庆", label: "重庆" },
]);
const onCityChange = (e) => {
city2.value = e.target.value;
};
</script>手动实现要点
- 通过
:value控制选中项,通过@change读取event.target.value同步数据 - 不要与
selected静态属性混用,以免状态不一致;统一交由数据驱动
多选下拉
multiple 模式下,v-model 绑定为数组:
vue
<template>
<select v-model="cities" multiple>
<option value="" disabled>请选择城市(可多选)</option>
<option v-for="item in cityOptions" :key="item.key" :value="item.key">
{{ item.label }}
</option>
</select>
<p>选择的城市:{{ cities.length ? cities.join("、") : "未选择" }}</p>
</template>
<script setup>
import { ref } from "vue";
const cities = ref([]);
const cityOptions = ref([
{ key: "成都", label: "成都" },
{ key: "北京", label: "北京" },
{ key: "上海", label: "上海" },
{ key: "广州", label: "广州" },
{ key: "重庆", label: "重庆" },
]);
</script>值类型提示
当选项值需要为数字或对象时,使用 :value 绑定非字符串类型:
vue
<option :value="1">一</option>
<option :value="2">二</option>此时 v-model 的值将是数字 1 / 2,而非字符串。
表单相关修饰符
.lazy:改为 change 事件后再更新
vue
<template>
<input type="text" v-model.lazy="lazyText" placeholder="失焦或回车后更新" />
<p>当前值(lazy):{{ lazyText }}</p>
</template>
<script setup>
import { ref } from "vue";
const lazyText = ref("");
</script>交互提示
.lazy 使用 change 事件,通常在失焦或提交时才触发更新,可能导致用户输入期间界面不即时反馈。谨慎使用,确保符合产品体验。
.number:将输入值转换为数字
vue
<template>
<input type="number" v-model.number="age" placeholder="请输入年龄" />
<p>年龄:{{ age }}(类型:{{ typeof age }})</p>
</template>
<script setup>
import { ref } from "vue";
const age = ref(18);
</script>使用建议
- 对数值场景优先使用
input[type=number] + v-model.number - 若输入无法解析为数字,将保持原值(请在业务层做校验与容错)
.trim:去除两端空格
vue
<template>
<input type="text" v-model.trim="keyword" placeholder="去除前后空格" />
<p>关键词:`{{ keyword }}`(长度:{{ keyword.length }})</p>
</template>
<script setup>
import { ref } from "vue";
const keyword = ref("");
</script>组合修饰符
修饰符可以组合使用,例如在回车或失焦时更新并去除空格:
vue
<template>
<input type="text" v-model.trim.lazy="inputValue" placeholder="lazy + trim" />
<p>值:`{{ inputValue }}`</p>
</template>
<script setup>
import { ref } from "vue";
const inputValue = ref("");
</script>常见问题与建议
易错点排查清单
- 不要同时使用
selected/checked与v-model:交由v-model控制 - 复选框组与多选下拉的绑定值必须是数组;切换为单选时改为“单值”
- 选项值为非字符串(数字、对象)时使用
:value绑定,保持类型一致 - 单个复选框的语义值请用
true-value/false-value;复选组用数组表示多个选择 - 不要在
option上设置selected,而是用占位的禁用项与v-model控制默认值
实用小建议
- 单选与复选建议配合
label包裹,提升可点击区域与可及性 - 下拉建议提供一个禁用的占位项(如“请选择”),避免绑定到空字符串难以区分
- 表单校验与类型转换交由业务层处理(如数值范围、必填校验、特殊字符过滤)
小结
v-model统一了表单元素的双向绑定,核心是“值 + 事件”的语法糖- 文本类与单选绑定单值;复选组与多选下拉绑定数组
- 合理使用
.lazy、.number、.trim提升交互与数据质量 - 避免与
selected/checked等静态属性混用,保持状态一致
