话不多说,先上效果图
代码部分
// 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 },
],
},
],
},
];