react實現(xiàn)帶葉子的可搜索樹形結(jié)構(gòu)

話不多說者冤,先上效果圖

1.png

然后是代碼部分:

// departmentTreeData為初始化樹形數(shù)據(jù)
// searchVal為搜索框的數(shù)據(jù)
// onSelectDepartment為選中節(jié)點的func

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

  // 展平數(shù)據(jù)
  const flatten = (data) => {
    if (data.length) {
      return data.reduce(
        (arr, { id, name, parentId, children = [] }) =>
          arr.concat([{ id, name, parentId, children }], flatten(children)),
        []
      );
    }
    return data;
  };

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

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

  // 當(dāng)前節(jié)點是否展開
  const isOpen = (item) => {
    return expandItems.find((option) => option.id === item.id);
  };

  // 點擊展開節(jié)點
  const openChildren = (item) => {
    // 如果已經(jīng)open欠动,則從expandItems中移除當(dāng)前id,反之添加
    if (isOpen(item)) {
      const filterKeys = expandItems.filter((option) => option.id !== item.id);
      setExpandItems([...filterKeys]);
    } else {
      setExpandItems([...expandItems, item]);
    }
  };

  // 該元素是否參與其父元素leafLine的構(gòu)成
  const isBefore = (key, item) => {
    let flag = true;
    // 為了讓key對應(yīng)parent,此處做一下reverse
    const parent = getParentTree(item).reverse()[key];
    const [lastChild] = parent.children.slice(-1);
    // 找到最后一個child在展開數(shù)據(jù)中的index與其比較
    // 如果child.index > item.index, 說明該父節(jié)點的最后一個子元素在當(dāng)前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(() => {
    // 找到包括該關(guān)鍵字的選項
    const filterLists = searchVal
      ? flattenData.filter((item) => item.name.includes(searchVal))
      : [];
    setFilterItems([...filterLists]);

    // 找到所有包括該expandKey的父節(jié)點
    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`暫無數(shù)據(jù)`}</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

對應(yīng)的數(shù)據(jù)格式為:

const optionsData = [
  { id: 1348, name: '司法臨時工啊叫', parentId: null },
  {
    id: 10,
    name: '產(chǎn)研部',
    parentId: null,
    children: [
      {
        id: 7,
        name: '研發(fā)部',
        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: '運(yùn)營部',
                        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: '產(chǎn)品運(yùn)營部', parentId: 74 }],
              },
              { id: 78, name: '子部門子部門子部門子部門子部門子部門子部門子部門', parentId: 73 },
            ],
          },
          { id: 75, name: '研發(fā)部-其他', parentId: 7 },
          {
            id: 154,
            name: '干活222',
            parentId: 7,
            children: [{ id: 155, name: '干活333', parentId: 154 }],
          },
        ],
      },
      { id: 30, name: '前端開發(fā)', parentId: 10 },
      { id: 47, name: '后端開發(fā)', parentId: 10 },
      {
        id: 133,
        name: '產(chǎn)品部',
        parentId: 10,
        children: [
          {
            id: 11,
            name: '支付寶產(chǎn)品部',
            parentId: 133,
            children: [{ id: 77, name: '123', parentId: 11 }],
          },
          { id: 134, name: '微信支付', parentId: 133 },
        ],
      },
    ],
  },
];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辨绊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子塞蹭,更是在濱河造成了極大的恐慌孽江,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件番电,死亡現(xiàn)場離奇詭異岗屏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)漱办,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門这刷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娩井,你說我怎么就攤上這事暇屋。” “怎么了撞牢?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵率碾,是天一觀的道長。 經(jīng)常有香客問我屋彪,道長所宰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任畜挥,我火速辦了婚禮仔粥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己躯泰,他們只是感情好谭羔,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著麦向,像睡著了一般瘟裸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诵竭,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天话告,我揣著相機(jī)與錄音,去河邊找鬼卵慰。 笑死沙郭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裳朋。 我是一名探鬼主播病线,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鲤嫡!你這毒婦竟也來了送挑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤泛范,失蹤者是張志新(化名)和其女友劉穎让虐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罢荡,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赡突,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了区赵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惭缰。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖笼才,靈堂內(nèi)的尸體忽然破棺而出漱受,到底是詐尸還是另有隱情,我是刑警寧澤骡送,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布昂羡,位于F島的核電站,受9級特大地震影響摔踱,放射性物質(zhì)發(fā)生泄漏虐先。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一派敷、第九天 我趴在偏房一處隱蔽的房頂上張望蛹批。 院中可真熱鬧撰洗,春花似錦、人聲如沸腐芍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猪勇。三九已至设褐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泣刹,已是汗流浹背络断。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留项玛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓弱判,卻偏偏與公主長得像襟沮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子昌腰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348