Skip to content

Autocomplete 自动补全输入框

概述

Autocomplete 自动补全输入框是一个带输入建议的输入框组件,可以根据用户输入提供相关的建议选项。它结合了输入框和下拉选择的功能,为用户提供智能的输入辅助。

学习目标

  • 掌握 Autocomplete 的基本概念和使用场景
  • 学会基础自动补全功能的实现
  • 了解数据源配置和过滤机制
  • 掌握自定义建议项模板
  • 学会远程搜索功能的实现
  • 了解防抖优化和性能提升
  • 掌握 API 的完整使用方法

基础用法

基本自动补全

最简单的自动补全功能:

vue
<template>
  <div>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="请输入餐厅名称"
      @select="handleSelect"
    />
    <p>选中的值:{{ state }}</p>
  </div>
</template>

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

const state = ref('')

const restaurants = [
  { value: '三全鲜食(北新泾店)', address: '长宁区新渔路144号' },
  { value: 'Hot honey 首尔炸鸡(仙霞路)', address: '上海市长宁区淞虹路661号' },
  { value: '新旺角茶餐厅', address: '上海市普陀区真北路988号创邑金沙谷6号楼113' },
  { value: '泷千家(天山西路店)', address: '天山西路438号' },
  { value: '胖仙女烧烤-精酿啤酒·新疆烧烤', address: '金沙江路1699号鑫乐惠美食广场A13' },
  { value: '麦当劳(天山西路店)', address: '天山西路438号' },
  { value: '肯德基(延安西路店)', address: '延安西路1228号' },
  { value: '星巴克(中山公园店)', address: '长宁路1158号' }
]

const querySearch = (queryString, cb) => {
  const results = queryString
    ? restaurants.filter(createFilter(queryString))
    : restaurants
  cb(results)
}

const createFilter = (queryString) => {
  return (restaurant) => {
    return (
      restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
    )
  }
}

const handleSelect = (item) => {
  console.log('选中的项目:', item)
}
</script>

自定义模板

使用作用域插槽自定义建议项的显示:

vue
<template>
  <div>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="请输入餐厅名称"
      @select="handleSelect"
    >
      <template #default="{ item }">
        <div class="suggestion-item">
          <div class="name">{{ item.value }}</div>
          <div class="address">{{ item.address }}</div>
        </div>
      </template>
    </el-autocomplete>
  </div>
</template>

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

const state = ref('')

const restaurants = [
  { value: '三全鲜食(北新泾店)', address: '长宁区新渔路144号' },
  { value: 'Hot honey 首尔炸鸡(仙霞路)', address: '上海市长宁区淞虹路661号' },
  { value: '新旺角茶餐厅', address: '上海市普陀区真北路988号创邑金沙谷6号楼113' },
  { value: '泷千家(天山西路店)', address: '天山西路438号' },
  { value: '胖仙女烧烤-精酿啤酒·新疆烧烤', address: '金沙江路1699号鑫乐惠美食广场A13' }
]

const querySearch = (queryString, cb) => {
  const results = queryString
    ? restaurants.filter(createFilter(queryString))
    : restaurants
  cb(results)
}

const createFilter = (queryString) => {
  return (restaurant) => {
    return (
      restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
    )
  }
}

const handleSelect = (item) => {
  console.log('选中的项目:', item)
}
</script>

<style scoped>
.suggestion-item {
  padding: 4px 0;
}

.name {
  font-weight: 500;
  color: #303133;
}

.address {
  font-size: 12px;
  color: #909399;
  margin-top: 2px;
}
</style>

远程搜索

从服务器搜索数据:

vue
<template>
  <div>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearchAsync"
      placeholder="请输入关键词"
      :debounce="600"
      @select="handleSelect"
    />
    <p>选中的值:{{ state }}</p>
  </div>
</template>

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

const state = ref('')

// 模拟远程数据
const mockData = [
  { value: 'Vue.js', category: '前端框架' },
  { value: 'React', category: '前端框架' },
  { value: 'Angular', category: '前端框架' },
  { value: 'Node.js', category: '后端技术' },
  { value: 'Express', category: '后端框架' },
  { value: 'Koa', category: '后端框架' },
  { value: 'MongoDB', category: '数据库' },
  { value: 'MySQL', category: '数据库' },
  { value: 'Redis', category: '缓存' },
  { value: 'Docker', category: '容器技术' }
]

const querySearchAsync = (queryString, cb) => {
  // 模拟网络延迟
  setTimeout(() => {
    const results = queryString
      ? mockData.filter(item => 
          item.value.toLowerCase().includes(queryString.toLowerCase())
        )
      : mockData
    cb(results)
  }, Math.random() * 200 + 200)
}

const handleSelect = (item) => {
  console.log('选中的项目:', item)
}
</script>

输入长度限制

设置触发搜索的最小输入长度:

vue
<template>
  <div>
    <h4>至少输入2个字符才触发搜索</h4>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="请输入至少2个字符"
      :trigger-on-focus="false"
      @select="handleSelect"
    />
  </div>
</template>

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

const state = ref('')

const cities = [
  { value: '北京', code: 'BJ' },
  { value: '上海', code: 'SH' },
  { value: '广州', code: 'GZ' },
  { value: '深圳', code: 'SZ' },
  { value: '杭州', code: 'HZ' },
  { value: '南京', code: 'NJ' },
  { value: '苏州', code: 'SZ' },
  { value: '成都', code: 'CD' },
  { value: '重庆', code: 'CQ' },
  { value: '武汉', code: 'WH' }
]

const querySearch = (queryString, cb) => {
  // 至少输入2个字符才进行搜索
  if (queryString.length < 2) {
    cb([])
    return
  }
  
  const results = cities.filter(city => 
    city.value.toLowerCase().includes(queryString.toLowerCase())
  )
  cb(results)
}

const handleSelect = (item) => {
  console.log('选中的城市:', item)
}
</script>

高亮匹配文本

自定义模板实现匹配文本高亮:

vue
<template>
  <div>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="请输入技术名称"
      @select="handleSelect"
    >
      <template #default="{ item }">
        <div class="suggestion-item">
          <div class="name" v-html="highlightMatch(item.value, state)"></div>
          <div class="category">{{ item.category }}</div>
        </div>
      </template>
    </el-autocomplete>
  </div>
</template>

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

const state = ref('')

const technologies = [
  { value: 'JavaScript', category: '编程语言' },
  { value: 'TypeScript', category: '编程语言' },
  { value: 'Python', category: '编程语言' },
  { value: 'Java', category: '编程语言' },
  { value: 'Vue.js', category: '前端框架' },
  { value: 'React', category: '前端框架' },
  { value: 'Angular', category: '前端框架' },
  { value: 'Node.js', category: '运行时环境' },
  { value: 'Express', category: '后端框架' },
  { value: 'Nest.js', category: '后端框架' }
]

const querySearch = (queryString, cb) => {
  const results = queryString
    ? technologies.filter(tech => 
        tech.value.toLowerCase().includes(queryString.toLowerCase())
      )
    : technologies
  cb(results)
}

const highlightMatch = (text, query) => {
  if (!query) return text
  
  const regex = new RegExp(`(${query})`, 'gi')
  return text.replace(regex, '<mark>$1</mark>')
}

const handleSelect = (item) => {
  console.log('选中的技术:', item)
}
</script>

<style scoped>
.suggestion-item {
  padding: 4px 0;
}

.name {
  font-weight: 500;
  color: #303133;
}

.name :deep(mark) {
  background-color: #409eff;
  color: white;
  padding: 0 2px;
  border-radius: 2px;
}

.category {
  font-size: 12px;
  color: #909399;
  margin-top: 2px;
}
</style>

不同状态

禁用状态

vue
<template>
  <div>
    <h4>禁用状态</h4>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="禁用状态"
      disabled
    />
  </div>
</template>

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

const state = ref('已禁用的输入框')

const querySearch = (queryString, cb) => {
  cb([])
}
</script>

可清空

vue
<template>
  <div>
    <h4>可清空</h4>
    <el-autocomplete
      v-model="state"
      :fetch-suggestions="querySearch"
      placeholder="可清空的输入框"
      clearable
      @clear="handleClear"
    />
  </div>
</template>

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

const state = ref('')

const suggestions = [
  { value: 'Vue.js' },
  { value: 'React' },
  { value: 'Angular' }
]

const querySearch = (queryString, cb) => {
  const results = queryString
    ? suggestions.filter(item => 
        item.value.toLowerCase().includes(queryString.toLowerCase())
      )
    : suggestions
  cb(results)
}

const handleClear = () => {
  console.log('输入框已清空')
}
</script>

带输入建议

前置/后置内容

vue
<template>
  <div>
    <h4>前置内容</h4>
    <el-autocomplete
      v-model="state1"
      :fetch-suggestions="querySearch"
      placeholder="请输入网址"
    >
      <template #prepend>https://</template>
    </el-autocomplete>
    
    <h4>后置内容</h4>
    <el-autocomplete
      v-model="state2"
      :fetch-suggestions="querySearch"
      placeholder="请输入邮箱"
    >
      <template #append>@gmail.com</template>
    </el-autocomplete>
    
    <h4>前缀/后缀图标</h4>
    <el-autocomplete
      v-model="state3"
      :fetch-suggestions="querySearch"
      placeholder="搜索"
    >
      <template #prefix>
        <el-icon><Search /></el-icon>
      </template>
      <template #suffix>
        <el-icon><Star /></el-icon>
      </template>
    </el-autocomplete>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Search, Star } from '@element-plus/icons-vue'

const state1 = ref('')
const state2 = ref('')
const state3 = ref('')

const websites = [
  { value: 'www.github.com' },
  { value: 'www.stackoverflow.com' },
  { value: 'www.vue.js.org' },
  { value: 'www.element-plus.org' }
]

const querySearch = (queryString, cb) => {
  const results = queryString
    ? websites.filter(site => 
        site.value.toLowerCase().includes(queryString.toLowerCase())
      )
    : websites
  cb(results)
}
</script>

实际应用示例

用户搜索组件

vue
<template>
  <div class="user-search">
    <h3>用户搜索</h3>
    
    <div class="search-container">
      <el-autocomplete
        v-model="searchQuery"
        :fetch-suggestions="searchUsers"
        placeholder="搜索用户(姓名、邮箱、部门)"
        :debounce="300"
        clearable
        @select="handleUserSelect"
        @clear="handleClear"
      >
        <template #default="{ item }">
          <div class="user-item">
            <div class="user-avatar">
              <img :src="item.avatar" :alt="item.name" />
            </div>
            <div class="user-info">
              <div class="user-name">{{ item.name }}</div>
              <div class="user-details">
                <span class="email">{{ item.email }}</span>
                <span class="department">{{ item.department }}</span>
              </div>
            </div>
            <div class="user-status">
              <el-tag :type="item.status === 'active' ? 'success' : 'info'" size="small">
                {{ item.status === 'active' ? '在线' : '离线' }}
              </el-tag>
            </div>
          </div>
        </template>
        
        <template #prefix>
          <el-icon><Search /></el-icon>
        </template>
      </el-autocomplete>
    </div>
    
    <div v-if="selectedUser" class="selected-user">
      <h4>选中的用户</h4>
      <div class="user-card">
        <img :src="selectedUser.avatar" :alt="selectedUser.name" class="avatar" />
        <div class="info">
          <h5>{{ selectedUser.name }}</h5>
          <p>{{ selectedUser.email }}</p>
          <p>{{ selectedUser.department }}</p>
          <el-tag :type="selectedUser.status === 'active' ? 'success' : 'info'" size="small">
            {{ selectedUser.status === 'active' ? '在线' : '离线' }}
          </el-tag>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const searchQuery = ref('')
const selectedUser = ref(null)

// 模拟用户数据
const users = [
  {
    id: 1,
    name: '张三',
    email: 'zhangsan@company.com',
    department: '技术部',
    status: 'active',
    avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
  },
  {
    id: 2,
    name: '李四',
    email: 'lisi@company.com',
    department: '产品部',
    status: 'inactive',
    avatar: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png'
  },
  {
    id: 3,
    name: '王五',
    email: 'wangwu@company.com',
    department: '设计部',
    status: 'active',
    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
  },
  {
    id: 4,
    name: '赵六',
    email: 'zhaoliu@company.com',
    department: '运营部',
    status: 'active',
    avatar: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48png.png'
  },
  {
    id: 5,
    name: '钱七',
    email: 'qianqi@company.com',
    department: '技术部',
    status: 'inactive',
    avatar: 'https://cube.elemecdn.com/f/4a/ce7e1961e1a051d0f2bf113d6018apng.png'
  }
]

const searchUsers = (queryString, cb) => {
  if (!queryString) {
    cb(users.slice(0, 5)) // 默认显示前5个用户
    return
  }
  
  const query = queryString.toLowerCase()
  const results = users.filter(user => 
    user.name.toLowerCase().includes(query) ||
    user.email.toLowerCase().includes(query) ||
    user.department.toLowerCase().includes(query)
  )
  
  cb(results)
}

const handleUserSelect = (user) => {
  selectedUser.value = user
  ElMessage.success(`选择了用户:${user.name}`)
}

const handleClear = () => {
  selectedUser.value = null
  ElMessage.info('已清空选择')
}
</script>

<style scoped>
.user-search {
  max-width: 600px;
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 8px;
}

.search-container {
  margin-bottom: 20px;
}

.search-container .el-autocomplete {
  width: 100%;
}

.user-item {
  display: flex;
  align-items: center;
  padding: 8px 0;
  gap: 12px;
}

.user-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;
}

.user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.user-info {
  flex: 1;
}

.user-name {
  font-weight: 500;
  color: #303133;
  margin-bottom: 4px;
}

.user-details {
  display: flex;
  gap: 12px;
  font-size: 12px;
  color: #909399;
}

.user-status {
  margin-left: auto;
}

.selected-user {
  padding-top: 20px;
  border-top: 1px solid #e4e7ed;
}

.user-card {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 8px;
}

.user-card .avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
}

.user-card .info h5 {
  margin: 0 0 8px 0;
  color: #303133;
}

.user-card .info p {
  margin: 4px 0;
  color: #606266;
  font-size: 14px;
}
</style>

标签输入组件

vue
<template>
  <div class="tag-input">
    <h3>标签输入</h3>
    
    <div class="input-container">
      <div class="tags">
        <el-tag
          v-for="tag in selectedTags"
          :key="tag.id"
          closable
          @close="removeTag(tag)"
        >
          {{ tag.name }}
        </el-tag>
      </div>
      
      <el-autocomplete
        v-model="tagInput"
        :fetch-suggestions="searchTags"
        placeholder="输入标签名称"
        :debounce="200"
        @select="addTag"
        @keyup.enter="addCustomTag"
        class="tag-autocomplete"
      >
        <template #default="{ item }">
          <div class="tag-item">
            <span class="tag-name">{{ item.name }}</span>
            <span class="tag-count">{{ item.count }} 次使用</span>
          </div>
        </template>
      </el-autocomplete>
    </div>
    
    <div class="tag-suggestions">
      <h4>推荐标签</h4>
      <div class="suggestion-tags">
        <el-tag
          v-for="tag in recommendedTags"
          :key="tag.id"
          :type="isTagSelected(tag) ? 'success' : 'info'"
          @click="toggleTag(tag)"
          style="cursor: pointer; margin: 4px;"
        >
          {{ tag.name }}
        </el-tag>
      </div>
    </div>
    
    <div class="selected-tags-info">
      <h4>已选标签 ({{ selectedTags.length }})</h4>
      <p v-if="selectedTags.length === 0" class="no-tags">暂无选中的标签</p>
      <div v-else class="tags-list">
        <span v-for="(tag, index) in selectedTags" :key="tag.id">
          {{ tag.name }}<span v-if="index < selectedTags.length - 1">, </span>
        </span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'

const tagInput = ref('')
const selectedTags = ref([])

// 模拟标签数据
const allTags = [
  { id: 1, name: 'Vue.js', count: 1250 },
  { id: 2, name: 'React', count: 980 },
  { id: 3, name: 'JavaScript', count: 2100 },
  { id: 4, name: 'TypeScript', count: 750 },
  { id: 5, name: 'Node.js', count: 650 },
  { id: 6, name: 'CSS', count: 1800 },
  { id: 7, name: 'HTML', count: 1900 },
  { id: 8, name: 'Python', count: 1100 },
  { id: 9, name: 'Java', count: 900 },
  { id: 10, name: 'Go', count: 400 },
  { id: 11, name: 'Rust', count: 200 },
  { id: 12, name: 'Docker', count: 550 },
  { id: 13, name: 'Kubernetes', count: 300 },
  { id: 14, name: 'MongoDB', count: 450 },
  { id: 15, name: 'MySQL', count: 800 }
]

const recommendedTags = computed(() => {
  return allTags.slice(0, 8) // 显示前8个推荐标签
})

const searchTags = (queryString, cb) => {
  if (!queryString) {
    cb(allTags.slice(0, 10))
    return
  }
  
  const query = queryString.toLowerCase()
  const results = allTags.filter(tag => 
    tag.name.toLowerCase().includes(query) &&
    !selectedTags.value.some(selected => selected.id === tag.id)
  )
  
  cb(results)
}

const addTag = (tag) => {
  if (!selectedTags.value.some(selected => selected.id === tag.id)) {
    selectedTags.value.push(tag)
    tagInput.value = ''
    ElMessage.success(`添加标签:${tag.name}`)
  } else {
    ElMessage.warning('标签已存在')
  }
}

const addCustomTag = () => {
  const tagName = tagInput.value.trim()
  if (!tagName) return
  
  // 检查是否已存在
  if (selectedTags.value.some(tag => tag.name.toLowerCase() === tagName.toLowerCase())) {
    ElMessage.warning('标签已存在')
    return
  }
  
  // 创建新标签
  const newTag = {
    id: Date.now(),
    name: tagName,
    count: 0
  }
  
  selectedTags.value.push(newTag)
  tagInput.value = ''
  ElMessage.success(`添加自定义标签:${newTag.name}`)
}

const removeTag = (tag) => {
  const index = selectedTags.value.findIndex(selected => selected.id === tag.id)
  if (index > -1) {
    selectedTags.value.splice(index, 1)
    ElMessage.info(`移除标签:${tag.name}`)
  }
}

const isTagSelected = (tag) => {
  return selectedTags.value.some(selected => selected.id === tag.id)
}

const toggleTag = (tag) => {
  if (isTagSelected(tag)) {
    removeTag(tag)
  } else {
    addTag(tag)
  }
}
</script>

<style scoped>
.tag-input {
  max-width: 600px;
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 8px;
}

.input-container {
  margin-bottom: 20px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
  min-height: 32px;
  padding: 8px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  background-color: #fafafa;
}

.tag-autocomplete {
  width: 100%;
}

.tag-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 0;
}

.tag-name {
  font-weight: 500;
  color: #303133;
}

.tag-count {
  font-size: 12px;
  color: #909399;
}

.tag-suggestions {
  margin-bottom: 20px;
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 6px;
}

.tag-suggestions h4 {
  margin: 0 0 12px 0;
  color: #303133;
}

.suggestion-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.selected-tags-info {
  padding-top: 16px;
  border-top: 1px solid #e4e7ed;
}

.selected-tags-info h4 {
  margin: 0 0 12px 0;
  color: #303133;
}

.no-tags {
  color: #909399;
  font-style: italic;
}

.tags-list {
  color: #606266;
  line-height: 1.5;
}
</style>

API 文档

Autocomplete Attributes

名称说明类型默认值
model-value / v-model绑定值string
placeholder输入框占位文本string
clearable是否可清空booleanfalse
disabled是否禁用booleanfalse
value-key输入建议对象中用于显示的键名stringvalue
debounce获取输入建议的去抖延时number300
placement菜单弹出位置enumbottom-start
fetch-suggestions返回输入建议的方法function
popper-classAutocomplete 下拉列表的类名string
trigger-on-focus是否在输入框 focus 时显示建议列表booleantrue
name原生属性string
select-when-unmatched在输入没有任何匹配建议的情况下,按下回车是否触发 select 事件booleanfalse
label输入框关联的 label 文字string
prefix-icon输入框头部图标string / Component
suffix-icon输入框尾部图标string / Component
hide-loading是否隐藏远程加载时的加载图标booleanfalse
popper-append-to-body是否将下拉列表插入至 body 元素booleanfalse
highlight-first-item是否默认突出显示远程搜索建议中的第一项booleanfalse

Autocomplete Events

名称说明类型
select点击选中建议项时触发Function
change在 Input 值改变时触发Function
focus在 Input 获得焦点时触发Function
blur在 Input 失去焦点时触发Function
clear在点击由 clearable 属性生成的清空按钮时触发Function

Autocomplete Methods

名称说明类型
focus使 input 获取焦点Function
blur使 input 失去焦点Function

Autocomplete Slots

名称说明
default自定义输入建议的内容,参数为
prefix输入框头部内容
suffix输入框尾部内容
prepend输入框前置内容
append输入框后置内容

实践练习

练习1:搜索引擎建议

创建一个类似搜索引擎的自动补全功能:

vue
<template>
  <div class="search-engine">
    <h3>搜索引擎</h3>
    <el-autocomplete
      v-model="searchQuery"
      :fetch-suggestions="getSearchSuggestions"
      placeholder="搜索任何内容..."
      :debounce="200"
      @select="handleSearch"
      @keyup.enter="handleSearch"
      class="search-input"
    >
      <template #default="{ item }">
        <div class="search-suggestion">
          <el-icon class="search-icon"><Search /></el-icon>
          <span class="suggestion-text">{{ item.value }}</span>
          <span class="suggestion-type">{{ item.type }}</span>
        </div>
      </template>
      
      <template #prefix>
        <el-icon><Search /></el-icon>
      </template>
    </el-autocomplete>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Search } from '@element-plus/icons-vue'

const searchQuery = ref('')

const searchSuggestions = [
  { value: 'Vue.js 教程', type: '教程' },
  { value: 'React 开发指南', type: '指南' },
  { value: 'JavaScript 基础', type: '基础' },
  { value: 'TypeScript 进阶', type: '进阶' },
  { value: 'Node.js 实战', type: '实战' },
  { value: 'CSS 布局技巧', type: '技巧' },
  { value: 'HTML5 新特性', type: '特性' },
  { value: 'Webpack 配置', type: '配置' },
  { value: 'Git 版本控制', type: '工具' },
  { value: 'Docker 容器化', type: '容器' }
]

const getSearchSuggestions = (queryString, cb) => {
  if (!queryString) {
    cb(searchSuggestions.slice(0, 5))
    return
  }
  
  const results = searchSuggestions.filter(item => 
    item.value.toLowerCase().includes(queryString.toLowerCase())
  )
  
  cb(results)
}

const handleSearch = (item) => {
  const query = typeof item === 'string' ? item : item.value
  console.log('搜索:', query)
  // 这里可以跳转到搜索结果页面
}
</script>

<style scoped>
.search-engine {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.search-input {
  width: 100%;
}

.search-suggestion {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}

.search-icon {
  color: #909399;
}

.suggestion-text {
  flex: 1;
  color: #303133;
}

.suggestion-type {
  font-size: 12px;
  color: #909399;
  background-color: #f5f7fa;
  padding: 2px 6px;
  border-radius: 4px;
}
</style>

练习2:邮箱地址补全

创建一个邮箱地址自动补全功能:

vue
<template>
  <div class="email-autocomplete">
    <h3>邮箱地址补全</h3>
    <el-autocomplete
      v-model="email"
      :fetch-suggestions="getEmailSuggestions"
      placeholder="请输入邮箱地址"
      @select="handleEmailSelect"
    >
      <template #default="{ item }">
        <div class="email-suggestion">
          <el-icon class="email-icon"><Message /></el-icon>
          <span class="email-text">{{ item.value }}</span>
        </div>
      </template>
    </el-autocomplete>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Message } from '@element-plus/icons-vue'

const email = ref('')

const emailDomains = [
  '@gmail.com',
  '@qq.com',
  '@163.com',
  '@126.com',
  '@sina.com',
  '@hotmail.com',
  '@yahoo.com',
  '@outlook.com',
  '@foxmail.com',
  '@sohu.com'
]

const getEmailSuggestions = (queryString, cb) => {
  if (!queryString || !queryString.includes('@')) {
    // 如果没有@符号,提供域名建议
    const suggestions = emailDomains.map(domain => ({
      value: queryString + domain
    }))
    cb(suggestions)
    return
  }
  
  const [localPart, domainPart] = queryString.split('@')
  if (!domainPart) {
    const suggestions = emailDomains.map(domain => ({
      value: localPart + domain
    }))
    cb(suggestions)
  } else {
    const matchingDomains = emailDomains.filter(domain => 
      domain.toLowerCase().includes('@' + domainPart.toLowerCase())
    )
    const suggestions = matchingDomains.map(domain => ({
      value: localPart + domain
    }))
    cb(suggestions)
  }
}

const handleEmailSelect = (item) => {
  console.log('选中的邮箱:', item.value)
}
</script>

<style scoped>
.email-autocomplete {
  max-width: 400px;
  padding: 20px;
}

.email-suggestion {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}

.email-icon {
  color: #409eff;
}

.email-text {
  color: #303133;
}
</style>

常见问题

1. 防抖优化

问题:频繁的输入导致过多的搜索请求

解决方案

javascript
// 使用 debounce 属性
<el-autocomplete
  :debounce="600"
  :fetch-suggestions="querySearch"
/>

// 或者在 fetch-suggestions 方法中实现防抖
let timeout = null
const querySearch = (queryString, cb) => {
  clearTimeout(timeout)
  timeout = setTimeout(() => {
    // 执行搜索逻辑
    const results = performSearch(queryString)
    cb(results)
  }, 300)
}

2. 远程搜索错误处理

问题:远程搜索失败时的处理

解决方案

javascript
const querySearchAsync = async (queryString, cb) => {
  try {
    const response = await fetch(`/api/search?q=${queryString}`)
    if (!response.ok) {
      throw new Error('搜索失败')
    }
    const data = await response.json()
    cb(data.results)
  } catch (error) {
    console.error('搜索错误:', error)
    cb([]) // 返回空数组
    ElMessage.error('搜索失败,请稍后重试')
  }
}

3. 大数据量优化

问题:建议列表数据量过大影响性能

解决方案

javascript
const querySearch = (queryString, cb) => {
  const results = queryString
    ? largeDataSet.filter(createFilter(queryString)).slice(0, 20) // 限制结果数量
    : largeDataSet.slice(0, 10)
  cb(results)
}

最佳实践

  1. 合理设置防抖时间:根据数据源类型调整防抖延时
  2. 限制建议数量:避免显示过多建议项影响用户体验
  3. 提供清空功能:让用户能够快速清空输入
  4. 键盘导航支持:确保组件支持键盘操作
  5. 错误处理:妥善处理远程搜索的错误情况
  6. 加载状态:为远程搜索提供加载指示

总结

Autocomplete 自动补全输入框是一个强大的输入辅助组件,支持:

  • 本地和远程数据搜索
  • 自定义建议项模板
  • 防抖优化和性能控制
  • 丰富的配置选项
  • 良好的用户体验

掌握 Autocomplete 组件的使用,能够为用户提供更智能、更便捷的输入体验。

参考资料

Element Plus Study Guide