Flutter中如何实现带搜索功能的下拉列表文本框?
实现带内嵌搜索功能的下拉文本框
如果找不到现成的适配组件包,自己封装是最灵活的方案。核心是把输入框、搜索逻辑、下拉列表三个部分组合起来,以下是不同技术栈的实现思路和代码示例:
核心逻辑梳理
- 监听输入框的输入事件,实时触发数据过滤/搜索
- 维护搜索结果状态,根据结果是否为空控制下拉列表的显示/隐藏
- 给下拉列表项绑定点击事件,选中后将内容回填到输入框,同时隐藏下拉
原生JavaScript实现(无框架)
HTML结构
<div class="search-input-container"> <input type="text" id="searchInput" placeholder="输入关键词搜索..." /> <ul id="searchResults" class="search-results"></ul> </div>
CSS样式
.search-input-container { position: relative; width: 300px; } #searchInput { width: 100%; padding: 8px 12px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } .search-results { position: absolute; top: 100%; left: 0; right: 0; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background: white; list-style: none; padding: 0; margin: 0; display: none; max-height: 200px; overflow-y: auto; } .search-results.show { display: block; } .search-results li { padding: 8px 12px; cursor: pointer; } .search-results li:hover { background-color: #f5f5f5; }
JavaScript逻辑
// 模拟数据源 const mockData = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig"]; const input = document.getElementById("searchInput"); const resultsList = document.getElementById("searchResults"); // 防抖函数,避免频繁触发搜索 function debounce(func, delay) { let timer; return function() { clearTimeout(timer); timer = setTimeout(() => func.apply(this, arguments), delay); }; } // 处理搜索逻辑 const handleSearch = debounce(function(keyword) { if (!keyword.trim()) { resultsList.classList.remove("show"); return; } // 过滤匹配数据(大小写不敏感) const matchedItems = mockData.filter(item => item.toLowerCase().includes(keyword.toLowerCase()) ); // 渲染下拉列表 renderResults(matchedItems); }, 200); // 渲染搜索结果 function renderResults(items) { resultsList.innerHTML = ""; if (items.length === 0) { resultsList.classList.remove("show"); return; } items.forEach(item => { const li = document.createElement("li"); li.textContent = item; li.addEventListener("click", () => { input.value = item; resultsList.classList.remove("show"); }); resultsList.appendChild(li); }); resultsList.classList.add("show"); } // 监听输入事件 input.addEventListener("input", (e) => { handleSearch(e.target.value); }); // 点击外部关闭下拉 document.addEventListener("click", (e) => { if (!input.contains(e.target) && !resultsList.contains(e.target)) { resultsList.classList.remove("show"); } });
React实现
import { useState, useEffect, useRef } from "react"; const SearchTextField = () => { const [inputValue, setInputValue] = useState(""); const [searchResults, setSearchResults] = useState([]); const containerRef = useRef(null); // 模拟数据源 const mockData = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig"]; // 防抖处理搜索 useEffect(() => { if (!inputValue.trim()) { setSearchResults([]); return; } const timer = setTimeout(() => { const matched = mockData.filter(item => item.toLowerCase().includes(inputValue.toLowerCase()) ); setSearchResults(matched); }, 200); return () => clearTimeout(timer); }, [inputValue]); // 点击外部关闭下拉 useEffect(() => { const handleClickOutside = (e) => { if (containerRef.current && !containerRef.current.contains(e.target)) { setSearchResults([]); } }; document.addEventListener("click", handleClickOutside); return () => document.removeEventListener("click", handleClickOutside); }, []); const handleItemClick = (item) => { setInputValue(item); setSearchResults([]); }; return ( <div ref={containerRef} style={{ position: "relative", width: "300px" }}> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="输入关键词搜索..." style={{ width: "100%", padding: "8px 12px", boxSizing: "border-box", border: "1px solid #ddd", borderRadius: "4px" }} /> {searchResults.length > 0 && ( <ul style={{ position: "absolute", top: "100%", left: 0, right: 0, border: "1px solid #ddd", borderTop: "none", borderRadius: "0 0 4px 4px", background: "white", listStyle: "none", padding: 0, margin: 0, maxHeight: "200px", overflowY: "auto" }}> {searchResults.map((item, idx) => ( <li key={idx} onClick={() => handleItemClick(item)} style={{ padding: "8px 12px", cursor: "pointer" }} onMouseEnter={(e) => e.target.style.backgroundColor = "#f5f5f5"} onMouseLeave={(e) => e.target.style.backgroundColor = "white"} > {item} </li> ))} </ul> )} </div> ); }; export default SearchTextField;
Vue3实现
<template> <div ref="containerRef" class="search-input-container"> <input type="text" v-model="inputValue" placeholder="输入关键词搜索..." class="search-input" /> <ul v-if="searchResults.length > 0" class="search-results"> <li v-for="(item, idx) in searchResults" :key="idx" @click="handleItemClick(item)" class="result-item" > {{ item }} </li> </ul> </div> </template> <script setup> import { ref, watch, onMounted, onUnmounted } from "vue"; const inputValue = ref(""); const searchResults = ref([]); const containerRef = ref(null); // 模拟数据源 const mockData = ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig"]; let searchTimer = null; // 监听输入变化触发搜索 watch(inputValue, (newVal) => { clearTimeout(searchTimer); if (!newVal.trim()) { searchResults.value = []; return; } searchTimer = setTimeout(() => { searchResults.value = mockData.filter(item => item.toLowerCase().includes(newVal.toLowerCase()) ); }, 200); }); // 点击外部关闭下拉 const handleClickOutside = (e) => { if (containerRef.value && !containerRef.value.contains(e.target)) { searchResults.value = []; } }; onMounted(() => { document.addEventListener("click", handleClickOutside); }); onUnmounted(() => { document.removeEventListener("click", handleClickOutside); clearTimeout(searchTimer); }); const handleItemClick = (item) => { inputValue.value = item; searchResults.value = []; }; </script> <style scoped> .search-input-container { position: relative; width: 300px; } .search-input { width: 100%; padding: 8px 12px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; } .search-results { position: absolute; top: 100%; left: 0; right: 0; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background: white; list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; } .result-item { padding: 8px 12px; cursor: pointer; } .result-item:hover { background-color: #f5f5f5; } </style>
关键优化点
- 防抖处理:避免输入时频繁触发搜索,减少性能消耗
- 点击外部关闭:提升用户体验,避免下拉列表一直显示
- 样式适配:确保下拉列表和输入框对齐,设置最大高度避免溢出
- 大小写不敏感搜索:提升搜索的易用性
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

