详述react + ant desgin自定义树的节点,对节点进行重命名新增删除等操作

一、自定义节点

        使用ant design中的Tree组件,基础树形组件只需要将treeData属性绑定一个树形结构的值(treeData={treeData})即可:

        但是由于单纯的展示名称已经不能满足这里的需求,使用将treeData处理后的TreeNode加到Tree的内部,代码如下,其中onSelect在点击树节点时触发:

  // 点击节点,第一次点击节点是选中,第二次点击同一个节点是取消选中,用keys来判断是否有选中
  const onSelect = (keys, info) => {
    if (keys.length > 0) {
      setSelectNode(info.node);
    } else {
      setSelectNode({});
    }
  };

//...

      <Tree
        style={
   
   { marginTop: "20px" }}
        showLine={false}
        showIcon={true}
        onSelect={onSelect}
      >
        {handleTreeData(treeData)}
      </Tree>

        获取treeData,笔者这里treeData的格式为:

      [{
        id:'1',
        name:'所有',
        count:'21',
        suffix:'江苏',
        childNodes:[
          {
            id:'1-1',
            name:'南京',
            count:'21',
            suffix:'',
            childNodes:[]
          }
        ]
      }]

        从接口获取:

  const _getTreeData= async () => {
    setTreeData([]);
    try {
      let result = await getTreeData();
      setTreeData(result);
    } catch (error) {
    }
  };

        下面笔者需要处理数据,由上面的代码可知,通过handleTreeData函数处理数据treeData。分析一下基础的树形结构,Tree的节点其实是TreeNode,对于每一节点treeNode,根据官网的介绍,只需要设置其title和key属性,一般key属性即为树形数据的节点id,由此递归出所有的TreeNode:

  //  重写树
  const handleTreeData = (treeData) => {
    return treeData?.map((treeNode) => handleNodeData(treeNode));
  };
  const handleNodeData = (treeNode) => {
    if (treeNode.toString() === "[object Object]") {
        treeNode.title = (
          <div>
            <span className="text-overflow" title={treeNode.name}>{treeNode.name}</span>
            <span>({treeNode.other.count}) _{treeNode.suffix}</span>
          </div>
        );

        return (
            <TreeNode title={treeNode.title} key={treeNode.id}>
                {treeNode?.childNodes?.map((n) => handleNodeData(n))}
            </TreeNode>
        );
    }
    return <TreeNode {...treeNode} />;
  };

        至此,该树已经按照笔者的需求展示:


 二、重命名节点

        给每个节点后面添加一个按钮,点击按钮将节点切换为编辑状态,默认是原节点名称,根据上文,很容易想到在handleNodeData()中在treeNode.title中添加编辑按钮,并绑定rename():

<Button onClick={() => rename()} size={"small"}>重命名</Button>

        此外,需要给每个节点增加isEditdefaultValue(用于取消重命名后使用原来的节点名称)的属性,isEdit为true表示编辑态,否则正常展示节点。初始化数据,将所有节点的isEdit全部置为false。defaultValue值为name的值

  // 设置不可编辑
  const setAllNotEdit = (arr) => {
    let data = [].concat(arr);
    data.forEach((val) => {
      val.isEdit = false;
      if (val.childNodes && val.childNodes.length) {
        setAllNotEdit(val.childNodes);
      }
    });
    return data;
  };
  // 查询组织树
  const _getTreeData= async () => {
    //...
      let data = setAllNotEdit(result);
      setTreeData(data);
    //...
  };

        点击重命名,触发rename():在树形数据中找到该节点数据(定义deepTree函数,用于找到目标节点数据,并将名为第三个入参的键值修改为第四个入参),将isEdit改为true。在handleNodeData中针对编辑态和常规态作出单独的节点定义:

  const deepTree = (arr, key, keyName, value, otherValue) => {
    let data = [].concat(arr);
    for (let i = 0; i < data.length; i++) {

      if (data[i].id === key) {
        data[i][keyName] = value;
      } else if (typeof otherValue === "boolean") {
        data[i][keyName] = otherValue;
      }
      if (data[i].childNodes && data[i].childNodes.length) {
        deepTree(data[i].childNodes, key, keyName, value, otherValue);
      }
    }
    return data;
  };
 
 // 重命名
  const rename = () => {
    if (selectNode && selectNode.key) {
      let data = deepTree(treeData, selectNode.key, "isEdit", true, false);
      setTreeData(data);
    } else {
      message.warning("请选择节点");
    }
  };

//...

const handleNodeData = (treeNode) => {
    if (treeNode.toString() === "[object Object]") {
      if (treeNode.isEdit) {
        treeNode.title = (
          <div>
            <input value={treeNode.name} onChange={(e) => { changeNodeName(e,treeNode.id); }}/>
            ({treeNode.count})
            _{treeNode.suffix}
            <Button onClick={() => { saveTreeNode(treeNode); }} size={"small"} type="link" >确定</Button>
            <Button onClick={() => { cancelRename(treeNode); }} size={"small"} type="link" >取消</Button>
          </div>
        );
      } else {
            //...
        }
        //...
    }
    return <TreeNode {...treeNode} />;
};

        到这里,当点击重命名按钮时,节点已经变为编辑态了,input后有确定和取消两个按钮。当在input中输入新名称,触发changeNodeName(),如果没有这一步,input的值将无法修改(因为始终绑定的是节点名称,节点名称没有改变过):

  // 修改节点名称
  const changeNodeName = (e, key) => {
    let data = deepTree(treeData, key, "name", e.target.value);
    setTreeData(data);
  };

        点击取消时,表示取消重命名,使用原来的名称。在节点数据中找到当前节点,将值修改为之前的值(这个值我们已经保存在defaultValue中了):

  // 取消修改节点名称
  const cancelRename = (treeNode) => {
    let dataHasReset = deepTree(
      treeData,
      treeNode.id,
      "name",
      treeNode.defaultValue
    );
    let data = setAllNotEdit(dataHasReset);
    setTreeData(data);
  };

        点击确定时,表示修改节点名。

  • 如果此时只需要页面的更新,那么只需要在节点数据中找到该节点,更新defaultValue:
  const saveTreeNode = (treeNode) => {
    let dataHasChangeDefaultVal = deepTree(
      treeData,
      treeNode.id,
      "defaultValue",
      treeNode.name
    );
    let data = setAllNotEdit(dataHasChangeDefaultVal);
    setTreeData(data);
  };
  • 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
  // 保存修改的节点名称
  const saveTreeNode = async (treeNode) => {
    try {
      await updateNode({
        //...
      });
      _getTreeData();
    } catch (e) {
    }
  };

        在Tree控件中,点击一次节点,表示选中当前节点,再次点击,表示取消选中,但是当切换为编辑态的时候,我们可能多次点击,为了防止数据丢失,修改onSelect如下(其中dataref是TreeNode的props):

  // 点击节点
  const onSelect = (keys, info) => {
    if (keys.length > 0 || info.node?.dataRef?.isEdit) {
      setSelectNode(info.node);
    } else {
      setSelectNode({});
    }
  };

const formatNodeData = (treeNode) => {
    if (treeNode.toString() === "[object Object]") {
      //...
      return (
        <TreeNode title={treeNode.title} key={treeNode.id} dataRef={treeNode}>
          {treeNode?.childNodes?.map((d) => formatNodeData(d))}
        </TreeNode>
      );
    }
    return <TreeNode {...treeData} />;
})

        在对选中的节点进行重命名之后,虽然树是新的树了,但是保存了节点选中的状态,也保存了被选的节点数据,为了防止数据不同步造成的误会,这里笔者每次得到新的树时,就会把选中的状态去掉,选中的数据也置空。选中的数据置空只需要setSelectNode({})即可。但是去掉选中的状态就要求使用Tree控件的另一个属性:selectedKeys,表示选中的节点,当加上这个属性,当点击节点后,需要将绑定的值也更新:

  // 点击节点
  const onSelect = (keys, info) => {
    setSelectedKeys(keys);
    //...
  };

//...
      <Tree
        showLine={false}
        showIcon={true}
        defaultExpandAll={true}
        onSelect={onSelect}
        selectedKeys={selectedKeys}
      >
        {formatTreeData(treeData)}
      </Tree>

        至此,重命名节点名称已经实现。


三、新增节点

        笔者这里添加的是子节点,兄弟节点也类似,不再赘述。

        所谓新增节点,其实就是处理树形结构的数据。

  • 如果此时只需要页面的更新,在当前选中节点的childNodes中增加一个对象,所以,递归找到选中的节点,push一个新节点即可:
  const onAdd = (arr) => {
    let data = [].concat(arr);
    data.forEach((item) => {
      if (item.id === selectNode.key) {
        if (!item.childNodes) {
          item.childNodes = [];
        }
        item.childNodes.push({
          name: "新节点",
          defaultValue: "新节点",
          id: selectNode.key + Math.random(100),
          suffix:'',
          count:'',
          isEditable: false,
          childNodes: [],
        });
        return;
      }
      if (item.childNodes) {
        onAdd(item.childNodes);
      }
    });
    return data;
  };
  const addNode = () => {
    if (selectNode && selectNode.key) {
        let data = onAdd(treeData);
        setTreeData(data);
    } else {
      message.warning("请选择节点");
    }
  };
  • 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
  // 添加下级
  const addNode = async () => {
    if (selectNode && selectNode.key) {
        try {
          let result = await addNode({
            //...
          });
          _getTreeData();
        } catch (e) {
        }
    }
  };

        至此,新增节点已经实现。


四、删除节点

        与新增节点相对,删除节点是在数据中找到选中节点,从childNodes中删除元素。同样从两种场景出发:

  • 如果此时只需要页面的更新,在当前选中节点的childNodes中删除一个对象,所以,递归找到选中的节点,splice即可:
  const onDelete = (arr) => {
    arr.forEach((item, index) => {
      if (item.id === selectNode.key) {
        arr.splice(index, 1);
        return;
      }
      if (item.childNodes) {
        onDelete(item.childNodes);
      }
    });
    return arr;
  };
  const delNode = () => {
    if (selectNode && selectNode.key) {
        let data = onDelete(treeData);
        setTreeData([].concat(data));
        setSelectNode({});
    } else {
      message.warning("请选择节点");
    }
  };
  • 如果调用接口更新节点,只需要调用接口,接口成功后重新加载树:
  // 删除节点
  const delNode = () => {
    if (selectNode && selectNode.key) {
      try {
        let result = await deleteNode({
           //...
        });
        _getTreeData();
      } catch (e) {}
    } else {}
  };

        至此,删除节点已经实现。


总结

        本篇详述了对于react + ant design的树形控件的自定义节点,以及对节点的增删改,如有建议,欢迎指教~

猜你喜欢

转载自blog.csdn.net/sxww_zyt/article/details/129927356
今日推荐