Antd-React-TreeSelect前端搜尋過濾

Evisu47(常安欧阳)發表於2024-08-20

在開發過程中,但是antd中的搜尋會把多餘的也會帶出來
就例如下圖,我們本想去搜尋1但是他會把其子節點都帶出來,其實我們的本意是像搜2一樣或者當中間隔層處理

但是我們該如何解決這樣的問題呢如何做到下面兩種情況
(1)搜尋過濾掉不匹配的內容只留下匹配的內容
這是沒有搜尋之前

這是搜尋之後,當我們去搜尋5的時候我們就會直接把213過濾掉

(2)搜尋中當子節點不是搜尋內容但是孫節點和祖孫節點中存在要搜尋的內容要把該子節點進行保留
這是沒有搜尋之前

這是搜尋之後,我們要保留的結果

那麼主要方法如下,antd-treeselect中的filterTreeNode屬性,是否根據輸入項進行篩選,預設用 treeNodeFilterProp 的值作為要篩選的 TreeNode 的屬性值

方法如下使用

//toLowerCase()的方法主要是為了使用不區分大小寫使用
 const filterTreeNode = (inputValue: string, treeNode: any) => {
    return treeNode.title.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
  }

接下來就是搜尋功能的具體實現方法

 // 此處操作主要用於前端處理搜尋樹時過濾掉搜尋出的父節點下與搜尋內容無關的其他子節點
    if (searchValue) {
      const fileData = [...oldfileTree]//主要用於記錄tree節點使用的 oldfileTree就是樹的節點功能
      // 使用者樹搜尋的功能
      const searchResult = getSearchList([...fileData], searchValue)
      // 將樹的列表更具搜尋的內容的所有父級節點和搜尋到內容id的合集
      let parentKeys
      if (name === 'apiManage') {
        parentKeys = contents
          .map((item: any) => {
            if (item.searchTitle.toLowerCase().indexOf(searchValue.toLowerCase()) > -1) {
              return getParentName(item.id, contents)
            }
            return null
          })
          .filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)
      } else {
        parentKeys = contents
          .map((item: any) => {
            if (item.searchTitle.toLowerCase().indexOf(searchValue.toLowerCase()) > -1) {
              return getParentKey(item.id, contents)
            }
            return null
          })
          .filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)
      }
      //所有需要的id扁平化處理
      const parentIdsList: any = parentKeys
        .flat(2)
        .filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)
      // 獲取需要展開的id集合由於過程中可能存在層級丟失,需要使用traverseParent向上尋找所有父級的id序列
      const getExpendKeys = parentIdsList
        .map((item: string) => {
          return traverseParent(searchResult, item)
        })
        .flat(2)
        .filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)
      //設定翻開節點
      setTreeExpandedKeys(getExpendKeys)
      // 將搜尋的集合轉換成列表形式
      generateList(searchResult)
      // 把集合做轉換成Map結構
      const listMap = dataList.reduce((map, item: any) => {
        map.set(item.id, item)
        return map
      }, new Map())
      //將所有展開的key與集合使用Map快速匹配對應值並將Map中存在的標記為true
      getExpendKeys.map((item: string) => {
        if (listMap.has(item)) {
          listMap.set(item, { ...listMap.get(item), hasSearch: true })
        }
      })
      // 將搜尋的結果和Map進行匹配,如果匹配成功則將該節點換成Map中該節點的內容
      const result = hasTree(searchResult, listMap)
      // 將融合好的hasSearch tree(是否是搜尋的節點)進行去除所有false的節點
      const filterTree = removeFalseNodes(result)
      // 形成所有搜尋的結果
      setFileTree([...filterTree] as treeDataNode[])
    } 

getSearchList 就是用於搜尋高亮使用的,hasSearch搜尋到的值為true,搜尋不到的值則為false

  const getSearchList = (data: treeDataNode[], searchValue: string) => {
    const result: treeDataNode[] = data.map(item => {
      const strTitle = item.searchTitle as string
      const index = strTitle.toLowerCase().indexOf(searchValue.toLowerCase())
      const beforeStr = strTitle.substring(0, index)
      const afterStr = strTitle.slice(index + searchValue.length)
      const regExp = new RegExp(searchValue, 'gi')
      const matches = strTitle.match(regExp)
      let value = ''
      if (matches) {
        strTitle.replace(regExp, (match: any) => {
          value = match
          return match
        })
      }

      const alias =
        index > -1 ? (
          <span>
            {beforeStr}
            <span className='site-tree-search-value'>{value}</span> //site-tree-search-value設定css樣式,設定你需要的高亮的顏色什麼顏色都可以
            {afterStr}
          </span>
        ) : (
          <span>{strTitle}</span>
        )

      if (item.children) {
        return {
          ...item,
          alias,
          value: item.id,
          hasSearch: index > -1 ? true : false, //將所有搜尋結果是真的標記為true否則為false
          children: getSearchList(item.children, searchValue)
        }
      }
      return {
        ...item,
        value: item.id,
        hasSearch: index > -1 ? true : false, //將所有搜尋結果是真的標記為true否則為false
        alias
      }
    })
    return result
  }

getParentKey 的目的是找到給定 key 所對應的節點的直接父節點,並返回該父節點的 id 和 parentId。
getParentKey 函式沒有明確處理未找到父節點的情況,可能會返回意外的結果或 undefined或者空陣列。因而要使用.flat(2).filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)來過濾

  const getParentKey = (key: React.Key, tree: any): React.Key => {
    let parentKey: any
    for (let i = 0; i < tree.length; i++) {
      const node = tree[i]
      if (node.children) {
        if (node.children.some((item: any) => item.id === key)) {
          parentKey = [node.id, node.parentId]
        } else if (getParentKey(key, node.children)) {
          parentKey = [getParentKey(key, node.children), node.parentId]
        }
      }
    }
    return parentKey
  }

traverseParent 的目的是遞迴地查詢給定 parentId 的所有祖先節點,並將它們的 id 收集到一個陣列中。
traverseParent 在未找到指定 parentId 的情況下會返回一個空陣列。因而要使用.flat(2).filter((item: any, i: any, self: any) => item && self.indexOf(item) === i)來過濾

  const traverseParent = (treeData: treeDataNode[], parentId?: string) => {
    let result: string[] = []
    function traverse(nodes: treeDataNode[], parentId: string) {
      for (let i = 0; i < nodes.length; i++) {
        const node = nodes[i]
        if (node.id === parentId) {
          result = [...result, node.id]
          if (node.parentId) {
            traverse(treeData, node.parentId)
          }
          break
        } else if (node.children) {
          traverse(node.children, parentId)
        }
      }
    }
    if (parentId) traverse(treeData, parentId)

    return result
  }

generateList 的目的是用於扁平化樹形資料結構並轉換每個節點的格式

 const dataList: { key: React.Key; title: string; name: string }[] = []
  const generateList = (data: treeDataNode[]) => {
    for (let i = 0; i < data.length; i++) {
      const node = data[i]
      dataList.push({ ...node, name: node.title })
      if (node.children) {
        generateList(node.children)
      }
    }
  }

hasTree 就是將樹重新構建,將樹中存在的與Map結構中同樣內容的值換成Map結構的資訊

  const hasTree = (tree: treeDataNode[], map: any) => {
    return tree.map(node => {
      if (map.has(node.id)) {
        node = map.get(node.id)
      }
      // 如果節點有子節點,遞迴處理子節點
      if (node.children && node.children.length > 0) {
        node.children = hasTree(node.children, map)
      }
      return node
    })
  }

removeFalseNodes 是刪除hasSearch 為false的置換成undefined在將其過濾掉最後剩下的就是搜尋出的結果

const removeFalseNodes = (data: treeDataNode[]) => {
    return data
      .map(item => {
        // 遞迴處理children陣列
        item.children = item.children && item.children.filter(child => child.hasSearch)
        if (item.children && item.children.length > 0) {
          removeFalseNodes(item.children)
        }
        // 如果當前物件的hasSearch為false且children為空陣列,則返回undefined以從結果中排除
        return item.hasSearch || (item.children && item.children.length > 0) ? item : undefined
      })
      .filter(item => item !== undefined)
  }

總之,在一些時候搜尋為了迎合需要不得不這麼操作,那麼該操作結合了antd官方的搜尋操作,在此之前需要保持清醒的頭腦

首先我們搜尋出來高亮這個操作antd TreeSelect的是可以實現,但是搜尋中我們發現實現不了搜尋過濾,但是又要解決這個問題,想嘗試使用陣列方法將不是的部分刪除,只能解決節點是的情況,當出現差層,何為差層就是當子節點不是搜尋內容但是孫節點和祖孫節點中存在要搜尋的內容要把該子節點進行保留的時候發現資料保留不住,不知道該如何解決,翻閱了ES6後發現使用Map做一層資料儲存,並結合搜尋情況將所有搜尋的父節點向上遍歷將其hasSearch設定為true,這樣在重新構建樹的時候可以將所有需要的節點變成true,再最後將所有節點是false的節點進行刪除,只保留hasSearch為true的節點。總之該操作中使用了陣列的方法,以及ES6的Map結構,當做出來的時候感覺雨過天晴,但是個人覺得這些還是太冗餘了,之後會更進方法,如果大家有什麼更好的方法請多指教 (´・Д・)」

最後就是如果這種問題可以放在後端在搜尋的時候進行請求來減少前端遍歷和重組的過程減少渲染次數會更好

相關文章