React-View-UI组件库封装——Tree选择器

简介: React-View-UI组件库封装——Tree选择器组件封装记录

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

Tree选择器结构比较特殊,类似于数据结构中的树,因此设计对于优化有很多的关系。

先看一下组件库文档
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
设计中主要用到的思路是递归,先看一下基础渲染吧:

const treeData = [
  {
   
   
    title: 'parent1',
    value: '0-0',
    children: [
      {
   
   
        title: 'parent 1-0',
        value: '0-0-1',
      },
      {
   
   
        title: 'parent 1-1',
        value: '0-0-2',
        children: [
          {
   
   
            title: 'leaf2',
            value: '0-0-0-1',
          },
        ],
      },
    ],
  },
  {
   
   
    title: 'parent2',
    value: '0-1',
    children: [
      {
   
   
        title: 'parent 2-0',
        value: '0-0-3',
      },
    ],
  },
];

渲染结构是这样的,就是一个树的结构,通过渲染函数将所有树节点递归渲染出来,核心代码如下:
在这里插入图片描述
在这里插入图片描述
而具体这个level是从哪里来的呢,其实使用者上文只需要传递title、value、children即可,设计中其实是在渲染之前对这个树节点进行了一些结构改造的,以便于组件开发。
在这里插入图片描述
在上图中,二次改造函数对每个节点都进行了height和level的计算和添加,这些后面都会用到,具体备注在图片中很清楚。

业务开发
核心点主要在切换菜单,切换菜单时我的设计是展开只进行下一层节点的展示;收起的话如果是对根节点进行收起操作,则将所有子节点收起,核心代码如下:
在这里插入图片描述
上面所讲的是切换的实现,如果是点击无子节点的节点呢?就是直接选中操作了。
在这里插入图片描述
这里选中分为了单选了多选,组件默认是单选的,如需要支持多选,需要给组件传递avaChooseMore属性,具体可参照文档案例。

组件完整源码index.tsx:

import React, {
   
    FC, memo, Fragment, useState, useEffect, useCallback } from 'react';
import {
   
    CaretRightOutlined, CaretDownOutlined } from '@ant-design/icons';
import Input from '../Input';
import './index.module.less';

interface treeProps {
   
   
  /**
   * @description Tree配置参数
   */
  treeData: Array<treeNode>;
  /**
   * @description 宽度
   * @default 200px
   */
  width?: string;
  /**
   * @description 支持搜索
   * @default false
   */
  avaSearch?: boolean;
  /**
   * @description 支持多选
   * @default false
   */
  avaChooseMore?: boolean;
  /**
   * @description 全展开
   * @default false
   */
  defaultOpen?: boolean;
  /**
   * @description 选择回调函数
   */
  chooseCallback?: Function;
}
interface treeNode {
   
   
  title: string;
  value: string;
  level: number;
  height?: string;
  children?: Array<treeNode>;
}

const Tree: FC<treeProps> = (props) => {
   
   
  const {
   
    width = '200', treeData, avaSearch, avaChooseMore, defaultOpen, chooseCallback } = props;

  const [stateTreeData, setStateTreeData] = useState<Array<treeNode>>(treeData); //树结构
  const [activedVal, setActivedVal] = useState<string>(''); //选中的节点值
  const [containerHeight, setContainerHeight] = useState<string>('0px'); //容器高度
  const [isFocus, setIsFocus] = useState(false); //聚焦状态

  useEffect(() => {
   
   
    resolveTreeData(treeData as Array<treeNode>, 1);
    window.addEventListener('click', () => setContainerHeight('0px'));
  }, []);

  const resolveTreeData = (treeData: Array<treeNode>, nowIndexLevel: number) => {
   
   
    //二次处理treeData
    treeData.forEach((treeNode: treeNode) => {
   
   
      treeNode.level = nowIndexLevel;
      if (defaultOpen) {
   
   
        //默认全展开
        treeNode.height = '30px';
      } else {
   
   
        treeNode.height = treeNode.level == 1 ? '30px' : '0';
      }
      if (treeNode?.children?.length) {
   
   
        //有子节点
        resolveTreeData(treeNode.children, nowIndexLevel + 1);
      } else {
   
   
        //没有子节点,重置level为当前层级,继续寻找
        nowIndexLevel = treeNode.level;
      }
    });
    setStateTreeData(treeData); //更新状态
  };
  const toggleTreeMenu = (clickTreeNode: treeNode) => {
   
   
    //菜单切换或直接选中终极节点
    if (clickTreeNode?.children?.length) {
   
   
      //菜单切换的情况
      const oldStateTree = [...stateTreeData];
      const editTreeNode = (treeNode: Array<treeNode>) => {
   
   
        //所选节点后代收起处理函数
        treeNode.forEach((child) => {
   
   
          //找到节点,对子节点进行处理
          if (child?.children?.length) {
   
   
            child.height = '0';
            editTreeNode(child.children);
          } else {
   
   
            child.height = '0';
          }
        });
      };
      const mapFn = (treeNode: Array<treeNode>) => {
   
   
        //深度优先查找节点函数
        treeNode.forEach((t: treeNode, i: number) => {
   
   
          if (t.title == clickTreeNode.title && t.value == t.value) {
   
   
            if (t?.children?.length) {
   
   
              //后代节点处理,如果打开,只需打开下一代即可,如果关闭,需要关闭所有后代
              if (t.children[0].height == '0') {
   
   
                //打开
                t.children = t.children.map((child: treeNode) => {
   
   
                  return {
   
   
                    ...child,
                    height: child.height == '0' ? '30px' : '0',
                  };
                });
              } else {
   
   
                //关闭
                editTreeNode(t.children); //对后代节点进行处理
              }
            }
          } else if (t?.children?.length) {
   
   
            mapFn(t.children);
          }
        });
      };
      mapFn(oldStateTree);
      setStateTreeData(oldStateTree);
    } else {
   
   
      //选中终极节点的情况
      if (avaChooseMore) {
   
   
        //多选
        if (activedVal.split(',').includes(clickTreeNode.title)) {
   
   
          //取消选中
          let updateVal: Array<string> | string = activedVal;
          updateVal = updateVal.split(',');
          updateVal.splice(
            activedVal.split(',').findIndex((t) => t == clickTreeNode.title),
            1,
          );
          updateVal = updateVal.join(',');
          setActivedVal(updateVal);
          chooseCallback && chooseCallback(updateVal);
        } else {
   
   
          setActivedVal(
            activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
          );
          chooseCallback &&
            chooseCallback(
              activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
            );
        }
      } else {
   
   
        //单选
        setActivedVal(clickTreeNode.title);
        chooseCallback && chooseCallback(clickTreeNode.title);
      }
    }
  };
  const handleIptChange = (val: string) => {
   
   
    //文本改变回调
    if (avaSearch) {
   
   
      setActivedVal(val);
    } else {
   
   
      setActivedVal('');
    }
  };
  const handleClick = () => {
   
   
    //点击回调
    if (avaSearch) {
   
   
      if (isFocus && containerHeight == '100%') {
   
   
        setContainerHeight('0px');
      } else {
   
   
        setContainerHeight('100%');
      }
    } else {
   
   
      setContainerHeight(containerHeight == '0px' ? '100%' : '0px');
    }
  };
  const handleIptFocus = () => {
   
   
    //聚焦回调
    setTimeout(() => {
   
   
      //异步,等待点击执行完毕
      if (!isFocus) {
   
   
        setIsFocus(true);
      }
    }, 150);
  };
  const handleIptBlur = () => {
   
   
    //失去焦点回调
    setIsFocus(false);
  };
  const searchStyle = useCallback(
    (treeNode: treeNode): string => {
   
   
      //搜索高亮样式
      if (treeNode.title.includes(activedVal) && activedVal !== '') {
   
   
        return '#1890FF';
      } else {
   
   
        return '#000000';
      }
    },
    [activedVal],
  );
  const activedStyle = useCallback(
    (treeNode: treeNode): string => {
   
   
      //选中高亮样式
      if (avaChooseMore) {
   
   
        //多选
        if (activedVal.split(',').includes(treeNode.title)) {
   
   
          return '#bae8ff';
        } else {
   
   
          return '#ffffff';
        }
      } else {
   
   
        //单选
        if (activedVal == treeNode.title) {
   
   
          return '#bae8ff';
        } else {
   
   
          return '#ffffff';
        }
      }
    },
    [activedVal],
  );
  const clearCallback = () => {
   
   
    //清空
    setActivedVal('');
  };
  const render = (data: Array<treeNode> = stateTreeData) => {
   
   
    //动态规划render函数
    return data.map((treeNode: treeNode, index) => {
   
   
      return (
        <Fragment key={
   
   index}>
          <div
            className="treeNode"
            style={
   
   {
   
   
              marginLeft: `${
     
     treeNode.level * 10}px`,
              height: `${
     
     treeNode.height}`,
              color: searchStyle(treeNode),
              background: activedStyle(treeNode),
            }}
            onClick={
   
   () => toggleTreeMenu(treeNode)}
          >
            {
   
   
              treeNode?.children?.length ? (
                treeNode.children[0].height == '0' ? (
                  <CaretDownOutlined />
                ) : (
                  <CaretRightOutlined />
                )
              ) : (
                <div style={
   
   {
   
    width: '12px', height: '12px' }}></div>
              ) //空间占位符
            }

            <span className="text">{
   
   treeNode.title}</span>
          </div>
          {
   
   treeNode?.children?.length && render(treeNode.children)}
        </Fragment>
      );
    });
  };

  return (
    <Fragment>
      <div className="tree-container" onClick={
   
   (e) => e.stopPropagation()}>
        <Input
          moreStyle={
   
   avaSearch ? {
   
   } : {
   
    caretColor: 'transparent' }}
          placeholder={
   
   avaSearch ? '请输入' : ''}
          width={
   
   width}
          defaultValue={
   
   activedVal}
          handleClick={
   
   handleClick}
          handleIptChange={
   
   handleIptChange}
          handleIptFocus={
   
   handleIptFocus}
          handleIptBlur={
   
   handleIptBlur}
          clearCallback={
   
   clearCallback}
          showClear
        />

        <div
          className="tree-select-dialog"
          style={
   
   {
   
   
            width: `${
     
     width}px`,
            height: containerHeight,
            opacity: containerHeight == '0px' ? '0' : '1',
            padding: containerHeight == '0px' ? '0 10px 0 10px' : '10px',
          }}
        >
          {
   
   render()}
        </div>
      </div>
    </Fragment>
  );
};

export default memo(Tree);

github path戳~
React-View-UI组件库文档地址戳~

如果对该组件库有兴趣,在github的readme中有介绍基本入门方式,可以下载尝试,最后感谢花时间阅读此文,如果对Tree组件设计有更好的建议欢迎留言呀。

目录
相关文章
echarts两个折线图共用x轴且合并tooltip功能代码
echarts两个折线图共用x轴且合并tooltip功能代码
1135 0
|
前端开发 API
API接口是什么❓
API即应用程序编程接口,常被称为接口,能够帮助软件之间进行通信并执行特定功能。就如同将黄豆放入豆浆机中,豆浆机(API)接收黄豆(请求参数),并输出豆浆(返回结果)。API使得软件开发更加高效便捷,是互联网产品设计和开发的关键环节。#API接口 #互联网 #产品经理 #技术知识
|
Cloud Native Java 应用服务中间件
【云原生 | 15】Docker commit与Docker build比较
最直观的理由就是,commit会把当前容器运行的日志等文件也打包到镜像中造成镜像体积偏大。从软件版本管理角度讲,Dockerfile本身是脚本文件,是可以版本化的。理论上使用Dockerfile,在任何时候任何机器上都可以重演镜像制作过程制作出一模一样的镜像,这是非常有利于工程化的。强烈建议使用Dockerfile来制作镜像...
824 0
【云原生 | 15】Docker commit与Docker build比较
|
Java 程序员 C++
大学生如果想从事游戏行业,游戏开发或者电竞选手,有哪些建议?
@[TOC](目录) 大学生如果想从事游戏行业,游戏开发或者电竞选手,有哪些建议? # 一、游戏行业概述 游戏行业是指以电子游戏为核心的产业链,包括游戏开发、游戏发行、游戏运营、游戏硬件设备等多个环节。近年来,随着互联网技术的飞速发展,游戏行业呈现出移动化、社交化、智能化的趋势,不断涌现出新的游戏类型和玩法。游戏行业已成为全球范围内最具活力和潜力的产业之一。 # 二、游戏开发 游戏开发是指游戏制作团队根据游戏设计方案,通过编程、美术、音效等手段将游戏构思转化为具体游戏产品的过程。游戏开发涉及多个领域,包括计算机科学、数学、物理学、艺术设计等。游戏开发团队通常包括程序员、美术设计师、音效师、策划
799 0
|
IDE 前端开发 JavaScript
【C#】C# 开发环境配置(Rider 一个.NET 跨平台集成开发环境)
【1月更文挑战第26天】【C#】C# 开发环境配置(Rider 一个.NET 跨平台集成开发环境)
|
Java
一文读懂阻塞、非阻塞、同步、异步IO
原文:一文读懂阻塞、非阻塞、同步、异步IO 介绍     在谈及网络IO的时候总避不开阻塞、非阻塞、同步、异步、IO多路复用、select、poll、epoll等这几个词语。在面试的时候也会被经常问到这几个的区别。
6277 156
|
存储 缓存 Cloud Native
阿里云 ClickHouse 企业版首发邀测&云原生 ClickHouse 技术揭秘
云数据库 ClickHouse 企业版是阿里云和 ClickHouse, Inc 战略合作打造的云原生ClickHouse 产品。企业版推出专属 SharedMergeTree 云原生引擎,支持存算分离,Serverless 秒级实时弹性,集群吞吐和查询效率线性扩展及 Lightweight update 实时更新能力。本文将详细揭秘 SharedMergeTree 实现机制,实时弹性扩展实现原理,lightweight update 技术实现原理,同时对企业版和开源版进行详细的性能测试对比。
|
PHP 数据库
PHP的yield使用场景是什么?底层原理是什么?
PHP的yield使用场景是什么?底层原理是什么?
856 0
|
运维 网络协议 Go
Go网络编程:基于TCP的网络服务端与客户端
本文介绍了使用 Go 语言的 `net` 包开发 TCP 网络服务的基础与进阶内容。首先简述了 TCP 协议的基本概念和通信流程,接着详细讲解了服务端与客户端的开发步骤,并提供了简单回显服务的示例代码。同时,文章探讨了服务端并发处理连接的方法,以及粘包/拆包、异常检测、超时控制等进阶技巧。最后通过群聊服务端的实战案例巩固知识点,并总结了 TCP 在高可靠性场景中的优势及 Go 并发模型带来的便利性。