文本输入框

文本输入框是最常见的表单元素,包括单行文本、密码框、多行文本等。虽然使用 v-model 很简单,但实际开发中有很多细节需要注意。

单行文本输入框

基本用法

<input type="text" v-model="message">
<p>{{ message }}</p>

<script>
new Vue({
  data: {
    message: ''
  }
})
</script>

不同类型的输入框

HTML5 提供了多种输入类型,v-model 都能正常工作:

<!-- 普通文本 -->
<input type="text" v-model="text">

<!-- 密码框 -->
<input type="password" v-model="password">

<!-- 邮箱 -->
<input type="email" v-model="email">

<!-- 数字 -->
<input type="number" v-model.number="age">

<!-- 电话 -->
<input type="tel" v-model="phone">

<!-- URL -->
<input type="url" v-model="website">

<!-- 搜索框 -->
<input type="search" v-model="keyword">

type="number" 的陷阱

type="number" 的输入框,v-model 返回的仍然是字符串。需要使用 .number 修饰符:

<!-- ❌ 返回字符串 -->
<input type="number" v-model="age">
<!-- age: "123" -->

<!-- ✅ 返回数字 -->
<input type="number" v-model.number="age">
<!-- age: 123 -->

占位符和默认值

<!-- 占位符 -->
<input type="text" v-model="name" placeholder="请输入姓名">

<!-- 只读 -->
<input type="text" v-model="name" readonly>

<!-- 禁用 -->
<input type="text" v-model="name" disabled>

<!-- 最大长度 -->
<input type="text" v-model="name" maxlength="20">

<!-- 自动聚焦 -->
<input type="text" v-model="name" autofocus>

多行文本 textarea

基本用法

<textarea v-model="content"></textarea>

<script>
new Vue({
  data: {
    content: ''
  }
})
</script>

常见错误

在 textarea 中使用插值语法不会生效:

<!-- ❌ 错误:插值不会生效 -->
<textarea>{{ content }}</textarea>

<!-- ✅ 正确:使用 v-model -->
<textarea v-model="content"></textarea>

设置尺寸

<!-- 使用 rows 和 cols -->
<textarea v-model="content" rows="5" cols="30"></textarea>

<!-- 使用 CSS -->
<textarea v-model="content" style="width: 100%; height: 200px;"></textarea>

<!-- 禁止调整大小 -->
<textarea v-model="content" style="resize: none;"></textarea>

字数统计

<div class="textarea-wrapper">
  <textarea 
    v-model="content" 
    maxlength="200"
    placeholder="请输入内容..."
  ></textarea>
  <span class="counter">{{ content.length }}/200</span>
</div>

<style>
.textarea-wrapper {
  position: relative;
}
.counter {
  position: absolute;
  bottom: 10px;
  right: 10px;
  color: #999;
  font-size: 12px;
}
</style>

输入验证

实时验证

<div class="form-group">
  <input 
    type="text" 
    v-model="username"
    @input="validateUsername"
    placeholder="用户名"
  >
  <span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>

<script>
new Vue({
  data: {
    username: '',
    errors: {
      username: ''
    }
  },
  methods: {
    validateUsername: function() {
      if (!this.username) {
        this.errors.username = '用户名不能为空'
      } else if (this.username.length < 3) {
        this.errors.username = '用户名至少3个字符'
      } else if (this.username.length > 20) {
        this.errors.username = '用户名最多20个字符'
      } else {
        this.errors.username = ''
      }
    }
  }
})
</script>

使用计算属性验证

<div class="form-group">
  <input type="email" v-model="email" placeholder="邮箱">
  <span v-if="email && !emailValid" class="error">邮箱格式不正确</span>
</div>

<script>
new Vue({
  data: {
    email: ''
  },
  computed: {
    emailValid: function() {
      const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      return re.test(this.email)
    }
  }
})
</script>

失焦验证

<div class="form-group">
  <input 
    type="text" 
    v-model="phone"
    @blur="validatePhone"
    placeholder="手机号"
  >
  <span v-if="errors.phone" class="error">{{ errors.phone }}</span>
</div>

<script>
new Vue({
  data: {
    phone: '',
    errors: {
      phone: ''
    }
  },
  methods: {
    validatePhone: function() {
      const re = /^1[3-9]\d{9}$/
      if (this.phone && !re.test(this.phone)) {
        this.errors.phone = '手机号格式不正确'
      } else {
        this.errors.phone = ''
      }
    }
  }
})
</script>

实时搜索

防抖处理

<input 
  type="search" 
  v-model="keyword"
  @input="debounceSearch"
  placeholder="搜索..."
>

<script>
new Vue({
  data: {
    keyword: '',
    timer: null
  },
  methods: {
    debounceSearch: function() {
      clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        this.search()
      }, 300)
    },
    search: function() {
      if (!this.keyword.trim()) return
      console.log('搜索:', this.keyword)
      // 调用搜索 API
    }
  }
})
</script>

使用 watch 监听

<input type="search" v-model="keyword" placeholder="搜索...">

<script>
new Vue({
  data: {
    keyword: ''
  },
  watch: {
    keyword: function(newVal) {
      this.debouncedSearch(newVal)
    }
  },
  created: function() {
    this.debouncedSearch = this.debounce(this.search, 300)
  },
  methods: {
    debounce: function(fn, delay) {
      let timer = null
      return function(...args) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          fn.apply(this, args)
        }, delay)
      }
    },
    search: function(keyword) {
      if (!keyword.trim()) return
      console.log('搜索:', keyword)
    }
  }
})
</script>

自动完成

<div class="autocomplete">
  <input 
    type="text" 
    v-model="query"
    @input="fetchSuggestions"
    @keydown.down="selectNext"
    @keydown.up="selectPrev"
    @keydown.enter="selectCurrent"
    placeholder="搜索..."
  >
  <ul v-if="suggestions.length > 0" class="suggestions">
    <li 
      v-for="(item, index) in suggestions"
      :key="item.id"
      :class="{ active: index === selectedIndex }"
      @click="selectSuggestion(item)"
    >
      {{ item.name }}
    </li>
  </ul>
</div>

<script>
new Vue({
  data: {
    query: '',
    suggestions: [],
    selectedIndex: 0
  },
  methods: {
    fetchSuggestions: function() {
      if (!this.query.trim()) {
        this.suggestions = []
        return
      }
      // 模拟 API 调用
      this.suggestions = [
        { id: 1, name: this.query + ' 建议一' },
        { id: 2, name: this.query + ' 建议二' },
        { id: 3, name: this.query + ' 建议三' }
      ]
      this.selectedIndex = 0
    },
    selectNext: function() {
      if (this.selectedIndex < this.suggestions.length - 1) {
        this.selectedIndex++
      }
    },
    selectPrev: function() {
      if (this.selectedIndex > 0) {
        this.selectedIndex--
      }
    },
    selectCurrent: function() {
      if (this.suggestions.length > 0) {
        this.selectSuggestion(this.suggestions[this.selectedIndex])
      }
    },
    selectSuggestion: function(item) {
      this.query = item.name
      this.suggestions = []
    }
  }
})
</script>

密码显示/隐藏

<div class="password-input">
  <input 
    :type="showPassword ? 'text' : 'password'"
    v-model="password"
    placeholder="密码"
  >
  <button @click="showPassword = !showPassword">
    {{ showPassword ? '隐藏' : '显示' }}
  </button>
</div>

<script>
new Vue({
  data: {
    password: '',
    showPassword: false
  }
})
</script>

格式化输入

手机号格式化

<input 
  type="text" 
  v-model="formattedPhone"
  @input="formatPhone"
  placeholder="手机号"
>

<script>
new Vue({
  data: {
    phone: '',
    formattedPhone: ''
  },
  methods: {
    formatPhone: function() {
      // 只保留数字
      let value = this.formattedPhone.replace(/\D/g, '')
      // 限制长度
      value = value.substring(0, 11)
      // 格式化:138 1234 5678
      if (value.length > 7) {
        this.formattedPhone = value.substring(0, 3) + ' ' + 
                              value.substring(3, 7) + ' ' + 
                              value.substring(7)
      } else if (value.length > 3) {
        this.formattedPhone = value.substring(0, 3) + ' ' + value.substring(3)
      } else {
        this.formattedPhone = value
      }
      // 保存纯数字
      this.phone = value
    }
  }
})
</script>

金额格式化

<input 
  type="text" 
  v-model="formattedAmount"
  @input="formatAmount"
  placeholder="金额"
>
<p>实际金额:{{ amount }}</p>

<script>
new Vue({
  data: {
    amount: 0,
    formattedAmount: ''
  },
  methods: {
    formatAmount: function() {
      // 只保留数字和小数点
      let value = this.formattedAmount.replace(/[^\d.]/g, '')
      // 只保留第一个小数点
      const parts = value.split('.')
      if (parts.length > 2) {
        value = parts[0] + '.' + parts.slice(1).join('')
      }
      // 限制小数位数
      if (parts[1] && parts[1].length > 2) {
        value = parts[0] + '.' + parts[1].substring(0, 2)
      }
      this.formattedAmount = value
      this.amount = parseFloat(value) || 0
    }
  }
})
</script>

小结

文本输入框处理要点:

  1. 类型选择:根据输入内容选择合适的 type
  2. 数字处理type="number" 配合 .number 修饰符
  3. 验证时机:实时验证 vs 失焦验证
  4. 用户体验:字数统计、格式化、自动完成