Tree选择器结构比较特殊,类似于数据结构中的树,因此设计对于优化有很多的关系。
先看一下组件库文档: 设计中主要用到的思路是递归,先看一下基础渲染吧:
const treeData = [
{
title: 'parent1',
value: '0-0',
children: [
{
title: 'parent 1-0',
value: '0-0-1',
},
{
title: 'parent 1-1',
value: '0-0-2',
children: [
{
title: 'leaf2',
value: '0-0-0-1',
},
],
},
],
},
{
title: 'parent2',
value: '0-1',
children: [
{
title: 'parent 2-0',
value: '0-0-3',
},
],
},
];
渲染结构是这样的,就是一个树的结构,通过渲染函数将所有树节点递归渲染出来,核心代码如下: 而具体这个level是从哪里来的呢,其实使用者上文只需要传递title、value、children即可,设计中其实是在渲染之前对这个树节点进行了一些结构改造的,以便于组件开发。 在上图中,二次改造函数对每个节点都进行了height和level的计算和添加,这些后面都会用到,具体备注在图片中很清楚。
业务开发 核心点主要在切换菜单,切换菜单时我的设计是展开只进行下一层节点的展示;收起的话如果是对根节点进行收起操作,则将所有子节点收起,核心代码如下: 上面所讲的是切换的实现,如果是点击无子节点的节点呢?就是直接选中操作了。 这里选中分为了单选了多选,组件默认是单选的,如需要支持多选,需要给组件传递avaChooseMore属性,具体可参照文档案例。
组件完整源码index.tsx:
import React, { FC, memo, Fragment, useState, useEffect, useCallback } from 'react';
import { CaretRightOutlined, CaretDownOutlined } from '@ant-design/icons';
import Input from '../Input';
import './index.module.less';
interface treeProps {
treeData: Array<treeNode>;
width?: string;
avaSearch?: boolean;
avaChooseMore?: boolean;
defaultOpen?: boolean;
chooseCallback?: Function;
}
interface treeNode {
title: string;
value: string;
level: number;
height?: string;
children?: Array<treeNode>;
}
const Tree: FC<treeProps> = (props) => {
const { width = '200', treeData, avaSearch, avaChooseMore, defaultOpen, chooseCallback } = props;
const [stateTreeData, setStateTreeData] = useState<Array<treeNode>>(treeData);
const [activedVal, setActivedVal] = useState<string>('');
const [containerHeight, setContainerHeight] = useState<string>('0px');
const [isFocus, setIsFocus] = useState(false);
useEffect(() => {
resolveTreeData(treeData as Array<treeNode>, 1);
window.addEventListener('click', () => setContainerHeight('0px'));
}, []);
const resolveTreeData = (treeData: Array<treeNode>, nowIndexLevel: number) => {
treeData.forEach((treeNode: treeNode) => {
treeNode.level = nowIndexLevel;
if (defaultOpen) {
treeNode.height = '30px';
} else {
treeNode.height = treeNode.level == 1 ? '30px' : '0';
}
if (treeNode?.children?.length) {
resolveTreeData(treeNode.children, nowIndexLevel + 1);
} else {
nowIndexLevel = treeNode.level;
}
});
setStateTreeData(treeData);
};
const toggleTreeMenu = (clickTreeNode: treeNode) => {
if (clickTreeNode?.children?.length) {
const oldStateTree = [...stateTreeData];
const editTreeNode = (treeNode: Array<treeNode>) => {
treeNode.forEach((child) => {
if (child?.children?.length) {
child.height = '0';
editTreeNode(child.children);
} else {
child.height = '0';
}
});
};
const mapFn = (treeNode: Array<treeNode>) => {
treeNode.forEach((t: treeNode, i: number) => {
if (t.title == clickTreeNode.title && t.value == t.value) {
if (t?.children?.length) {
if (t.children[0].height == '0') {
t.children = t.children.map((child: treeNode) => {
return {
...child,
height: child.height == '0' ? '30px' : '0',
};
});
} else {
editTreeNode(t.children);
}
}
} else if (t?.children?.length) {
mapFn(t.children);
}
});
};
mapFn(oldStateTree);
setStateTreeData(oldStateTree);
} else {
if (avaChooseMore) {
if (activedVal.split(',').includes(clickTreeNode.title)) {
let updateVal: Array<string> | string = activedVal;
updateVal = updateVal.split(',');
updateVal.splice(
activedVal.split(',').findIndex((t) => t == clickTreeNode.title),
1,
);
updateVal = updateVal.join(',');
setActivedVal(updateVal);
chooseCallback && chooseCallback(updateVal);
} else {
setActivedVal(
activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
);
chooseCallback &&
chooseCallback(
activedVal == '' ? clickTreeNode.title : activedVal + ',' + clickTreeNode.title,
);
}
} else {
setActivedVal(clickTreeNode.title);
chooseCallback && chooseCallback(clickTreeNode.title);
}
}
};
const handleIptChange = (val: string) => {
if (avaSearch) {
setActivedVal(val);
} else {
setActivedVal('');
}
};
const handleClick = () => {
if (avaSearch) {
if (isFocus && containerHeight == '100%') {
setContainerHeight('0px');
} else {
setContainerHeight('100%');
}
} else {
setContainerHeight(containerHeight == '0px' ? '100%' : '0px');
}
};
const handleIptFocus = () => {
setTimeout(() => {
if (!isFocus) {
setIsFocus(true);
}
}, 150);
};
const handleIptBlur = () => {
setIsFocus(false);
};
const searchStyle = useCallback(
(treeNode: treeNode): string => {
if (treeNode.title.includes(activedVal) && activedVal !== '') {
return '#1890FF';
} else {
return '#000000';
}
},
[activedVal],
);
const activedStyle = useCallback(
(treeNode: treeNode): string => {
if (avaChooseMore) {
if (activedVal.split(',').includes(treeNode.title)) {
return '#bae8ff';
} else {
return '#ffffff';
}
} else {
if (activedVal == treeNode.title) {
return '#bae8ff';
} else {
return '#ffffff';
}
}
},
[activedVal],
);
const clearCallback = () => {
setActivedVal('');
};
const render = (data: Array<treeNode> = stateTreeData) => {
return data.map((treeNode: treeNode, index) => {
return (
<Fragment key={index}>
<div
className="treeNode"
style={{
marginLeft: `${treeNode.level * 10}px`,
height: `${treeNode.height}`,
color: searchStyle(treeNode),
background: activedStyle(treeNode),
}}
onClick={() => toggleTreeMenu(treeNode)}
>
{
treeNode?.children?.length ? (
treeNode.children[0].height == '0' ? (
<CaretDownOutlined />
) : (
<CaretRightOutlined />
)
) : (
<div style={{ width: '12px', height: '12px' }}></div>
)
}
<span className="text">{treeNode.title}</span>
</div>
{treeNode?.children?.length && render(treeNode.children)}
</Fragment>
);
});
};
return (
<Fragment>
<div className="tree-container" onClick={(e) => e.stopPropagation()}>
<Input
moreStyle={avaSearch ? {} : { caretColor: 'transparent' }}
placeholder={avaSearch ? '请输入' : ''}
width={width}
defaultValue={activedVal}
handleClick={handleClick}
handleIptChange={handleIptChange}
handleIptFocus={handleIptFocus}
handleIptBlur={handleIptBlur}
clearCallback={clearCallback}
showClear
/>
<div
className="tree-select-dialog"
style={{
width: `${width}px`,
height: containerHeight,
opacity: containerHeight == '0px' ? '0' : '1',
padding: containerHeight == '0px' ? '0 10px 0 10px' : '10px',
}}
>
{render()}
</div>
</div>
</Fragment>
);
};
export default memo(Tree);
github path戳~ React-View-UI组件库文档地址戳~
如果对该组件库有兴趣,在github的readme中有介绍基本入门方式,可以下载尝试,最后感谢花时间阅读此文,如果对Tree组件设计有更好的建议欢迎留言呀。
|