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) 数据回显功能。
功能说明
- 搜索功能:下拉框支持输入搜索,实时过滤选项
- 动态按钮:当搜索无结果时显示添加按钮,有匹配结果时隐藏
- 弹窗交互:点击按钮弹出含单选表格的模态框
- 数据回显:从表格选择数据后自动关闭弹窗并回显到下拉框
- 自定义配置:支持通过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-selectv-model:value="selectedValue"show-searchplaceholder="请选择单位名称"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-buttonv-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-modalv-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 = valuehighlightedIndex.value = -1 // 重置高亮索引
}, 300)// 搜索处理
const handleSearch = (value) => {debouncedSearch(value)
}// 选择变化处理
const handleSelectionChange = (value) => {selectedValue.value = valuesearchValue.value = '' // 清空搜索值highlightedIndex.value = -1
}// 键盘导航处理
const handleKeyDown = (event) => {const { key } = eventconst visibleOptions = filteredOptions.valueif (visibleOptions.length === 0) returnswitch (key) {case 'ArrowDown':event.preventDefault()highlightedIndex.value = Math.min(highlightedIndex.value + 1, visibleOptions.length - 1)breakcase 'ArrowUp':event.preventDefault()highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)breakcase 'Enter':event.preventDefault()if (highlightedIndex.value >= 0 && highlightedIndex.value < visibleOptions.length) {const selectedOption = visibleOptions[highlightedIndex.value]handleSelectionChange(selectedOption.value)}breakcase 'Escape':event.preventDefault()searchValue.value = ''highlightedIndex.value = -1if (selectRef.value) {selectRef.value.blur()}break}
}// 表格单选配置
const rowSelection = computed(() => ({type: 'radio',selectedRowKeys: selectedRowKeys.value,onChange: (selectedKeys) => {selectedRowKeys.value = selectedKeys}
}))// 打开云端检索弹窗
const openCloudSearchModal = () => {modalVisible.value = trueselectedRowKeys.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.codemodalVisible.value = falseselectedRowKeys.value = []searchValue.value = ''message.success('单位选择成功')}
}// 弹窗取消处理
const handleModalCancel = () => {modalVisible.value = falseselectedRowKeys.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"}
}