Skip to content

Vue.js 表单处理

v-model 双向绑定

v-model 是 Vue 提供的表单输入绑定指令,它可以在表单元素和数据之间创建双向绑定。

文本输入

vue
<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <div>
    <input v-model="message" placeholder="请输入内容" />
    <p>输入的内容:{{ message }}</p>
  </div>
</template>

多行文本

vue
<script setup>
import { ref } from 'vue'

const description = ref('')
</script>

<template>
  <div>
    <textarea v-model="description" placeholder="请输入描述"></textarea>
    <p>字符数:{{ description.length }}</p>
    <pre>{{ description }}</pre>
  </div>
</template>

复选框

单个复选框

vue
<script setup>
import { ref } from 'vue'

const agreed = ref(false)
</script>

<template>
  <div>
    <label>
      <input type="checkbox" v-model="agreed" />
      我同意服务条款
    </label>
    <p>{{ agreed ? '已同意' : '未同意' }}</p>
  </div>
</template>

多个复选框

vue
<script setup>
import { ref } from 'vue'

const hobbies = ref([])
</script>

<template>
  <div>
    <label><input type="checkbox" v-model="hobbies" value="reading" /> 阅读</label>
    <label><input type="checkbox" v-model="hobbies" value="sports" /> 运动</label>
    <label><input type="checkbox" v-model="hobbies" value="music" /> 音乐</label>
    <label><input type="checkbox" v-model="hobbies" value="travel" /> 旅游</label>
    
    <p>选中的爱好:{{ hobbies.join(', ') }}</p>
  </div>
</template>

单选按钮

vue
<script setup>
import { ref } from 'vue'

const gender = ref('')
</script>

<template>
  <div>
    <label><input type="radio" v-model="gender" value="male" /> 男</label>
    <label><input type="radio" v-model="gender" value="female" /> 女</label>
    <label><input type="radio" v-model="gender" value="other" /> 其他</label>
    
    <p>选择的性别:{{ gender }}</p>
  </div>
</template>

下拉选择

单选

vue
<script setup>
import { ref } from 'vue'

const selected = ref('')
</script>

<template>
  <div>
    <select v-model="selected">
      <option disabled value="">请选择</option>
      <option value="apple">苹果</option>
      <option value="banana">香蕉</option>
      <option value="orange">橙子</option>
    </select>
    
    <p>选择的水果:{{ selected }}</p>
  </div>
</template>

多选

vue
<script setup>
import { ref } from 'vue'

const selected = ref([])
</script>

<template>
  <div>
    <select v-model="selected" multiple>
      <option value="apple">苹果</option>
      <option value="banana">香蕉</option>
      <option value="orange">橙子</option>
      <option value="grape">葡萄</option>
    </select>
    
    <p>选择的水果:{{ selected.join(', ') }}</p>
  </div>
</template>

v-model 修饰符

.lazy

默认情况下,v-model 在每次 input 事件后更新数据。使用 .lazy 修饰符可以改为在 change 事件后更新:

vue
<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <div>
    <input v-model.lazy="message" placeholder="失去焦点后更新" />
    <p>{{ message }}</p>
  </div>
</template>

.number

自动将用户输入转换为数字:

vue
<script setup>
import { ref } from 'vue'

const age = ref(0)
</script>

<template>
  <div>
    <input v-model.number="age" type="number" />
    <p>年龄:{{ age }} (类型:{{ typeof age }})</p>
  </div>
</template>

.trim

自动去除首尾空格:

vue
<script setup>
import { ref } from 'vue'

const username = ref('')
</script>

<template>
  <div>
    <input v-model.trim="username" placeholder="自动去除空格" />
    <p>用户名:'{{ username }}'</p>
  </div>
</template>

实战示例:用户注册表单

vue
<script setup>
import { ref, computed } from 'vue'

const form = ref({
  username: '',
  email: '',
  password: '',
  confirmPassword: '',
  gender: '',
  hobbies: [],
  country: '',
  agreed: false
})

const errors = ref({})

const isFormValid = computed(() => {
  return Object.keys(errors.value).length === 0 &&
         form.value.username &&
         form.value.email &&
         form.value.password &&
         form.value.confirmPassword &&
         form.value.agreed
})

function validateUsername() {
  if (!form.value.username) {
    errors.value.username = '用户名不能为空'
  } else if (form.value.username.length < 3) {
    errors.value.username = '用户名至少3个字符'
  } else {
    delete errors.value.username
  }
}

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!form.value.email) {
    errors.value.email = '邮箱不能为空'
  } else if (!emailRegex.test(form.value.email)) {
    errors.value.email = '邮箱格式不正确'
  } else {
    delete errors.value.email
  }
}

function validatePassword() {
  if (!form.value.password) {
    errors.value.password = '密码不能为空'
  } else if (form.value.password.length < 6) {
    errors.value.password = '密码至少6个字符'
  } else {
    delete errors.value.password
  }
  validateConfirmPassword()
}

function validateConfirmPassword() {
  if (!form.value.confirmPassword) {
    errors.value.confirmPassword = '请确认密码'
  } else if (form.value.password !== form.value.confirmPassword) {
    errors.value.confirmPassword = '两次密码不一致'
  } else {
    delete errors.value.confirmPassword
  }
}

function handleSubmit() {
  validateUsername()
  validateEmail()
  validatePassword()
  validateConfirmPassword()
  
  if (isFormValid.value) {
    console.log('表单数据:', form.value)
    alert('注册成功!')
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="register-form">
    <h2>用户注册</h2>
    
    <!-- 用户名 -->
    <div class="form-group">
      <label>用户名 *</label>
      <input 
        v-model.trim="form.username" 
        @blur="validateUsername"
        placeholder="请输入用户名"
      />
      <span v-if="errors.username" class="error">{{ errors.username }}</span>
    </div>
    
    <!-- 邮箱 -->
    <div class="form-group">
      <label>邮箱 *</label>
      <input 
        v-model.trim="form.email" 
        @blur="validateEmail"
        type="email"
        placeholder="请输入邮箱"
      />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    
    <!-- 密码 -->
    <div class="form-group">
      <label>密码 *</label>
      <input 
        v-model="form.password" 
        @blur="validatePassword"
        type="password"
        placeholder="请输入密码"
      />
      <span v-if="errors.password" class="error">{{ errors.password }}</span>
    </div>
    
    <!-- 确认密码 -->
    <div class="form-group">
      <label>确认密码 *</label>
      <input 
        v-model="form.confirmPassword" 
        @blur="validateConfirmPassword"
        type="password"
        placeholder="请再次输入密码"
      />
      <span v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</span>
    </div>
    
    <!-- 性别 -->
    <div class="form-group">
      <label>性别</label>
      <div class="radio-group">
        <label><input type="radio" v-model="form.gender" value="male" /> 男</label>
        <label><input type="radio" v-model="form.gender" value="female" /> 女</label>
        <label><input type="radio" v-model="form.gender" value="other" /> 其他</label>
      </div>
    </div>
    
    <!-- 爱好 -->
    <div class="form-group">
      <label>爱好</label>
      <div class="checkbox-group">
        <label><input type="checkbox" v-model="form.hobbies" value="reading" /> 阅读</label>
        <label><input type="checkbox" v-model="form.hobbies" value="sports" /> 运动</label>
        <label><input type="checkbox" v-model="form.hobbies" value="music" /> 音乐</label>
        <label><input type="checkbox" v-model="form.hobbies" value="travel" /> 旅游</label>
      </div>
    </div>
    
    <!-- 国家 -->
    <div class="form-group">
      <label>国家</label>
      <select 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 class="checkbox-label">
        <input type="checkbox" v-model="form.agreed" />
        我已阅读并同意服务条款 *
      </label>
    </div>
    
    <!-- 提交按钮 -->
    <button type="submit" :disabled="!isFormValid">注册</button>
  </form>
</template>

<style scoped>
.register-form {
  max-width: 500px;
  margin: 20px auto;
  padding: 30px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #fff;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
  color: #333;
}

.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.form-group input:focus,
.form-group select:focus {
  outline: none;
  border-color: #42b983;
}

.error {
  display: block;
  color: #f56c6c;
  font-size: 12px;
  margin-top: 5px;
}

.radio-group,
.checkbox-group {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}

.radio-group label,
.checkbox-group label {
  font-weight: normal;
  display: flex;
  align-items: center;
  gap: 5px;
}

.checkbox-label {
  font-weight: normal !important;
  display: flex;
  align-items: center;
  gap: 8px;
}

button {
  width: 100%;
  padding: 12px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}

button:hover:not(:disabled) {
  background-color: #35a372;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

实战示例:动态表单

vue
<script setup>
import { ref } from 'vue'

const formFields = ref([
  { id: 1, label: '姓名', value: '', type: 'text' }
])

let nextId = 2

function addField() {
  formFields.value.push({
    id: nextId++,
    label: `字段 ${nextId - 1}`,
    value: '',
    type: 'text'
  })
}

function removeField(id) {
  formFields.value = formFields.value.filter(field => field.id !== id)
}

function handleSubmit() {
  const data = formFields.value.reduce((acc, field) => {
    acc[field.label] = field.value
    return acc
  }, {})
  console.log('表单数据:', data)
  alert(JSON.stringify(data, null, 2))
}
</script>

<template>
  <div class="dynamic-form">
    <h2>动态表单</h2>
    
    <div v-for="field in formFields" :key="field.id" class="field-row">
      <input v-model="field.label" placeholder="字段名称" class="field-label" />
      <input v-model="field.value" :placeholder="`请输入${field.label}`" class="field-value" />
      <button @click="removeField(field.id)" class="remove-btn">删除</button>
    </div>
    
    <div class="actions">
      <button @click="addField" class="add-btn">添加字段</button>
      <button @click="handleSubmit" class="submit-btn">提交</button>
    </div>
  </div>
</template>

<style scoped>
.dynamic-form {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
}

.field-row {
  display: flex;
  gap: 10px;
  margin-bottom: 10px;
}

.field-label {
  width: 150px;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.field-value {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.remove-btn {
  padding: 8px 16px;
  background-color: #f56c6c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.add-btn,
.submit-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  color: white;
}

.add-btn {
  background-color: #409eff;
}

.submit-btn {
  background-color: #42b983;
}
</style>

总结

  • v-model 实现表单元素的双向绑定
  • 支持文本、复选框、单选按钮、下拉选择等多种表单元素
  • 使用修饰符 .lazy.number.trim 优化输入处理
  • 结合验证逻辑创建完整的表单系统
  • 可以创建动态表单满足复杂需求

下一步

接下来学习 组件通信,了解父子组件之间如何传递数据。