You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 08:10:04