Concis组件库封装——TreeView树形控件

简介: Concis组件库封装——TreeView树形控件组件封装

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

上一篇文章实现了Tree选择器,本文将介绍TreeView树形控件的实现。

其实在笔者进行TreeData的配置项,原本一直在纠结于树结构的递归,进行封装,但仔细想想,其实可以把他改造成一个双向链表,每个节点在children的基础上(子节点),加入prev(父节点),并且当前节点和prev指向同一个内存地址,进行更快速的中间查询和修改,所以树形控件其实是设计了一个链表的数据结构,如图:
在这里插入图片描述
在这里插入图片描述
在组件内部进行二次封装Tree结构,变成了这样:
在这里插入图片描述
对于二次封装,多了prev这一部分,具体的实现如图所示:
在这里插入图片描述

TreeView的切换菜单、递归渲染所有层级和Tree选择器的实现是一样的,本文就不再介绍,但是TreeView实际上是多选,而TreeSelect是进行单层选择,并且与其他层节点没有关联;而TreeView在特定情况,如:三个儿子节点都选中,父节点也被选中,多了这一块的交互,具体的业务判断如图:
在这里插入图片描述
链表对于这部分处理有了很大的帮助。

对于拖拽,主要用到的是js的drag和drop两个事件,记录拖拽和目标节点,对TreeData进行整体处理,代码如图:
在这里插入图片描述
上图,是获取拖拽节点并处理TreeData的流程。
在这里插入图片描述
上图是基于拖拽节点,查找到释放节点,并将拖拽节点插入到TreeData的流程。

组件源码如下:

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

type treeViewProps = {
   
   
  /**
   * @description Tree配置参数
   */
  treeData: Array<treeData>;
  /**
   * @description 默认展开
   * @default false
   */
  defaultOpen?: boolean;
  /**
   * @description 禁用
   * @default false
   */
  disabled?: boolean;
  /**
   * @description 可拖拽
   * @default false
   */
  avaDrop?: boolean;
  /**
   * @description 选中回调函数
   */
  checkCallback?: Function;
  /**
   * @description 拖拽回调函数
   */
  dropCallback?: Function;
};
interface treeData {
   
   
  title: string;
  value: string;
  group: number;
  level: number;
  prev: treeData | null;
  height?: string;
  disabled?: boolean;
  checked: boolean;
  children?: Array<treeData>;
}

const TreeView: FC<treeViewProps> = (props) => {
   
   
  const {
   
    treeData, defaultOpen, avaDrop, checkCallback, dropCallback } = props;

  const [stateTreeData, setStateTreeData] = useState<Array<treeData>>(treeData); //树结构
  const [hoverTreeNode, setHoverTreeNode] = useState(''); //当前覆盖的节点

  useEffect(() => {
   
   
    resolveTreeData(stateTreeData as Array<treeData>, 1, null);
  }, []);

  const resolveTreeData = (
    treeData: Array<treeData>,
    nowIndexLevel: number,
    prev: treeData | null,
  ) => {
   
   
    //二次处理treeData
    const newTreeData = [...treeData];
    newTreeData.forEach((treeNode: treeData, index) => {
   
   
      treeNode.level = nowIndexLevel;
      if (defaultOpen) {
   
   
        //默认全展开
        treeNode.height = '30px';
      } else {
   
   
        treeNode.height = treeNode.level == 1 ? '30px' : '0';
      }
      treeNode.checked = false;

      treeNode.prev = prev;
      if (treeNode?.children?.length) {
   
   
        //有子节点
        resolveTreeData(treeNode.children, nowIndexLevel + 1, treeNode);
      } else {
   
   
        //没有子节点,重置level为当前层级,继续寻找
        nowIndexLevel = treeNode.level;
      }
    });
    setStateTreeData(newTreeData); //更新状态
  };
  const toggleTreeMenu = (clickTreeNode: treeData) => {
   
   
    //菜单切换或直接选中终极节点
    if (clickTreeNode?.children?.length) {
   
   
      //菜单切换的情况
      const oldStateTree = [...stateTreeData];
      const editTreeNode = (treeNode: Array<treeData>) => {
   
   
        //所选节点后代收起处理函数
        treeNode.forEach((child) => {
   
   
          //找到节点,对子节点进行处理
          if (child?.children?.length) {
   
   
            child.height = '0';
            editTreeNode(child.children);
          } else {
   
   
            child.height = '0';
          }
        });
      };
      const mapFn = (treeNode: Array<treeData>) => {
   
   
        //深度优先查找节点函数
        treeNode.forEach((t: treeData, i: number) => {
   
   
          if (t.title == clickTreeNode.title && t.value == clickTreeNode.value) {
   
   
            if (t?.children?.length) {
   
   
              //后代节点处理,如果打开,只需打开下一代即可,如果关闭,需要关闭所有后代
              if (t.children[0].height == '0') {
   
   
                //打开
                t.children = t.children.map((child: treeData) => {
   
   
                  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 {
   
   
    }
  };
  const checkTreeNode = (clickTreeNode: treeData) => {
   
   
    //选中节点
    if (clickTreeNode.disabled) {
   
   
      return;
    }
    const oldStateTree = [...stateTreeData];
    const editTreeNode = (treeNode: Array<treeData>, status: boolean) => {
   
   
      //所选节点后代处理函数
      treeNode.forEach((child) => {
   
   
        //找到节点,对子节点进行处理
        if (child?.children?.length) {
   
   
          child.checked = status;
          editTreeNode(child.children, status);
        } else {
   
   
          child.checked = status;
        }
      });
    };
    const mapFn = (treeNode: Array<treeData>, prevNode: treeData | null) => {
   
   
      //当前节点/上一代节点/爷爷节点
      //深度优先查找节点函数
      treeNode.forEach((t: treeData, i: number) => {
   
   
        if (t.title == clickTreeNode.title && t.value == clickTreeNode.value) {
   
   
          t.checked = !t.checked;
          //前代链表节点的处理
          if (prevNode) {
   
   
            //如果链表有上层节点
            let nowTreeList = treeNode; //当前层链表所有节点
            while (prevNode !== null) {
   
   
              //链表到起始点,结束,从后往前查找
              if (nowTreeList.every((c) => c.checked)) {
   
   
                //当前层全部选中才改变上层链表
                prevNode.checked = true;
                nowTreeList.map((c) => (c.prev = prevNode));
                nowTreeList = prevNode.children as Array<treeData>;
                prevNode = prevNode.prev as treeData;
              } else {
   
   
                break;
              }
            }
          }
          //后代链表节点的处理
          if (t?.children?.length) {
   
   
            editTreeNode(t.children, t.checked); //对后代节点进行处理
          }
        } else if (t?.children?.length) {
   
   
          //递归继续遍历,直到找到所选节点
          mapFn(t.children, t);
        }
      });
    };
    mapFn(oldStateTree, null);
    setStateTreeData(oldStateTree);
    checkCallback && checkCallback(oldStateTree);
  };
  const checkBoxRender = useCallback(
    (treeData: treeData) => {
   
   
      //根据index对指定数据进行查找
      if (treeData.disabled) {
   
   
        return <div className="disblaed-checkBox"></div>;
      }
      if (!treeData?.children?.length) {
   
   
        //无子节点
        if (treeData.checked) {
   
   
          //选中
          return (
            <div className="checkBox-actived" onClick={
   
   () => checkTreeNode(treeData)}>
              <CheckOutlined />
            </div>
          );
        } else {
   
   
          //未选中
          return <div className="checkBox-noActived" onClick={
   
   () => checkTreeNode(treeData)}></div>;
        }
      } else if (treeData.children && treeData.children.length) {
   
   
        //有子节点
        let treeList: Array<number> = []; //0 -> 子节点未选中,1->  子节点选中
        const mapFn = (treeNode: treeData): any => {
   
   
          for (let i = 0; i < (treeNode.children as Array<treeData>).length; i++) {
   
   
            const child: treeData = (treeNode.children as Array<treeData>)[i];
            treeList.push(child.checked ? 1 : 0);
            if (child.children && child.children.length) {
   
   
              //还有后代
              return mapFn(child);
            } else {
   
   
              //到终点,无子节点
              if (i == (treeNode.children as Array<treeData>).length - 1) {
   
   
                if (treeList.every((c) => c == 0)) {
   
   
                  //全都没选中
                  return (
                    <div
                      className="checkBox-noActived"
                      onClick={
   
   () => checkTreeNode(treeData)}
                    ></div>
                  );
                } else if (treeList.every((c) => c == 1)) {
   
   
                  //全都选中
                  return (
                    <div className="checkBox-actived" onClick={
   
   () => checkTreeNode(treeData)}>
                      <CheckOutlined />
                    </div>
                  );
                } else {
   
   
                  //部分选中
                  return (
                    <div className="checkBox-activedLess" onClick={
   
   () => checkTreeNode(treeData)}>
                      <div className="less-box"></div>
                    </div>
                  );
                }
              }
            }
          }
        };
        return mapFn(treeData);
      }
    },
    [stateTreeData],
  );

  const dragStartTree = (e: any, treeData: treeData) => {
   
   
    //开始拖拽
    if (!avaDrop) return;
    if (stateTreeData.length == 1 && treeData.level == 1) {
   
   
      const oldTree = [...stateTreeData];
      const mapTree = (tree: treeData) => {
   
   
        if (tree.level !== 1) {
   
   
          tree.height = '0';
        }
        if (tree?.children?.length) {
   
   
          tree.children.forEach((c) => {
   
   
            c.height = '0';
            if (c?.children?.length) {
   
   
              c.children.forEach((child) => {
   
   
                mapTree(child);
              });
            }
          });
        }
      };
      mapTree(oldTree[0]);
      setStateTreeData(oldTree);
    } else {
   
   
      e.nativeEvent.dataTransfer.setData('dargTree', treeData.title);
    }
  };
  const dropOver = (e: any, treeNode: treeData) => {
   
   
    //拖拽完成
    if (!avaDrop) return;
    e.nativeEvent.preventDefault();
    if (treeNode.title && treeNode.title !== hoverTreeNode) {
   
   
      setHoverTreeNode(treeNode.title);
      const oldTree = [...stateTreeData];
      const mapFn = (tree: treeData) => {
   
   
        tree?.children?.forEach((c) => {
   
   
          if (c.title == treeNode.title) {
   
   
            c.height = '30px';
            c?.children?.forEach((childNode) => {
   
   
              childNode.height = '30px';
            });
          } else if (c?.children?.length) {
   
   
            mapFn(c);
          }
        });
      };
      oldTree.forEach((c) => {
   
   
        mapFn(c);
      });
      setStateTreeData(oldTree);
    }
  };
  const drop = (e: any, treeNode: treeData) => {
   
   
    //拖拽完成,进行更新
    if (!avaDrop) return;
    e.nativeEvent.preventDefault();
    var dragTreeNode = e.nativeEvent.dataTransfer.getData('dargTree'); //被拖拽的树节点
    if (!dragTreeNode) {
   
   
      return;
    }
    const oldStateTree = [...stateTreeData];
    const findDragNode = (treeList: treeData) => {
   
   
      //寻找拖拽节点在链表中的位置
      if (treeList.title == dragTreeNode && treeNode !== treeList) {
   
   
        dragTreeNode = treeList;
        if (treeList.level == 1) {
   
   
          oldStateTree.splice(treeList.group, 1);
        } else {
   
   
          treeList?.children?.splice(0, 1);
        }
        if (treeList?.children?.length == 0) {
   
   
          delete treeList.children;
        }
        return;
      }
      if (treeList?.children?.length) {
   
   
        treeList.children.forEach((c, i) => {
   
   
          if (c.title == dragTreeNode) {
   
   
            dragTreeNode = c;
            treeList?.children?.splice(i, 1);
            if (treeList?.children?.length == 0) {
   
   
              delete treeList.children;
            }
          }
          if (c.children) {
   
   
            findDragNode(c);
          }
        });
      }
    };
    oldStateTree.forEach((c) => {
   
   
      findDragNode(c);
    });

    const mapFn = (treeList: treeData) => {
   
   
      //寻找放置节点在链表中的位置
      if (treeList.title == treeNode.title) {
   
   
        dragTreeNode.level = treeList.level + 1;
        dragTreeNode.prev = treeList;
        if (treeList.children) {
   
   
          treeList.children.splice(0, 0, dragTreeNode);
        } else {
   
   
          treeList.children = [dragTreeNode];
        }
        return;
      } else if (treeList?.children?.length) {
   
   
        treeList.children.forEach((child: treeData, index) => {
   
   
          if (child.title == treeNode.title) {
   
   
            dragTreeNode.level = child.level;
            dragTreeNode.prev = treeList;
            if (treeList.children) {
   
   
              treeList.children.splice(index + 1, 0, dragTreeNode);
              if (treeList.children[index + 1].children) {
   
   
                treeList.children[index + 1].children = (
                  treeList?.children[index + 1]?.children as Array<treeData>
                ).map((c) => {
   
   
                  return {
   
   
                    ...c,
                    level: (treeList?.children as Array<treeData>)[index + 1].level + 1,
                  };
                });
              }
            } else {
   
   
              treeList.children = [dragTreeNode];
            }
          } else if (child?.children?.length) {
   
   
            mapFn(child);
          }
        });
      }
    };
    if (typeof dragTreeNode == 'object')
      oldStateTree.forEach((c) => {
   
   
        mapFn(c);
      });
    if (dragTreeNode.group == treeNode.group && dragTreeNode.level < treeNode.level) {
   
   
      //如果拖拽的层级比落地的层级小,不做更新
      return;
    }
    setStateTreeData(oldStateTree);
    dropCallback && dropCallback(oldStateTree);
  };

  const render = useCallback(
    (data: Array<treeData> = stateTreeData) => {
   
   
      //动态规划render函数
      return data.map((treeNode: treeData, index) => {
   
   
        return (
          <Fragment key={
   
   index}>
            <div
              className="treeNode"
              style={
   
   {
   
   
                marginLeft: `${
     
     treeNode.level * 10}px`,
                height: `${
     
     treeNode.height}`,
              }}
              draggable={
   
   avaDrop}
              onDragStart={
   
   (e) => dragStartTree(e, treeNode)}
              onDrop={
   
   (e) => drop(e, treeNode)}
              onDragOver={
   
   (e) => dropOver(e, treeNode)}
            >
              {
   
   
                treeNode?.children?.length ? (
                  treeNode.children[0].height == '0' ? (
                    <CaretDownOutlined onClick={
   
   () => toggleTreeMenu(treeNode)} />
                  ) : (
                    <CaretRightOutlined onClick={
   
   () => toggleTreeMenu(treeNode)} />
                  )
                ) : (
                  <div style={
   
   {
   
    width: '14px', height: '14px' }}></div>
                ) //空间占位符
              }
              {
   
   checkBoxRender(treeNode)}
              <span
                className="text"
                onClick={
   
   () => toggleTreeMenu(treeNode)}
                style={
   
   treeNode.disabled ? {
   
    color: '#00000040' } : {
   
    color: '#000000' }}
              >
                {
   
   treeNode.title}
              </span>
            </div>
            {
   
   treeNode?.children?.length && render(treeNode.children)}
          </Fragment>
        );
      });
    },
    [stateTreeData],
  );

  return (
    <Fragment>
      <div className="tree-select-dialog">{
   
   render(stateTreeData)}</div>
    </Fragment>
  );
};

export default memo(TreeView);

组件的文档如下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
API:
在这里插入图片描述

如果在线上文档体验满意的话,欢迎npm下载呀,详细教程在github中~~

目录
相关文章
|
6月前
|
缓存 前端开发 JavaScript
前端性能优化:打造流畅的用户体验
前端性能优化:打造流畅的用户体验
|
6月前
|
前端开发
响应式布局实战:告别移动端布局混乱
响应式布局实战:告别移动端布局混乱
149 0
|
6月前
|
缓存 前端开发 JavaScript
《打破微前端困局:样式冲突与资源隔离破局指南》
微前端架构因灵活开发、独立部署等优势,日益受到青睐。然而,其在实际应用中也面临样式冲突与资源隔离等难题。本文深入剖析这些问题的根源与影响,并提供CSS Modules、Shadow DOM、模块加载器等实用解决方案,助力开发者构建稳定高效的微前端系统。
198 0
|
10月前
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
1120 4
YOLOv11改进策略【损失函数篇】| 通过辅助边界框计算IoU提升检测效果(Inner_GIoU、Inner_DIoU、Inner_CIoU、Inner_EIoU、Inner_SIoU)
|
11月前
|
JavaScript 前端开发 数据安全/隐私保护
npm账户需要登录问题npm error probably out of date. To correct this please try logging in again with优雅草央千澈解决方案
npm账户需要登录问题npm error probably out of date. To correct this please try logging in again with优雅草央千澈解决方案
1178 0
npm账户需要登录问题npm error probably out of date. To correct this please try logging in again with优雅草央千澈解决方案
|
JavaScript
JS 你可能没用过的【回调函数式替换】replace()
JS 你可能没用过的【回调函数式替换】replace()
267 0
|
缓存 监控
webpack 提高构建速度的方式
【10月更文挑战第23天】需要根据项目的具体情况和需求,综合运用这些方法,不断进行优化和改进,以达到最佳的构建速度和效果。同时,随着项目的发展和变化,还需要持续关注和调整构建速度的相关措施,以适应不断变化的需求。
|
前端开发
技术经验分享:CSS画三角形(三种方法)
技术经验分享:CSS画三角形(三种方法)
206 0
解决 Blocked a frame with origin “xxx“ from accessing a cross-origin frame
解决 Blocked a frame with origin “xxx“ from accessing a cross-origin frame
5327 0