react實現帶葉子的可搜索樹形結構

話很少說,先上效果圖

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 },
        ],
      },
    ],
  },
];
複製代碼
相關文章
相關標籤/搜索