我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Tree组件搜索过滤功能实现干货

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Tree组件搜索过滤功能实现干货

1 Tree 组件搜索过滤功能简介

本文源于 Vue DevUI 开源组件库实践。

树节点的搜索功能主要是为了方便用户能够快速查找到自己需要的节点。过滤功能不仅要满足搜索的特性,同时还需要隐藏掉与匹配节点同层级的其它未能匹配的节点。

搜索功能主要包括以下功能:

  • 与搜索过滤字段匹配的节点需要进行标识,和普通节点进行区分
  • 子节点匹配时,其所有父节点需要展开,方便用户查看层级关系
  • 对于大数据量,采用虚拟滚动时,搜索过滤完成后滚动条需滚动至第一个匹配节点的位置

搜索会将匹配到的节点高亮:

过滤除了将匹配到的节点高亮之外,还会将不匹配的节点筛除掉:

2 组件交互逻辑分析

2.1 对于匹配节点的标识如何呈现?

通过将节点与搜索字段相匹配的 label 部分文字进行高亮加粗的方式进行标记。易于用户一眼就能够找到搜索到的节点。

2.2 用户如何调用 tree 组件的搜索过滤功能?

通过添加searchTree方法,用户通过ref的方式进行调用。并通过option参数配置区分搜索、过滤。

2.3 对于匹配的节点其父节点及兄弟节点如何获取及处理?

对于节点的获取及处理是搜索过滤功能的核心。尤其在大数据量的情况下,带来的性能消耗如何优化,将在实现原理中详情阐述。

3 实现原理和步骤

3.1 第一步:需要熟悉 tree 组件整个代码及逻辑组织方式

tree组件的文件结构:

tree
├── index.ts
├── class="lazy" data-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 带来的便利。逻辑层之间的分离,方便代码组织及后续问题的定位。能够让开发者只专心于自己的特性,非常有利于后期维护。

添加文件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的接口定义,matchKeypattern的配置增添了搜索的匹配方式多样性。

export interface SearchFilterOption {
  isFilter: boolean; // 是否是过滤节点
  matchKey?: string; // node节点中匹配搜索过滤的字段名
  pattern?: RegExp; // 搜索过滤时匹配的正则表达式
}

tree.tsx主文件中添加文件use-search-fliter.ts的引用, 并将searchTree方法暴露给第三方调用者。

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,
    });
  }

3.2 第二步:需要熟悉 tree 组件整个nodes数据结构是怎样的

nodes数据结构直接决定如何访问及处理匹配节点的父节点及兄弟节点

use-core.ts文件中可以看出, 整个数据结构采用的是扁平结构,并不是传统的树结构,所有的节点包含在一个一维的数组中。

const treeData = ref<IInnerTreeNode[]>(generateInnerTree(tree));
// 内部数据结构使用扁平结构
export interface IInnerTreeNode extends ITreeNode {
  level: number;
  idType?: 'random';
  parentId?: string;
  isLeaf?: boolean;
  parentChildNodeCount?: number;
  currentIndex?: number;
  loading?: boolean; // 节点是否显示加载中
  childNodeCount?: number; // 该节点的子节点的数量
  // 搜索过滤
  isMatched?: boolean; // 搜索过滤时是否匹配该节点
  childrenMatched?: boolean; // 搜索过滤时是否有子节点存在匹配
  isHide?: boolean; // 过滤后是否不显示该节点
  matchedText?: string; // 节点匹配的文字(需要高亮显示)
}

3.3 第三步: 处理匹配节点及其父节点的展开属性

节点中添加以下属性,用于标识匹配关系

  isMatched?: boolean; // 搜索过滤时是否匹配该节点
  childrenMatched?: boolean; // 搜索过滤时是否有子节点存在匹配
  matchedText?: string; // 节点匹配的文字(需要高亮显示)

通过 dealMatchedData 方法来处理所有节点关于搜索属性的设置。

它主要做了以下事情:

  • 将用户传入的搜索字段进行大小写转换
  • 循环所有节点,先处理自身节点是否与搜索字段匹配,匹配就设置 selfMatched = true。首先判断用户是否通过自定义字段进行搜索 ( matchKey 参数),如果有,设置匹配属性为node中自定义属性,否则为默认 label 属性;然后判断是否进行正则匹配 ( pattern 参数),如果有,就进行正则匹配,否则为默认的忽略大小写的模糊匹配。
  • 如果自身节点匹配时, 设置节点 matchedText 属性值,用于高亮标识。
  • 判断自身节点有无 parentId,无此属性值时,为根节点,无须处理父节点。有此属性时,需要进行内层循环处理父节点的搜索属性。利用set保存节点的 parentId , 依次向前查找,找到parent节点,判读是否该parent节点被处理过,如果没有,设置父节点的 childrenMatchedexpanded 属性为true,再将parent节点的 parentId 属性加入set中,while循环重复这个操作,直到遇到第一个已经处理过的父节点或者直到根节点停止循环。
  • 整个双层循环将所有节点处理完毕。

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;
        // 需要向前找父节点,处理父节点的childrenMatched、expand参数(子节点匹配到时,父节点需要展开)
        if (selfMatched) {
            data.value[i].matchedText = matchKey ? data.value[i].label : trimmedTarget;
            if (!data.value[i].parentId) {
                // 没有parentId表示时根节点,不需要再向前遍历
                continue;
            }
            let L = i - 1;
            const set = new Set();
            set.add(data.value[i].parentId);
            // 没有parentId时,表示此节点的纵向parent已访问完毕
            // 没有父节点被处理过,表示时第一次向上处理当前纵向父节点
            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--;
            }
            // 循环结束时需要额外处理根节点一层
            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>) => {
    // 当访问到同一层级前已经有匹配时前一个已经处理过父节点了,不需要继续访问
    // 当访问到第一父节点的childrenMatched为true的时,不再需要向上寻找,防止重复访问
    return (
    (data.value[pre].parentId === data.value[cur].parentId && data.value[pre].isMatched) ||
    (parentIdSet.has(data.value[pre].id) && data.value[pre].childrenMatched)
    );
};

3.4 第四步: 如果是过滤功能时,需要将未匹配到的节点进行隐藏

节点中添加以下属性,用于标识节点是否隐藏。

  isHide?: boolean; // 过滤后是否不显示该节点

同3.3中核心处理逻辑大同小异,通过双层循环, 节点的 isMatchedchildrenMatched 以及父节点的 isMatched 设置自身节点是否显示。

核心代码如下:

const dealNodeHideProperty = () => {
  data.value.forEach((item, index) => {
    if (item.isMatched || item.childrenMatched) {
      item.isHide = false;
    } else {
      // 需要判断是否有父节点有匹配
      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) {
        // 没有parentId, 说明已经访问到当前节点所在的根节点
        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;
};

3.5 第五步:处理匹配节点的高亮显示

如果该节点被匹配,将节点的label处理成[preMatchedText, matchedText, postMatchedText]格式的数组。 matchedText添加 span标签包裹,通过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>

3.6 第六步:

tree组件采用虚拟列表时,需将滚动条滚动至第一个匹配的节点,方便用户查看

先得到目前整个树显示出来的节点,找到第一个匹配的节点下标。调用虚拟列表组件的 scrollTo 方法滚动至该匹配节点。

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);

通过 scrollTo 方法定位至第一个匹配项效果图:

原始树结构显示图:

过滤功能:

4 使用searchTree对Tree进行搜索过滤

到这里 Tree 组件的搜索过滤功能就开发完了,我们来使用下吧。

<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) => {
  // 只需要调用 Tree 组件实例的 searchTree 方法即可实现搜索过滤
  treeRef.value.treeFactory.searchTree(keyword);
};
</script>
<template>
  <d-search @search="onSearch"></d-search>
  <d-tree ref="treeRef" :data="data"></d-tree>
</template>

是不是非常简单?

searchTree 方法一共有两个参数:

keyword 搜索关键字

options 配置选项

  • isFilter 是否需要过滤
  • matchKey node节点中匹配搜索过滤的字段名
  • pattern 搜索过滤时匹配的正则表达式

5 遇到的难点问题

5.1 搜索的核心在于对匹配节点的所有父节点的访问以及处理

整棵树数据结构就是一个一维数组,向上需要将匹配节点所有的父节点全部展开, 向下需要知道有没有子节点存在匹配。传统tree组件的数据结构是树形结构,通过递归的方式完成节点的访问及处理。对于扁平的数据结构应该如何处理?

  • 方案一:扁平数据结构 --> 树形结构 --> 递归处理 --> 扁平数据结构 (NO)
  • 方案二: node添加parent属性,保存该节点父级节点内容 --> 遍历节点处理自身节点及parent节点 (No)
  • 方案三: 同过双层循环,第一层循环处理当前节点,第二层循环处理父节点 (Yes)

方案一:通过数据结构的转换处理,不仅丢掉了扁平数据结构的优势,还增加了数据格式转换的成本,并带来了更多的性能消耗。

方案二:parent属性添加其实就是一种树形结构的模仿,增加内存消耗,保存很多无用重复数据。循环访问节点时也存在节点的重复访问。节点越靠后,重复访问越严重,无用的性能消耗。

方案三: 利用扁平数据结构的优势,节点是有顺序的。即:树节点的显示顺序就是节点在数组中的顺序,父节点一定是在子节点之前。父节点访问处理只需要遍历该节点之前的节点,通过 childrenMatched属性标识该父节点有子节点存在匹配。 不用添加parent字段存取所有的父节点信息,不用通过数据转换,再递归寻找处理节点。

5.2 处理父级节点时进行优化,防止内层遍历重复处理已经访问过的父级节点,带来性能提升

外层循环,如果该节点没有匹配搜索字段,将不进行内层循环,直接跳过。 详见3.3中的代码

通过对内层循环终止条件的优化,防止重复访问同一个父节点

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>) => {
    // 当访问到同一层级前已经有匹配时前一个已经处理过父节点了,不需要继续访问
    // 当访问到第一父节点的childrenMatched为true的时,不再需要向上寻找,防止重复访问
    return (
    (data.value[pre].parentId === data.value[cur].parentId && data.value[pre].isMatched) ||
    (parentIdSet.has(data.value[pre].id) && data.value[pre].childrenMatched)
    );
};

5.3 对于过滤功能,还需处理节点的显示隐藏

同样通过双层循环、以及处理匹配数据时增加的isMatchedchildrenMatched属性来共同决定节点的isHide属性,详见3.4中的代码、

通过对内层循环终止条件的优化,与设置 childrenMatched时的判断有所区别。

const hasParentNodeMatched = (pre: number, cur: number, parentIdSet: Set<unknown>) => {
    return parentIdSet.has(data.value[pre].id) && data.value[pre].isMatched;
};

6 小结

虽然是一个组件下一个小特性的开发,但是从特性的交互分析开始,一步步到最终的功能实现,整个过程还是收获满满。

平时开发中很少能够从方案设计到功能实现有一个整体的规划,往往都是先上手代码,在开发过程中才发现方案选取不合理,就会走很多弯路。

所以,刚开始的特性分析和方案设计就显得尤为重要。 分析 --> 设计 --> 方案探讨 --> 方案确定 --> 功能实现 --> 逻辑优化。每个过程都能锻炼提升自己的能力。

以上就是Tree 组件搜索过滤功能实现干货的详细内容,更多关于Tree 组件搜索过滤的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Tree组件搜索过滤功能实现干货

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

vue封装tree组件实现搜索功能

本文主要介绍了vue封装tree组件实现搜索功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
2023-05-20

如何通过vue封装tree组件实现搜索功能

本篇文章和大家了解一下如何通过vue封装tree组件实现搜索功能。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。我使用的是 vue2 + antd, 那么 antd 的 tree 组件中没有给我们封装搜索,其官网提供的搜索
2023-07-06

Vue2使用cube-ui实现搜索过滤、高亮功能

cube-ui是基于 Vue.js实现的精致移动端组件库,由于很长一段时间没有学习cube-ui的功能实现示例代码了,今天通过本文给大家介绍下Vue2使用cube-ui实现搜索过滤、高亮功能,感兴趣的朋友跟随小编一起看看吧
2023-01-07

Sphinx PHP 实现邮件系统的全文搜索与过滤功能

随着电子邮件的广泛应用,人们越来越关注快速检索与过滤邮件的效率。Sphinx是一款开源的全文搜索引擎,其高效的搜索速度与强大的过滤功能使其成为邮件系统的理想选择。本文将介绍如何使用Sphinx PHP实现邮件系统的全文搜索与过滤功能,并给出
2023-10-21

如何使用Vue的过滤器功能来实现模糊搜索

Vue.js是一款流行的JavaScript框架之一,它提供了许多有用的功能,包括Vue的过滤器。在本文中,我们将介绍如何使用Vue的过滤器功能来实现模糊搜索。在Vue.js中,过滤器是用于转换文本的函数,常常用于格式化文本输出。在本例中,我们将使用过滤器来实现模糊搜索,这可以帮助用户更快速地找到他们所需的内容。首先,我们需要在Vue.js中定义我们的过滤器。我们将使用Vue.
2023-05-14

RiSearch PHP 实现搜索结果的智能排序与过滤

作为一个开发者,我们经常会遇到需要实现搜索功能的情况。而在实际的项目中,如何对搜索结果进行智能排序和过滤是一个非常关键的问题。本文将介绍如何使用 RiSearch PHP 来实现搜索结果的智能排序与过滤,并提供具体的代码示例。RiSearc
2023-10-21

Django组合条件的搜索功能实现是怎么样的

这期内容当中小编将会给大家带来有关Django组合条件的搜索功能实现是怎么样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。一直想着如何做组合条件的搜索!如:前端有三个输入框....输入后过滤,后台写一条
2023-06-04

怎么使用elementUI组件实现表格的分页及搜索功能

今天小编给大家分享一下怎么使用elementUI组件实现表格的分页及搜索功能的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。主
2023-07-05

如何使用Vue3+Vant组件实现App搜索历史记录功能

这篇文章给大家分享的是有关如何使用Vue3+Vant组件实现App搜索历史记录功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。最近在开发一款新的app项目,我自己也是第一次接触app开发,经过团队的一段时间研究
2023-06-15

使用vue自定义如何实现Tree组件和拖拽功能

这篇文章主要介绍了使用vue自定义如何实现Tree组件和拖拽功能,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
2022-12-09

基于Python实现通过微信搜索功能查看谁把你删除了

场景:查找who删了我,直接copy代码保存到一个python文件who.py,在python环境下运行此文件代码如下,copy保存到who.py文件在python环境直接运行:#!/usr/bin/env python # coding=
2022-06-04

编程热搜

目录