Skip to content

表单输入绑定

你可以用 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>

最佳实践

  1. 使用合适的修饰符:根据需求选择 .lazy.number.trim 修饰符
  2. 表单验证:结合计算属性进行表单验证
  3. 用户体验:提供清晰的标签和占位符文本
  4. 响应式设计:确保表单在不同设备上都能正常使用
  5. 数据类型:注意 v-model 绑定的数据类型

下一步

基于 Vue.js 官方文档构建的学习宝典