Ant Design - 组件之 Tree树形控件
针对tree树形组件封装了一个树形组件
1.组件ui
2.组件名称
ThemeCatalog
上面是image目录中的svg
3.组件代码
index.js
import React, {useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import Icon, {FolderOpenOutlined, ReloadOutlined, SearchOutlined} from '@ant-design/icons';
import {Button, Input, message, Spin, Tree} from 'antd';
import {cloneDeep, isEmpty} from 'lodash';
import './index.less';
import {fetchApi} from 'utils';
import {api} from '../../config';
import themeIcon from './image/theme_icon.svg';
import businessIcon from './image/business_icon.svg';
import entityIcon from './image/entity_icon.svg';
import {businessSvg, entitySvg, themeSvg} from './svg';
const prefixCls = 'theme-catalog-component';
const arrayTreeFilter = (data, predicate, filterText) => {
const nodes = cloneDeep(data);
// 如果已经没有节点了,结束递归
if (!(nodes && nodes.length)) {
return;
}
const newChildren = [];
for (const node of nodes) {
if (predicate(node, filterText)) {
// 如果自己(节点)符合条件,直接加入到新的节点集
newChildren.push(node);
// 并接着处理其 children,(因为父节点符合,子节点一定要在,所以这一步就不递归了)
node.childList = arrayTreeFilter(node.childList, predicate, filterText);
} else {
// 如果自己不符合条件,需要根据子集来判断它是否将其加入新节点集
// 根据递归调用 arrayTreeFilter() 的返回值来判断
const subs = arrayTreeFilter(node.childList, predicate, filterText);
// 以下两个条件任何一个成立,当前节点都应该加入到新子节点集中
// 1. 子孙节点中存在符合条件的,即 subs 数组中有值
// 2. 自己本身符合条件
if ((subs && subs.length) || predicate(node, filterText)) {
node.childList = subs;
newChildren.push(node);
}
}
}
return newChildren;
};
const filterFn = (data, filterText) => { //过滤函数
if (!filterText) {
return true;
}
return (
new RegExp(filterText, 'i').test(data.nodeName) //我是一title过滤 ,你可以根据自己需求改动
);
};
const expandedKeysFun = (treeData) => { //展开 key函数
if (treeData && treeData.length === 0) {
return [];
}
const arr = [];
const expandedKeysFn = (treeData) => {
if (!isEmpty(treeData)) {
treeData.map((item, index) => {
arr.push(item.id);
if (item.childList && item.childList.length > 0) {
expandedKeysFn(item.childList);
}
});
}
};
expandedKeysFn(treeData);
return arr;
};
const ThemeCatalog = (props) => {
const {
pageWidth = 300,
pageHeight = '100%',
inputPlaceholder,
onlyPublished,
onChangeSelect
} = props;
const [loading, setLoading] = useState(true);
// 主题目录数据
const [themeCatalog, setThemeCatalog] = useState([]);
// 备份主题目录数据
const [copyTreeData, setCopyTreeData] = useState([]);
// 搜索框绑定内容
const [searchTxt, setSearchTxt] = useState('');
// 树中的受控keys
const [expandedKeys, setExpandedKeys] = useState([-1]);
// 是否自动展开父节点
const [autoExpandParent, setAutoExpandParent] = useState(true);
// 选中的树对应的id
const [selectedKeys, setSelectedKeys] = useState([]);
useEffect(() => {
getThemeCatalog();
}, []);
// 获取数据
const getThemeCatalog = () => {
fetchApi({
method: 'post',
api: api.themeCatalogTree,
data: {
onlyPublished
},
success: (res) => {
const copyData = { ...cloneDeep(res) };
// 添加主题图标
copyData.icon = (<FolderOpenOutlined />);
// 处理子层级的图标
copyData.childList = addLevelIcon(res.childList || []);
setThemeCatalog([copyData]);
// 拷贝一份数据用于搜索条件
setCopyTreeData([cloneDeep(copyData)]);
// 设置展开所有
setExpandedKeys(defaultExpandAll([copyData]));
},
error: (err) => {
setThemeCatalog([]);
message.warning('请求错误');
},
complete: () => {
setLoading(false);
}
});
};
const defaultExpandAll = (data) => {
return generateList(data).map((item) => item.id);
};
// 更新数据
const updateData = () => {
setSearchTxt('');
setExpandedKeys([-1]);
getThemeCatalog();
};
// 将树形结构转化成一维数组
const generateList = (data = [], dataList = []) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
dataList.push({
...node,
childList: null,
});
if (node.childList) {
generateList(node.childList, dataList);
}
}
return dataList;
};
// 添加层级对应的图标
const addLevelIcon = (data) => {
data = data.map((item) => {
if (item.nodeType === 1) {
item.icon = (<Icon component={themeSvg}/>);
}
if (item.nodeType === 2) {
item.icon = (<Icon component={businessSvg}/>);
}
if (item.nodeType === 3) {
item.icon = (<Icon component={entitySvg}/>);
}
return {
...item,
childList: !isEmpty(item.childList) ? addLevelIcon(item.childList) : item.childList
};
});
return data;
};
// 搜索数据
const searchData = (e) => {
const { value } = e.target;
if (String(value).trim() === '') {
setThemeCatalog(copyTreeData);
setExpandedKeys([-1]);
} else {
const res = arrayTreeFilter(copyTreeData, filterFn, value);
const expkey = expandedKeysFun(res);
setThemeCatalog(res);
setExpandedKeys(expkey);
setAutoExpandParent(true);
}
};
const onSelect = (selectedKey, info) => {
if (!isEmpty(selectedKey)) {
setSelectedKeys(selectedKey);
const checkObj = getCheckTreeOtherObj(selectedKey);
onChangeSelect({ ...checkObj[0] });
}
};
// 获取选中树形数据中的其他数据
const getCheckTreeOtherObj = (id) => {
return generateList(themeCatalog, []).filter((item) => {
return item.id === id[0];
});
};
const onExpand = (newExpandedKeys) => {
setExpandedKeys(newExpandedKeys);
setAutoExpandParent(false);
};
/*tipsDom*/
const tipsDom = () => {
return (
<div className={`${prefixCls}-tips`}>
<div className={`${prefixCls}-tips-item`}><img src={themeIcon} alt="主题域"/>主题域</div>
<div className={`${prefixCls}-tips-item`}><img src={businessIcon} alt="业务模块"/>业务模块</div>
<div className={`${prefixCls}-tips-item`}><img src={entityIcon} alt="统计实体"/>统计实体</div>
</div>
);
};
/*搜索DOM*/
const searchInputDom = () => {
return (
<div className={`${prefixCls}-search`}>
<Input
allowClear={true}
placeholder={inputPlaceholder}
suffix={
<SearchOutlined style={{color: 'rgba(0,0,0,0.25)', fontSize: '16px'}}/>}
onChange={(e) => {
setSearchTxt(e.target.value);
searchData(e);
}}
value={searchTxt}
/>
<div className={`${prefixCls}-search-update`}>
<Button icon={<ReloadOutlined
style={
{
fontSize: '16px',
fontWeight: 'bold',
color: 'rgba(0,0,0,0.45)'
}
}
onClick={() => {
updateData();
}}
/>} size="large"/>
</div>
</div>
);
};
return (
<div
style={{
width: pageWidth,
height: pageHeight
}}
className={prefixCls}>
{/*搜索*/}
{
searchInputDom()
}
{/*目录树*/}
{
<div className={`${prefixCls}-tree`}>
<Spin spinning={loading} tip="请求数据中" size="small">
{
(themeCatalog && themeCatalog.length > 0)
? <Tree
onExpand={onExpand}
blockNode
showIcon
autoExpandParent={autoExpandParent}
expandedKeys={expandedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={themeCatalog}
fieldNames={
{
title: 'nodeName',
key: 'id',
children: 'childList',
}
}
/> : <div className={`${prefixCls}-empty`}>暂无数据</div>
}
</Spin>
</div>
}
{/*提示*/}
{
tipsDom()
}
</div>
);
};
ThemeCatalog.propTypes = {
// 页面布局宽度
pageWidth: PropTypes.oneOfType([
PropTypes.string, PropTypes.number
]),
// 页面布局高度
pageHeight: PropTypes.oneOfType([
PropTypes.string, PropTypes.number
]),
// 输入框placeholder
inputPlaceholder: PropTypes.string,
// 是否只包含已发布节点, true是,false否
onlyPublished: PropTypes.oneOf([true, false]),
// 获取点击之后的内容,会返回对应点击数据中的除子层级之外的所有后台接口返回的信息,第一层级id为固定值-1
onChangeSelect: PropTypes.func,
};
ThemeCatalog.defaultProps = {
pageHeight: '100%',
pageWidth: 300,
inputPlaceholder: '请输入主题名称',
onlyPublished: false
};
export default ThemeCatalog;
index.less
@charset "UTF-8";
/* @describe: 主题目录
* @author: sgjy
* @date: 2023/4/10 14:52
*/
.theme-catalog-component {
position: relative;
height: 100%;
min-height: 300px;
padding-right: 20px;
border-right: 1px solid rgba(0, 0, 0, 0.08);
&-search {
display: flex;
align-content: space-between;
height: 40px;
margin-bottom: 16px;
&-update {
margin-left: 12px;
}
}
&-tree {
height: calc(100% - 76px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
&:hover {
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
}
&-empty {
line-height: 35px;
text-align: center;
font-size: 14px;
color: #ccc;
}
&-tips {
display: flex;
&-item {
display: flex;
align-content: space-between;
justify-content: center;
line-height: 20px;
height: 20px;
flex: 1;
font-size: 14px;
img {
width: 20px;
padding-right: 5px;
}
}
}
.ehome-admin-tree-switcher {
line-height: 40px;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper {
height: 40px;
line-height: 40px;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper .ehome-admin-tree-iconEle {
height: 40px;
line-height: 40px;
}
.ehome-admin-tree-treenode-selected {
background: rgba(7,166,240,0.16);
color: #07A6F0;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper.ehome-admin-tree-node-selected {
background-color: transparent;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper:hover {
background-color: transparent;
}
.ehome-admin-tree .ehome-admin-tree-treenode:hover {
background: rgba(7,166,240,0.16);
color: #07A6F0;
}
}
svg.js
const themeSvg = () => (
<svg t="1681367321335" className="icon" fill="currentColor" viewBox="0 0 1024 1024" version="1.1"
p-id="12896"
width="1em" height="1em">
<path
d="M948.736 320h-246.272v-235.52c0-47.104-26.112-84.48-73.216-84.48H97.28C50.176 0 0 37.376 0 84.48v531.968c0 47.104 50.176 86.016 97.28 86.016h222.72v233.472c0 47.104 49.664 86.528 96.768 86.528h531.968c47.104 0 73.728-39.424 73.728-86.528v-532.48c0-46.592-26.624-83.456-73.728-83.456zM72.704 629.76V72.704H629.76v248.32H414.72c-45.568 0-93.696 35.84-93.696 81.408V629.76H72.704z m565.76-245.76v254.464H384V384h254.464zM947.2 947.2H396.8v-246.272h234.496c45.056 0 69.632-37.376 69.632-81.92V396.288H947.2V947.2z"
p-id="12897"></path>
</svg>
);
const businessSvg = () => (
<svg t="1681368042447" className="icon" viewBox="0 0 1024 1024" version="1.1"
p-id="655"
fill="currentColor"
width="17px" height="17px">
<path
d="M 341.333 298.667 c 0 25.6 -17.0667 42.6667 -42.6667 42.6667 H 213.333 c -25.6 0 -42.6667 -17.0667 -42.6667 -42.6667 V 213.333 c 0 -25.6 17.0667 -42.6667 42.6667 -42.6667 h 85.3333 c 25.6 0 42.6667 17.0667 42.6667 42.6667 v 85.3333 Z M 853.333 298.667 c 0 25.6 -21.3333 42.6667 -42.6667 42.6667 h -85.3333 c -21.3333 0 -42.6667 -17.0667 -42.6667 -42.6667 V 213.333 c 0 -25.6 21.3333 -42.6667 42.6667 -42.6667 h 85.3333 c 21.3333 0 42.6667 17.0667 42.6667 42.6667 v 85.3333 Z M 341.333 810.667 c 0 21.3333 -17.0667 42.6667 -42.6667 42.6667 H 213.333 c -25.6 0 -42.6667 -21.3333 -42.6667 -42.6667 v -85.3333 c 0 -21.3333 17.0667 -42.6667 42.6667 -42.6667 h 85.3333 c 25.6 0 42.6667 21.3333 42.6667 42.6667 v 85.3333 Z M 853.333 810.667 c 0 21.3333 -21.3333 42.6667 -42.6667 42.6667 h -85.3333 c -21.3333 0 -42.6667 -21.3333 -42.6667 -42.6667 v -85.3333 c 0 -21.3333 21.3333 -42.6667 42.6667 -42.6667 h 85.3333 c 21.3333 0 42.6667 21.3333 42.6667 42.6667 v 85.3333 Z"
p-id="656"></path>
<path d="M 725.333 298.667 v 426.667 H 298.667 V 298.667 h 426.667 m 42.6667 -42.6667 H 256 v 512 h 512 V 256 Z"
p-id="657"></path>
<path
d="M 640 597.333 c 0 25.6 -17.0667 42.6667 -42.6667 42.6667 h -170.667 c -25.6 0 -42.6667 -17.0667 -42.6667 -42.6667 v -170.667 c 0 -25.6 17.0667 -42.6667 42.6667 -42.6667 h 170.667 c 25.6 0 42.6667 17.0667 42.6667 42.6667 v 170.667 Z"
p-id="658"></path>
</svg>
);
const entitySvg = () => (
<svg t="1681366289899" fill="currentColor" className="icon" viewBox="0 0 1024 1024" version="1.1"
width="1em" height="1em"
p-id="4856">
<path
d="M934.4 236.8C908.8 224 563.2 32 531.2 12.8 518.4 0 512 6.4 499.2 12.8c-12.8 12.8-384 211.2-403.2 217.6-19.2 6.4-25.6 25.6-25.6 38.4v480c0 6.4 6.4 19.2 12.8 25.6 12.8 6.4 76.8 38.4 147.2 83.2 102.4 57.6 236.8 134.4 268.8 147.2 6.4 6.4 12.8 6.4 19.2 6.4 6.4 0 12.8 0 19.2-6.4 12.8-6.4 44.8-25.6 204.8-115.2 83.2-44.8 166.4-96 179.2-102.4 19.2-12.8 32-19.2 32-44.8V256c0-6.4-6.4-12.8-19.2-19.2z m-51.2 505.6c-25.6 19.2-281.6 160-332.8 185.6V505.6h6.4c12.8-6.4 57.6-32 115.2-64 115.2-64 204.8-115.2 230.4-128v422.4M172.8 256c38.4-19.2 294.4-153.6 332.8-185.6 32 19.2 345.6 192 345.6 192h6.4s-6.4 0-6.4 6.4c-32 12.8-294.4 153.6-332.8 179.2-44.8-19.2-281.6-147.2-345.6-192 0 6.4 0 6.4 0 0m313.6 256v422.4c-51.2-25.6-313.6-172.8-352-198.4V320c0-6.4 6.4 0 12.8 0 38.4 19.2 307.2 172.8 339.2 192z"
p-id="4857" />
</svg>
);
export {
themeSvg,
entitySvg,
businessSvg
};
4.组件说明
参数说明:
ThemeCatalog.propTypes = {
// 页面布局宽度
pageWidth: PropTypes.oneOfType([
PropTypes.string, PropTypes.number
]),
// 页面布局高度
pageHeight: PropTypes.oneOfType([
PropTypes.string, PropTypes.number
]),
// 输入框placeholder
inputPlaceholder: PropTypes.string,
// 是否只包含已发布节点, true是,false否
onlyPublished: PropTypes.oneOf([true, false]),
// 获取点击之后的内容,会返回对应点击数据中的除子层级之外的所有后台接口返回的信息,第一层级id为固定值-1
onChangeSelect: PropTypes.func,
};
其中
onlyPublished: 这个参数是后台返回数据判断用的,可根据自身情况删减。如果你也有和读者一样的需求你可以用这个参数来返回节点层级,需要找后台配合。
onChangeSelect:需要传入一个函数,会返回树节点点击之后的对应节点内容。
index.js文件说明:
注意:红线对应官方文档自行查阅是用来干什么的:可能导致你对接后台的接口之后,树节点显示不出来。
其中有一些方法要对应的修改为这里的字段值,否则可能导致功能错误。
icon:你想修改对应ui前面的icon,那么你要对应修改引入进来的svg.js中的导出的svg相关函数。
svg.js
fill=“currentColor”: 这个属性能让你鼠标hover的时候图标和文字颜色一致,否者你还得动脑筋去想图标颜色怎么修改。
index.less
.ehome-admin-tree-switcher {
line-height: 40px;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper {
height: 40px;
line-height: 40px;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper .ehome-admin-tree-iconEle {
height: 40px;
line-height: 40px;
}
.ehome-admin-tree-treenode-selected {
background: rgba(7,166,240,0.16);
color: #07A6F0;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper.ehome-admin-tree-node-selected {
background-color: transparent;
}
.ehome-admin-tree .ehome-admin-tree-node-content-wrapper:hover {
background-color: transparent;
}
.ehome-admin-tree .ehome-admin-tree-treenode:hover {
background: rgba(7,166,240,0.16);
color: #07A6F0;
}
上面这段是关于tree组件相关样式的修改,你可根据自己的需求修改。
5.组件应用
const ThemeManagement = () => {
const [check, setCheck] = useState({
id: -1
});
useEffect(() => {
console.log(check);
}, [check]);
return (
<div className={prefixCls} >
<ThemeCatalog
onChangeSelect={
(select)=>{setCheck(select)}
}
/>
</div>
);
};