话不多说,先上效果图
然后是代码部分:
// departmentTreeData为初始化树形数据
// searchVal为搜索框的数据
// onSelectDepartment为选中节点的func
const DepartmentTreeList = ({ departmentTreeData, searchVal, onSelectDepartment }) => {
const [flattenData, setFlattenData] = useState([]);
const [filterItems, setFilterItems] = useState([]);
const [expandItems, setExpandItems] = useState([]);
// 展平数据
const flatten = (data) => {
if (data.length) {
return data.reduce(
(arr, { id, name, parentId, children = [] }) =>
arr.concat([{ id, name, parentId, children }], flatten(children)),
[]
);
}
return data;
};
// 找到当前元素的index
const indexInFlattenData = (item) => {
return flattenData.findIndex((val) => val.id === item.id);
};
// 找到包含该expandKey的父节点
const getParentTree = (item, temp = []) => {
const parent = flattenData.find((d) => d.id === item.parentId);
if (parent) {
temp.push(parent);
getParentTree(parent, temp);
}
return temp;
};
// 当前节点是否展开
const isOpen = (item) => {
return expandItems.find((option) => option.id === item.id);
};
// 点击展开节点
const openChildren = (item) => {
// 如果已经open,则从expandItems中移除当前id,反之添加
if (isOpen(item)) {
const filterKeys = expandItems.filter((option) => option.id !== item.id);
setExpandItems([...filterKeys]);
} else {
setExpandItems([...expandItems, item]);
}
};
// 该元素是否参与其父元素leafLine的构成
const isBefore = (key, item) => {
let flag = true;
// 为了让key对应parent,此处做一下reverse
const parent = getParentTree(item).reverse()[key];
const [lastChild] = parent.children.slice(-1);
// 找到最后一个child在展开数据中的index与其比较
// 如果child.index > item.index, 说明该父节点的最后一个子元素在当前item下方,所以要加上leafLine
if (indexInFlattenData(lastChild) > indexInFlattenData(item)) {
flag = false;
}
return flag;
};
// 渲染leafLine
const renderLeafLine = (index, item) => {
// index表示要在此元素前方插入多少个占位span
const data = [...new Array(index - 1).keys()];
return data.map((key) => (
<span
key={key}
className={classNames(styles.treeIndent, {
[styles.displayNone]: isBefore(key, item),
})}
style={{
left: `${(key + 1) * 30}px`,
}}
/>
));
};
const renderList = (data, index = 0) => {
// 通过index控制样式
index += 1;
return data.map((item) => {
const hasChildren = item.children && item.children.length;
const openChildFlag = isOpen(item);
return (
<React.Fragment key={item.id}>
<li
className={styles.listItem}
style={{
paddingLeft: `${(index - 1) * 30}px`,
}}
onClick={() => onSelectDepartment(item)}
>
{index > 1 && renderLeafLine(index, item)}
<span className={styles.leafLine} />
{hasChildren && (
<span
className={styles.childIcon}
onClick={(e) => {
e.stopPropagation();
openChildren(item);
}}
>
<Icon name={openChildFlag ? 'down' : 'right'} />
</span>
)}
{searchVal && item.name.includes(searchVal) ? (
<span
dangerouslySetInnerHTML={{
__html: item.name.replace(
searchVal,
`<span class=${styles.labelKeyword}>${searchVal}</span>`
),
}}
/>
) : (
<span>{item.name}</span>
)}
</li>
{hasChildren && openChildFlag ? renderList(item.children, index) : null}
</React.Fragment>
);
});
};
useEffect(() => {
const data = flatten(departmentTreeData);
setFlattenData([...data]);
// 初始化全部展开
// setExpandItems([...data]);
}, [departmentTreeData]);
useEffect(() => {
// 找到包括该关键字的选项
const filterLists = searchVal
? flattenData.filter((item) => item.name.includes(searchVal))
: [];
setFilterItems([...filterLists]);
// 找到所有包括该expandKey的父节点
let result = [];
filterLists.forEach((items) => {
const parent = getParentTree(items);
result.push(...parent);
});
setExpandItems([...new Set(result)]);
}, [searchVal]);
return (
<ul className={styles.listBody}>
{searchVal ? (
filterItems.length ? (
<ul className={styles.listBody}>{renderList(departmentTreeData)}</ul>
) : (
<div className={styles.noData}>{i18n.t`暂无数据`}</div>
)
) : (
<ul className={styles.listBody}>{renderList(departmentTreeData)}</ul>
)}
</ul>
);
};
DepartmentTreeList.defaultProps = {
departmentTreeData: [],
searchVal: '',
onSelectDepartment: () => {},
};
DepartmentTreeList.propTypes = {
departmentTreeData: PropTypes.array,
searchVal: PropTypes.string,
onSelectDepartment: PropTypes.func,
};
export default DepartmentTreeList;
css部分:
@import '~@SDVariable'
.list-body
padding 8px 0 0 8px
.list-item
position relative
padding 12px 0
.tree-indent
position absolute
display inline-block
width 22px
&::before
position absolute
top -33px
height 45px
border-left 1px solid #dddfe3
content " "
.display-none
display none
.leaf-line
position relative
display inline-block
width 22px
height 100%
&::before
position absolute
top -49px
height 44px
border-left 1px solid n20
content " "
&::after
position absolute
top -5px
width 21px
border-bottom 1px solid n20
content " "
.child-icon
position relative
z-index 1
width 16px
height 16px
margin-right 8px
border-radius 50%
border 1px solid n20
background n0
.label-keyword
color b50
.no-data
text-align center
color #9a9fac
对应的数据格式为:
const optionsData = [
{ id: 1348, name: '司法临时工啊叫', parentId: null },
{
id: 10,
name: '产研部',
parentId: null,
children: [
{
id: 7,
name: '研发部',
parentId: 10,
children: [
{
id: 3,
name: '自动化测试',
parentId: 7,
children: [
{
id: 1,
name: '自动化测试下一级部门',
parentId: 3,
children: [
{
id: 70,
name: '部门1',
parentId: 1,
children: [
{
id: 82,
name: '运营部',
parentId: 70,
children: [{ id: 83, name: '1', parentId: 82 }],
},
],
},
{ id: 71, name: '部门2', parentId: 1 },
],
},
],
},
{
id: 31,
name: '后端小组',
parentId: 7,
children: [{ id: 79, name: '仅校招使用', parentId: 31 }],
},
{
id: 73,
name: '赵正果测试',
parentId: 7,
children: [
{ id: 72, name: '部门3', parentId: 73 },
{
id: 74,
name: '部门1-子部门-子部门',
parentId: 73,
children: [{ id: 12, name: '产品运营部', parentId: 74 }],
},
{ id: 78, name: '子部门子部门子部门子部门子部门子部门子部门子部门', parentId: 73 },
],
},
{ id: 75, name: '研发部-其他', parentId: 7 },
{
id: 154,
name: '干活222',
parentId: 7,
children: [{ id: 155, name: '干活333', parentId: 154 }],
},
],
},
{ id: 30, name: '前端开发', parentId: 10 },
{ id: 47, name: '后端开发', parentId: 10 },
{
id: 133,
name: '产品部',
parentId: 10,
children: [
{
id: 11,
name: '支付宝产品部',
parentId: 133,
children: [{ id: 77, name: '123', parentId: 11 }],
},
{ id: 134, name: '微信支付', parentId: 133 },
],
},
],
},
];