Skip to content

✨ 表单处理 👌

本章聚焦 v-model 在表单中的双向绑定用法,覆盖文本输入、复选框、单选框、下拉列表以及常用修饰符,并配合 VuePress 的 ::: 容器突出重点与注意事项。

本章目标

  • 掌握 v-model 在常见表单元素上的绑定与取值方式
  • 正确使用修饰符 .lazy.number.trim
  • 理解复选框组与多选下拉的数组绑定与取值
  • 避免常见坑:selected/checkedv-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/checkedv-model:交由 v-model 控制
  • 复选框组与多选下拉的绑定值必须是数组;切换为单选时改为“单值”
  • 选项值为非字符串(数字、对象)时使用 :value 绑定,保持类型一致
  • 单个复选框的语义值请用 true-value / false-value;复选组用数组表示多个选择
  • 不要在 option 上设置 selected,而是用占位的禁用项与 v-model 控制默认值

实用小建议

  • 单选与复选建议配合 label 包裹,提升可点击区域与可及性
  • 下拉建议提供一个禁用的占位项(如“请选择”),避免绑定到空字符串难以区分
  • 表单校验与类型转换交由业务层处理(如数值范围、必填校验、特殊字符过滤)

小结

  • v-model 统一了表单元素的双向绑定,核心是“值 + 事件”的语法糖
  • 文本类与单选绑定单值;复选组与多选下拉绑定数组
  • 合理使用 .lazy.number.trim 提升交互与数据质量
  • 避免与 selected / checked 等静态属性混用,保持状态一致