您好,登錄后才能下訂單哦!
這篇文章主要介紹“Tree組件搜索過(guò)濾功能如何實(shí)現(xiàn)”的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“Tree組件搜索過(guò)濾功能如何實(shí)現(xiàn)”文章能幫助大家解決問(wèn)題。
樹(shù)節(jié)點(diǎn)的搜索功能主要是為了方便用戶能夠快速查找到自己需要的節(jié)點(diǎn)。過(guò)濾功能不僅要滿足搜索的特性,同時(shí)還需要隱藏掉與匹配節(jié)點(diǎn)同層級(jí)的其它未能匹配的節(jié)點(diǎn)。
搜索功能主要包括以下功能:
與搜索過(guò)濾字段匹配的節(jié)點(diǎn)需要進(jìn)行標(biāo)識(shí),和普通節(jié)點(diǎn)進(jìn)行區(qū)分
子節(jié)點(diǎn)匹配時(shí),其所有父節(jié)點(diǎn)需要展開(kāi),方便用戶查看層級(jí)關(guān)系
對(duì)于大數(shù)據(jù)量,采用虛擬滾動(dòng)時(shí),搜索過(guò)濾完成后滾動(dòng)條需滾動(dòng)至第一個(gè)匹配節(jié)點(diǎn)的位置
搜索會(huì)將匹配到的節(jié)點(diǎn)高亮:
過(guò)濾除了將匹配到的節(jié)點(diǎn)高亮之外,還會(huì)將不匹配的節(jié)點(diǎn)篩除掉:
通過(guò)將節(jié)點(diǎn)與搜索字段相匹配的 label
部分文字進(jìn)行高亮加粗的方式進(jìn)行標(biāo)記。易于用戶一眼就能夠找到搜索到的節(jié)點(diǎn)。
通過(guò)添加searchTree
方法,用戶通過(guò)ref的方式進(jìn)行調(diào)用。并通過(guò)option
參數(shù)配置區(qū)分搜索、過(guò)濾。
對(duì)于節(jié)點(diǎn)的獲取及處理是搜索過(guò)濾功能的核心。尤其在大數(shù)據(jù)量的情況下,帶來(lái)的性能消耗如何優(yōu)化,將在實(shí)現(xiàn)原理中詳情闡述。
tree
組件的文件結(jié)構(gòu):
tree ├── index.ts ├── src | ├── components | | ├── tree-node.tsx | | ├── ... | ├── composables | | ├── use-check.ts | | ├── use-core.ts | | ├── use-disable.ts | | ├── use-merge-nodes.ts | | ├── use-operate.ts | | ├── use-select.ts | | ├── use-toggle.ts | | ├── ... | ├── tree.scss | ├── tree.tsx └── __tests__ └── tree.spec.ts
可以看出,vue3.0中 composition-api
帶來(lái)的便利。邏輯層之間的分離,方便代碼組織及后續(xù)問(wèn)題的定位。能夠讓開(kāi)發(fā)者只專心于自己的特性,非常有利于后期維護(hù)。
添加文件use-search-filter.ts
, 文件中定義searchTree
方法。
import { Ref, ref } from 'vue'; import { trim } from 'lodash'; import { IInnerTreeNode, IUseCore, IUseSearchFilter, SearchFilterOption } from './use-tree-types'; export default function () { return function useSearchFilter(data: Ref<IInnerTreeNode[]>, core: IUseCore): IUseSearchFilter { const searchTree = (target: string, option: SearchFilterOption): void => { // 搜索主邏輯 }; return { virtualListRef, searchTree, }; } }
SearchFilterOption
的接口定義,matchKey
與 pattern
的配置增添了搜索的匹配方式多樣性。
export interface SearchFilterOption { isFilter: boolean; // 是否是過(guò)濾節(jié)點(diǎn) matchKey?: string; // node節(jié)點(diǎn)中匹配搜索過(guò)濾的字段名 pattern?: RegExp; // 搜索過(guò)濾時(shí)匹配的正則表達(dá)式 }
在tree.tsx
主文件中添加文件use-search-fliter.ts
的引用, 并將searchTree
方法暴露給第三方調(diào)用者。
import useSearchFilter from './composables/use-search-filter'; setup(props: TreeProps, context: SetupContext) { const userPlugins = [useSelect(), useOperate(), useMergeNodes(), useSearchFilter()]; const treeFactory = useTree(data.value, userPlugins, context); expose({ treeFactory, }); }
nodes數(shù)據(jù)結(jié)構(gòu)直接決定如何訪問(wèn)及處理匹配節(jié)點(diǎn)的父節(jié)點(diǎn)及兄弟節(jié)點(diǎn)
在use-core.ts
文件中可以看出, 整個(gè)數(shù)據(jù)結(jié)構(gòu)采用的是扁平結(jié)構(gòu),并不是傳統(tǒng)的樹(shù)結(jié)構(gòu),所有的節(jié)點(diǎn)包含在一個(gè)一維的數(shù)組中。
const treeData = ref<IInnerTreeNode[]>(generateInnerTree(tree));
// 內(nèi)部數(shù)據(jù)結(jié)構(gòu)使用扁平結(jié)構(gòu) export interface IInnerTreeNode extends ITreeNode { level: number; idType?: 'random'; parentId?: string; isLeaf?: boolean; parentChildNodeCount?: number; currentIndex?: number; loading?: boolean; // 節(jié)點(diǎn)是否顯示加載中 childNodeCount?: number; // 該節(jié)點(diǎn)的子節(jié)點(diǎn)的數(shù)量 // 搜索過(guò)濾 isMatched?: boolean; // 搜索過(guò)濾時(shí)是否匹配該節(jié)點(diǎn) childrenMatched?: boolean; // 搜索過(guò)濾時(shí)是否有子節(jié)點(diǎn)存在匹配 isHide?: boolean; // 過(guò)濾后是否不顯示該節(jié)點(diǎn) matchedText?: string; // 節(jié)點(diǎn)匹配的文字(需要高亮顯示) }
節(jié)點(diǎn)中添加以下屬性,用于標(biāo)識(shí)匹配關(guān)系
isMatched?: boolean; // 搜索過(guò)濾時(shí)是否匹配該節(jié)點(diǎn) childrenMatched?: boolean; // 搜索過(guò)濾時(shí)是否有子節(jié)點(diǎn)存在匹配 matchedText?: string; // 節(jié)點(diǎn)匹配的文字(需要高亮顯示)
通過(guò) dealMatchedData
方法來(lái)處理所有節(jié)點(diǎn)關(guān)于搜索屬性的設(shè)置。
它主要做了以下事情:
將用戶傳入的搜索字段進(jìn)行大小寫轉(zhuǎn)換
循環(huán)所有節(jié)點(diǎn),先處理自身節(jié)點(diǎn)是否與搜索字段匹配,匹配就設(shè)置 selfMatched = true
。首先判斷用戶是否通過(guò)自定義字段進(jìn)行搜索 ( matchKey
參數(shù)),如果有,設(shè)置匹配屬性為node中自定義屬性,否則為默認(rèn) label
屬性;然后判斷是否進(jìn)行正則匹配 ( pattern
參數(shù)),如果有,就進(jìn)行正則匹配,否則為默認(rèn)的忽略大小寫的模糊匹配。
如果自身節(jié)點(diǎn)匹配時(shí), 設(shè)置節(jié)點(diǎn) matchedText
屬性值,用于高亮標(biāo)識(shí)。
判斷自身節(jié)點(diǎn)有無(wú) parentId
,無(wú)此屬性值時(shí),為根節(jié)點(diǎn),無(wú)須處理父節(jié)點(diǎn)。有此屬性時(shí),需要進(jìn)行內(nèi)層循環(huán)處理父節(jié)點(diǎn)的搜索屬性。利用set保存節(jié)點(diǎn)的 parentId
, 依次向前查找,找到parent節(jié)點(diǎn),判讀是否該parent節(jié)點(diǎn)被處理過(guò),如果沒(méi)有,設(shè)置父節(jié)點(diǎn)的 childrenMatched
和 expanded
屬性為true,再將parent節(jié)點(diǎn)的 parentId
屬性加入set中,while循環(huán)重復(fù)這個(gè)操作,直到遇到第一個(gè)已經(jīng)處理過(guò)的父節(jié)點(diǎn)或者直到根節(jié)點(diǎn)停止循環(huán)。
整個(gè)雙層循環(huán)將所有節(jié)點(diǎn)處理完畢。
dealMatchedData
核心代碼如下:
const dealMatchedData = (target: string, matchKey: string | undefined, pattern: RegExp | undefined) => { const trimmedTarget = trim(target).toLocaleLowerCase(); for (let i = 0; i < data.value.length; i++) { const key = matchKey ? data.value[i][matchKey] : data.value[i].label; const selfMatched = pattern ? pattern.test(key) : key.toLocaleLowerCase().includes(trimmedTarget); data.value[i].isMatched = selfMatched; // 需要向前找父節(jié)點(diǎn),處理父節(jié)點(diǎn)的childrenMatched、expand參數(shù)(子節(jié)點(diǎn)匹配到時(shí),父節(jié)點(diǎn)需要展開(kāi)) if (selfMatched) { data.value[i].matchedText = matchKey ? data.value[i].label : trimmedTarget; if (!data.value[i].parentId) { // 沒(méi)有parentId表示時(shí)根節(jié)點(diǎn),不需要再向前遍歷 continue; } let L = i - 1; const set = new Set(); set.add(data.value[i].parentId); // 沒(méi)有parentId時(shí),表示此節(jié)點(diǎn)的縱向parent已訪問(wèn)完畢 // 沒(méi)有父節(jié)點(diǎn)被處理過(guò),表示時(shí)第一次向上處理當(dāng)前縱向父節(jié)點(diǎn) while (L >= 0 && data.value[L].parentId && !hasDealParentNode(L, i, set)) { if (set.has(data.value[L].id)) { data.value[L].childrenMatched = true; data.value[L].expanded = true; set.add(data.value[L].parentId); } L--; } // 循環(huán)結(jié)束時(shí)需要額外處理根節(jié)點(diǎn)一層 if (L >= 0 && !data.value[L].parentId && set.has(data.value[L].id)) { data.value[L].childrenMatched = true; data.value[L].expanded = true; } } } }; const hasDealParentNode = (pre: number, cur: number, parentIdSet: Set<unknown>) => { // 當(dāng)訪問(wèn)到同一層級(jí)前已經(jīng)有匹配時(shí)前一個(gè)已經(jīng)處理過(guò)父節(jié)點(diǎn)了,不需要繼續(xù)訪問(wèn) // 當(dāng)訪問(wèn)到第一父節(jié)點(diǎn)的childrenMatched為true的時(shí),不再需要向上尋找,防止重復(fù)訪問(wèn) return ( (data.value[pre].parentId === data.value[cur].parentId && data.value[pre].isMatched) || (parentIdSet.has(data.value[pre].id) && data.value[pre].childrenMatched) ); };
節(jié)點(diǎn)中添加以下屬性,用于標(biāo)識(shí)節(jié)點(diǎn)是否隱藏。
isHide?: boolean; // 過(guò)濾后是否不顯示該節(jié)點(diǎn)
同3.3中核心處理邏輯大同小異,通過(guò)雙層循環(huán), 節(jié)點(diǎn)的 isMatched
和 childrenMatched
以及父節(jié)點(diǎn)的 isMatched
設(shè)置自身節(jié)點(diǎn)是否顯示。
核心代碼如下:
const dealNodeHideProperty = () => { data.value.forEach((item, index) => { if (item.isMatched || item.childrenMatched) { item.isHide = false; } else { // 需要判斷是否有父節(jié)點(diǎn)有匹配 if (!item.parentId) { item.isHide = true; return; } let L = index - 1; const set = new Set(); set.add(data.value[index].parentId); while (L >= 0 && data.value[L].parentId && !hasParentNodeMatched(L, index, set)) { if (set.has(data.value[L].id)) { set.add(data.value[L].parentId); } L--; } if (!data.value[L].parentId && !data.value[L].isMatched) { // 沒(méi)有parentId, 說(shuō)明已經(jīng)訪問(wèn)到當(dāng)前節(jié)點(diǎn)所在的根節(jié)點(diǎn) item.isHide = true; } else { item.isHide = false; } } }); }; const hasParentNodeMatched = (pre: number, cur: number, parentIdSet: Set<unknown>) => { return parentIdSet.has(data.value[pre].id) && data.value[pre].isMatched; };
如果該節(jié)點(diǎn)被匹配,將節(jié)點(diǎn)的label
處理成[preMatchedText, matchedText, postMatchedText]
格式的數(shù)組。 matchedText
添加 span
標(biāo)簽包裹,通過(guò)CSS樣式顯示高亮效果。
const matchedContents = computed(() => { const matchItem = data.value?.matchedText || ''; const label = data.value?.label || ''; const reg = (str: string) => str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); const regExp = new RegExp('(' + reg(matchItem) + ')', 'gi'); return label.split(regExp); });
<span class={nodeTitleClass.value}> { !data.value?.matchedText && data.value?.label } { data.value?.matchedText && matchedContents.value.map((item: string, index: number) => ( index % 2 === 0 ? item : <span class={highlightCls}>{item}</span> )) } </span>
tree組件采用虛擬列表時(shí),需將滾動(dòng)條滾動(dòng)至第一個(gè)匹配的節(jié)點(diǎn),方便用戶查看
先得到目前整個(gè)樹(shù)顯示出來(lái)的節(jié)點(diǎn),找到第一個(gè)匹配的節(jié)點(diǎn)下標(biāo)。調(diào)用虛擬列表組件的 scrollTo
方法滾動(dòng)至該匹配節(jié)點(diǎn)。
const getFirstMatchIndex = (): number => { let index = 0; const showTreeData = getExpendedTree().value; while (index <= showTreeData.length - 1 && !showTreeData[index].isMatched) { index++; } return index >= showTreeData.length ? 0 : index; }; const scrollIndex = getFirstMatchIndex(); virtualListRef.value.scrollTo(scrollIndex);
通過(guò) scrollTo
方法定位至第一個(gè)匹配項(xiàng)效果圖:
原始樹(shù)結(jié)構(gòu)顯示圖:
過(guò)濾功能:
到這里 Tree 組件的搜索過(guò)濾功能就開(kāi)發(fā)完了,我們來(lái)使用下吧。
<script setup lang="ts"> import { ref } from 'vue'; const treeRef = ref(); const data = ref([ { label: 'parent node 1', }, { label: 'parent node 2', children: [ { label: 'child node 2-1', children: [ { label: 'child node 2-1-1', }, { label: 'child node 2-1-2', }, ], }, { label: 'child node 2-2', children: [ { label: 'child node 2-2-1', }, { label: 'child node 2-2-2', }, ], }, ], }, ]); const onSearch = (keyword) => { // 只需要調(diào)用 Tree 組件實(shí)例的 searchTree 方法即可實(shí)現(xiàn)搜索過(guò)濾 treeRef.value.treeFactory.searchTree(keyword); }; </script> <template> <d-search @search="onSearch"></d-search> <d-tree ref="treeRef" :data="data"></d-tree> </template>
是不是非常簡(jiǎn)單?
searchTree 方法一共有兩個(gè)參數(shù):
keyword 搜索關(guān)鍵字
options 配置選項(xiàng)
isFilter 是否需要過(guò)濾
matchKey node節(jié)點(diǎn)中匹配搜索過(guò)濾的字段名
pattern 搜索過(guò)濾時(shí)匹配的正則表達(dá)式
整棵樹(shù)數(shù)據(jù)結(jié)構(gòu)就是一個(gè)一維數(shù)組,向上需要將匹配節(jié)點(diǎn)所有的父節(jié)點(diǎn)全部展開(kāi), 向下需要知道有沒(méi)有子節(jié)點(diǎn)存在匹配。傳統(tǒng)tree
組件的數(shù)據(jù)結(jié)構(gòu)是樹(shù)形結(jié)構(gòu),通過(guò)遞歸的方式完成節(jié)點(diǎn)的訪問(wèn)及處理。對(duì)于扁平的數(shù)據(jù)結(jié)構(gòu)應(yīng)該如何處理?
方案一:扁平數(shù)據(jù)結(jié)構(gòu) --> 樹(shù)形結(jié)構(gòu) --> 遞歸處理 --> 扁平數(shù)據(jù)結(jié)構(gòu) (NO)
方案二: node添加parent屬性,保存該節(jié)點(diǎn)父級(jí)節(jié)點(diǎn)內(nèi)容 --> 遍歷節(jié)點(diǎn)處理自身節(jié)點(diǎn)及parent節(jié)點(diǎn) (No)
方案三: 同過(guò)雙層循環(huán),第一層循環(huán)處理當(dāng)前節(jié)點(diǎn),第二層循環(huán)處理父節(jié)點(diǎn) (Yes)
方案一:通過(guò)數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換處理,不僅丟掉了扁平數(shù)據(jù)結(jié)構(gòu)的優(yōu)勢(shì),還增加了數(shù)據(jù)格式轉(zhuǎn)換的成本,并帶來(lái)了更多的性能消耗。
方案二:parent屬性添加其實(shí)就是一種樹(shù)形結(jié)構(gòu)的模仿,增加內(nèi)存消耗,保存很多無(wú)用重復(fù)數(shù)據(jù)。循環(huán)訪問(wèn)節(jié)點(diǎn)時(shí)也存在節(jié)點(diǎn)的重復(fù)訪問(wèn)。節(jié)點(diǎn)越靠后,重復(fù)訪問(wèn)越嚴(yán)重,無(wú)用的性能消耗。
方案三: 利用扁平數(shù)據(jù)結(jié)構(gòu)的優(yōu)勢(shì),節(jié)點(diǎn)是有順序的。即:樹(shù)節(jié)點(diǎn)的顯示順序就是節(jié)點(diǎn)在數(shù)組中的順序,父節(jié)點(diǎn)一定是在子節(jié)點(diǎn)之前。父節(jié)點(diǎn)訪問(wèn)處理只需要遍歷該節(jié)點(diǎn)之前的節(jié)點(diǎn),通過(guò) childrenMatched
屬性標(biāo)識(shí)該父節(jié)點(diǎn)有子節(jié)點(diǎn)存在匹配。 不用添加parent字段存取所有的父節(jié)點(diǎn)信息,不用通過(guò)數(shù)據(jù)轉(zhuǎn)換,再遞歸尋找處理節(jié)點(diǎn)。
外層循環(huán),如果該節(jié)點(diǎn)沒(méi)有匹配搜索字段,將不進(jìn)行內(nèi)層循環(huán),直接跳過(guò)。 詳見(jiàn)3.3中的代碼
通過(guò)對(duì)內(nèi)層循環(huán)終止條件的優(yōu)化,防止重復(fù)訪問(wèn)同一個(gè)父節(jié)點(diǎn)
let L = index - 1; const set = new Set(); set.add(data.value[index].parentId); while (L >= 0 && data.value[L].parentId && !hasParentNodeMatched(L, index, set)) { if (set.has(data.value[L].id)) { set.add(data.value[L].parentId); } L--; }
const hasDealParentNode = (pre: number, cur: number, parentIdSet: Set<unknown>) => { // 當(dāng)訪問(wèn)到同一層級(jí)前已經(jīng)有匹配時(shí)前一個(gè)已經(jīng)處理過(guò)父節(jié)點(diǎn)了,不需要繼續(xù)訪問(wèn) // 當(dāng)訪問(wèn)到第一父節(jié)點(diǎn)的childrenMatched為true的時(shí),不再需要向上尋找,防止重復(fù)訪問(wèn) return ( (data.value[pre].parentId === data.value[cur].parentId && data.value[pre].isMatched) || (parentIdSet.has(data.value[pre].id) && data.value[pre].childrenMatched) ); };
同樣通過(guò)雙層循環(huán)、以及處理匹配數(shù)據(jù)時(shí)增加的isMatched
、 childrenMatched
屬性來(lái)共同決定節(jié)點(diǎn)的isHide
屬性,詳見(jiàn)3.4中的代碼、
通過(guò)對(duì)內(nèi)層循環(huán)終止條件的優(yōu)化,與設(shè)置 childrenMatched
時(shí)的判斷有所區(qū)別。
const hasParentNodeMatched = (pre: number, cur: number, parentIdSet: Set<unknown>) => { return parentIdSet.has(data.value[pre].id) && data.value[pre].isMatched; };
關(guān)于“Tree組件搜索過(guò)濾功能如何實(shí)現(xiàn)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。