npm package: @rc-utils/mc-tree
Description: ## 安装
Latest version: 1.0.1-rc.2

# @rc-utils/mc-tree

## 安装

使用 npm:

```bash
$ npm i -g npm
$ npm i @rc-utils/mc-tree
```

## 使用方法

> 直接引用 `import console from '@rc-utils/mc-tree'`

```tsx
import Tree, { TreeActionType, TreeProps } from '@rc-utils/mc-tree'
import console from '@rc-utils/consoles'
import {
  Button,
  DotLoading,
  SearchBar,
  Space,
  Toast,
  Checkbox,
  Radio,
} from 'antd-mobile'
import { TeamOutline, UserOutline } from 'antd-mobile-icons'
import React, { useEffect, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
import './demo.less'
import {
  initTreeData,
  mockRequest,
  mockSearchRequest,
  updateTreeData,
} from './utils'

const App = () => {
  const actionRef = useRef<TreeActionType>()
  const [expandedKeys, setExpandedKeys] = useState<TreeProps['expandedKeys']>(
    []
  )
  const [checkedKeys, setCheckedKeys] = useState<TreeProps['checkedKeys']>([
    'node_0',
  ])
  const [selectedKeys, setSelectedKeys] = useState<TreeProps['selectedKeys']>([
    'node_0',
  ])
  const [treeData, setTreeData] = useState(initTreeData)
  const [loading, setLoading] = useState<boolean>(false)
  const [checkedAll, setCheckedAll] = useState<boolean>(false)
  const ref = useRef<HTMLDivElement>()

  const handlerReloadNode = () => {
    actionRef.current?.reloadNode()
  }

  const handlerGoBackNode = () => {
    actionRef.current?.goBackNode()
  }

  const handlerGoBackPointNode = () => {
    actionRef.current?.goBackPointNode('node_0')
  }

  const handlerCheckAllNode = () => {
    console.log('🚀🚀 ~ handlerCheckAllNode ~ expandedKeys', expandedKeys)
    actionRef.current?.checkedAll(!checkedAll)
    setCheckedAll(!checkedAll)
  }

  const handlerEmptySelectedNode = () => {
    console.log('🚀🚀 ~ handlerEmptySelectedNode')
    setCheckedKeys([])
    setSelectedKeys([])
  }

  const handlerShowNodePath = () => {
    console.log('🚀🚀 ~ handlerShowNodePath ~ expandedKeys', expandedKeys)
    const entities = actionRef.current?.getKeyEntities(expandedKeys)
    console.log('🚀🚀 ~ handlerShowNodePath ~ entities', entities)
  }

  const handlerScrollToTop = () => {
    actionRef.current?.scrollTo(0)
  }

  const onExpand: TreeProps['onExpand'] = (expandedKeysValue, info) => {
    console.log('🚀🚀 ~ onExpand expandedKeysValue', expandedKeysValue)
    console.log('🚀🚀 ~ onExpand info', info)
    setExpandedKeys(expandedKeysValue)
  }

  const onSelect: TreeProps['onSelect'] = (checkedKeysValue, info) => {
    console.log('🚀🚀 ~ info', info)
    console.log('🚀🚀 ~ checked', checkedKeysValue)
    setSelectedKeys(checkedKeysValue)
  }

  const onCheck: TreeProps['onCheck'] = (checkedKeysValue, info) => {
    console.log('🚀🚀 ~ info', info)
    console.log('🚀🚀 ~ checked', checkedKeysValue)
    setCheckedKeys(checkedKeysValue)
  }

  const onCheckAll: TreeProps['onCheckAll'] = (checkedKeysValue, info) => {
    console.log('🚀🚀 ~ info', info)
    console.log('🚀🚀 ~ onCheckAll', checkedKeysValue)
    setCheckedKeys(checkedKeysValue)
  }

  const onBack: TreeProps['onBack'] = (expandedKeysValue, info) => {
    console.log('🚀🚀 ~ onBack expandedKeysValue', expandedKeysValue)
    console.log('🚀🚀 ~ onBack info', info)
    setExpandedKeys(expandedKeysValue)
  }

  useEffect(() => {
    console.log('🚀🚀 ~ checkedKeys', checkedKeys)
  }, [checkedKeys])

  return (
    <div className="demo-wrapper">
      <div className="demo-searchbar">
        <SearchBar
          placeholder="请输入内容"
          defaultValue="node_0_0"
          onSearch={async (keyword) => {
            setLoading(true)
            if (keyword) {
              const result = await mockSearchRequest(keyword)
              console.log('🚀🚀 ~ result', result)
              setTreeData(result)
            } else {
              setTreeData(initTreeData)
            }
            setLoading(false)
          }}
        />
      </div>
      <div className="demo-toolbar">
        <Space wrap>
          <Button color="primary" onClick={() => handlerReloadNode()}>
            重置节点
          </Button>
          <Button color="primary" onClick={() => handlerGoBackNode()}>
            返回节点
          </Button>
          <Button color="primary" onClick={() => handlerGoBackPointNode()}>
            返回指定节点
          </Button>
          <Button color="primary" onClick={() => handlerCheckAllNode()}>
            {checkedAll ? '全不选' : '全选'}
          </Button>
          <Button color="primary" onClick={() => handlerEmptySelectedNode()}>
            清空全部选中
          </Button>
          <Button color="primary" onClick={() => handlerShowNodePath()}>
            节点路径
          </Button>
          <Button color="primary" onClick={() => handlerScrollToTop()}>
            回到顶部
          </Button>
        </Space>
      </div>
      <div
        className="demo-toolbar"
        style={{
          display: loading ? 'block' : 'none',
        }}
      >
        <DotLoading color="primary" /> 数据加载中
      </div>
      <div className="demo-tree" ref={ref}>
        <Tree
          treeData={treeData}
          loadData={async (node, props) => {
            setLoading(true)
            const children = await mockRequest(node)
            // const newTreeData = [...treeData, ...children]
            // setTreeData(newTreeData)
            setTreeData((origin) => updateTreeData(origin, node.key, children))
            setLoading(false)
          }}
          actionRef={actionRef}
          // virtualRoot={{
          //   key: 'root',
          //   title: '根节点',
          //   icon: 'Ico',
          //   switcherIcon: '下级',
          // }}
          expandAction="click"
          onlySelectLeaf
          // showLeafIcon={false}
          expandedKeys={expandedKeys}
          checkedKeys={checkedKeys}
          // multiple={false}
          selectedKeys={selectedKeys}
          // checkStrictly={false}
          // maxCount={2}
          icon={(props) => {
            if (props.isLeaf) {
              return <UserOutline />
            }
            return <TeamOutline />
          }}
          switcherIcon={() => {
            return '下级'
          }}
          onOverMaxCount={(maxCount, info) => {
            console.log('🚀🚀 ~ App ~ info', info)
            Toast.show(`${maxCount}`)
          }}
          onExpand={onExpand}
          onSelect={onSelect}
          onCheck={onCheck}
          onCheckAll={onCheckAll}
          onBack={onBack}
          style={{
            height: '100%',
          }}
          renderCheckbox={(info) => {
            return (
              <Checkbox
                checked={info.checked}
                value={info.value}
                disabled={info.disabled}
                indeterminate={info.halfChecked}
              />
            )
          }}
          renderRadio={(info) => {
            return (
              <Radio
                checked={info.checked}
                value={info.value}
                disabled={info.disabled}
              />
            )
          }}
        />
      </div>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
```

> > demo.less 文件

```less
html,
body,
#root {
  height: 100%;
  padding: 0;
  margin: 0;
}

.demo-wrapper {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100vh;

  .demo-searchbar {
    flex: 0 0 auto;
    padding: 10px;
  }

  .demo-toolbar {
    flex: 0 0 auto;
    padding: 10px;
  }

  .demo-tree {
    position: relative;
    flex: 1 1 auto;
    overflow: auto;
    padding: 10px;
    font-size: 18px;
  }
}

::-webkit-scrollbar-track-piece {
  background-color: transparent;
}

::-webkit-scrollbar {
  width: 3px;
  height: 10px;
}

::-webkit-scrollbar-thumb {
  height: 50px;
  background-color: #b8b8b8;
  border-radius: 6px;
  outline: 2px solid #fff;
  outline-offset: -2px;
  filter: alpha(opacity=50);
  -moz-opacity: 0.5;
  -khtml-opacity: 0.5;
  opacity: 0.5;
}

::-webkit-scrollbar-thumb:hover {
  height: 50px;
  background-color: #878987;
  border-radius: 6px;
}

* {
  scrollbar-color: #b8b8b8 transparent;
  scrollbar-width: thin;
}
```

> > utils.ts

```tsx
import { TreeDataNode, TreeEventDataNode, TreeNodeKey } from '@rc-utils/mc-tree'
// import { httpRequest } from "../../demos/utils";

const TREE_LENGTH = 20
const TREE_LEVEL = 4

const sleep = (time: number) =>
  new Promise((resolve) => setTimeout(resolve, time))

const genTreeNode = (
  index: number,
  length: number,
  level: number,
  parentId: TreeNodeKey
) => {
  const itemId = `${parentId ?? 'node'}_${index}`
  let item: TreeDataNode = {
    key: itemId,
    title: `文件_${itemId}`,
    level,
    parentId,
    description: `描述内容`,
    // icon: 'Ico',
    // switcherIcon: '下级',
  }
  if (level !== 0 && index < length / 3) {
    // item.children = genStaticTreeData(length, itemId, level - 1);
    // item.selectable = false;
    // item.disabled = true;
    // item.checkable = false;
    // item.disableCheckbox = true;
    item.title = `文件夹_${itemId}(${level ?? 0} Level)`
  } else {
    item.disabled = index === 6
    item.isLeaf = true
  }
  return item
}

// It's just a simple demo. You can use tree map to optimize update perf.
export const updateTreeData = (
  list: TreeDataNode[],
  key: TreeNodeKey,
  children: TreeDataNode[]
): TreeDataNode[] =>
  list.map((node) => {
    if (node.key === key) {
      return {
        ...node,
        children,
      }
    }
    if (node.children) {
      return {
        ...node,
        children: updateTreeData(node.children, key, children),
      }
    }
    return node
  })

const genStaticTreeData = (
  length: number,
  level = 2,
  parentId?: TreeNodeKey
) => {
  return Array.from({ length }, (val, index) => {
    const item = genTreeNode(index, length, level, parentId)
    if (!item.isLeaf) {
      item.children = genStaticTreeData(length, level - 1, item.key)
    }
    return item
  })
}

const genLazyTreeData = (length: number, level = 2, parentId?: TreeNodeKey) => {
  return Array.from({ length }, (val, index) => {
    const item = genTreeNode(index, length, level, parentId)
    return item
  })
}

export const treeData = genStaticTreeData(TREE_LENGTH, TREE_LEVEL)
export const initTreeData = genLazyTreeData(TREE_LENGTH, TREE_LEVEL)

const buildTreeSelectedNodes = (keys: TreeNodeKey[]): TreeDataNode[] => {
  console.log('🚀🚀 ~ file: utils.ts ~ line 109 ~ keys', keys)
  return keys.map((key) => {
    const parentKey = `${key}`.match(/(\S*)_/)[1]
    return {
      key: key,
      title: `默认选中节点${key}`,
      level: 2,
      parentKey,
      isLeaf: true,
    }
  })
}

export const mockRequest = async (
  treeNode: TreeEventDataNode
): Promise<TreeDataNode[] | null | undefined> => {
  await sleep(1000)
  console.group('🚀🚀 Mock Request')
  console.log('🚀🚀 ~ file: utils.ts ~ line 105 ~ treeNode', treeNode)
  // console.log("🚀🚀 ~ file: utils.ts ~ line 105 ~ props", props)
  let childrenData: TreeDataNode[] | null = null
  const level = treeNode?.level ?? 0
  if (level > 0) {
    childrenData = genLazyTreeData(
      Math.round(Math.random() * 100 + 1),
      level - 1,
      treeNode.key
    )
  }
  console.log('🚀🚀 ~ childrenData', childrenData)
  console.groupEnd()
  return Promise.resolve(childrenData)
}

export const mockNodesRequest = async (
  keys: TreeNodeKey[]
): Promise<TreeDataNode[] | null | undefined> => {
  await sleep(5000)
  console.group('🚀🚀 Mock Nodes Request')
  console.log('🚀🚀 ~ file: utils.ts ~ line 105 ~ keys', keys)
  // console.log("🚀🚀 ~ file: utils.ts ~ line 105 ~ props", props)
  let nodeItems: TreeDataNode[] = buildTreeSelectedNodes(keys)
  console.groupEnd()
  return Promise.resolve(nodeItems)
}

// 字符串包含
const isInclude = (str: string, matchStr: string) => {
  if (matchStr) {
    return str.toLowerCase().indexOf(matchStr.toLowerCase()) >= 0
  }
  return true
}

const findTreeNodeByKeyWord = (treeNodes: TreeDataNode[], keyWord: string) => {
  const nodes: TreeDataNode[] = []
  if (treeNodes) {
    treeNodes.forEach(({ children, ...treeNode }) => {
      if (isInclude(treeNode.title as string, keyWord)) {
        nodes.push(treeNode)
      }
      if (children) {
        const childNodes = findTreeNodeByKeyWord(children, keyWord)
        nodes.push(...childNodes)
      }
    })
  }

  return nodes
}

export const mockSearchRequest = async (keyWord: string) => {
  let tree = genStaticTreeData(TREE_LENGTH, TREE_LEVEL)
  const searchNodes = findTreeNodeByKeyWord(tree, keyWord)

  return searchNodes
}
```