需求背景
实现带搜索框的可编辑树,有以下功能在:
1、搜索相关节点高亮
2、配合treeSelect规范数据处理
3、节点可添加和编辑(编辑可修改上级)
4、节点可删除
5、移入显示编辑图标
0、带搜索框的树
默认数据:
搜索exa后:搜索exa,展开包含exa的相关父节点,并且高亮,收起不包含的父节点
return (
<div className={styles.detail__container}>
<Search style={{ marginBottom: 8 }} placeholder={`搜索`} onChange={onChange} />
{
treeData?.length > 0 && <Tree
onExpand={onExpand}
autoExpandParent={autoExpandParent}
defaultExpandAll
onCheck={checkDep}
checkedKeys={checkedKeys}
checkStrictly
onSelect={onSelect}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
expandedKeys={expandedKeys}
>
{renderTreeNodes(treeData)}
</Tree>
}
</div>
);
1、搜索相关节点高亮
0.节点渲染时搜索后,高亮显示
const renderTreeNodes = (data: any) => {
const menu = (item: any) => {
return (
<Menu>
<Menu.Item key="1" onClick={() => onAdd(item, 'edit',)}>修改类别名</Menu.Item>
<Menu.Item key="2" onClick={() => handleDelete(item.id)}>删除类别</Menu.Item>
</Menu>
)
}
let nodeArr = data.map((item: any) => {
const index = item && item.name.indexOf(searchValue);
const beforeStr = item.name.substr(0, index);
const afterStr = item.name.substr(index + searchValue.length);
item.value = index > -1 ? (
<span>
{beforeStr}
<span className={styles.site_tree_search_value}>{searchValue}</span>
{afterStr}
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</span>
) : (
<div>
<span>{item.name}</span>
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</div>
);
if (item.children) {
return (
<TreeNode title={item.value} key={item.id}>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return <TreeNode title={item.value} key={item.id} />;
});
return nodeArr;
};
1.搜索组件
<Search style={{ marginBottom: 8 }} placeholder={`搜索${typeName}`} onChange={onChange} />
2.搜索方法(输入触发)
注意:展开拥有相关节点的父节点,收起无关节点
// 搜索节点
const onChange = (e: any) => {
let { value } = e.target
if (!value) {
setExpandedKeys(defaultExpandedKeys);
setSearchValue(value)
return
}
value = String(value).trim()
const dataList: any[] = generateList(treeData, [])
let expandedKeys: any = dataList
.map((item: any) => {
if (item && item.name.indexOf(value) > -1) {
return getParentKey(item.key, treeData)
}
return null;
})
.filter((item: any, i: number, self: any) => item && self.indexOf(item) === i)
setExpandedKeys(expandedKeys)
setAutoExpandParent(true)
setSearchValue(value)
}
// tree树形匹配方法
const getParentKey = (key: number | string, tree: any): any => {
let parentKey
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item: any) => item.id === key)) {
parentKey = node.id;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
}
// 树节点展开/收缩
const onExpand = (expandedKeys: any) => {
setExpandedKeys(expandedKeys)
setAutoExpandParent(false)
}
2、配合treeSelect规范数据处理
- 递归修改data属性值,配合treeSelect规范数据
// 递归修改data属性值,配合treeSelect规范数据
const handleData = (data: TList[]) => {
let item: TreeData[] = [];
data.map((list: any, i: number) => {
let newItem: any = {};
newItem.key = list.id;
newItem.value = list.id;
newItem.title = list.name;
newItem.children = list.children ? handleData(list.children) : []; // 如果还有子集,就再次调用自己
// 合并新属性和原有属性
item.push({ ...list, ...newItem });
});
return item;
};
1.将树形节点改为一维数组
// 将树形节点改为一维数组
const generateList = (data: any, dataList: any[]) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { name, id, source, role, parentId, level } = node;
dataList.push({ name, id, source, role, key: id, title: name, parentId, level });
if (node.children) {
generateList(node.children, dataList);
}
}
return dataList
}
3、节点可添加编辑(编辑可修改上级)
添加
编辑
0.定义的key和editVisiable
const [key, setKey] = useState('');
const [name, setName] = useState('');
const [type, setType] = useState('add');
const [currentItem, setCurrentItem] = useState<any>({})
const [createVisible, setCreateVisible] = useState(false);
1.添加编辑方法
const onAdd = (item: any, type: string) => {
setType(type);
setName(item.name);
setKey(item.key);
setCurrentItem(item);
if (type === 'edit') {
form.setFieldsValue({
name: item?.name,
parentId: item?.parentId,
})
} else if (type === 'add') {
form.setFieldsValue({
parentId: item?.id,
})
}
setCreateVisible(true);
};
2.在节点render加入添加/编辑图标 – renderAddTree
const renderTreeNodes = (data: any) => {
const menu = (item: any) => {
return (
<Menu>
<Menu.Item key="1" onClick={() => onAdd(item, 'edit',)}>修改类别名</Menu.Item>
<Menu.Item key="2" onClick={() => handleDelete(item.id)}>删除类别</Menu.Item>
</Menu>
)
}
let nodeArr = data.map((item: any) => {
const index = item && item.name.indexOf(searchValue);
const beforeStr = item.name.substr(0, index);
const afterStr = item.name.substr(index + searchValue.length);
item.value = index > -1 ? (
<span>
{beforeStr}
<span className={styles.site_tree_search_value}>{searchValue}</span>
{afterStr}
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</span>
) : (
<div>
<span>{item.name}</span>
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</div>
);
if (item.children) {
return (
<TreeNode title={item.value} key={item.id}>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return <TreeNode title={item.value} key={item.id} />;
});
return nodeArr;
};
3.添加/编辑弹窗
<Modal
destroyOnClose
title={type === 'add' ? `新建${typeName}` : `修改${typeName}`}
visible={createVisible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={() => {
setCreateVisible(false)
form.resetFields();
setCurrentItem({});
setUpValue('');
}}
>
<Form
name="createForm"
form={form}
labelCol={{ span: 7 }}
wrapperCol={{ span: 17 }}
onFinish={handleFinish}
>
{
currentItem.level !== 1 && treeType === 3 && <Form.Item
label="上级类别"
name="parentId"
rules={[
{
required: true,
message: '请选择上级类别',
},
]}
>
<TreeSelect
showSearch
style={{ width: '100%' }}
value={upValue || currentItem?.parentId}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder="请选择上级类别"
allowClear
treeDefaultExpandAll
onChange={(value, label,) => onUpChange(value, label,)}
>
{renderAddTree(treeTypeList || [])}
</TreeSelect>
</Form.Item>
}
<Form.Item
label={`${typeName}名称`}
name="name"
rules={[
{
required: true,
message: `请输入${typeName}名称`,
},
]}
>
<Input placeholder={`请输入${typeName}名称`} />
</Form.Item>
</Form>
</Modal>
4.上级列表树节点渲染
注意:编辑限制除了自己以外其他分类可以选,自己不能选
const renderAddTree = (data: any) =>
data?.length > 0 && data.map((item: any) => {
if (item?.children?.length > 0) {
// 编辑限制除了自己以外其他分类可以选,自己不能选
return (
<TreeNode
key={item.id}
title={item.name}
value={item.id}
disabled={type === 'edit' && item.id === currentItem.id ? true : false}
>
{renderAddTree(item.children)}
</TreeNode>
);
}
return <TreeNode
{...item}
key={item.id}
title={item.name}
value={item.id}
disabled={type === 'edit' && item.id === currentItem.id ? true : false}
/>;
});
5.添加编辑保存
const handleOk = () => {
form.submit();
};
const handleFinish = async (values: { [name: string]: string }) => {
setConfirmLoading(true);
const params = {
//参数
}
if (type === 'edit') {
params.id = currentItem?.id;
} else if (type === 'add') {
params.parentId = currentItem?.id || "";
}
//请求接口
};
4、节点可删除
1.在节点render加入删除
const menu = (item: any) => {
return (
<Menu>
<Menu.Item key="1" onClick={() => onAdd(item, 'edit',)}>修改类别名</Menu.Item>
<Menu.Item key="2" onClick={() => handleDelete(item.id)}>删除类别</Menu.Item>
</Menu>
)
}
2.删除方法
const handleDelete = (id: string) => {
Modal.confirm({
title: `确认删除${typeName}`,
content: `确认删除该${typeName}?`,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
//删除接口
},
});
};
5.移入显示编辑图标
0.定义的key和editVisiable
const [key, setKey] = useState('');
const [editVisiable, setEditVisible] = useState(false);
1.树组件添加移入移出属性
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
2.移入移出方法
const onMouseEnter = (e: any) => {
const key = e?.node?.key;
setEditVisible(true)
setKey(key);
};
const onMouseLeave = (e: any) => {
setEditVisible(false)
};
3.渲染节点根据editVisible 和key判断是否显示
const renderTreeNodes = (data: any) => {
const menu = (item: any) => {
return (
<Menu>
<Menu.Item key="1" onClick={() => onAdd(item, 'edit',)}>修改类别名</Menu.Item>
<Menu.Item key="2" onClick={() => handleDelete(item.id)}>删除类别</Menu.Item>
</Menu>
)
}
let nodeArr = data.map((item: any) => {
const index = item && item.name.indexOf(searchValue);
const beforeStr = item.name.substr(0, index);
const afterStr = item.name.substr(index + searchValue.length);
item.value = index > -1 ? (
<span>
{beforeStr}
<span className={styles.site_tree_search_value}>{searchValue}</span>
{afterStr}
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</span>
) : (
<div>
<span>{item.name}</span>
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</div>
);
if (item.children) {
return (
<TreeNode title={item.value} key={item.id}>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return <TreeNode title={item.value} key={item.id} />;
});
return nodeArr;
};
6、完整代码
import React, { useEffect, useState } from 'react';
import { dispatch, getState } from '@@/store';
import { Input, Tree, Modal, Form, Menu, Dropdown, message, TreeSelect } from 'antd';
import styles from './index.less';
import {
EditOutlined,
PlusCircleTwoTone,
MinusOutlined,
} from "@ant-design/icons";
const { Search } = Input;
const { TreeNode } = Tree;
type ITreeTypeProps = {
typeName: string;
changeKey: (visible: string) => void;
onNode: (node: any) => void;
onTree?: (tree: any) => void;
treeType: number;
};
type TreeData = {
title?: string,
value?: number,
key?: number,
children?: TreeData[],
}
const treeList = [
{
title: 'parent 1',
key: '0-0',
children: [
{
title: 'parent 1-0',
key: '0-0-0',
children: [
{
title: 'leaf',
key: '0-0-0-0',
},
{
title: 'leaf',
key: '0-0-0-1',
},
{
title: 'leaf',
key: '0-0-0-2',
},
],
},
{
title: 'parent 1-1',
key: '0-0-1',
children: [
{
title: 'leaf',
key: '0-0-1-0',
},
],
},
]
}
]
const TreeType: React.FC<ITreeTypeProps> = ({
changeKey,
typeName,
treeType,
onNode,
onTree,
}) => {
const [form] = Form.useForm();
const [createVisible, setCreateVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [name, setName] = useState('');
const [upValue, setUpValue] = useState('');
const [type, setType] = useState('add');
const [expandedKeys, setExpandedKeys] = useState<any[]>([]);
const [defaultExpandedKeys, setDefaultExpandedKeys] = useState<any[]>([]);
const [selectdKeys, setSelectdKeys] = useState<any[]>([]);
const [checkedKeys, setCheckedKeys] = useState<any[]>([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState('')
const [key, setKey] = useState('');
const [editVisiable, setEditVisible] = useState(false);
const [currentItem, setCurrentItem] = useState<any>({})
const [treeData, setTreeData] = useState<TCategoryList[]>([]);
const [treeTypeList, setTreeTypeList] = useState<TreeData[]>([]);
useEffect(() => {
getList()
}, [])
// 自定义规则数据
const getFilter = (treeData: any[],) => {
const data = treeData.filter(v => v.source === 1);
const TreeData = handleData(data);
onTree && onTree(TreeData);
setTreeTypeList(TreeData);
}
// 递归修改data属性值,配合treeSelect规范数据
const handleData = (data: TCategoryList[]) => {
let item: TreeData[] = [];
data.map((list: any, i: number) => {
let newItem: any = {};
newItem.key = list.id;
newItem.value = list.id;
newItem.title = list.name;
newItem.children = list.children ? handleData(list.children) : []; // 如果还有子集,就再次调用自己
// 合并新属性和原有属性
item.push({ ...list, ...newItem });
});
return item;
};
// 将树形节点改为一维数组
const generateList = (data: any, dataList: any[]) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { name, id, parentId, level } = node;
dataList.push({ name, id, key: id, title: name, parentId, level });
if (node.children) {
generateList(node.children, dataList);
}
}
return dataList
}
// 获取树形节点数据
const getList = async (params?: TCategoryParams) => {
const res = await CATEGORY.categoryList(params);
setTreeData(res || []);
getFilter(res);
if (res) {
let list: any[] = generateList(res, [])
let expendList: any[] = [];
list?.map((v: any) => {
expendList.push(v.id);
if (v.children) {
v?.children?.map((item: any) => {
expendList.push(item.id);
})
}
})
//默认展开所有
setExpandedKeys(expendList || []);
setDefaultExpandedKeys(expendList || []);
}
}
// 搜索节点
const onChange = (e: any) => {
let { value } = e.target
if (!value) {
setExpandedKeys(defaultExpandedKeys);
setSearchValue(value)
return
}
value = String(value).trim()
const dataList: any[] = generateList(treeData, [])
let expandedKeys: any = dataList
.map((item: any) => {
if (item && item.name.indexOf(value) > -1) {
return getParentKey(item.key, treeData)
}
return null;
})
.filter((item: any, i: number, self: any) => item && self.indexOf(item) === i)
setExpandedKeys(expandedKeys)
setAutoExpandParent(true)
setSearchValue(value)
}
// tree树形匹配方法
const getParentKey = (key: number | string, tree: any): any => {
let parentKey
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item: any) => item.id === key)) {
parentKey = node.id;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
// console.log(key, parentKey, tree,)
return parentKey;
}
// 树节点展开/收缩
const onExpand = (expandedKeys: any) => {
setExpandedKeys(expandedKeys)
setAutoExpandParent(false)
}
// 选择节点
const checkDep = (val: any, data: any) => {
if (data && data.checkedNodes && data.checkedNodes.length) {
let checkedNodes = [...data.checkedNodes]
setSelectdKeys(checkedNodes)
setCheckedKeys(
checkedNodes.map((subItem: any) => {
return subItem.key
}),
)
} else {
setSelectdKeys([])
setCheckedKeys([])
}
}
//选中节点时触发
const onSelect = (selectedKeys: any, info: any) => {
const key = info?.node?.key;
onCurrent(key)
}
//获取当前节点数据
const onCurrent = (key: any) => {
const treeList = generateList(treeData, []);
const node = treeList.find(v => v.id === key);
setCurrentItem(node);
onNode(node);
changeKey(key);
}
const onAdd = (item: any, type: string) => {
setType(type);
setName(item.name);
setKey(item.key);
setCurrentItem(item);
if (type === 'edit') {
form.setFieldsValue({
name: item?.name,
parentId: item?.parentId,
})
} else if (type === 'add') {
form.setFieldsValue({
parentId: item?.id,
})
}
setCreateVisible(true);
};
const onMouseEnter = (e: any) => {
const key = e?.node?.key;
setEditVisible(true)
setKey(key);
};
const onMouseLeave = (e: any) => {
setEditVisible(false)
};
const renderTreeNodes = (data: any) => {
const menu = (item: any) => {
return (
<Menu>
<Menu.Item key="1" onClick={() => onAdd(item, 'edit',)}>修改类别名</Menu.Item>
<Menu.Item key="2" onClick={() => handleDelete(item.id)}>删除类别</Menu.Item>
</Menu>
)
}
let nodeArr = data.map((item: any) => {
const index = item && item.name.indexOf(searchValue);
const beforeStr = item.name.substr(0, index);
const afterStr = item.name.substr(index + searchValue.length);
item.value = index > -1 ? (
<span>
{beforeStr}
<span className={styles.site_tree_search_value}>{searchValue}</span>
{afterStr}
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</span>
) : (
<div>
<span>{item.name}</span>
{
//核心代码
editVisiable && item.id === key && <span>
<Dropdown overlay={menu(item)} trigger={['hover']}>
<EditOutlined style={{ marginLeft: 10 }} />
</Dropdown>
<PlusCircleTwoTone twoToneColor="#ff571a" style={{ marginLeft: 10 }} onClick={() => onAdd(item, 'add')} />
</span>
}
</div>
);
if (item.children) {
return (
<TreeNode title={item.value} key={item.id}>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return <TreeNode title={item.value} key={item.id} />;
});
return nodeArr;
};
const handleFinish = async (values: { [name: string]: string }) => {
setConfirmLoading(true);
const params = {
}
if (type === 'edit') {
params.id = currentItem?.id;
// params.parentId = currentItem?.parentId;
} else if (type === 'add') {
params.parentId = currentItem?.id || "";
}
addOrEditCategory(params);
};
// 新增/编辑
const addOrEditCategory = async (params: TCategoryListAdd) => {
};
// 删除
const onDelete = async (id: string) => {
};
const handleOk = () => {
form.submit();
};
const handleDelete = (id: string) => {
Modal.confirm({
title: `确认删除${typeName}类别`,
content: `确认删除该${typeName}类别?`,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
onDelete(id);
},
});
};
const onUpChange = (value: any, label: any,) => {
setUpValue(value);
};
const renderAddTree = (data: any) =>
data?.length > 0 && data.map((item: any) => {
if (item?.children?.length > 0) {
// 编辑限制除了自己以外其他分类可以选,自己不能选
return (
<TreeNode
key={item.id}
title={item.name}
value={item.id}
disabled={type === 'edit' && item.id === currentItem.id ? true : false}
>
{renderAddTree(item.children)}
</TreeNode>
);
}
return <TreeNode
{...item}
key={item.id}
title={item.name}
value={item.id}
disabled={type === 'edit' && item.id === currentItem.id ? true : false}
/>;
});
return (
<div className={styles.detail__container}>
<Search style={{ marginBottom: 8 }} placeholder={`搜索${typeName}类别`} onChange={onChange} />
{
treeData?.length > 0 && <Tree
onExpand={onExpand}
autoExpandParent={autoExpandParent}
defaultExpandAll
onCheck={checkDep}
checkedKeys={checkedKeys}
checkStrictly
onSelect={onSelect}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
expandedKeys={expandedKeys}
>
{renderTreeNodes(treeData)}
</Tree>
}
<Modal
destroyOnClose
title={type === 'add' ? `新建${typeName}类别` : `修改${typeName}类别名`}
visible={createVisible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={() => {
setCreateVisible(false)
form.resetFields();
setCurrentItem({});
setUpValue('');
}}
>
<Form
name="createForm"
form={form}
labelCol={{ span: 7 }}
wrapperCol={{ span: 17 }}
onFinish={handleFinish}
>
{
<Form.Item
label="上级类别"
name="parentId"
rules={[
{
required: true,
message: '请选择上级类别',
},
]}
>
<TreeSelect
showSearch
style={{ width: '100%' }}
value={upValue || currentItem?.parentId}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder="请选择上级类别"
allowClear
treeDefaultExpandAll
onChange={(value, label,) => onUpChange(value, label,)}
>
{renderAddTree(treeTypeList || [])}
</TreeSelect>
</Form.Item>
}
<Form.Item
label={`${typeName}名称`}
name="name"
rules={[
{
required: true,
message: `请输入${typeName}名称`,
},
]}
>
<Input placeholder={`请输入${typeName}名称`} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default TreeType;