話不多說者冤,先上效果圖
然后是代碼部分:
// departmentTreeData為初始化樹形數(shù)據(jù)
// searchVal為搜索框的數(shù)據(jù)
// onSelectDepartment為選中節(jié)點的func
const DepartmentTreeList = ({ departmentTreeData, searchVal, onSelectDepartment }) => {
const [flattenData, setFlattenData] = useState([]);
const [filterItems, setFilterItems] = useState([]);
const [expandItems, setExpandItems] = useState([]);
// 展平數(shù)據(jù)
const flatten = (data) => {
if (data.length) {
return data.reduce(
(arr, { id, name, parentId, children = [] }) =>
arr.concat([{ id, name, parentId, children }], flatten(children)),
[]
);
}
return data;
};
// 找到當(dāng)前元素的index
const indexInFlattenData = (item) => {
return flattenData.findIndex((val) => val.id === item.id);
};
// 找到包含該expandKey的父節(jié)點
const getParentTree = (item, temp = []) => {
const parent = flattenData.find((d) => d.id === item.parentId);
if (parent) {
temp.push(parent);
getParentTree(parent, temp);
}
return temp;
};
// 當(dāng)前節(jié)點是否展開
const isOpen = (item) => {
return expandItems.find((option) => option.id === item.id);
};
// 點擊展開節(jié)點
const openChildren = (item) => {
// 如果已經(jīng)open欠动,則從expandItems中移除當(dāng)前id,反之添加
if (isOpen(item)) {
const filterKeys = expandItems.filter((option) => option.id !== item.id);
setExpandItems([...filterKeys]);
} else {
setExpandItems([...expandItems, item]);
}
};
// 該元素是否參與其父元素leafLine的構(gòu)成
const isBefore = (key, item) => {
let flag = true;
// 為了讓key對應(yīng)parent,此處做一下reverse
const parent = getParentTree(item).reverse()[key];
const [lastChild] = parent.children.slice(-1);
// 找到最后一個child在展開數(shù)據(jù)中的index與其比較
// 如果child.index > item.index, 說明該父節(jié)點的最后一個子元素在當(dāng)前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(() => {
// 找到包括該關(guān)鍵字的選項
const filterLists = searchVal
? flattenData.filter((item) => item.name.includes(searchVal))
: [];
setFilterItems([...filterLists]);
// 找到所有包括該expandKey的父節(jié)點
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`暫無數(shù)據(jù)`}</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
對應(yīng)的數(shù)據(jù)格式為:
const optionsData = [
{ id: 1348, name: '司法臨時工啊叫', parentId: null },
{
id: 10,
name: '產(chǎn)研部',
parentId: null,
children: [
{
id: 7,
name: '研發(fā)部',
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: '運(yùn)營部',
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: '產(chǎn)品運(yùn)營部', parentId: 74 }],
},
{ id: 78, name: '子部門子部門子部門子部門子部門子部門子部門子部門', parentId: 73 },
],
},
{ id: 75, name: '研發(fā)部-其他', parentId: 7 },
{
id: 154,
name: '干活222',
parentId: 7,
children: [{ id: 155, name: '干活333', parentId: 154 }],
},
],
},
{ id: 30, name: '前端開發(fā)', parentId: 10 },
{ id: 47, name: '后端開發(fā)', parentId: 10 },
{
id: 133,
name: '產(chǎn)品部',
parentId: 10,
children: [
{
id: 11,
name: '支付寶產(chǎn)品部',
parentId: 133,
children: [{ id: 77, name: '123', parentId: 11 }],
},
{ id: 134, name: '微信支付', parentId: 133 },
],
},
],
},
];