Ant Design - 组件之 Tree树形控件

发布时间 2023-04-28 10:55:31作者: 上官靖宇

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>
    );
};