手搓了一个组件,放在博客里说不定自定义的时候用的到,用组件库根据项目需要想改加功能非常头疼,所以我喜欢自己写组件,代码全部放上去了,复制就能用哈,选中数据返回我没写,因为不知道用的时候需要那种格式,这个结果返回很简单的
3个文件:tree主文件,item树形子文件,check选择框,之所以拆分这个check,方便我后面写其他组件直接用组件打包的下载地址
文件结构示例
使用展示
import React from 'react';
import Tree from '@/compontent/form/tree';
const treeOption = [];
const ceshi = () => {
function treeChange(param) {
console.log(param);
}
return <Tree change={treeChange.bind(this)} options={treeOption} />;
};
export default ceshi;
tree - index.tsx
import React, { useState, useEffect } from 'react';
import Item from './item/index';
import './index.less';
import * as Interface from './interface';
// checked 0:未选中 1选中 2子元素部分选中
const UiTree = (props: Interface.Iprop) => {
const { options, change } = props;
const [list, setList] = useState<Array<Interface.ListItemT>>([]);
// 子项展开/收起
function visibleChange(item: Interface.ListItemT) {
item.visible = !item.visible;
setList([...list]);
}
// 选中切换
function checkedChange(item: Interface.ListItemT, indexList) {
item.checked = item.checked === 0 ? 1 : 0;
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
checkedChildren(item.children[i], item.checked);
}
}
setList([...list]);
if (indexList.length !== 1) {
checkedFather(indexList);
}
change(list);
}
// 子项检测
function checkedChildren(item: Interface.ListItemT, value) {
item.checked = value;
setList([...list]);
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
checkedChildren(item.children[i], item.checked);
}
}
}
// 父级检测-获取到子项的索引-反推父项并核对节点状态
function checkedFather(indexList) {
const arr = recursionFn(indexList, list, 0);
let count = 0;
const len = arr.children.length;
for (let i = 0; i < len; i++) {
if (arr.children[i].checked === 2) {
count = -1;
break;
}
if (arr.children[i].checked === 1) {
count++;
}
}
if (count === 0) {
arr.checked = 0;
} else if (count === len) {
arr.checked = 1;
} else {
arr.checked = 2;
}
setList([...list]);
if (indexList.length > 2) {
indexList.splice(indexList.length - 1, 1);
checkedFather(indexList);
}
}
// 通过索引反推获取父级
function recursionFn(indexList, arr, index: number) {
if (indexList.length - 2 === index) {
return arr[indexList[index]];
}
if (index < indexList.length - 2) {
const num = index + 1; //不可使用index++代替
return recursionFn(indexList, arr[indexList[index]].children, num);
}
}
// 树节点渲染
function renderItem(newlist: Array<Interface.ListItemT>, num = 0, fatherIndex: Array<number> = []) {
if (!newlist) return;
return newlist.map((item, index) => {
if (!item.children || item.children.length === 0) {
return <Item checkedChange={checkedChange.bind(this, item, [...fatherIndex, index])} key={index} params={{ ...item, left: num }} />;
} else {
return (
<div style={{ marginLeft: `${num}px` }} className="ui-tree-children" key={index}>
<div className="ui-tree-father">
<span onClick={visibleChange.bind(this, item)} className={`ui-tree-icon ui-tree-${item.visible ? '' : 'iconright'}`}>
<i className="iconfont icon-caret-down"></i>
</span>
<Item checkedChange={checkedChange.bind(this, item, [...fatherIndex, index])} key={index} params={{ ...item, left: 0 }} />
</div>
<div
style={{
height: item.visible ? '' : '0px'
}}
className="ui-tree-panel"
>
{renderItem(item.children, 20, [...fatherIndex, index])}
</div>
</div>
);
}
});
}
// 数据过滤
function listFilter(arr) {
return arr.map(item => {
if (item.children && item.children.length > 0) return { visible: true, checked: 0, ...item, children: listFilter(item.children) };
else return { visible: true, checked: 0, ...item };
});
}
useEffect(() => {
setList(listFilter(options));
}, [options]);
return <div className="ui-tree">{renderItem(list)}</div>;
};
export default UiTree;
tree - index.less
.ui-tree {
&-father {
display: flex;
}
&-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
cursor: pointer;
transition: all 0.3s;
}
&-iconright {
transform: rotate(-90deg);
}
&-panel {
overflow: hidden;
transition: all 0.3s;
}
}
tree - interface.ts
export type ListItemT = {
label: string;
value: number | string;
visible?: boolean;
checked?: number;
children?: Array<ListItemT>;
};
export interface Iprop {
options: Array<ListItemT>;
change: (param: Array<ListItemT>) => void;
}
export const treeOption = [
{
label: '选项1',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项1-1',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项1-1-1',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项1-1-1-1',
value: 0,
visible: true,
checked: 0
},
{
label: '选项1-1-1-2',
value: 0,
visible: true,
checked: 0
}
]
}
]
},
{
label: '选项1-2',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项1-2-1',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项1-2-1-1',
value: 0,
visible: true,
checked: 0
},
{
label: '选项1-2-1-2',
value: 0,
visible: true,
checked: 0
}
]
}
]
}
]
},
{
label: '选项2',
value: 0,
visible: true,
checked: 0,
children: [
{
label: '选项2-1',
value: 0,
visible: true,
checked: 0
},
{
label: '选项2-2',
value: 0,
visible: true,
checked: 0
}
]
},
{
label: '选项3',
value: 0,
visible: true,
checked: 0
}
];
tree - checkbox - index.tsx
import React, { useState } from 'react';
import './index.less';
interface Iprops {
checked: number;
}
const statusStatic = ['', 'checked', 'indeterminate'];
const UiCheckbox = (props: Iprops) => {
const { checked } = props;
const [status] = useState(statusStatic);
return (
<div className="ui-checkbox">
<span className={`ui-tree-checkbox ui-tree-checkbox-${status[checked]}`}>
<span className="ui-tree-checkbox-inner"></span>
</span>
</div>
);
};
export default UiCheckbox;
tree - checkbox - index.less
.ui-checkbox {
display: inline-block;
}
.ui-tree-checkbox {
box-sizing: border-box;
top: 3px !important;
margin: 0;
padding: 0;
color: #000000d9;
font-size: 14px;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: 'tnum';
position: relative;
line-height: 1;
white-space: nowrap;
outline: none;
cursor: pointer;
}
.ui-tree-checkbox-checked .ui-tree-checkbox-inner {
background-color: #1890ff;
border-color: #1890ff;
}
.ui-tree-checkbox-inner {
position: relative;
top: 0;
left: 0;
display: inline-block !important;
width: 16px;
height: 16px;
direction: ltr;
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: 2px;
border-collapse: separate;
transition: all 0.3s;
}
.ui-tree-checkbox-checked .ui-tree-checkbox-inner:after {
position: absolute;
display: table;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transform: rotate(45deg) scale(1) translate(-50%, -50%);
opacity: 1;
transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
content: ' ';
}
.ui-tree-checkbox-inner:after {
position: absolute;
top: 50%;
left: 21.5%;
display: table;
width: 5.71428571px;
height: 9.14285714px;
border: 2px solid #fff;
border-top: 0;
border-left: 0;
transform: rotate(45deg) scale(0) translate(-50%, -50%);
opacity: 0;
transition: all 0.1s cubic-bezier(0.71, -0.46, 0.88, 0.6), opacity 0.1s;
content: ' ';
}
.ui-tree-checkbox-checked:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid #1890ff;
border-radius: 2px;
visibility: hidden;
animation: antCheckboxEffect 0.36s ease-in-out;
animation-fill-mode: backwards;
content: '';
}
.ui-tree-checkbox-indeterminate .ui-tree-checkbox-inner:after {
top: 50%;
left: 50%;
width: 8px;
height: 8px;
background-color: #1890ff;
border: 0;
transform: translate(-50%, -50%) scale(1);
opacity: 1;
content: ' ';
}
tree - item - index.tsx
import React from 'react';
import * as Interface from '../interface';
import UiCheckBox from '../checkbox/index';
import './index.less';
interface Iprops {
params;
checkedChange: (param: Interface.ListItemT) => void;
}
const UiTree = (props: Iprops) => {
const { params, checkedChange } = props;
return (
<div style={{ marginLeft: `${params.left}px` }} className="ui-tree-item">
{params.children ? '' : <span className="ui-tree-space"></span>}
<span onClick={checkedChange.bind(this, params)}>
<UiCheckBox checked={params.checked} />
</span>
<div className="ui-tree-label">{params.label}</div>
</div>
);
};
export default UiTree;
tree - item - index.less
.ui-tree-item {
display: flex;
}
.ui-tree {
&-space {
display: inline-block;
width: 20px;
height: 20px;
background-color: #fff;
}
&-label {
padding: 0 10px;
}
}