Vue3 表单输入 v-model 指令详解
核心概念理解
什么是 v-model?
v-model
是 Vue 的双向数据绑定指令,它实现了:
- 数据 → 视图:当数据变化时,视图自动更新
- 视图 → 数据:当用户输入时,数据自动更新
基本工作原理
vue
<!-- v-model 是语法糖,等价于: -->
<input :value="message" @input="message = $event.target.value">
或
<input :value="text" @input="event => message = event.target.value">
<!-- 简写为: -->
<input v-model="message">
基础用法
1. 文本输入框
vue
<template>
<div class="form-demo">
<h2>基础表单输入</h2>
<!-- 文本输入 -->
<div class="input-group">
<label>姓名:</label>
<input
v-model="name"
type="text"
placeholder="请输入姓名"
class="form-input"
>
<p>输入的姓名: {{ name }}</p>
</div>
<!-- 多行文本 -->
<div class="input-group">
<label>个人简介:</label>
<textarea
v-model="bio"
placeholder="请输入个人简介"
rows="4"
class="form-textarea"
></textarea>
<p>字数统计: {{ bio.length }}</p>
</div>
<!-- 密码输入 -->
<div class="input-group">
<label>密码:</label>
<input
v-model="password"
type="password"
placeholder="请输入密码"
class="form-input"
>
<p>密码长度: {{ password.length }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('')
const bio = ref('')
const password = ref('')
</script>
<style>
.form-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.input-group {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #495057;
}
.form-input, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.2s ease;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-textarea {
resize: vertical;
font-family: inherit;
}
</style>
各种表单元素的 v-model
1. 复选框 (Checkbox)
vue
<template>
<div class="checkbox-demo">
<h2>复选框</h2>
<!-- 单个复选框 -->
<div class="checkbox-group">
<h3>单个复选框</h3>
<label class="checkbox-label">
<input
v-model="isChecked"
type="checkbox"
class="checkbox-input"
>
<span class="checkbox-text">同意用户协议</span>
</label>
<p>状态: {{ isChecked ? '已同意' : '未同意' }}</p>
</div>
<!-- 多个复选框 -->
<div class="checkbox-group">
<h3>兴趣爱好 (多选)</h3>
<div class="checkbox-list">
<label
v-for="hobby in hobbies"
:key="hobby.value"
class="checkbox-label"
>
<input
v-model="selectedHobbies"
:value="hobby.value"
type="checkbox"
class="checkbox-input"
>
<span class="checkbox-text">{{ hobby.label }}</span>
</label>
</div>
<p>选择的兴趣: {{ selectedHobbies.join(', ') }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isChecked = ref(false)
const hobbies = ref([
{ value: 'reading', label: '阅读' },
{ value: 'music', label: '音乐' },
{ value: 'sports', label: '运动' },
{ value: 'travel', label: '旅行' },
{ value: 'cooking', label: '烹饪' }
])
const selectedHobbies = ref([])
</script>
<style>
.checkbox-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.checkbox-group {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.checkbox-group h3 {
margin-top: 0;
color: #495057;
}
.checkbox-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin: 15px 0;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.checkbox-input {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
}
.checkbox-text {
font-size: 16px;
color: #333;
}
</style>
2. 单选框 (Radio)
vue
<template>
<div class="radio-demo">
<h2>单选框</h2>
<!-- 性别选择 -->
<div class="radio-group">
<h3>性别</h3>
<div class="radio-list">
<label
v-for="gender in genders"
:key="gender.value"
class="radio-label"
>
<input
v-model="selectedGender"
:value="gender.value"
type="radio"
name="gender"
class="radio-input"
>
<span class="radio-text">{{ gender.label }}</span>
</label>
</div>
<p>选择的性别: {{ selectedGender }}</p>
</div>
<!-- 年龄段选择 -->
<div class="radio-group">
<h3>年龄段</h3>
<div class="radio-list">
<label
v-for="age in ageGroups"
:key="age.value"
class="radio-label"
>
<input
v-model="selectedAgeGroup"
:value="age.value"
type="radio"
name="age-group"
class="radio-input"
>
<span class="radio-text">{{ age.label }}</span>
</label>
</div>
<p>选择的年龄段: {{ selectedAgeGroup }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const genders = ref([
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' }
])
const ageGroups = ref([
{ value: '18-25', label: '18-25岁' },
{ value: '26-35', label: '26-35岁' },
{ value: '36-45', label: '36-45岁' },
{ value: '46+', label: '46岁以上' }
])
const selectedGender = ref('')
const selectedAgeGroup = ref('')
</script>
<style>
.radio-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.radio-group {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.radio-group h3 {
margin-top: 0;
color: #495057;
}
.radio-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 15px 0;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.radio-input {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
}
.radio-text {
font-size: 16px;
color: #333;
}
</style>
3. 选择框 (Select)
vue
<template>
<div class="select-demo">
<h2>选择框</h2>
<!-- 单选下拉框 -->
<div class="select-group">
<h3>城市选择</h3>
<select v-model="selectedCity" class="form-select">
<option value="">请选择城市</option>
<option
v-for="city in cities"
:key="city.value"
:value="city.value"
>
{{ city.label }}
</option>
</select>
<p>选择的城市: {{ selectedCity || '未选择' }}</p>
</div>
<!-- 多选下拉框 -->
<div class="select-group">
<h3>技能选择 (多选)</h3>
<select
v-model="selectedSkills"
multiple
class="form-select multiple"
size="5"
>
<option
v-for="skill in skills"
:key="skill.value"
:value="skill.value"
>
{{ skill.label }}
</option>
</select>
<p>选择的技能: {{ selectedSkills.join(', ') || '未选择' }}</p>
</div>
<!-- 分组选择框 -->
<div class="select-group">
<h3>分组选择</h3>
<select v-model="selectedProduct" class="form-select">
<option value="">请选择产品</option>
<optgroup
v-for="category in productCategories"
:key="category.name"
:label="category.name"
>
<option
v-for="product in category.products"
:key="product.value"
:value="product.value"
>
{{ product.label }}
</option>
</optgroup>
</select>
<p>选择的产品: {{ selectedProduct || '未选择' }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const cities = ref([
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'hangzhou', label: '杭州' }
])
const skills = ref([
{ value: 'javascript', label: 'JavaScript' },
{ value: 'vue', label: 'Vue.js' },
{ value: 'react', label: 'React' },
{ value: 'node', label: 'Node.js' },
{ value: 'python', label: 'Python' },
{ value: 'java', label: 'Java' }
])
const productCategories = ref([
{
name: '电子产品',
products: [
{ value: 'phone', label: '手机' },
{ value: 'laptop', label: '笔记本电脑' },
{ value: 'tablet', label: '平板电脑' }
]
},
{
name: '家用电器',
products: [
{ value: 'tv', label: '电视机' },
{ value: 'fridge', label: '冰箱' },
{ value: 'washer', label: '洗衣机' }
]
}
])
const selectedCity = ref('')
const selectedSkills = ref([])
const selectedProduct = ref('')
</script>
<style>
.select-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.select-group {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.select-group h3 {
margin-top: 0;
color: #495057;
}
.form-select {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
background-color: white;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-select.multiple {
height: auto;
cursor: default;
}
</style>
v-model 修饰符
1. .lazy 修饰符
vue
<template>
<div class="modifier-demo">
<h2>v-model 修饰符</h2>
<!-- 默认:实时更新 -->
<div class="modifier-group">
<h3>默认模式 (实时更新)</h3>
<input
v-model="realTimeInput"
placeholder="输入文字实时更新"
class="form-input"
>
<p>实时值: {{ realTimeInput }}</p>
</div>
<!-- .lazy:失去焦点时更新 -->
<div class="modifier-group">
<h3>.lazy 修饰符 (失去焦点更新)</h3>
<input
v-model.lazy="lazyInput"
placeholder="输入完成后失去焦点才更新"
class="form-input"
>
<p>延迟值: {{ lazyInput }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const realTimeInput = ref('')
const lazyInput = ref('')
</script>
<style>
.modifier-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.modifier-group {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.modifier-group h3 {
margin-top: 0;
color: #495057;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
margin-bottom: 10px;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
</style>
2. .number 修饰符
vue
<template>
<div class="number-demo">
<h2>.number 修饰符</h2>
<!-- 不使用 .number -->
<div class="number-group">
<h3>普通输入</h3>
<input
v-model="normalNumber"
type="number"
placeholder="输入数字"
class="form-input"
>
<p>值: {{ normalNumber }} (类型: {{ typeof normalNumber }})</p>
</div>
<!-- 使用 .number -->
<div class="number-group">
<h3>.number 修饰符</h3>
<input
v-model.number="typedNumber"
type="number"
placeholder="输入数字"
class="form-input"
>
<p>值: {{ typedNumber }} (类型: {{ typeof typedNumber }})</p>
</div>
<!-- 计算示例 -->
<div class="calculation-demo">
<h3>数字计算</h3>
<div class="calc-inputs">
<input
v-model.number="num1"
type="number"
placeholder="第一个数字"
class="form-input small"
>
<select v-model="operator" class="form-select operator">
<option value="+">+</option>
<option value="-">-</option>
<option value="*">×</option>
<option value="/">÷</option>
</select>
<input
v-model.number="num2"
type="number"
placeholder="第二个数字"
class="form-input small"
>
</div>
<p class="result">结果: {{ calculateResult }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const normalNumber = ref('')
const typedNumber = ref('')
const num1 = ref(0)
const num2 = ref(0)
const operator = ref('+')
const calculateResult = computed(() => {
switch (operator.value) {
case '+': return num1.value + num2.value
case '-': return num1.value - num2.value
case '*': return num1.value * num2.value
case '/': return num2.value !== 0 ? num1.value / num2.value : '除数不能为0'
default: return 0
}
})
</script>
<style>
.number-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.number-group {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.number-group h3 {
margin-top: 0;
color: #495057;
}
.calculation-demo {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.calc-inputs {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.form-input.small {
flex: 1;
max-width: 120px;
}
.form-select.operator {
width: 60px;
padding: 12px;
}
.result {
font-size: 18px;
font-weight: bold;
color: #28a745;
text-align: center;
margin: 0;
}
.form-input, .form-select {
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
</style>
3. .trim 修饰符
vue
<template>
<div class="trim-demo">
<h2>.trim 修饰符</h2>
<!-- 不使用 .trim -->
<div class="trim-group">
<h3>普通输入 (保留空格)</h3>
<input
v-model="normalInput"
placeholder="输入文字 (包含空格)"
class="form-input"
>
<p>值: "{{ normalInput }}"</p>
<p>长度: {{ normalInput.length }}</p>
</div>
<!-- 使用 .trim -->
<div class="trim-group">
<h3>.trim 修饰符 (去除首尾空格)</h3>
<input
v-model.trim="trimmedInput"
placeholder="输入文字 (自动去除空格)"
class="form-input"
>
<p>值: "{{ trimmedInput }}"</p>
<p>长度: {{ trimmedInput.length }}</p>
</div>
<!-- 实际应用示例 -->
<div class="username-demo">
<h3>用户名输入优化</h3>
<div class="input-row">
<input
v-model.trim="username"
placeholder="请输入用户名"
class="form-input"
>
<button
@click="validateUsername"
:disabled="!username"
class="validate-btn"
>
验证
</button>
</div>
<div v-if="username" class="validation-result">
<p>输入的用户名: "{{ username }}"</p>
<p>长度: {{ username.length }} 字符</p>
<p class="status" :class="usernameStatus.class">
{{ usernameStatus.message }}
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const normalInput = ref('')
const trimmedInput = ref('')
const username = ref('')
const usernameStatus = computed(() => {
if (username.value.length === 0) {
return { message: '请输入用户名', class: 'info' }
} else if (username.value.length < 3) {
return { message: '用户名至少3个字符', class: 'error' }
} else if (username.value.length > 20) {
return { message: '用户名不能超过20个字符', class: 'error' }
} else {
return { message: '用户名格式正确 ✅', class: 'success' }
}
})
const validateUsername = () => {
alert(`验证用户名: "${username.value}"`)
}
</script>
<style>
.trim-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.trim-group {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.trim-group h3 {
margin-top: 0;
color: #495057;
}
.username-demo {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.input-row {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.form-input {
flex: 1;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.validate-btn {
padding: 12px 20px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s ease;
}
.validate-btn:hover:not(:disabled) {
background-color: #0056b3;
}
.validate-btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.validation-result {
margin-top: 15px;
padding: 15px;
background-color: #fff;
border-radius: 4px;
}
.status {
font-weight: bold;
margin: 5px 0 0 0;
}
.status.success {
color: #28a745;
}
.status.error {
color: #dc3545;
}
.status.info {
color: #17a2b8;
}
</style>
实际应用示例
1. 完整的用户注册表单
vue
<template>
<div class="registration-form">
<h2>用户注册</h2>
<form @submit.prevent="handleSubmit" class="form-container">
<!-- 用户名 -->
<div class="form-group">
<label for="username">用户名 *</label>
<input
id="username"
v-model.trim="form.username"
type="text"
placeholder="请输入用户名 (3-20个字符)"
:class="{ 'error': errors.username }"
@blur="validateUsername"
>
<span v-if="errors.username" class="error-message">
{{ errors.username }}
</span>
</div>
<!-- 邮箱 -->
<div class="form-group">
<label for="email">邮箱 *</label>
<input
id="email"
v-model.trim="form.email"
type="email"
placeholder="请输入邮箱地址"
:class="{ 'error': errors.email }"
@blur="validateEmail"
>
<span v-if="errors.email" class="error-message">
{{ errors.email }}
</span>
</div>
<!-- 密码 -->
<div class="form-group">
<label for="password">密码 *</label>
<input
id="password"
v-model="form.password"
type="password"
placeholder="请输入密码 (至少6个字符)"
:class="{ 'error': errors.password }"
@blur="validatePassword"
>
<span v-if="errors.password" class="error-message">
{{ errors.password }}
</span>
</div>
<!-- 确认密码 -->
<div class="form-group">
<label for="confirmPassword">确认密码 *</label>
<input
id="confirmPassword"
v-model="form.confirmPassword"
type="password"
placeholder="请再次输入密码"
:class="{ 'error': errors.confirmPassword }"
@blur="validateConfirmPassword"
>
<span v-if="errors.confirmPassword" class="error-message">
{{ errors.confirmPassword }}
</span>
</div>
<!-- 性别 -->
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label
v-for="gender in genders"
:key="gender.value"
class="radio-label"
>
<input
v-model="form.gender"
:value="gender.value"
type="radio"
name="gender"
>
<span>{{ gender.label }}</span>
</label>
</div>
</div>
<!-- 兴趣爱好 -->
<div class="form-group">
<label>兴趣爱好</label>
<div class="checkbox-group">
<label
v-for="hobby in hobbies"
:key="hobby.value"
class="checkbox-label"
>
<input
v-model="form.hobbies"
:value="hobby.value"
type="checkbox"
>
<span>{{ hobby.label }}</span>
</label>
</div>
</div>
<!-- 城市 -->
<div class="form-group">
<label for="city">所在城市</label>
<select
id="city"
v-model="form.city"
class="form-select"
>
<option value="">请选择城市</option>
<option
v-for="city in cities"
:key="city.value"
:value="city.value"
>
{{ city.label }}
</option>
</select>
</div>
<!-- 个人简介 -->
<div class="form-group">
<label for="bio">个人简介</label>
<textarea
id="bio"
v-model.trim="form.bio"
placeholder="请输入个人简介"
rows="4"
class="form-textarea"
></textarea>
<div class="char-count">
{{ form.bio.length }}/200 字符
</div>
</div>
<!-- 协议同意 -->
<div class="form-group">
<label class="checkbox-label agreement">
<input
v-model="form.agree"
type="checkbox"
required
>
<span>我已阅读并同意 <a href="#" @click.prevent="showAgreement">用户协议</a></span>
</label>
<span v-if="errors.agree" class="error-message">
{{ errors.agree }}
</span>
</div>
<!-- 提交按钮 -->
<div class="form-actions">
<button
type="submit"
:disabled="!isFormValid"
class="submit-btn"
>
注册
</button>
<button
@click="resetForm"
type="button"
class="reset-btn"
>
重置
</button>
</div>
</form>
<!-- 提交成功提示 -->
<div v-if="isSubmitted" class="success-message">
<h3>🎉 注册成功!</h3>
<p>欢迎 {{ submittedData.username }} 加入我们!</p>
<button @click="resetAll" class="reset-btn">重新注册</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const form = ref({
username: '',
email: '',
password: '',
confirmPassword: '',
gender: '',
hobbies: [],
city: '',
bio: '',
agree: false
})
const errors = ref({
username: '',
email: '',
password: '',
confirmPassword: '',
agree: ''
})
const isSubmitted = ref(false)
const submittedData = ref({})
// 选项数据
const genders = ref([
{ value: 'male', label: '男' },
{ value: 'female', label: '女' },
{ value: 'other', label: '其他' }
])
const hobbies = ref([
{ value: 'reading', label: '阅读' },
{ value: 'music', label: '音乐' },
{ value: 'sports', label: '运动' },
{ value: 'travel', label: '旅行' },
{ value: 'cooking', label: '烹饪' },
{ value: 'gaming', label: '游戏' }
])
const cities = ref([
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'hangzhou', label: '杭州' },
{ value: 'chengdu', label: '成都' }
])
// 验证函数
const validateUsername = () => {
if (!form.value.username) {
errors.value.username = '用户名不能为空'
} else if (form.value.username.length < 3) {
errors.value.username = '用户名至少3个字符'
} else if (form.value.username.length > 20) {
errors.value.username = '用户名不能超过20个字符'
} else {
errors.value.username = ''
}
}
const validateEmail = () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!form.value.email) {
errors.value.email = '邮箱不能为空'
} else if (!emailRegex.test(form.value.email)) {
errors.value.email = '请输入有效的邮箱地址'
} else {
errors.value.email = ''
}
}
const validatePassword = () => {
if (!form.value.password) {
errors.value.password = '密码不能为空'
} else if (form.value.password.length < 6) {
errors.value.password = '密码至少6个字符'
} else {
errors.value.password = ''
validateConfirmPassword() // 验证确认密码
}
}
const validateConfirmPassword = () => {
if (form.value.password !== form.value.confirmPassword) {
errors.value.confirmPassword = '两次输入的密码不一致'
} else {
errors.value.confirmPassword = ''
}
}
// 计算属性:表单是否有效
const isFormValid = computed(() => {
return (
!errors.value.username &&
!errors.value.email &&
!errors.value.password &&
!errors.value.confirmPassword &&
!errors.value.agree &&
form.value.username &&
form.value.email &&
form.value.password &&
form.value.confirmPassword &&
form.value.agree
)
})
// 表单提交
const handleSubmit = () => {
// 触发所有验证
validateUsername()
validateEmail()
validatePassword()
validateConfirmPassword()
if (!form.value.agree) {
errors.value.agree = '请同意用户协议'
} else {
errors.value.agree = ''
}
if (isFormValid.value) {
isSubmitted.value = true
submittedData.value = { ...form.value }
console.log('表单提交数据:', form.value)
} else {
alert('请检查表单信息')
}
}
// 重置表单
const resetForm = () => {
form.value = {
username: '',
email: '',
password: '',
confirmPassword: '',
gender: '',
hobbies: [],
city: '',
bio: '',
agree: false
}
// 清空错误信息
Object.keys(errors.value).forEach(key => {
errors.value[key] = ''
})
}
const resetAll = () => {
resetForm()
isSubmitted.value = false
}
// 显示协议
const showAgreement = () => {
alert('这里是用户协议内容...')
}
</script>
<style>
.registration-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-container {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #495057;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.2s ease;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-input.error, .form-select.error, .form-textarea.error {
border-color: #dc3545;
}
.radio-group, .checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 10px;
}
.radio-label, .checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.radio-label input, .checkbox-label input {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
}
.agreement {
font-weight: normal;
}
.agreement a {
color: #007bff;
text-decoration: none;
}
.agreement a:hover {
text-decoration: underline;
}
.char-count {
text-align: right;
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
display: block;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
.submit-btn, .reset-btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.submit-btn {
background-color: #28a745;
color: white;
}
.submit-btn:hover:not(:disabled) {
background-color: #218838;
}
.submit-btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.reset-btn {
background-color: #6c757d;
color: white;
}
.reset-btn:hover {
background-color: #5a6268;
}
.success-message {
text-align: center;
padding: 40px 20px;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
}
.success-message h3 {
color: #155724;
margin-bottom: 15px;
}
.success-message p {
color: #155724;
margin-bottom: 20px;
}
</style>
2. 动态表单示例
vue
<template>
<div class="dynamic-form">
<h2>动态表单</h2>
<!-- 表单类型选择 -->
<div class="form-selector">
<label>选择表单类型:</label>
<select v-model="selectedFormType" class="form-select">
<option value="contact">联系信息</option>
<option value="feedback">意见反馈</option>
<option value="survey">问卷调查</option>
</select>
</div>
<!-- 动态表单 -->
<form @submit.prevent="submitDynamicForm" class="dynamic-form-container">
<div
v-for="field in currentFormFields"
:key="field.name"
class="form-group"
>
<label :for="field.name">{{ field.label }}</label>
<!-- 文本输入 -->
<input
v-if="field.type === 'text' || field.type === 'email'"
:id="field.name"
v-model="formData[field.name]"
:type="field.type"
:placeholder="field.placeholder"
:required="field.required"
class="form-input"
>
<!-- 文本域 -->
<textarea
v-else-if="field.type === 'textarea'"
:id="field.name"
v-model="formData[field.name]"
:placeholder="field.placeholder"
:rows="field.rows || 4"
:required="field.required"
class="form-textarea"
></textarea>
<!-- 选择框 -->
<select
v-else-if="field.type === 'select'"
:id="field.name"
v-model="formData[field.name]"
:required="field.required"
class="form-select"
>
<option value="">{{ field.placeholder }}</option>
<option
v-for="option in field.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<!-- 复选框 -->
<div
v-else-if="field.type === 'checkbox'"
class="checkbox-group"
>
<label
v-for="option in field.options"
:key="option.value"
class="checkbox-label"
>
<input
v-model="formData[field.name]"
:value="option.value"
type="checkbox"
>
<span>{{ option.label }}</span>
</label>
</div>
<!-- 单选框 -->
<div
v-else-if="field.type === 'radio'"
class="radio-group"
>
<label
v-for="option in field.options"
:key="option.value"
class="radio-label"
>
<input
v-model="formData[field.name]"
:value="option.value"
type="radio"
:name="field.name"
>
<span>{{ option.label }}</span>
</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="submit-btn">提交</button>
<button @click="resetDynamicForm" type="button" class="reset-btn">重置</button>
</div>
</form>
<!-- 提交结果 -->
<div v-if="dynamicSubmitted" class="result-display">
<h3>提交结果:</h3>
<pre>{{ JSON.stringify(formData, null, 2) }}</pre>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedFormType = ref('contact')
const formData = ref({})
const dynamicSubmitted = ref(false)
// 表单配置
const formConfig = {
contact: [
{ name: 'name', label: '姓名', type: 'text', placeholder: '请输入姓名', required: true },
{ name: 'email', label: '邮箱', type: 'email', placeholder: '请输入邮箱', required: true },
{ name: 'phone', label: '电话', type: 'text', placeholder: '请输入电话号码', required: false },
{ name: 'message', label: '留言', type: 'textarea', placeholder: '请输入留言内容', required: true }
],
feedback: [
{ name: 'satisfaction', label: '满意度', type: 'radio', required: true, options: [
{ value: 'very-satisfied', label: '非常满意' },
{ value: 'satisfied', label: '满意' },
{ value: 'neutral', label: '一般' },
{ value: 'unsatisfied', label: '不满意' }
]},
{ name: 'improvements', label: '改进建议', type: 'textarea', placeholder: '请提出您的宝贵建议', required: false },
{ name: 'contact', label: '希望联系', type: 'checkbox', options: [
{ value: 'email', label: '通过邮箱联系' },
{ value: 'phone', label: '通过电话联系' }
]}
],
survey: [
{ name: 'age', label: '年龄段', type: 'select', placeholder: '请选择年龄段', required: true, options: [
{ value: '18-25', label: '18-25岁' },
{ value: '26-35', label: '26-35岁' },
{ value: '36-45', label: '36-45岁' },
{ value: '46+', label: '46岁以上' }
]},
{ name: 'interests', label: '兴趣爱好', type: 'checkbox', options: [
{ value: 'technology', label: '科技' },
{ value: 'sports', label: '体育' },
{ value: 'music', label: '音乐' },
{ value: 'travel', label: '旅行' },
{ value: 'reading', label: '阅读' }
]},
{ name: 'comments', label: '其他意见', type: 'textarea', placeholder: '请分享您的其他想法', required: false }
]
}
// 计算当前表单字段
const currentFormFields = computed(() => {
return formConfig[selectedFormType.value] || []
})
// 初始化表单数据
const initializeFormData = () => {
const newFormData = {}
currentFormFields.value.forEach(field => {
if (field.type === 'checkbox') {
newFormData[field.name] = []
} else {
newFormData[field.name] = ''
}
})
formData.value = newFormData
}
// 监听表单类型变化
import { watch } from 'vue'
watch(selectedFormType, () => {
initializeFormData()
dynamicSubmitted.value = false
}, { immediate: true })
// 提交动态表单
const submitDynamicForm = () => {
dynamicSubmitted.value = true
console.log('动态表单提交:', formData.value)
}
// 重置动态表单
const resetDynamicForm = () => {
initializeFormData()
dynamicSubmitted.value = false
}
</script>
<style>
.dynamic-form {
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.form-selector {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.form-selector label {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: #495057;
}
.dynamic-form-container {
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 20px;
}
.result-display {
padding: 20px;
background-color: #e9ecef;
border-radius: 8px;
}
.result-display pre {
background-color: #fff;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin: 0;
}
.form-select, .form-input, .form-textarea {
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
width: 100%;
}
.form-select:focus, .form-input:focus, .form-textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.checkbox-group, .radio-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.submit-btn, .reset-btn {
padding: 12px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.submit-btn {
background-color: #007bff;
color: white;
flex: 1;
}
.submit-btn:hover {
background-color: #0056b3;
}
.reset-btn {
background-color: #6c757d;
color: white;
flex: 1;
}
.reset-btn:hover {
background-color: #5a6268;
}
</style>
注意事项和最佳实践
1. v-model 与不同输入类型的配合
vue
<template>
<div class="best-practices">
<h2>v-model 最佳实践</h2>
<!-- 1. 使用正确的输入类型 -->
<div class="practice-group">
<h3>✅ 正确使用输入类型</h3>
<div class="input-row">
<input
v-model="email"
type="email"
placeholder="邮箱"
class="form-input"
>
<input
v-model.number="age"
type="number"
placeholder="年龄"
class="form-input"
>
</div>
</div>
<!-- 2. 合理使用修饰符 -->
<div class="practice-group">
<h3>✅ 合理使用修饰符</h3>
<div class="input-row">
<input
v-model.lazy="lazyInput"
placeholder="失去焦点更新"
class="form-input"
>
<input
v-model.trim="trimmedInput"
placeholder="去除空格"
class="form-input"
>
<input
v-model.number="numberInput"
type="number"
placeholder="数字类型"
class="form-input"
>
</div>
</div>
<!-- 3. 表单验证 -->
<div class="practice-group">
<h3>✅ 表单验证</h3>
<div class="input-row">
<input
v-model="validatedInput"
:class="{ error: validationError }"
placeholder="必填项"
class="form-input"
>
<button @click="validateInput" class="validate-btn">验证</button>
</div>
<p v-if="validationError" class="error-message">{{ validationError }}</p>
</div>
<!-- 4. 避免直接修改数组索引 -->
<div class="practice-group">
<h3>⚠️ 注意数组绑定</h3>
<div class="array-demo">
<div v-for="(item, index) in items" :key="index" class="array-item">
<!-- ❌ 不推荐 -->
<input v-model="items[index]" placeholder="不推荐" class="form-input small">
<!-- ✅ 推荐 -->
<input
:value="item"
@input="updateItem(index, $event.target.value)"
placeholder="推荐"
class="form-input small"
>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const email = ref('')
const age = ref('')
const lazyInput = ref('')
const trimmedInput = ref('')
const numberInput = ref('')
const validatedInput = ref('')
const validationError = ref('')
const items = ref(['苹果', '香蕉', '橙子'])
const validateInput = () => {
if (!validatedInput.value.trim()) {
validationError.value = '此字段为必填项'
} else {
validationError.value = ''
}
}
const updateItem = (index, value) => {
items.value[index] = value
}
</script>
<style>
.best-practices {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.practice-group {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.practice-group h3 {
margin-top: 0;
color: #495057;
}
.input-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.form-input {
flex: 1;
min-width: 150px;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.form-input.error {
border-color: #dc3545;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-input.small {
min-width: 100px;
}
.validate-btn {
padding: 12px 15px;
border: none;
border-radius: 4px;
background-color: #28a745;
color: white;
cursor: pointer;
white-space: nowrap;
}
.validate-btn:hover {
background-color: #218838;
}
.error-message {
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
.array-demo {
margin-top: 15px;
}
.array-item {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
</style>
总结
v-model 修饰符速查表
修饰符 | 作用 | 适用场景 |
---|---|---|
.lazy |
失去焦点时更新 | 减少频繁更新的性能开销 |
.number |
自动转换为数字 | 数字输入框 |
.trim |
去除首尾空格 | 文本输入优化 |
表单元素对应的数据类型
元素类型 | 绑定值类型 | 示例 |
---|---|---|
文本框 | 字符串 | ref('') |
复选框(单个) | 布尔值 | ref(false) |
复选框(多个) | 数组 | ref([]) |
单选框 | 字符串/数字 | ref('') |
选择框(单选) | 字符串/数字 | ref('') |
选择框(多选) | 数组 | ref([]) |
使用建议
- 选择合适的输入类型 :使用
type="email"
、type="number"
等 - 合理使用修饰符 :根据需求选择
.lazy
、.number
、.trim
- 做好表单验证 :结合
@blur
事件进行实时验证 - 注意性能 :对于大量输入使用
.lazy
修饰符 - 保持一致性:统一的表单样式和交互体验
记忆口诀:
- v-model:双向绑定神器
- .trim:去除空格很贴心
- .number:数字类型自动转
- .lazy:失去焦点才更新
- 表单验证:用户体验要保证