iis服务器助手广告广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >Tree组件搜索过滤功能实现干货
  • 894
分享到

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

2024-04-02 19:04:59 894人浏览 泡泡鱼
摘要

目录1 Tree 组件搜索过滤功能简介2 组件交互逻辑分析2.1 对于匹配节点的标识如何呈现?2.2 用户如何调用 tree 组件的搜索过滤功能?2.3 对于匹配的节点其父节点及兄弟

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

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

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

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

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

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

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

2 组件交互逻辑分析

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

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

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

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

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

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

3 实现原理和步骤

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

tree组件的文件结构:

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

添加文件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 组件搜索过滤的资料请关注编程网其它相关文章!

--结束END--

本文标题: Tree组件搜索过滤功能实现干货

本文链接: https://www.lsjlt.com/news/164695.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

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

下载Word文档
猜你喜欢
  • Tree组件搜索过滤功能实现干货
    目录1 Tree 组件搜索过滤功能简介2 组件交互逻辑分析2.1 对于匹配节点的标识如何呈现?2.2 用户如何调用 tree 组件的搜索过滤功能?2.3 对于匹配的节点其父节点及兄弟...
    99+
    2022-11-13
  • vue封装tree组件实现搜索功能
    我使用的是 vue2 + antd, 那么 antd 的 tree 组件中没有给我们封装搜索,其官网提供的搜索也不是封装好的,而且限制比较大,因为我的树形进来要默认展开,官方的代码是...
    99+
    2023-05-20
    vue tree搜索 vue封装tree
  • 如何通过vue封装tree组件实现搜索功能
    本篇文章和大家了解一下如何通过vue封装tree组件实现搜索功能。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。我使用的是 vue2 + antd, 那么 antd 的 tree 组件中没有给我们封装搜索,其官网提供的搜索...
    99+
    2023-07-06
  • Sphinx PHP 实现邮件系统的全文搜索与过滤功能
    随着电子邮件的广泛应用,人们越来越关注快速检索与过滤邮件的效率。Sphinx是一款开源的全文搜索引擎,其高效的搜索速度与强大的过滤功能使其成为邮件系统的理想选择。本文将介绍如何使用Sphinx PHP实现邮件系统的全文搜索与过滤功能,并给出...
    99+
    2023-10-21
    邮件系统 - 指涉包括发送
  • Vue2使用cube-ui实现搜索过滤、高亮功能
    目录前言一、需求流程:功能实现总结介绍 cube-ui 是基于 Vue.js 实现的精致移动端组件库。 特性 质量可靠由滴滴内部组件库精简提炼而来,经历了业务一年多的考验,...
    99+
    2023-01-07
    vue搜索过滤高亮 vue搜索过滤
  • springboot结合redis实现搜索栏热搜功能及文字过滤
    使用java和redis实现一个简单的热搜功能,具备以下功能: 1:搜索栏展示当前登陆的个人用户的搜索历史记录,删除个人历史记录 2:用户在搜索栏输入某字符,则将该字符记录下来 以z...
    99+
    2022-11-13
  • 如何使用Vue的过滤器功能来实现模糊搜索
    Vue.js是一款流行的JavaScript框架之一,它提供了许多有用的功能,包括Vue的过滤器。在本文中,我们将介绍如何使用Vue的过滤器功能来实现模糊搜索。在Vue.js中,过滤器是用于转换文本的函数,常常用于格式化文本输出。在本例中,...
    99+
    2023-05-14
  • RiSearch PHP 实现搜索结果的智能排序与过滤
    作为一个开发者,我们经常会遇到需要实现搜索功能的情况。而在实际的项目中,如何对搜索结果进行智能排序和过滤是一个非常关键的问题。本文将介绍如何使用 RiSearch PHP 来实现搜索结果的智能排序与过滤,并提供具体的代码示例。RiSearc...
    99+
    2023-10-21
    过滤 智能排序 RiSearch
  • Angular如何实现搜索、过滤、批量删除、添加、表单验证功能
    这篇文章将为大家详细讲解有关Angular如何实现搜索、过滤、批量删除、添加、表单验证功能,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。具体代码如下所示;<!DOC...
    99+
    2022-10-19
  • 如何在 ASP 中使用 JavaScript 数组实现实时搜索和过滤?
    ASP 是一种广泛使用的 Web 应用程序框架,而 JavaScript 数组则是在 Web 开发中十分常用的数据结构之一。在本文中,我们将讨论如何在 ASP 中使用 JavaScript 数组来实现实时搜索和过滤功能,以提高 Web 应用...
    99+
    2023-07-22
    实时 javascript 数组
  • Django组合条件的搜索功能实现是怎么样的
    这期内容当中小编将会给大家带来有关Django组合条件的搜索功能实现是怎么样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。一直想着如何做组合条件的搜索!如:前端有三个输入框....输入后过滤,后台写一条...
    99+
    2023-06-04
  • C/C++ Qt Tree与Tab组件实现分页菜单功能
    虽然TreeWidget组件可以实现多节点的增删改查,但多节点操作显然很麻烦,在一般的应用场景中基本上只使用一层结构即可解决大部分开发问题,TreeWidget组件通常可配合TabW...
    99+
    2022-11-12
  • 如何使用Vue3+Vant组件实现App搜索历史记录功能
    这篇文章给大家分享的是有关如何使用Vue3+Vant组件实现App搜索历史记录功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。最近在开发一款新的app项目,我自己也是第一次接触app开发,经过团队的一段时间研究...
    99+
    2023-06-15
  • 怎么使用elementUI组件实现表格的分页及搜索功能
    今天小编给大家分享一下怎么使用elementUI组件实现表格的分页及搜索功能的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。主...
    99+
    2023-07-05
  • Vue如何实现数组更新及过滤排序功能
    这篇文章给大家分享的是有关Vue如何实现数组更新及过滤排序功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。变异方法  Vue 包含一组观察数组的变异方法,它们将会触发视图更新,...
    99+
    2022-10-19
  • 使用vue自定义如何实现Tree组件和拖拽功能
    目录vue自定义实现Tree组件和拖拽功能vue2 + js版vue2 + ts 版总结vue自定义实现Tree组件和拖拽功能 实现功能:树结构、右键菜单、拖拽 效果图 vue2 ...
    99+
    2022-12-09
    vue自定义Tree组件 vue Tree组件 vue拖拽功能
  • 使用Vue3+Vant组件实现App搜索历史记录功能(示例代码)
    最近在开发一款新的app项目,我自己也是第一次接触app开发,经过团队的一段时间研究调查,决定使用Vue3+Vant前端组件的模式进行开发,vue2开发我们已经用过几个项目了,所以决...
    99+
    2022-11-12
  • ASP.NETCore模仿中间件方式实现列表过滤功能
    我们的很多功能当中都会遇到对版本进行过滤的场合,例如你可能需要对列表中的数据的时间进行过滤、版本过滤、渠道以及地区信息进行过滤。 原本的做法:设计很多个过滤方法,通过枚举的方式组合,...
    99+
    2022-11-13
  • React通过conetxt实现多组件传值功能
    该功能实现效果类似于vue的provide/inject 而React可通过context进行完成 定义一个公共的文件context/Theme.jsx import { cr...
    99+
    2022-11-12
  • 基于Python实现通过微信搜索功能查看谁把你删除了
    场景:查找who删了我,直接copy代码保存到一个python文件who.py,在python环境下运行此文件 代码如下,copy保存到who.py文件在python环境直接运行: #!/usr/bi...
    99+
    2022-06-04
    把你 看谁 搜索功能
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作