Vue3+AntDesign实现带搜索功能的下拉单选组件

目录

一、需求如下

功能说明

关键实现点

二、效果图

三、代码结构图

四、实现代码


详细需求设计文档,及实施任务请参见本篇文章:

设计文档和需求文档及实施计划-CSDN博客文章浏览阅读169次。本文档描述了一个集成企查查云端检索功能的智能下拉选择器组件,采用Vue 3和Ant Design Vue实现。组件包含本地搜索、云端检索、数据选择三大核心功能,支持通过API与企查查服务交互。文档详细说明了组件架构、接口定义、数据模型及核心实现逻辑,包括本地选项过滤、云端检索触发条件判断、API调用封装以及选择结果回显机制。组件设计考虑了错误处理、数据验证和用户体验,提供了完整的搜索选择解决方案。 https://blog.csdn.net/nndsb/article/details/150352714?spm=1011.2415.3001.5331

Gitee 代码仓储:https://gitee.com/lzm52cml/vue3_demo1.githttps://gitee.com/lzm52cml/vue3_demo1.git

一、需求如下

实现一个带搜索功能的下拉单选组件,左侧为支持搜索的下拉框,中间按钮默认隐藏(仅当搜索无结果时显示),右侧为提示文本。当搜索无匹配项时,展示按钮;点击按钮弹出含单选表格的弹窗,选中数据后关闭弹窗并将指定字段回显至下拉框。该交互需满足:

1)下拉框搜索匹配时隐藏按钮

2)弹窗表格支持单选

3) 数据回显功能。

功能说明

  1. 搜索功能:下拉框支持输入搜索,实时过滤选项
  2. 动态按钮:当搜索无结果时显示添加按钮,有匹配结果时隐藏
  3. 弹窗交互:点击按钮弹出含单选表格的模态框
  4. 数据回显:从表格选择数据后自动关闭弹窗并回显到下拉框
  5. 自定义配置:支持通过props传递选项数据、表格数据、列配置等

关键实现点

  • 使用show-search属性启用Ant Design Select的搜索功能
  • 通过filteredOptions计算属性实现搜索过滤逻辑
  • 使用showAddButton计算属性控制按钮显示状态
  • 通过rowSelection配置实现表格单选功能
  • 使用v-model:value实现双向数据绑定

该组件已完整实现需求中的所有交互功能,可根据实际项目需求调整样式和配置参数。

二、效果图

三、代码结构图

四、实现代码

<template>
  <div class="searchable-dropdown-container">
    <!-- 主要搜索区域 -->
    <div class="dropdown-section">
      <label class="dropdown-label">客户单位名称</label>
      <a-select
        v-model:value="selectedValue"
        show-search
        placeholder="请选择单位名称"
        class="dropdown-select"
        :filter-option="false"
        :options="filteredOptions"
        @search="handleSearch"
        @change="handleSelectionChange"
        @keydown="handleKeyDown"
        ref="selectRef"
      >
        <template #notFoundContent>
          <div v-if="showNoResultsMessage" class="no-results-content">
            <div class="no-results-text">
              没有找到匹配的单位信息
            </div>
            <div class="help-message">
              您可以点击启动企查查云端检索查询<br />
              若单位未在企查查内登记,请联系CRM管理员
            </div>
          </div>
        </template>
      </a-select>
    </div>

    <!-- 云端检索按钮区域 -->
    <div class="button-section">
      <a-button
        v-if="showCloudSearchButton"
        type="primary"
        class="cloud-search-button"
        :loading="loading"
        @click="openCloudSearchModal"
      >
        启动企查查云端检索
      </a-button>
    </div>

    <!-- 右侧提示文本 -->
    <div class="help-text-section">
      <span class="help-text">
        {{ helpText }}
      </span>
    </div>
  </div>

  <!-- 云端检索弹窗 -->
  <a-modal
    v-model:open="modalVisible"
    title="企查查单位查询"
    width="800px"
    :confirm-loading="loading"
    @ok="handleModalConfirm"
    @cancel="handleModalCancel"
  >
    <a-table
      :columns="tableColumns"
      :data-source="cloudSearchResults"
      row-key="id"
      :pagination="false"
      :row-selection="rowSelection"
      :loading="loading"
      size="small"
    />
  </a-modal>
</template>

<script setup>
import { ref, computed, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { debounce } from './utils/debounce'

// 组件引用
const selectRef = ref(null)

// 原始下拉选项
const options = ref([
  { label: '阿里巴巴集团', value: 'alibaba' },
  { label: '腾讯科技有限公司', value: 'tencent' },
  { label: '百度在线网络技术公司', value: 'baidu' },
  { label: '京东集团', value: 'jd' },
  { label: '网易公司', value: 'netease' },
  { label: '新浪微博', value: 'weibo' }
])

// 响应式状态
const selectedValue = ref(null)
const searchValue = ref('')
const loading = ref(false)
const modalVisible = ref(false)
const selectedRowKeys = ref([])
const highlightedIndex = ref(-1)

// 右侧提示文本
const helpText = ref('请输入单位名称进行搜索,如果找不到可以使用云端检索功能')

// 云端检索结果数据
const cloudSearchResults = ref([
  { id: 1, name: '字节跳动有限公司', code: '91110108MA01', address: '北京市海淀区知春路63号' },
  { id: 2, name: '小米科技有限责任公司', code: '9111010855', address: '北京市海淀区西二旗中路33号' },
  { id: 3, name: '美团点评', code: '9111010877', address: '北京市朝阳区望京东路6号' }
])

// 表格列配置
const tableColumns = [
  { title: '单位名称', dataIndex: 'name', key: 'name', width: '40%' },
  { title: '统一社会信用代码', dataIndex: 'code', key: 'code', width: '30%' },
  { title: '注册地址', dataIndex: 'address', key: 'address', width: '30%' }
]

// 防抖函数已从 utils/debounce.js 导入

// 计算属性:过滤后的选项(大小写不敏感)
const filteredOptions = computed(() => {
  if (!searchValue.value || searchValue.value.trim() === '') {
    return options.value
  }
  
  const searchTerm = searchValue.value.toLowerCase().trim()
  return options.value.filter(option => 
    option.label.toLowerCase().includes(searchTerm)
  )
})

// 计算属性:是否显示无结果消息
const showNoResultsMessage = computed(() => {
  return searchValue.value && 
         searchValue.value.trim() !== '' && 
         filteredOptions.value.length === 0
})

// 计算属性:是否显示云端检索按钮
const showCloudSearchButton = computed(() => {
  return searchValue.value && 
         searchValue.value.trim() !== '' && 
         filteredOptions.value.length === 0
})

// 防抖搜索处理函数
const debouncedSearch = debounce((value) => {
  searchValue.value = value
  highlightedIndex.value = -1 // 重置高亮索引
}, 300)

// 搜索处理
const handleSearch = (value) => {
  debouncedSearch(value)
}

// 选择变化处理
const handleSelectionChange = (value) => {
  selectedValue.value = value
  searchValue.value = '' // 清空搜索值
  highlightedIndex.value = -1
}

// 键盘导航处理
const handleKeyDown = (event) => {
  const { key } = event
  const visibleOptions = filteredOptions.value
  
  if (visibleOptions.length === 0) return
  
  switch (key) {
    case 'ArrowDown':
      event.preventDefault()
      highlightedIndex.value = Math.min(
        highlightedIndex.value + 1, 
        visibleOptions.length - 1
      )
      break
      
    case 'ArrowUp':
      event.preventDefault()
      highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
      break
      
    case 'Enter':
      event.preventDefault()
      if (highlightedIndex.value >= 0 && highlightedIndex.value < visibleOptions.length) {
        const selectedOption = visibleOptions[highlightedIndex.value]
        handleSelectionChange(selectedOption.value)
      }
      break
      
    case 'Escape':
      event.preventDefault()
      searchValue.value = ''
      highlightedIndex.value = -1
      if (selectRef.value) {
        selectRef.value.blur()
      }
      break
  }
}

// 表格单选配置
const rowSelection = computed(() => ({
  type: 'radio',
  selectedRowKeys: selectedRowKeys.value,
  onChange: (selectedKeys) => {
    selectedRowKeys.value = selectedKeys
  }
}))

// 打开云端检索弹窗
const openCloudSearchModal = () => {
  modalVisible.value = true
  selectedRowKeys.value = []
}

// 弹窗确认处理
const handleModalConfirm = () => {
  if (selectedRowKeys.value.length === 0) {
    message.warning('请选择一个单位')
    return
  }
  
  const selectedRow = cloudSearchResults.value.find(
    row => row.id === selectedRowKeys.value[0]
  )
  
  if (selectedRow) {
    // 检查是否已存在,避免重复添加
    const existingOption = options.value.find(opt => opt.value === selectedRow.code)
    if (!existingOption) {
      options.value.push({
        label: selectedRow.name,
        value: selectedRow.code
      })
    }
    
    // 设置选中值并关闭弹窗
    selectedValue.value = selectedRow.code
    modalVisible.value = false
    selectedRowKeys.value = []
    searchValue.value = ''
    
    message.success('单位选择成功')
  }
}

// 弹窗取消处理
const handleModalCancel = () => {
  modalVisible.value = false
  selectedRowKeys.value = []
}

// 搜索相关函数已移至 utils/searchUtils.js
</script>

<style scoped>
.searchable-dropdown-container {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  border-bottom: 1px solid #eee;
}

.dropdown-section {
  display: flex;
  align-items: center;
  gap: 8px;
}

.dropdown-label {
  margin-right: 8px;
  white-space: nowrap;
  font-weight: 500;
}

.dropdown-select {
  width: 250px;
}

.button-section {
  flex-shrink: 0;
}

.cloud-search-button {
  white-space: nowrap;
}

.help-text-section {
  flex: 1;
  margin-left: 16px;
}

.help-text {
  color: #ff4d4f;
  font-size: 14px;
  line-height: 1.4;
}

.no-results-content {
  padding: 8px 12px;
  text-align: center;
}

.no-results-text {
  margin-bottom: 4px;
  color: #999;
  font-weight: 500;
}

.help-message {
  color: #999;
  font-size: 12px;
  line-height: 1.4;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .searchable-dropdown-container {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }

  .dropdown-section {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }

  .dropdown-select {
    width: 100%;
  }

  .help-text-section {
    margin-left: 0;
  }
}
</style>
{
  "name": "my-vue3-antd-app-no-jsx",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:run": "vitest run"
  },
  "dependencies": {
    "ant-design-vue": "^4.2.3",
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.4",
    "@vue/test-utils": "^2.4.0",
    "jsdom": "^23.0.0",
    "vite": "^5.2.0",
    "vitest": "^1.0.0"
  }
}

Ant Design Vue (ADVue) 的下拉选择框组件通常用于提供预设选项让用户选择,但如果需要实现用户可以随意输入并保持单选功能,我们可以结合`<a-input>`(输入框)和`<a-select>`(下拉选择框组件以及自定义的搜索功能来达到这个效果。 首先,你可以创建一个`<a-select>`组件,设置`searchable`属性为`true`,这样当用户输入时会选择列表中的匹配项: ```html <a-select :options="options" v-model="selectedValue" placeholder="请选择" searchable> <a-option v-for="(item, index) in options" :key="index" :value="item.value"> {{ item.label }} </a-option> </a-select> <!-- 配合一个输入框,监听input事件 --> <a-input v-model="searchText" @input="filterOptions"></a-input> ``` 然后,在`methods`里定义`filterOptions`函数,根据用户的输入动态筛选选项: ```javascript export default { data() { return { options: [], // 初始化你的下拉选项数组 selectedValue: null, searchText: &#39;&#39;, }; }, methods: { filterOptions(e) { this.options = this.options.filter(item => item.label.toLowerCase().includes(e.target.value.toLowerCase()) ); }, }, }; ``` 这样一来,用户可以在输入框内输入文字,系统会实时过滤下拉选项,显示包含输入内容的结果。如果用户选择了一个选项,`v-model`绑定的值`selectedValue`就会记录这个选项。 请注意,为了实现实时搜索,你可能需要将远程数据源和搜索逻辑分开处理,这里仅展示了基本的本地搜索示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战族狼魂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值