目錄
一、實現目標
1.1 需求
1.2 實現示例圖:
二、實現步驟
2.1?實現方法簡述
2.2 簡單科普
2.3 實現步驟及代碼
?
一、實現目標
1.1 需求
搜索聯想——自動補全
????????(1)實現搜索輸入框,用戶輸入時能顯示模糊匹配結果
????????(2)模糊結果在輸入框下方浮動顯示,并能點擊選中
????????(3)輸入防抖功能(自己手寫)
1.2 實現示例圖:
? ? ??
聯想框動畫絲滑,整體效果也不錯,代碼給了超詳細的注釋 , 感興趣的小伙伴可以按下面步驟試試
那么我們開始吧 !
二、實現步驟
2.1?實現方法簡述
????????我們先實現后端根據關鍵詞進行模糊查詢的接口,這里會用到mybatis工具,數據庫操作部分是基于 MyBatis 實現的,大家需要先去項目里面的pop.xml文件里面引入必要的依賴;
????????接著實現前端頁面部分,搜索框這我選擇自定義組件,原因主要有兩點:一是 uni-search-bar 可能存在兼容性問題,部分樣式易被覆蓋導致顯示異常;二是將搜索功能抽象為獨立組件后,可在多個頁面中復用,提高代碼復用性;
????????此外,搜索輸入的防抖功能是通過自定義邏輯實現的,如果大家是拿來練手或者學習的話,我們自己手寫的防抖功能就已經可以完全滿足業務需求,相比于使用lodash.debounce來說手寫的防抖功能可以減少依賴體積也更適配業務,當然如果咱們的是大型項目或需要處理多種防抖場景的需求的話 lodash.debounce?功能更多會更合適。
2.2 簡單科普
(1). 什么是防抖?
????????防抖的核心邏輯是:當函數被連續觸發時,只有在觸發停止后的指定時間內不再有新觸發,函數才會執行一次。
????????例如:搜索輸入框中,用戶快速輸入文字時,不會每輸入一個字符就立即請求接口,而是等用戶暫停輸入(比如停頓 300ms)后,再執行搜索請求,減少無效請求次數。
(2).lodash.debounce 是什么?
????????
lodash.debounce
?是 JavaScript 工具庫?Lodash?提供的一個核心函數,用于實現?防抖(Debounce)?功能。它能控制函數在高頻觸發場景下的執行時機,避免函數被頻繁調用導致的性能問題(如頻繁請求接口、頻繁渲染等)。
2.3 實現步驟及代碼
1.后端部分
新增一個接口可以根據關鍵詞模糊查詢商家
我這是模糊查詢商家名稱大家根據自己的業務需求做相應的更改
Controller層:
/*** 根據關鍵詞模糊查詢商家(搜索聯想)* @param keyword 搜索關鍵詞* @return 匹配的商家列表*/
@GetMapping("/searchSuggest")
public Result searchSuggest(String keyword) {// 構建查詢條件,根據商家名稱模糊匹配Business business = new Business();business.setName(keyword);// 只查詢狀態為"通過"的商家(與現有邏輯保持一致)business.setStatus("通過");List<Business> list = businessService.selectAll(business);return Result.success(list);
}
service層
/*** 查詢所有商家信息* @param business 查詢條件,可為空對象表示查詢所有* @return 符合條件的商家列表*/public List<Business> selectAll(Business business) {List<Business> businesses = businessMapper.selectAll(business);for (Business b : businesses) {wrapBusiness(b); // 我這個函數是用來封裝評分、訂單數等信息 // 大家根據自己的項目需求寫}return businesses;}
Mapper 層支持模糊查詢
List<Business> selectAll(Business business);
Mapper.xml
<select id="selectAll" parameterType="com.example.entity.Business" resultType="com.example.entity.Business">select * from business<where><if test="id != null">and id = #{id}</if><if test="username != null">and username like concat('%', #{username}, '%')</if><if test="name != null">and name like concat('%', #{name}, '%')</if><if test="status != null">and status = #{status}</if><if test="type != null">and type = #{type}</if></where>order by id desc</select>
當傳遞?name = keyword?時,會自動生成?name like '%關鍵詞%'?的 SQL,滿足模糊查詢需求。
2.前端部分
CustomSearchBar.vue組件
<template><view class="custom-search-bar"><view class="search-box" :style="{borderRadius: radius + 'px', backgroundColor: bgColor}" @click="searchClick"><view class="search-icon"><uni-icons color="#c0c4cc" size="18" type="search" /></view><input v-if="show || searchVal" :focus="showSync" :disabled="readonly" :placeholder="placeholderText" :maxlength="maxlength"class="search-input" confirm-type="search" type="text" v-model="searchVal" :style="{color: textColor}"@confirm="confirm" @blur="blur" @focus="emitFocus"/><text v-else class="placeholder-text">{{ placeholder }}</text><view v-if="show && (clearButton === 'always' || clearButton === 'auto' && searchVal !== '') && !readonly"class="clear-icon" @click="clear"><uni-icons color="#c0c4cc" size="20" type="clear" /></view></view><text @click="cancel" class="cancel-text"v-if="cancelButton === 'always' || show && cancelButton === 'auto'">{{ cancelText || '取消' }}</text></view>
</template><script>export default {name: "CustomSearchBar",props: {placeholder: {type: String,default: "請輸入搜索商家"},radius: {type: [Number, String],default: 5},clearButton: {type: String,default: "auto" // 值為 "auto" 時,組件會根據搜索框的狀態動態決定是否顯示 “取消” 按鈕:},cancelButton: {type: String,default: "auto" // "always":無論搜索框是否激活,始終顯示 “取消” 按鈕。},cancelText: {type: String,default: ""},bgColor: {type: String,default: "#F8F8F8"},textColor: {type: String,default: "#000000"},maxlength: {type: [Number, String],default: 100},value: {type: [Number, String],default: ""},modelValue: {type: [Number, String],default: ""},focus: {type: Boolean,default: false},readonly: {type: Boolean,default: false}},data() {return {show: false,showSync: false,searchVal: '',isAdvanced: true // 初始為 false,代表“未開啟高級模式”}},computed: {placeholderText() {return this.placeholder // 返回 props 中定義的 placeholder 的值// 在模板中,輸入框的占位符使用的是 placeholderText 而非直接使用 this.placeholder// placeholderText 作為中間層,將 props 中的 placeholder 值傳遞給輸入框的占位符屬性// 假設未來需要對 placeholder 進行處理(比如根據語言環境翻譯、添加動態后綴等),直接修改 placeholderText 即可,無需改動模板和 props// return this.placeholder + (this.isAdvanced ? "(支持模糊搜索)" : "");}},watch: {value: { // 監聽父組件通過 value 屬性傳入的搜索值。immediate: true, // 初始化時立即執行一次handlerhandler(newVal) {this.searchVal = newVal // 將外部傳入的value同步到組件內部的searchValif (newVal) {this.show = true // 如果有值,顯示搜索框}}},modelValue: { // 適配 Vue 的 v-model 語法糖(modelValue 是 v-model 的默認綁定屬性)immediate: true,handler(newVal) {this.searchVal = newVal // 同步v-model綁定的值到searchValif (newVal) {this.show = true}}},focus: { // 監聽父組件傳入的 focus 屬性(控制搜索框是否聚焦)immediate: true,handler(newVal) {if (newVal) { // 如果父組件要求聚焦if(this.readonly) return // 只讀狀態不處理this.show = true; // 顯示搜索框this.$nextTick(() => {this.showSync = true // 確保在 DOM 更新后再設置聚焦,避免操作還未渲染的元素})}}},searchVal(newVal, oldVal) { // 監聽組件內部的搜索值 searchVal(用戶輸入的內容)this.$emit("input", newVal) // 觸發input事件,同步值給父組件this.$emit("update:modelValue", newVal) // 觸發v-model更新}},methods: {/*** 搜索框容器點擊事件處理* 功能:點擊搜索框區域時,激活搜索框并設置聚焦狀態* 場景:用戶點擊搜索框外部容器時觸發,用于喚起輸入狀態*/searchClick() {// 只讀狀態下不響應點擊(禁止交互)if(this.readonly) return// 若搜索框已激活,無需重復操作if (this.show) {return}// 激活搜索框(控制輸入框和清除按鈕的顯示)this.show = true;// 使用$nextTick確保DOM更新后再聚焦,避免操作未渲染的元素this.$nextTick(() => {// 觸發輸入框聚焦(showSync與input的:focus屬性綁定)this.showSync = true})},/*** 清除按鈕點擊事件處理* 功能:清空搜索框內容并通知父組件* 場景:用戶點擊搜索框內的清除圖標時觸發*/clear() {// 清空組件內部的搜索值this.searchVal = ""// 等待DOM更新后再通知父組件(確保值已同步清空)this.$nextTick(() => {// 向父組件發送清除事件,傳遞空值this.$emit("clear", { value: "" })})},/*** 取消按鈕點擊事件處理* 功能:取消搜索操作,重置組件狀態并通知父組件* 場景:用戶點擊"取消"按鈕時觸發,用于退出搜索狀態*/cancel() {// 只讀狀態下不響應取消操作if(this.readonly) return// 向父組件發送取消事件,攜帶當前搜索值(可能用于后續處理)this.$emit("cancel", {value: this.searchVal});// 清空搜索框內容this.searchVal = ""// 隱藏搜索框(重置激活狀態)this.show = false// 取消輸入框聚焦this.showSync = false// 關閉鍵盤(優化移動端體驗,避免鍵盤殘留)uni.hideKeyboard()},/*** 搜索確認事件處理* 功能:處理搜索確認邏輯(回車或搜索按鈕)并通知父組件* 場景:用戶輸入完成后點擊鍵盤搜索鍵或組件內確認按鈕時觸發*/confirm() {// 關閉鍵盤(輸入完成后隱藏鍵盤)uni.hideKeyboard();// 向父組件發送確認事件,攜帶當前搜索值(觸發實際搜索邏輯)this.$emit("confirm", {value: this.searchVal})},/*** 輸入框失焦事件處理* 功能:輸入框失去焦點時通知父組件并關閉鍵盤* 場景:用戶點擊輸入框外部區域導致輸入框失去焦點時觸發*/blur() {// 關閉鍵盤(失焦后自動隱藏鍵盤)uni.hideKeyboard();// 向父組件發送失焦事件,攜帶當前搜索值(用于狀態同步)this.$emit("blur", {value: this.searchVal})},/*** 輸入框聚焦事件處理* 功能:輸入框獲取焦點時通知父組件* 場景:用戶點擊輸入框或通過代碼觸發聚焦時觸發* @param {Object} e*/emitFocus(e) {// 向父組件發送聚焦事件,傳遞焦點事件詳情(如光標位置等)this.$emit("focus", e.detail)}}};
</script><style scoped>
.custom-search-bar {display: flex;align-items: center;padding: 10rpx;
}.search-box {display: flex;align-items: center;flex: 1;padding: 0 20rpx;height: 75rpx;position: relative;
}.search-icon {margin-right: 14rpx;
}.search-input {flex: 1;height: 100%;font-size: 30rpx;background: transparent;border: none;outline: none;
}.placeholder-text {flex: 1;font-size: 30rpx;color: #c0c4cc;
}.clear-icon {margin-left: 10rpx;padding: 5rpx;
}.cancel-text {margin-left: 20rpx;font-size: 30rpx;color: #007aff;padding: 10rpx;
}
</style>
父組件 html 模版部分
<!-- 搜索 --><view class="search-container"><custom-search-bar class="custom-searchbar" @confirm="search" @input="handleInput" @focus="showSuggest = true" @blur="hideSuggest" v-model="searchValue" placeholder="請輸入要搜索的商家" ></custom-search-bar><!-- 聯想結果浮層 --><view class="suggest-container" v-if="showSuggest && suggestList.length > 0"@click.stop><view class="suggest-item" v-for="(item, index) in suggestList" :key="index"@click="selectSuggest(item)"><view class="suggest-content"><uni-icons type="shop" size="16" color="#666" class="suggest-icon"></uni-icons><text class="suggest-text">{{ item.name }}</text></view><uni-icons type="right" size="14" color="#ccc" class="arrow-icon"></uni-icons></view></view></view><!-- 搜索結束 -->
js部分
<script>import CustomSearchBar from '@/components/CustomSearchBar.vue'export default {components: {CustomSearchBar},data() {return {// 你的項目其他數據searchValue: '', // 雙向綁定到搜索組件的輸入框,存儲用戶輸入的搜索關鍵詞suggestList: [], // 存儲根據搜索關鍵詞從接口獲取的聯想建議數據,用于展示搜索提示showSuggest: false, // 通過布爾值控制聯想結果浮層是否顯示debounceTimer: null // 存儲防抖函數中的定時器 ID,用于在用戶輸入過程中清除未執行的定時器,避免頻繁請求} },onLoad() {// this.load()},methods: {/*** 手寫防抖函數* 功能:限制目標函數的執行頻率,避免短時間內頻繁調用* 原理:每次觸發時清除之前的定時器,重新計時,延遲指定時間后執行目標函數* @param {Function} func - 需要防抖的目標函數(如搜索聯想請求函數)* @param {Number} delay - 延遲時間(毫秒),默認300ms* @returns {Function} 經過防抖處理的包裝函數*/debounce(func, delay) {return function(...args) {// 清除上一次未執行的定時器,避免重復觸發clearTimeout(this.debounceTimer)// 設置新定時器,延遲指定時間后執行目標函數this.debounceTimer = setTimeout(() => {// 用apply綁定上下文,確保目標函數中的this指向當前組件func.apply(this, args)}, delay)}.bind(this) //// 綁定當前組件上下文,確保定時器中的this正確},/*** 搜索輸入框內容變化處理函數* 功能:監聽用戶輸入,同步搜索值并觸發防抖聯想請求* @param {String} value - 輸入框當前值*/handleInput(value) {// 同步輸入值到組件數據,實現雙向綁定this.searchValue = value// 輸入為空時重置聯想狀態(清空列表并隱藏浮層)if (!value.trim()) {this.suggestList = []this.showSuggest = falsereturn}// 使用防抖處理后的函數觸發聯想請求,減少接口調用次數this.debouncedSearch(value)},/*** 獲取搜索聯想結果* 功能:根據關鍵詞請求接口,獲取并更新聯想列表數據* @param {String} keyword - 搜索關鍵詞*/async fetchSuggest(keyword) {try {console.log('搜索關鍵詞:', keyword)// 調用接口獲取聯想結果,傳遞關鍵詞參數const res = await this.$request.get('/business/searchSuggest', { keyword })console.log('搜索聯想結果:', res)// 接口返回成功且有數據時,更新聯想列表并顯示浮層if (res.code === '200' && res.data) {this.suggestList = res.datathis.showSuggest = true} else { // 接口返回異常或無結果時,清空列表并隱藏浮層this.suggestList = []this.showSuggest = false}} catch (err) { // 捕獲請求異常(如網絡錯誤),重置聯想狀態console.error('獲取搜索聯想失敗', err)this.suggestList = []this.showSuggest = false}},/*** 選中聯想項處理函數* 功能:用戶點擊聯想項時,同步值到搜索框并關閉聯想浮層* @param {Object} item - 選中的聯想項數據(包含name等字段)*/selectSuggest(item) {console.log('選中聯想項:', item)// 將聯想項名稱同步到搜索框this.searchValue = item.name// 隱藏聯想浮層并清空列表this.showSuggest = falsethis.suggestList = []},/*** 隱藏聯想浮層處理函數* 功能:搜索框失焦時延遲隱藏浮層,解決快速交互沖突* 說明:延遲200ms確保點擊聯想項的事件能正常觸發*/hideSuggest() {setTimeout(() => {this.showSuggest = false}, 200)},/*** 搜索確認處理函數* 功能:用戶確認搜索時,跳轉到搜索結果頁并重置搜索狀態*/search() {let value = this.searchValue// 搜索值不為空時執行跳轉if (value.trim()) {// 跳轉到搜索結果頁,通過URL傳遞關鍵詞(encodeURIComponent處理特殊字符)uni.navigateTo({url: '/pages/search/search?name=' + encodeURIComponent(value)})// 重置搜索狀態(清空值、列表和浮層)this.searchValue = ''this.suggestList = []this.showSuggest = false}},// 你的項目其他方法},/*** 組件創建生命周期函數* 功能:初始化防抖函數實例,為搜索聯想請求添加防抖處理* 說明:在組件創建時生成延遲300ms的防抖函數,綁定到debouncedSearch*/created() {// 創建防抖函數this.debouncedSearch = this.debounce(this.fetchSuggest, 300)}}
</script>
css樣式部分
<style>
/* 商家分類項樣式 */
.categgory-item {flex: 1; /* 等分父容器寬度 */display: flex; /* 使用flex布局 */flex-direction: column; /* 垂直方向排列子元素(圖標在上,文字在下) */justify-content: center; /* 垂直居中對齊 */align-items: center; /* 水平居中對齊 */grid-gap: 10rpx; /* 子元素之間的間距(圖標與文字間距) */color: #333; /* 文字顏色(深灰色) */
}/* 全局修改uni-icons圖標樣式 */
::v-deep .uni-icons {color: #F4683d !important; /* 圖標顏色(橙色),!important強制覆蓋組件內部樣式 */fill: #F4683d !important; /* 圖標填充色(與顏色一致,確保圖標顯示正常) */
}/* 自定義搜索欄樣式優化 - 最小化邊距 */
::v-deep .custom-searchbar{padding: 0 !important; /* 清除內邊距,讓搜索欄緊貼容器 */margin: 0 !important; /* 清除外邊距,避免額外留白 */
}/* 搜索容器 - 最小化邊距 */
.search-container {position: relative; /* 相對定位,用于聯想浮層的絕對定位參考 */padding: 0; /* 清除內邊距 */margin: 0; /* 清除外邊距 */z-index: 1000; /* 設置層級,確保搜索欄在頁面上層 */
}/* 聯想容器 - 緊貼搜索欄 */
.suggest-container {position: absolute; /* 絕對定位,相對于搜索容器定位 */top: 100%; /* 頂部對齊搜索容器底部,實現“緊貼搜索欄下方”效果 */left: 0; /* 左側對齊搜索容器 */right: 0; /* 右側對齊搜索容器,與搜索欄同寬 */background-color: #ffffff; /* 白色背景,與頁面區分 */border: 1px solid #e0e0e0; /* 灰色邊框,增強邊界感 */border-top: none; /* 移除頂部邊框,與搜索欄無縫連接 */border-radius: 0 0 8rpx 8rpx; /* 只保留底部圓角,優化視覺效果 */box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); /* 底部陰影,增強浮層感 */z-index: 1001; /* 層級高于搜索容器,確保浮層顯示在最上層 */max-height: 400rpx; /* 限制最大高度,避免內容過多溢出 */overflow-y: auto; /* 內容超出時顯示垂直滾動條 */
}/* 聯想項 - 美化樣式 */
.suggest-item {padding: 16rpx 20rpx; /* 內邊距,增加點擊區域 */border-bottom: 1px solid #f0f0f0; /* 底部灰色分隔線,區分相鄰項 */transition: all 0.2s ease; /* 過渡動畫,優化交互體驗 */display: flex; /* flex布局,實現內容與箭頭左右排列 */align-items: center; /* 垂直居中對齊 */justify-content: space-between; /* 內容靠左,箭頭靠右 */
}/* 最后一個聯想項移除底部邊框 */
.suggest-item:last-child {border-bottom: none; /* 避免最后一項多余邊框 */
}/* 聯想項點擊狀態樣式 */
.suggest-item:active {background-color: #f8f9fa; /* 點擊時背景變淺灰色,反饋交互 */transform: translateX(4rpx); /* 輕微右移,增強點擊反饋 */
}/* 聯想內容區域 */
.suggest-content {display: flex; /* flex布局,圖標與文字橫向排列 */align-items: center; /* 垂直居中對齊 */flex: 1; /* 占據剩余空間,確保箭頭靠右 */
}/* 聯想圖標樣式 */
.suggest-icon {margin-right: 12rpx; /* 圖標與文字間距 */flex-shrink: 0; /* 圖標不縮放,保持固定大小 */
}/* 箭頭圖標樣式 */
.arrow-icon {flex-shrink: 0; /* 箭頭不縮放,保持固定大小 */
}/* 聯想文字樣式 */
.suggest-text {font-size: 28rpx; /* 文字大小 */color: #333333; /* 文字顏色(深灰色) */line-height: 1.4; /* 行高,優化多行顯示 */flex: 1; /* 占據剩余空間,文字過長時自動換行 */
}/* 定義聯想浮層顯示動畫 */
@keyframes slideIn {from {opacity: 0; /* 初始狀態完全透明 */transform: translateY(-10rpx); /* 初始位置向上偏移10rpx */}to {opacity: 1; /* 結束狀態完全不透明 */transform: translateY(0); /* 結束位置回歸正常 */}
}/* 為聯想容器應用動畫 */
.suggest-container {animation: slideIn 0.2s ease-out; /* 應用slideIn動畫,0.2秒完成,緩出效果 */
}
</style>
好了 , 代碼就到這了 , 快去試試吧
每天進步一點點 , 加油 !
?
?