目录
已将该组件封装进我常用的react组件库中,对应的使用文档如下https://lsuyun.github.io/suyun-react-components/tree-select
一、功能效果图:
看题目标题可能还是会感觉有点绕,那我们先来看功能效果:
1、当不输入搜索内容的时候数据如下显示:
2、当输入“可以”进行搜索时会搜出带有“可以”的数据以及其所有的上级
二、实现思路
由于我在ant-design没有找到可以现用的组件,于是就用Input+Tree组件结合重新封装一个组件,
先来分析一下我实现这个功能的思路:
1、我在接口拿到的数据是树结构的一个数据结构:
[
{ id:1,
menuName:'一级测试',
parentId:0,
children:[]
}
]
我是先将树结构的数据类型扁平化arr1
//treeData-树状结构的数组
const flattenTree = getFlatArr(treeData);
// 扁平化数组
const getFlatArr = (treeArr) => {
let res = [];
treeArr.map((item) => {
let data = {};
data = { children: [], child: item.children }; //child对于勾选功能有用
const finItem = { ...item, ...data };
res.push(finItem);
if (item.children && Array.isArray(item.children)) {
res = res.concat(getFlatArr(item.children));
}
});
return res;
};
2、将扁平化后的数组arr1进行轮询,拿到包含搜索内容的数组arr2
if (fieldNames) {//fieldNames 自定义搜索内容title,值key取哪个字段
flattenTree.forEach((ele) => {
if (ele[fieldNames.title].indexOf(searchV) !== -1) {
searchArr.push(ele);
}
});
} else {
flattenTree.forEach((ele) => {
if (ele.title.indexOf(searchV) !== -1) {
searchArr.push(ele);
}
});
}
3、接着再将arr2进行循环找到其所有父级,将arr2和其里面每条数据的所有上级放在一个数组里,定为arr3
let parentNode = [];
searchArr.forEach((sA) => {
parentNode = [
...parentNode,
...getAllParentN(flattenTree, sA.parentId, fieldNames ? fieldNames.key : 'key', parentId),
];
});
/**
* @description: 获取全部父级
* @param {*} arr:遍历的数组 key:和id比对的key id:当前唯一值 parentId:最高级父节点id
* @return {*} 返回全部的父级 []
*/
const getAllParentN = (arr, id, key, parentId = -1) => {
let res = [];
arr.forEach((item) => {
if (item[key] == id) {
res.push(item);
if (item.parentId != parentId) {
res = res.concat(getAllParentN(arr, item.parentId, key, parentId));
}
}
});
return res;
};
4、这个时候获得的arr3肯定会有重复的,因此对arr3进行去重,去重之后再将arr3转为树结构的数组。
const allArr = getTreeData(
arrNotequal([...searchArr, ...parentNode], fieldNames ? fieldNames.key : 'key'),
fieldNames ? fieldNames.key : 'key',
parentId,
);
// 数组去重
const arrNotequal = (arr, key) => {
const obj = {};
const peno = arr.reduce((cur, next) => {
obj[next[key]] ? '' : (obj[next[key]] = true && cur.push(next));
return cur;
}, []);
return peno;
};
/**
* @description: 将平级数组转为tree
* @param {*} arr:数组 必须包含parentId parentId:最顶层父节点的parentId
* @return {*}
*/
const getTreeData = (arr, key = 'id', parentId = -1) => {
arr.forEach((item) => {
if (item.hasOwnProperty('parentId')) {
item.parentN = item.parentId;
}
});
const tree = arr.filter((father) => {
const arrN = arr.filter((child) => {
return father[key] === child.parentN;
});
if (arrN.length > 0) {
father.children = arrN;
}
return father.parentN == parentId;
});
return tree;
};
三、使用方法
使用方法如下:
1、可通过treeData传入树结构的数组,默认值[]
2、checkable控制是否有勾选的功能,默认值为true
3、onCheck为点击勾选时的触发的方法
4、fieldNames自定义字段(title:对该字段的值进行搜索匹配 key为选中的值)
5、parentId当该数据为最顶级父级节点时的parentId的值,默认值为-1
6、onSelect选中整条数据时触发的事件
7、checkedKeysExt传入的选中的值-为数组,当checkable为true时有用
8、checkStrictly:父子节点选中状态是否有关联,默认值为false
9、isSelfRender:是否自定义渲染的内容,默认值为false
例1:
<TreeSelect treeData={treeData} isSelfRender={true} fieldNames={fieldNames} />
isSelfRender为true时可自定义内容样式
const treeData = [
title:<div>自定义内容{item.menuName}</div>, //注:自定义的内容必须放在title字段
id:'10',
children:[]
]
const fieldNames = {
key: 'id',
title: 'menuName', //对搜索内容进行匹配的字段
};
四、完整代码
TreeSelect.jsx
import React from 'react';
import { Tree, Input } from 'antd';
import { useState, useEffect, useMemo } from 'react';
import { arrNotequal, getAllParentN, getTreeData, getFlatArr } from '@/utils';
import { useDebounce } from '@umijs/hooks';
/**
* @description:
* @param {*}treeData-树类型数据(title,key,children) isSelfRender-是否自定义title onSelect-点击,返回点击的数据 fieldNames(key,title)修改渲染字段
* @return {*}
*/
const TreeSelect = ({
treeData,
checkable,
onCheck,
fieldNames,
parentId = -1,
onSelect,
checkedKeysExt,
checkStrictly = false,
isSelfRender = false,
}) => {
const [checkedKeys, setCheckedKeys] = useState([]);
const [showTreeD, setShowTreeD] = useState(treeData);
const [searchV, setSearchV] = useState('');
const debouncedValue = useDebounce(searchV, 300); // 防抖
// const [defaultExpandAll, setDefaultExpandAll] = useState(true);
let allCheckedKeys = [];
// 获取所有子级
const getChildNode = (arr, key) => {
let res = [];
arr.forEach((arrD) => {
res.push(arrD[key]);
if (arrD.children) {
res = res.concat(getChildNode(arrD.children, key));
}
});
return res;
};
const handleCheck = (checkedKeysValue, e) => {
if (checkStrictly) {
setCheckedKeys(checkedKeysValue.checked);
return false;
}
const flattenTree = getFlatArr(treeData);
let doId = [];
if (e.node.children) {
getFlatArr(e.node.children).forEach((ele) => {
doId.push(fieldNames ? ele.key : ele[fieldNames.key]);
});
}
doId.push(e.node.key);
if (e.checked) {
allCheckedKeys = Array.from(new Set([...checkedKeys, ...doId]));
} else {
// 当取消选中时需要将上级的父节点清除掉,数据筛选时需要
let childId = [];
flattenTree.forEach((ft) => {
// eslint-disable-next-line
if (ft.id == e.node.key) {
if (ft.child) {
childId = getChildNode(ft.child, fieldNames ? fieldNames.key : 'key');
}
}
});
doId = [...childId, ...doId];
const parentNode = getAllParentN(
flattenTree,
e.node.key,
fieldNames ? fieldNames.key : 'key',
parentId,
);
parentNode.forEach((pd) => {
doId.push(fieldNames ? pd[fieldNames.key] : pd.key);
});
allCheckedKeys = checkedKeys.filter((item) => !doId.includes(item));
}
setTimeout(() => {
setCheckedKeys(allCheckedKeys);
});
if (onCheck) {
onCheck(allCheckedKeys);
}
};
const changeTreeD = (data) => {
data.forEach((ele) => {
ele.key = ele[fieldNames.key];
ele.title = ele[fieldNames.title];
if (ele.children) {
changeTreeD(ele.children);
}
});
};
const filterTreeF = () => {
const flattenTree = getFlatArr(treeData);
const searchArr = [];
if (fieldNames) {
flattenTree.forEach((ele) => {
if (ele[fieldNames.title].indexOf(searchV) !== -1) {
searchArr.push(ele);
}
});
} else {
flattenTree.forEach((ele) => {
if (ele.title.indexOf(searchV) !== -1) {
searchArr.push(ele);
}
});
}
let parentNode = [];
searchArr.forEach((sA) => {
parentNode = [
...parentNode,
...getAllParentN(flattenTree, sA.parentId, fieldNames ? fieldNames.key : 'key', parentId),
];
});
const allArr = getTreeData(
arrNotequal([...searchArr, ...parentNode], fieldNames ? fieldNames.key : 'key'),
fieldNames ? fieldNames.key : 'key',
parentId,
);
setShowTreeD(allArr);
setTimeout(() => {
setCheckedKeys(checkedKeys);
}, 100);
};
// 搜索
const onChange = (e) => {
setSearchV(e.target.value);
};
const handleSelect = (value, e) => {
if (onSelect) {
const flattenTree = getFlatArr(treeData);
if (fieldNames) {
flattenTree.forEach((ele) => {
// eslint-disable-next-line
if (ele[fieldNames.key] == e.node.key) {
onSelect(ele);
}
});
} else {
flattenTree.forEach((ele) => {
// eslint-disable-next-line
if (ele.key == e.node.key) {
onSelect(ele);
}
});
}
}
};
useMemo(() => {
if (!isSelfRender) {
changeTreeD(treeData);
}
setShowTreeD(treeData);
}, [treeData, isSelfRender]);
useEffect(() => {
if (checkable) {
allCheckedKeys = checkedKeys;
}
filterTreeF();
}, [debouncedValue]);
useEffect(() => {
if (checkedKeysExt) {
setCheckedKeys(checkedKeysExt);
}
}, [checkedKeysExt]);
return (
<>
<Input
style={{ marginBottom: 8 }}
placeholder="搜索"
onChange={onChange}
value={searchV}
style={{ width: '80%', marginBottom: '20px' }}
/>
{showTreeD.length ? (
<Tree
onCheck={handleCheck}
checkedKeys={checkedKeys}
onSelect={handleSelect}
checkable={checkable}
checkStrictly={checkStrictly}
blockNode
defaultExpandAll
treeData={showTreeD}
>
{/* {renderNode(showTreeD)} */}
</Tree>
) : (
''
)}
</>
);
};
export default React.memo(TreeSelect);
utils/index.js
// 数组去重
const arrNotequal = (arr, key) => {
const obj = {};
const peno = arr.reduce((cur, next) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
obj[next[key]] ? '' : (obj[next[key]] = true && cur.push(next));
return cur;
}, []);
return peno;
};
/**
* @description: 获取全部父级
* @param {*} arr:遍历的数组 key:和id比对的key id:当前唯一值 parentId:最高级父节点id
* @return {*} 返回全部的父级 []
*/
const getAllParentN = (arr, id, key, parentId = -1) => {
let res = [];
arr.forEach((item) => {
if (item[key] == id) {
res.push(item);
if (item.parentId != parentId) {
res = res.concat(getAllParentN(arr, item.parentId, key, parentId));
}
}
});
return res;
};
/**
* @description: 将平级数组转为tree
* @param {*} arr:数组 必须包含parentId parentId:最顶层父节点的parentId
* @return {*}
*/
const getTreeData = (arr, key = 'id', parentId = -1) => {
arr.forEach((item) => {
if (item.hasOwnProperty('parentId')) {
item.parentN = item.parentId;
}
});
const tree = arr.filter((father) => {
const arrN = arr.filter((child) => {
return father[key] === child.parentN;
});
if (arrN.length > 0) {
father.children = arrN;
}
return father.parentN == parentId;
});
return tree;
};
// 扁平化数组
const getFlatArr = (treeArr) => {
let res = [];
treeArr.map((item) => {
let data = {};
data = { children: [], child: item.children };
const finItem = { ...item, ...data };
res.push(finItem);
if (item.children && Array.isArray(item.children)) {
res = res.concat(getFlatArr(item.children));
}
});
return res;
};