Ant Design - 组件之 Tree树形控件
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> ); };