react实现带叶子的可搜索树形结构

417 阅读3分钟

话不多说,先上效果图

1.png

代码部分

// departmentTreeData为初始化树形数据
// searchVal为搜索框的数据
// onSelectDepartment为选中节点的func

const DepartmentTreeList = ({ departmentTreeData, searchVal, onSelectDepartment }) => {
  const [flattenData, setFlattenData] = useState([]);
  const [filterItems, setFilterItems] = useState([]);
  const [expandItems, setExpandItems] = useState([]);

  // 展平数据
  const flatten = (data) => {
    if (data.length) {
      return data.reduce(
        (arr, { id, name, parentId, children = [] }) =>
          arr.concat([{ id, name, parentId, children }], flatten(children)),
        []
      );
    }
    return data;
  };

  // 找到当前元素的index
  const indexInFlattenData = (item) => {
    return flattenData.findIndex((val) => val.id === item.id);
  };

  // 找到包含该expandKey的父节点
  const getParentTree = (item, temp = []) => {
    const parent = flattenData.find((d) => d.id === item.parentId);
    if (parent) {
      temp.push(parent);
      getParentTree(parent, temp);
    }
    return temp;
  };

  // 当前节点是否展开
  const isOpen = (item) => {
    return expandItems.find((option) => option.id === item.id);
  };

  // 点击展开节点
  const openChildren = (item) => {
    // 如果已经open,则从expandItems中移除当前id,反之添加
    if (isOpen(item)) {
      const filterKeys = expandItems.filter((option) => option.id !== item.id);
      setExpandItems([...filterKeys]);
    } else {
      setExpandItems([...expandItems, item]);
    }
  };

  // 该元素是否参与其父元素leafLine的构成
  const isBefore = (key, item) => {
    let flag = true;
    // 为了让key对应parent,此处做一下reverse
    const parent = getParentTree(item).reverse()[key];
    const [lastChild] = parent.children.slice(-1);
    // 找到最后一个child在展开数据中的index与其比较
    // 如果child.index > item.index, 说明该父节点的最后一个子元素在当前item下方,所以要加上leafLine
    if (indexInFlattenData(lastChild) > indexInFlattenData(item)) {
      flag = false;
    }
    return flag;
  };

  // 渲染leafLine
  const renderLeafLine = (index, item) => {
    // index表示要在此元素前方插入多少个占位span
    const data = [...new Array(index - 1).keys()];
    return data.map((key) => (
      <span
        key={key}
        className={classNames(styles.treeIndent, {
          [styles.displayNone]: isBefore(key, item),
        })}
        style={{
          left: `${(key + 1) * 30}px`,
        }}
      />
    ));
  };

  const renderList = (data, index = 0) => {
    // 通过index控制样式
    index += 1;
    return data.map((item) => {
      const hasChildren = item.children && item.children.length;
      const openChildFlag = isOpen(item);
      return (
        <React.Fragment key={item.id}>
          <li
            className={styles.listItem}
            style={{
              paddingLeft: `${(index - 1) * 30}px`,
            }}
            onClick={() => onSelectDepartment(item)}
          >
            {index > 1 && renderLeafLine(index, item)}
            <span className={styles.leafLine} />
            {hasChildren && (
              <span
                className={styles.childIcon}
                onClick={(e) => {
                  e.stopPropagation();
                  openChildren(item);
                }}
              >
                <Icon name={openChildFlag ? 'down' : 'right'} />
              </span>
            )}
            {searchVal && item.name.includes(searchVal) ? (
              <span
                dangerouslySetInnerHTML={{
                  __html: item.name.replace(
                    searchVal,
                    `<span class=${styles.labelKeyword}>${searchVal}</span>`
                  ),
                }}
              />
            ) : (
              <span>{item.name}</span>
            )}
          </li>
          {hasChildren && openChildFlag ? renderList(item.children, index) : null}
        </React.Fragment>
      );
    });
  };

  useEffect(() => {
    const data = flatten(departmentTreeData);
    setFlattenData([...data]);
    // 初始化全部展开
    // setExpandItems([...data]);
  }, [departmentTreeData]);

  useEffect(() => {
    // 找到包括该关键字的选项
    const filterLists = searchVal
      ? flattenData.filter((item) => item.name.includes(searchVal))
      : [];
    setFilterItems([...filterLists]);

    // 找到所有包括该expandKey的父节点
    let result = [];
    filterLists.forEach((items) => {
      const parent = getParentTree(items);
      result.push(...parent);
    });
    setExpandItems([...new Set(result)]);
  }, [searchVal]);

  return (
    <ul className={styles.listBody}>
      {searchVal ? (
        filterItems.length ? (
          <ul className={styles.listBody}>{renderList(departmentTreeData)}</ul>
        ) : (
          <div className={styles.noData}>{i18n.t`暂无数据`}</div>
        )
      ) : (
        <ul className={styles.listBody}>{renderList(departmentTreeData)}</ul>
      )}
    </ul>
  );
};

DepartmentTreeList.defaultProps = {
  departmentTreeData: [],
  searchVal: '',
  onSelectDepartment: () => {},
};

DepartmentTreeList.propTypes = {
  departmentTreeData: PropTypes.array,
  searchVal: PropTypes.string,
  onSelectDepartment: PropTypes.func,
};

export default DepartmentTreeList;

css部分

@import '~@SDVariable'
.list-body
  padding 8px 0 0 8px

  .list-item
    position relative
    padding 12px 0

    .tree-indent
      position absolute
      display inline-block
      width 22px

      &::before
        position absolute
        top -33px
        height 45px
        border-left 1px solid #dddfe3
        content " "

    .display-none
      display none

    .leaf-line
      position relative
      display inline-block
      width 22px
      height 100%

      &::before
        position absolute
        top -49px
        height 44px
        border-left 1px solid n20
        content " "

      &::after
        position absolute
        top -5px
        width 21px
        border-bottom 1px solid n20
        content " "

    .child-icon
      position relative
      z-index 1
      width 16px
      height 16px
      margin-right 8px
      border-radius 50%
      border 1px solid n20
      background n0

    .label-keyword
      color b50

.no-data
  text-align center
  color #9a9fac

对应的数据格式

const optionsData = [
  { id: 1348, name: '司法临时工啊叫', parentId: null },
  {
    id: 10,
    name: '产研部',
    parentId: null,
    children: [
      {
        id: 7,
        name: '研发部',
        parentId: 10,
        children: [
          {
            id: 3,
            name: '自动化测试',
            parentId: 7,
            children: [
              {
                id: 1,
                name: '自动化测试下一级部门',
                parentId: 3,
                children: [
                  {
                    id: 70,
                    name: '部门1',
                    parentId: 1,
                    children: [
                      {
                        id: 82,
                        name: '运营部',
                        parentId: 70,
                        children: [{ id: 83, name: '1', parentId: 82 }],
                      },
                    ],
                  },
                  { id: 71, name: '部门2', parentId: 1 },
                ],
              },
            ],
          },
          {
            id: 31,
            name: '后端小组',
            parentId: 7,
            children: [{ id: 79, name: '仅校招使用', parentId: 31 }],
          },
          {
            id: 73,
            name: '赵正果测试',
            parentId: 7,
            children: [
              { id: 72, name: '部门3', parentId: 73 },
              {
                id: 74,
                name: '部门1-子部门-子部门',
                parentId: 73,
                children: [{ id: 12, name: '产品运营部', parentId: 74 }],
              },
              { id: 78, name: '子部门子部门子部门子部门子部门子部门子部门子部门', parentId: 73 },
            ],
          },
          { id: 75, name: '研发部-其他', parentId: 7 },
          {
            id: 154,
            name: '干活222',
            parentId: 7,
            children: [{ id: 155, name: '干活333', parentId: 154 }],
          },
        ],
      },
      { id: 30, name: '前端开发', parentId: 10 },
      { id: 47, name: '后端开发', parentId: 10 },
      {
        id: 133,
        name: '产品部',
        parentId: 10,
        children: [
          {
            id: 11,
            name: '支付宝产品部',
            parentId: 133,
            children: [{ id: 77, name: '123', parentId: 11 }],
          },
          { id: 134, name: '微信支付', parentId: 133 },
        ],
      },
    ],
  },
];