表单输入绑定
你可以用 v-model
指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。
v-model
本质上不过是语法糖。它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。
基本用法
文本输入
vue
<template>
<div class="text-input-demo">
<h2>文本输入</h2>
<div class="demo-section">
<h3>单行文本</h3>
<input v-model="message" placeholder="请输入消息">
<p>消息是: {{ message }}</p>
</div>
<div class="demo-section">
<h3>多行文本</h3>
<textarea v-model="multilineMessage" placeholder="请输入多行文本"></textarea>
<p>多行消息是:</p>
<pre>{{ multilineMessage }}</pre>
</div>
<div class="demo-section">
<h3>密码输入</h3>
<input v-model="password" type="password" placeholder="请输入密码">
<p>密码长度: {{ password.length }}</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
const multilineMessage = ref('')
const password = ref('')
</script>
<style scoped>
.text-input-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.demo-section input,
.demo-section textarea {
width: 100%;
padding: 10px;
border: 2px solid #ced4da;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.demo-section input:focus,
.demo-section textarea:focus {
outline: none;
border-color: #007bff;
}
.demo-section textarea {
height: 100px;
resize: vertical;
}
.demo-section pre {
background-color: #e9ecef;
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
复选框
vue
<template>
<div class="checkbox-demo">
<h2>复选框</h2>
<div class="demo-section">
<h3>单个复选框</h3>
<input id="checkbox" type="checkbox" v-model="checked">
<label for="checkbox">{{ checked ? '已选中' : '未选中' }}</label>
</div>
<div class="demo-section">
<h3>多个复选框</h3>
<div class="checkbox-group">
<div class="checkbox-item">
<input id="jack" type="checkbox" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
</div>
<div class="checkbox-item">
<input id="john" type="checkbox" value="John" v-model="checkedNames">
<label for="john">John</label>
</div>
<div class="checkbox-item">
<input id="mike" type="checkbox" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
</div>
</div>
<p>已选择的名字: {{ checkedNames }}</p>
</div>
<div class="demo-section">
<h3>兴趣爱好选择</h3>
<div class="hobby-grid">
<div
v-for="hobby in hobbies"
:key="hobby.id"
class="hobby-item"
>
<input
:id="hobby.id"
type="checkbox"
:value="hobby.name"
v-model="selectedHobbies"
>
<label :for="hobby.id">{{ hobby.name }}</label>
</div>
</div>
<div class="selected-hobbies">
<h4>已选择的爱好:</h4>
<div class="hobby-tags">
<span
v-for="hobby in selectedHobbies"
:key="hobby"
class="hobby-tag"
>
{{ hobby }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const checked = ref(false)
const checkedNames = ref([])
const selectedHobbies = ref([])
const hobbies = [
{ id: 'reading', name: '阅读' },
{ id: 'music', name: '音乐' },
{ id: 'sports', name: '运动' },
{ id: 'travel', name: '旅行' },
{ id: 'cooking', name: '烹饪' },
{ id: 'photography', name: '摄影' },
{ id: 'gaming', name: '游戏' },
{ id: 'art', name: '艺术' }
]
</script>
<style scoped>
.checkbox-demo {
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.checkbox-item label {
cursor: pointer;
font-size: 16px;
}
.hobby-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.hobby-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
transition: all 0.3s;
}
.hobby-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
}
.hobby-item input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.hobby-item label {
cursor: pointer;
font-size: 14px;
flex: 1;
}
.selected-hobbies h4 {
margin: 0 0 10px 0;
color: #333;
}
.hobby-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.hobby-tag {
padding: 6px 12px;
background-color: #007bff;
color: white;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
@media (max-width: 768px) {
.hobby-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
}
</style>
单选按钮
vue
<template>
<div class="radio-demo">
<h2>单选按钮</h2>
<div class="demo-section">
<h3>基本单选</h3>
<div class="radio-group">
<div class="radio-item">
<input id="one" type="radio" value="One" v-model="picked">
<label for="one">One</label>
</div>
<div class="radio-item">
<input id="two" type="radio" value="Two" v-model="picked">
<label for="two">Two</label>
</div>
<div class="radio-item">
<input id="three" type="radio" value="Three" v-model="picked">
<label for="three">Three</label>
</div>
</div>
<p>已选择: {{ picked }}</p>
</div>
<div class="demo-section">
<h3>主题选择</h3>
<div class="theme-selector">
<div
v-for="theme in themes"
:key="theme.value"
class="theme-option"
:class="{ active: selectedTheme === theme.value }"
>
<input
:id="theme.value"
type="radio"
:value="theme.value"
v-model="selectedTheme"
>
<label :for="theme.value" class="theme-label">
<div class="theme-preview" :style="{ backgroundColor: theme.color }"></div>
<span>{{ theme.name }}</span>
</label>
</div>
</div>
<div class="current-theme" :style="{ backgroundColor: currentThemeColor }">
当前主题: {{ currentThemeName }}
</div>
</div>
<div class="demo-section">
<h3>评分选择</h3>
<div class="rating-group">
<div
v-for="rating in ratings"
:key="rating.value"
class="rating-item"
>
<input
:id="'rating-' + rating.value"
type="radio"
:value="rating.value"
v-model="selectedRating"
>
<label :for="'rating-' + rating.value" class="rating-label">
<div class="stars">
<span
v-for="star in 5"
:key="star"
class="star"
:class="{ filled: star <= rating.value }"
>
★
</span>
</div>
<span class="rating-text">{{ rating.text }}</span>
</label>
</div>
</div>
<div class="rating-result">
<p>您的评分: {{ selectedRating }} 星</p>
<div class="rating-display">
<span
v-for="star in 5"
:key="star"
class="star large"
:class="{ filled: star <= selectedRating }"
>
★
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const picked = ref('')
const selectedTheme = ref('light')
const selectedRating = ref(0)
const themes = [
{ value: 'light', name: '浅色主题', color: '#ffffff' },
{ value: 'dark', name: '深色主题', color: '#333333' },
{ value: 'blue', name: '蓝色主题', color: '#007bff' },
{ value: 'green', name: '绿色主题', color: '#28a745' }
]
const ratings = [
{ value: 1, text: '很差' },
{ value: 2, text: '较差' },
{ value: 3, text: '一般' },
{ value: 4, text: '良好' },
{ value: 5, text: '优秀' }
]
const currentThemeColor = computed(() => {
const theme = themes.find(t => t.value === selectedTheme.value)
return theme ? theme.color : '#ffffff'
})
const currentThemeName = computed(() => {
const theme = themes.find(t => t.value === selectedTheme.value)
return theme ? theme.name : '未选择'
})
</script>
<style scoped>
.radio-demo {
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-item {
display: flex;
align-items: center;
gap: 8px;
}
.radio-item input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.radio-item label {
cursor: pointer;
font-size: 16px;
}
.theme-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.theme-option {
position: relative;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
}
.theme-option.active {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
}
.theme-option input[type="radio"] {
position: absolute;
opacity: 0;
cursor: pointer;
}
.theme-label {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background-color: white;
border: 2px solid #dee2e6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.theme-option.active .theme-label {
border-color: #007bff;
background-color: #e3f2fd;
}
.theme-preview {
width: 40px;
height: 40px;
border-radius: 50%;
margin-bottom: 10px;
border: 2px solid #dee2e6;
}
.current-theme {
padding: 15px;
border-radius: 8px;
text-align: center;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
.rating-group {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.rating-item input[type="radio"] {
position: absolute;
opacity: 0;
cursor: pointer;
}
.rating-label {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 15px;
background-color: white;
border: 2px solid #dee2e6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.rating-item input[type="radio"]:checked + .rating-label {
border-color: #ffc107;
background-color: #fff8e1;
}
.stars {
display: flex;
gap: 2px;
}
.star {
color: #ddd;
font-size: 20px;
transition: color 0.3s;
}
.star.filled {
color: #ffc107;
}
.star.large {
font-size: 30px;
}
.rating-text {
font-size: 16px;
font-weight: 500;
}
.rating-result {
text-align: center;
padding: 20px;
background-color: white;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.rating-display {
margin-top: 10px;
}
@media (max-width: 768px) {
.theme-selector {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.rating-label {
flex-direction: column;
gap: 10px;
text-align: center;
}
}
</style>
选择器
vue
<template>
<div class="select-demo">
<h2>选择器</h2>
<div class="demo-section">
<h3>单选选择器</h3>
<select v-model="selected" class="select-input">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>已选择: {{ selected }}</p>
</div>
<div class="demo-section">
<h3>多选选择器</h3>
<select v-model="multiSelected" multiple class="select-input multi">
<option>A</option>
<option>B</option>
<option>C</option>
<option>D</option>
<option>E</option>
</select>
<p>已选择: {{ multiSelected }}</p>
</div>
<div class="demo-section">
<h3>动态选项</h3>
<select v-model="selectedOption" class="select-input">
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>
<p>已选择的值: {{ selectedOption }}</p>
<p>已选择的文本: {{ selectedOptionText }}</p>
</div>
<div class="demo-section">
<h3>城市选择器</h3>
<div class="city-selector">
<div class="select-group">
<label>省份:</label>
<select v-model="selectedProvince" @change="onProvinceChange" class="select-input">
<option value="">请选择省份</option>
<option
v-for="province in provinces"
:key="province.id"
:value="province.id"
>
{{ province.name }}
</option>
</select>
</div>
<div class="select-group">
<label>城市:</label>
<select v-model="selectedCity" :disabled="!selectedProvince" class="select-input">
<option value="">请选择城市</option>
<option
v-for="city in availableCities"
:key="city.id"
:value="city.id"
>
{{ city.name }}
</option>
</select>
</div>
</div>
<div class="selection-result">
<p>已选择: {{ fullAddress }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('')
const multiSelected = ref([])
const selectedOption = ref('')
const selectedProvince = ref('')
const selectedCity = ref('')
const options = [
{ text: '选项 A', value: 'a' },
{ text: '选项 B', value: 'b' },
{ text: '选项 C', value: 'c' },
{ text: '选项 D', value: 'd' }
]
const provinces = [
{ id: 'beijing', name: '北京市' },
{ id: 'shanghai', name: '上海市' },
{ id: 'guangdong', name: '广东省' },
{ id: 'zhejiang', name: '浙江省' }
]
const cities = {
beijing: [
{ id: 'dongcheng', name: '东城区' },
{ id: 'xicheng', name: '西城区' },
{ id: 'chaoyang', name: '朝阳区' },
{ id: 'haidian', name: '海淀区' }
],
shanghai: [
{ id: 'huangpu', name: '黄浦区' },
{ id: 'xuhui', name: '徐汇区' },
{ id: 'changning', name: '长宁区' },
{ id: 'jingan', name: '静安区' }
],
guangdong: [
{ id: 'guangzhou', name: '广州市' },
{ id: 'shenzhen', name: '深圳市' },
{ id: 'dongguan', name: '东莞市' },
{ id: 'foshan', name: '佛山市' }
],
zhejiang: [
{ id: 'hangzhou', name: '杭州市' },
{ id: 'ningbo', name: '宁波市' },
{ id: 'wenzhou', name: '温州市' },
{ id: 'jiaxing', name: '嘉兴市' }
]
}
const selectedOptionText = computed(() => {
const option = options.find(opt => opt.value === selectedOption.value)
return option ? option.text : ''
})
const availableCities = computed(() => {
return selectedProvince.value ? cities[selectedProvince.value] || [] : []
})
const fullAddress = computed(() => {
if (!selectedProvince.value || !selectedCity.value) return ''
const province = provinces.find(p => p.id === selectedProvince.value)
const city = availableCities.value.find(c => c.id === selectedCity.value)
return `${province?.name} ${city?.name}`
})
function onProvinceChange() {
selectedCity.value = ''
}
</script>
<style scoped>
.select-demo {
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.select-input {
width: 100%;
padding: 10px;
border: 2px solid #ced4da;
border-radius: 6px;
font-size: 16px;
background-color: white;
transition: border-color 0.3s;
box-sizing: border-box;
}
.select-input:focus {
outline: none;
border-color: #007bff;
}
.select-input:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
.select-input.multi {
height: 120px;
}
.city-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.select-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.select-group label {
font-weight: bold;
color: #555;
}
.selection-result {
padding: 15px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
text-align: center;
}
@media (max-width: 768px) {
.city-selector {
grid-template-columns: 1fr;
}
}
</style>
修饰符
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
vue
<template>
<div class="lazy-demo">
<h3>.lazy 修饰符</h3>
<div class="input-comparison">
<div class="input-group">
<label>普通输入 (实时更新):</label>
<input v-model="normalInput" placeholder="实时更新">
<p>值: {{ normalInput }}</p>
</div>
<div class="input-group">
<label>懒加载输入 (失去焦点时更新):</label>
<input v-model.lazy="lazyInput" placeholder="失去焦点时更新">
<p>值: {{ lazyInput }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const normalInput = ref('')
const lazyInput = ref('')
</script>
.number
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符:
vue
<template>
<div class="number-demo">
<h3>.number 修饰符</h3>
<div class="input-comparison">
<div class="input-group">
<label>普通输入:</label>
<input v-model="normalNumber" type="number" placeholder="输入数字">
<p>值: {{ normalNumber }} (类型: {{ typeof normalNumber }})</p>
</div>
<div class="input-group">
<label>数字修饰符:</label>
<input v-model.number="numberInput" type="number" placeholder="自动转换为数字">
<p>值: {{ numberInput }} (类型: {{ typeof numberInput }})</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const normalNumber = ref('')
const numberInput = ref(0)
</script>
.trim
如果你想要自动过滤用户输入的首尾空白字符,你可以在 v-model
后添加 .trim
修饰符:
vue
<template>
<div class="trim-demo">
<h3>.trim 修饰符</h3>
<div class="input-comparison">
<div class="input-group">
<label>普通输入:</label>
<input v-model="normalTrim" placeholder="输入带空格的文本">
<p>值: "{{ normalTrim }}" (长度: {{ normalTrim.length }})</p>
</div>
<div class="input-group">
<label>自动去除首尾空格:</label>
<input v-model.trim="trimInput" placeholder="自动去除首尾空格">
<p>值: "{{ trimInput }}" (长度: {{ trimInput.length }})</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const normalTrim = ref('')
const trimInput = ref('')
</script>
实际应用示例
用户注册表单
vue
<template>
<div class="registration-form">
<h2>用户注册表单</h2>
<form @submit.prevent="handleSubmit" class="form">
<div class="form-group">
<label for="username">用户名 *</label>
<input
id="username"
v-model.trim="form.username"
type="text"
placeholder="请输入用户名"
required
>
</div>
<div class="form-group">
<label for="email">邮箱 *</label>
<input
id="email"
v-model.trim="form.email"
type="email"
placeholder="请输入邮箱"
required
>
</div>
<div class="form-group">
<label for="password">密码 *</label>
<input
id="password"
v-model="form.password"
type="password"
placeholder="请输入密码"
required
>
</div>
<div class="form-group">
<label for="age">年龄</label>
<input
id="age"
v-model.number="form.age"
type="number"
placeholder="请输入年龄"
min="1"
max="120"
>
</div>
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" value="male" v-model="form.gender">
男
</label>
<label class="radio-label">
<input type="radio" value="female" v-model="form.gender">
女
</label>
<label class="radio-label">
<input type="radio" value="other" v-model="form.gender">
其他
</label>
</div>
</div>
<div class="form-group">
<label>兴趣爱好</label>
<div class="checkbox-grid">
<label
v-for="hobby in hobbies"
:key="hobby"
class="checkbox-label"
>
<input type="checkbox" :value="hobby" v-model="form.hobbies">
{{ hobby }}
</label>
</div>
</div>
<div class="form-group">
<label for="country">国家</label>
<select id="country" v-model="form.country">
<option value="">请选择国家</option>
<option value="china">中国</option>
<option value="usa">美国</option>
<option value="japan">日本</option>
<option value="korea">韩国</option>
</select>
</div>
<div class="form-group">
<label for="bio">个人简介</label>
<textarea
id="bio"
v-model.trim="form.bio"
placeholder="请输入个人简介"
rows="4"
></textarea>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="form.agreeTerms" required>
我同意用户协议和隐私政策 *
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="form.newsletter">
订阅我们的新闻通讯
</label>
</div>
<button type="submit" class="submit-btn" :disabled="!isFormValid">
注册
</button>
</form>
<div class="form-preview">
<h3>表单预览</h3>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const form = ref({
username: '',
email: '',
password: '',
age: null,
gender: '',
hobbies: [],
country: '',
bio: '',
agreeTerms: false,
newsletter: false
})
const hobbies = ['阅读', '音乐', '运动', '旅行', '烹饪', '摄影']
const isFormValid = computed(() => {
return form.value.username &&
form.value.email &&
form.value.password &&
form.value.agreeTerms
})
function handleSubmit() {
if (isFormValid.value) {
alert('注册成功!')
console.log('提交的表单数据:', form.value)
}
}
</script>
<style scoped>
.registration-form {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.form {
background-color: #f8f9fa;
padding: 30px;
border-radius: 10px;
border: 1px solid #dee2e6;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #ced4da;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #007bff;
}
.radio-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.radio-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: normal !important;
}
.checkbox-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: normal !important;
}
.submit-btn {
width: 100%;
padding: 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.submit-btn:hover:not(:disabled) {
background-color: #0056b3;
transform: translateY(-1px);
}
.submit-btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.form-preview {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.form-preview h3 {
margin: 0 0 15px 0;
color: #333;
}
.form-preview pre {
background-color: #e9ecef;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
font-size: 14px;
line-height: 1.4;
}
@media (max-width: 768px) {
.radio-group {
flex-direction: column;
gap: 10px;
}
.checkbox-grid {
grid-template-columns: 1fr;
}
}
</style>
最佳实践
- 使用合适的修饰符:根据需求选择
.lazy
、.number
、.trim
修饰符 - 表单验证:结合计算属性进行表单验证
- 用户体验:提供清晰的标签和占位符文本
- 响应式设计:确保表单在不同设备上都能正常使用
- 数据类型:注意
v-model
绑定的数据类型