Tree组件的数据处理往往需要使用递归,本文归纳一下常见的数据处理情景,持续更新;

一、Tree数据重置

  • 递归的标志就是寻找子元素的集合字段,一般为children,将所有节点依次过滤,
  • 遍历过程类似于先序遍历,递归得到的返回值重新组成新的children数据,这个过程类似于后序遍历
  • 遍历过程主要是增加必要的属性比如value、key,还可以根据节点数据动态设置icon
  • 注意展开项expandedKeys的收集,根据数据自主控制
import { SmileOutlined } from "@ant-design/icons";
import { Button, Form, Tree } from "antd";
import React, { useMemo } from "react";
const treeDataTest = [
  { name: "0", id: "0", children: [{ name: "1", id: "0-0" }] },
  {
    name: "1",
    id: "1",
    sub: [
      {
        name: "1-0",
        id: "1-0",
        children: [
          { name: "1-0-0", id: "1-0-0" },
          { name: "1-0-1", id: "1-0-1" }
        ]
      },
      {
        name: "2-0",
        id: "2-0",
        sub: [
          {
            name: "2-0-0",
            id: "2-0-0",
            sub: [{ name: "2-0-0-0", id: "2-0-0-0", children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
          }
        ]
      }
    ]
  }
];
export default function TreePage() {
  const [form] = Form.useForm();
  const [expandedKeys, setExpandedKeys] = React.useState([]);
  const labelWarpBtn = {
    offset: 6,
    span: 14
  };
  const onValuesChange = (changedValues, allValues) => {
    console.log("changedValues: ", changedValues);
    console.log("allValues: ", allValues);
  };
  // 转换每一个节点,只区分children、sub属性、icon属性
  const transformData = (data, expandedKeys) => {
    data.forEach((item) => {
      item.title = item.name;
      item.key = item.id;
      if (item.children) {
        expandedKeys.push(item.key);
        item.children = transformData(item.children, expandedKeys);
      } else if (item.sub) {
        item.children = transformData(item.sub, expandedKeys);
      } else {
        item.icon = <SmileOutlined />;
      }
    });
    return data;
  };

  // 单纯过滤数据添加属性
  const treeData = useMemo(() => {
    const expandedKeys = [];
    const data = transformData(treeDataTest, expandedKeys);
    setExpandedKeys(expandedKeys);
    return data;
  }, [treeDataTest]);

  return (
    <div>
      <Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }} onValuesChange={onValuesChange}>
        <Form.Item name="tree" label="tree">
          {/* fieldNames={{ title: "name", key: "id", children: "children" }} */}
          {/* 这里只是初步定义,只能扩充三个字段,想要更灵活的属性,在数据层修改就好了 */}
          {/* 一般处理数据后还要关注expandedKeys等属性 */}
          <Tree treeData={treeData} expandedKeys={expandedKeys} onExpand={setExpandedKeys} showIcon></Tree>
        </Form.Item>
        <Form.Item wrapperCol={labelWarpBtn}>
          <Button type="primary" htmlType="submit" onClick={console.log(form.getFieldsValue())}>
            Submit
          </Button>
        </Form.Item>
      </Form>
    </div>
  );
}


二、Tree拆分成二级数据

1、过滤数据

  • 过滤不存在有效数据的节点,假设num表示该节点下级存在的有效数据的数量,通过num可以进行空数据过滤
  • 递归中遇到报错可以使用debugger查看问题,调用次数太多使用console也无法定位
const treeDataTest = [
  { name: "0", id: "0", children: [{ name: "1", id: "0-0" }] },
  {
    name: "1",
    id: "1",
    sub: [
      {
        name: "1-0",
        id: "1-0",
        children: [
          { name: "1-0-0", id: "1-0-0" },
          { name: "1-0-1", id: "1-0-1" }
        ]
      },
      {
        name: "2-0",
        id: "2-0",
        sub: [
          {
            name: "2-0-0",
            id: "2-0-0",
            sub: [{ name: "2-0-0-0", id: "2-0-0-0", children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
          }
        ]
      }
    ]
  }
];
  // 过滤数据,只展示存在有效数据的节点
  const filterData = (data) => {
    const filter = (arr) => {
      return arr.filter((item) => {
        if (item.num > 0) {
          // debugger;
          if (item.sub?.length > 0) {
            item.sub = filter(item.sub);
          }
          return true;
        }
        return false;
      });
    };
    return filter(JSON.parse(JSON.stringify(data)));
  };

2、二级数据

  • 当需要拆分成两级时,需要把中间层级省略,保留末端children数据(假设有效数据都保存在children中)
  • 设定目标数据的层级为两级,就可以遍历最外层,而内层递归,逐个往数组里添加末端children数据
import { SmileOutlined } from "@ant-design/icons";
import { Button, Form, Tree } from "antd";
import React, { useCallback, useMemo } from "react";
const treeDataTest = [
  { name: "0", id: "0", num: 1, children: [{ name: "1", id: "0-0" }], sub: [] },
  {
    name: "1",
    id: "1",
    num: 3,
    sub: [
      {
        name: "1-0",
        id: "1-0",
        num: 2,
        children: [
          { name: "1-0-0", id: "1-0-0" },
          { name: "1-0-1", id: "1-0-1" }
        ]
      },
      {
        name: "2-0",
        id: "2-0",
        num: 1,
        sub: [
          {
            name: "2-0-0",
            id: "2-0-0",
            num: 1,
            sub: [{ name: "2-0-0-0", id: "2-0-0-0", num: 1, children: [{ name: "2-0-0-0-0", id: "2-0-0-0-0" }] }]
          }
        ]
      }
    ]
  },
  {
    name: "2",
    id: "2",
    num: 0,
    sub: [{ name: "2-0", id: "2-0", num: 0, sub: [{ name: "2-0-0", id: "2-0-0", num: 0 }] }]
  }
];
export default function TreePage() {
  const [form] = Form.useForm();
  const [expandedKeys, setExpandedKeys] = React.useState([]);
  const labelWarpBtn = {
    offset: 6,
    span: 14
  };
  const onValuesChange = (changedValues, allValues) => {
    console.log("changedValues: ", changedValues);
    console.log("allValues: ", allValues);
  };

  // 转换为二级树结构,方便展示数据
  const transformChildrenOnly = (data) => {
    data.forEach((item) => {
      item.title = item.name;
      item.key = item.id;
      item.icon = <SmileOutlined />;
    });
    return data;
  };

  const transformChildren = (data, arr) => {
    data.forEach((item) => {
      // 这里递归的条件仅限于sub,因为他是叶子节点,不被需要
      // 如果有level层级,可以采取更灵活的条件去拆分数据
      if (item.children) {
        arr.push(...transformChildrenOnly(item.children));
      } else if (item.sub) {
        transformChildren(item.sub, arr);
      }
    });
    return data;
  };

  const transformDataToSecondTree = useCallback((data) => {
    const newData = [];
    const expandedKeys = [];
    data.forEach((item) => {
      const arr = [];
      item.title = item.name;
      item.key = item.id;
      expandedKeys.push(item.key);
      if (item.children) {
        // 如果第二层就是children
        arr.push(...transformChildrenOnly(item.children));
      } else if (item.sub) {
        // 如果第二层是sub属性,sub代表他是叶子节点,不是最终节点
        transformChildren(item.sub, arr);
      }
      newData.push({ ...item, children: arr });
    });
    setExpandedKeys(expandedKeys);
    return newData;
  }, []);

  // 过滤数据,只展示存在有效数据的节点
  const filterData = (data) => {
    const filter = (arr) => {
      return arr.filter((item) => {
        if (item.num > 0) {
          // debugger;
          if (item.sub?.length > 0) {
            item.sub = filter(item.sub);
          }
          return true;
        }
        return false;
      });
    };
    return filter(JSON.parse(JSON.stringify(data)));
  };

  // 转换为二级树结构
  const treeData = useMemo(() => transformDataToSecondTree(filterData(treeDataTest)), [treeDataTest]);
  return (
    <div>
      <Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 14 }} onValuesChange={onValuesChange}>
        <Form.Item name="tree" label="tree">
          {/* fieldNames={{ title: "name", key: "id", children: "children" }} */}
          {/* 这里只是初步定义,只能扩充三个字段,想要更灵活的属性,在数据层修改就好了 */}
          {/* 一般处理数据后还要关注expandedKeys等属性 */}
          <Tree treeData={treeData} expandedKeys={expandedKeys} onExpand={setExpandedKeys} showIcon></Tree>
        </Form.Item>
        <Form.Item wrapperCol={labelWarpBtn}>
          <Button type="primary" htmlType="submit" onClick={console.log(form.getFieldsValue())}>
            Submit
          </Button>
        </Form.Item>
      </Form>
    </div>
  );
}

案例图片

三、搜索

1、所有节点

所有节点都进行属性搜索,回调函数进行判断

// 搜索
const searchTree = (data, value) => {
  const filter = (arr, callback) => {
    return arr
      .map((item) => ({ ...item }))
      .filter((item) => {
        // 假设item.children默认为空数组[]
        item.children = item.children && filter(item.children, callback);
        // 1、children存在元素,意味着他的子节点满足搜索条件,返回true
        // 2、callback(item)为true,意味着该节点自己满足条件,返回true
        return callback(item) || item.children?.length > 0;
      });
  };
  const searchFunc = (item) => {
    return item?.title?.includes(value);
  };
  return filter(JSON.parse(JSON.stringify(data)), searchFunc);
};

2、最末一级保留子节点

  • 如果最后一级才有实际作用(比如城市地址选择最后一级才有用),那么当最后一级的父级满足条件,展示父级下所有的子节点
  • 其余情况依旧是筛选所有节点
const searchTreeSaveLast = (data, value) => {
  const filter = (arr, callback) => {
    return arr
      .map((item) => ({ ...item }))
      .filter((item) => {
        // 保存children
        const children = item.children;
        // 假设item.children默认为空数组[]
        item.children = item.children && filter(item.children, callback);
        // 如果最末一级的父元素,满足条件,保留所有子节点
        // islastparent代表是否是最后一级父元素
        if (callback(item) && item.islastparent) {
          item.children = children;
          return true;
        }
        // 1、children存在元素,意味着他的子节点满足搜索条件,返回true
        // 2、callback(item)为true,意味着该节点自己满足条件,返回true
        return callback(item) || item.children?.length > 0;
      });
  };
  const searchFunc = (item) => {
    return item?.title?.includes(value);
  };
  return filter(JSON.parse(JSON.stringify(data)), searchFunc);
};

3、找到满足条件的第一个节点

const findNodeIndex = (tree: any[], func: (obj: any) => boolean): { index: number; node: any } => {
  for (let i = 0; i < tree.length; i++) {
    if (func(tree[i]))
      return {
        index: i,
        node: tree[i],
      };
    if (tree[i].children) {
      const res = findNodeIndex(tree[i].children, func);
      if (res) return res;
    }
  }
  return null;
};

四、节点遍历

1、遍历、筛选

  // 遍历
  // 应该是没有中序遍历的
  const mapTree = (arr) => {
    arr.forEach((item) => {
      // 前序遍历的操作位置
      item.type = 1;
      if (item.children) {
        mapTree(item.children);
      }
      // 后序遍历的操作位置
      item.count = item.children.length;
    });
  };

  // filter 有返回值,筛选子节点
  const filterTree = (arr) => {
    return arr.filter((item) => {
      // 前序遍历的操作位置
      item.type = 1;
      if (item.children) {
        item.children = filterTree(item.children);
      }
      // 后序遍历的操作位置
      item.count = item.children.length;
      return item.children?.length;
    });
  };

  // 层序遍历,对于同一层的节点进行处理
  const sequenceTree = (arr) => {
    if (arr.length <= 0) return;
    const queue = [...arr];
    while (queue.length) {
      const item = queue.shift();
      console.log("item: ", item);
      if (item?.children) {
        queue.push(...item.children);
      }
    }
  };

五、常见问题

1、默认展开问题

  • autoExpandParent默认为false,比如1-11-111三级,你的必须设置展开项为[1,11,111]这样才可以展开所有
  • 如果不需要默认,那你无需设置expandedKey、autoExpandParent
  • 如果需要默认,一种是你按照规范传值,一种是改为autoExpandParent=true,设置展开项[111],也能展开各级
  • 这里获取所有末级节点展开,autoExpandParent=true,注意在onExpand时再设置为false,这样点击展开图标也会正常,expandedKey数据依旧是包含末级节点的;
  • 这个展开项有点绕,相比selectedKeys和checkedKeys对于单个元素的确定性,expandedKey具有展示的区别
  // 递归获取所有节点的 key 叶子节点
  const allKeys = useCallback((data: IPushTreeItem[]): string[] => {
    return data.reduce((acc: string[], item: IPushTreeItem) => {
      if (item.isLeaf) {
        acc.push(item.key);
      }
      if (item.children && item.children.length) {
        acc.push(...allKeys(item.children));
      }
      return acc;
    }, []);
  }, []);
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐