封装树组件

1,856 阅读3分钟

实现功能:

  1. 根据父组件传来树数据渲染出树,默认展开最大一级
  2. 父组件传来的搜索字段若变化,则筛选出符合项构成的树结构
  3. 自定义节点标题要渲染的字段,默认name,(有时候后端返回的数据字段不是name)
  4. 将节点传来的数据记录下来,当节点被点击时返回相关的数据
  5. 根据数据传来的disabled,disableCheckbox等设置节点的状态(禁用,不可点击,显示复选框)

效果图:

  1. 展开所有节点:
  2. 父节自定义搜索条件,传搜索字段
  3. 树数据及点击拿到的数据
    技术栈:hooks+antd3.X

实现要点:

1. 搜索功能
  1. 只返回匹配的节点构成的树结构,不匹配的节点不返回。在筛选过程子节点若匹配则父节点也要返回,因而判断某个节点是否返回的依据是该节点是否匹配及其子节点是否匹配
  2. 字段匹配是根据节点的title字段,这字段可自定义,不一样为name,借助解构赋值的方法,赋值给name字段,方便后续字段读取。eg:[treeNodeTitle]: name
  3. 若父组件传来的筛选值为空字符串,则所有的节点都会匹配成功,此时若不作处理,所有的节点都为红色,不好看。加了判断,将符合项置为空,但展开整棵树。 eg:'name'.includes('')==>true
 useEffect(() => {
    handleSearchData(searchVal)
  }, [searchVal])
  
  // 数据筛选:返回符合的项的树结构,
  // 将符合项记录到数组selectKeysTemp
  const onFilterGdata = useCallback(
    (item, value) => {
      //将含有搜索值项id记录到selectKeysTemp
      //key可能是自定义字段
      function add({ [treeNodeTitle]: name, [treeNodeKey]: key }) {
         name && name.includes(value)  && selectKeysTemp.push(key)
      }
      if ((item.children || []).length) {
        //如果有子级,遍历子级,子级长度代表了是否为可筛选项
        item.children = item.children && item.children.filter((i) => onFilterGdata(i, value))
        add(item)
        return item.children.length
      } else {
        add(item)
        return item[treeNodeTitle] && item[treeNodeTitle].indexOf(value) > -1
      }
    },
    [selectKeysTemp]
  )
  /**
   * 查询数据
   */
  const handleSearchData = useCallback(
    (value) => {
        // 清除前后空格
      const values = typeof value === 'string' && value && value.trim()
      selectKeysTemp = [] //选中项重置
      let filterList = [],
        selectKeys = []
      if (!selectKeys) {
        filterList = TreeData
      } else {
        const dataSource = cloneDeep(TreeData) || [] //深克隆
        filterList = dataSource.filter((item) => onFilterGdata(item, values))
        selectKeys = values ? selectKeysTemp : [] //搜索空字符时重置选中行为空,不然一片红,
      }
      let expandedKeys = getTreeNodeKeys(filterList) //展开项:获取筛选树的节点id
      setSelectKeys(selectKeys) //onFilterGdata递归结束后重置选中行到useState。
      setDataSource(filterList) //重置树
      setExpandedRowKeys(expandedKeys) //展开项
    },
    [TreeData, selectKeysTemp]
  )
  
  // 获取tree的所有节点key
  const getTreeNodeKeys = useCallback((tree) => {
    let parentKey = []
    tree.map((node) => {
      if (node.children) {
        //如果有子节点,存入此id和子节点的id
        parentKey = [...parentKey, ...getTreeNodeKeys(node.children), `${node[treeNodeKey]}`]
      }
    })
    return parentKey
  }, [])
  
2.存储节点的信息

这里用cusData变量存着数据,在点击的时候从cusData拿

        <TreeNode
            cusData={item}
            key={item[treeNodeKey]}
            title={item[treeNodeTitle]}
            disabled={item.disabled}
            disableCheckbox={item.disableCheckbox}
            checkable={item.checkable}
        >
          
   //节点点击事件,传item:{name:标题,pos:位置,key,及节点传来的其他属性}
  const onSelect = useCallback((key, event) => {
    getTreeData &&
      getTreeData({
        key: key[0],
        ...event.node.props.cusData,
        name: event.node.props.title,
        pos: event.node.props.pos,//位置
      })
  })Ï
          

完整代码

import './index.less'
import { Tree } from 'antd'
const { TreeNode } = Tree
import PropTypes from 'prop-types'
import cloneDeep from 'lodash/cloneDeep'
import React, { useState, useCallback, useEffect } from 'react'
const treePage = (props) => {
  const { TreeData = [], getTreeData, searchVal,  treeNodeTitle, treeNodeKey } = props
  const [dataSource, setDataSource] = useState([]) // 列表数据源
  const [selectKeys, setSelectKeys] = useState([]) // 选中项
  const [expandedRowKeys, setExpandedRowKeys] = useState([]) // 展开的节点
  let selectKeysTemp = [] //筛选符合项id
  useEffect(() => {
    setDataSource(TreeData)
    setExpandedRowKeys(TreeData[0] && [TreeData[0][treeNodeKey]]) //默认展开父级
  }, [TreeData])
  useEffect(() => {
    handleSearchData(searchVal)
  }, [searchVal])

  //节点点击事件,传item:{name:标题,pos:位置,key,及节点传来的其他属性}
  const onSelect = useCallback((key, event) => {
    getTreeData &&
      getTreeData({
        key: key[0],
        ...event.node.props.cusData,
        name: event.node.props.title,
        pos: event.node.props.pos,//位置
      })
  })
  // 数据筛选:返回符合的项的树结构,
  // 将符合项记录到数组selectKeysTemp
  const onFilterGdata = useCallback(
    (item, value) => {
      //将含有搜索值项id记录到selectKeysTemp
      //key可能是自定义字段
      function add({ [treeNodeTitle]: name, [treeNodeKey]: key }) {
         name && name.includes(value)  && selectKeysTemp.push(key)
      }
      if ((item.children || []).length) {
        //如果有子级,遍历子级,子级长度代表了是否为可筛选项
        item.children = item.children && item.children.filter((i) => onFilterGdata(i, value))
        add(item)
        return item.children.length
      } else {
        add(item)
        return item[treeNodeTitle] && item[treeNodeTitle].indexOf(value) > -1
      }
    },
    [selectKeysTemp]
  )
  /**
   * 查询数据
   */
  const handleSearchData = useCallback(
    (value) => {
        // 清除前后空格
      const values = typeof value === 'string' && value && value.trim()
      selectKeysTemp = [] //选中项重置
      let filterList = [],
        selectKeys = []
      if (!selectKeys) {
        filterList = TreeData
      } else {
        const dataSource = cloneDeep(TreeData) || [] //深克隆
        filterList = dataSource.filter((item) => onFilterGdata(item, values))
        selectKeys = values ? selectKeysTemp : [] //搜索空字符时重置选中行为空,不然一片红,
      }
      let expandedKeys = getTreeNodeKeys(filterList) //展开项:获取筛选树的节点id
      setSelectKeys(selectKeys) //onFilterGdata递归结束后重置选中行到useState。
      setDataSource(filterList) //重置树
      setExpandedRowKeys(expandedKeys) //展开项
    },
    [TreeData, selectKeysTemp]
  )
  
  // 获取tree的所有节点key
  const getTreeNodeKeys = useCallback((tree) => {
    let parentKey = []
    tree.map((node) => {
      if (node.children) {
        //如果有子节点,存入此id和子节点的id
        parentKey = [...parentKey, ...getTreeNodeKeys(node.children), `${node[treeNodeKey]}`]
      }
    })
    return parentKey
  }, [])
  
  //选中项加标识
  const filterTreeNode = useCallback(
    (node) => {
      return selectKeys.includes(node.props.eventKey)
    },
    [selectKeys]
  )

  //展开项
  const onExpand = useCallback((expandedKeys, { expanded, node }) => {
    setExpandedRowKeys(expandedKeys) //展开的节点
  }, [])
  const loop = useCallback((data) => {
    return data.map((item) => {
      if (item.children && item.children.length) {
        return (
          <TreeNode
            cusData={item}
            key={item[treeNodeKey]}
            title={item[treeNodeTitle]}
            disabled={item.disabled}
            disableCheckbox={item.disableCheckbox}
            checkable={item.checkable}
          >
            {loop(item.children)}
          </TreeNode>
        )
      }
      return (
        <TreeNode
          cusData={item}
          title={item[treeNodeTitle]}
          key={item[treeNodeKey]}
          disabled={item.disabled} //禁掉响应,默认是false
          disableCheckbox={item.disableCheckbox} //禁掉 checkbox,默认false
          checkable={item.checkable} //当树为 checkable 时,设置独立节点是否展示 Checkbox,默认true
        />
      )
    })
  }, [])

  return (
    <div className="tree_page" style={{ height: 'calc(100vh - 155px)', ...props.style }}>
      <Tree
        onSelect={onSelect}
        onExpand={onExpand}
        // defaultExpandAll={true} //仅在组件第一次渲染时有效,异步获取数据后需要使用非受控方式展开全部结点
        expandedKeys={expandedRowKeys}
        filterTreeNode={filterTreeNode}
        {...props}
      >
        {loop(dataSource)}
      </Tree>
    </div>
  )
}
treePage.propTypes = {
  TreeData: PropTypes.array, // 树节点配置源,必传参数
  getTreeData: PropTypes.func, // 获取树节点数据回调,非必传
  style: PropTypes.object, //树节点样式
  searchVal: PropTypes.string, //搜索字段
}

treePage.defaultProps = {
  TreeData: [], // 树节点配置源
  getTreeData: () => {}, // 获取树节点数据回调
  style: {}, //树节点样式
  searchVal: '', //搜索字段
  treeNodeTitle: 'name', //指定树节点标题字段,默认name
  treeNodeKey: 'id', //指定树节点key属性字段,默认id
}

export default React.memo(treePage)