react的tree弹窗封装

181 阅读4分钟

react的tree弹窗封装

ezgif.com-optimize.gif

怎么又双叒叕在写,这次比上次的好多了,上次的搜索的时候是展开拼接上级的,现在做成了精确查找,然后展示子集。点击查看以前写的内容➡️可搜索并左右切换的穿梭框

需求/思路

封装弹窗,2个tab栏内容并展示选择数量,每个tab里有tree,可勾选/取消,可搜索,右侧展示内容。
每次搜索后都不知道该怎么展示,也没看到合适的案例。我的思路就搜索的时候就精准搜索,找到搜索的列,并展示。

代码

弹窗内容:

import React, { useState, useRef, useEffect } from 'react';
import DragableModal from '@/components/common/draggable-modal';
import { Button, Radio, Tabs } from "antd";
import Content from './components/content'
import { SysClassificationAPI } from "@/app/goods/service/sys-classification";
import styles from './index.less'

const CategoryModal = ({ visible, onClose, onOk,value,data }:any) => {
    const [change, setChange] = useState(1);
    const [activeKey, setActiveKey] = useState('2');
    // const standardRef:any = useRef({});
    // const customRef:any = useRef({});
    const standardRef:any = useRef(null);
    const customRef:any = useRef(null);
    const [contentData, setContentData] = useState({
        standardArr:{},
        customArr:{},
    });
    const [tabNum, setTabNum] = useState({
        standardNum: 0,
        customNum: 0,
    });
    useEffect(() => {
        //根据分类id组数据
        if(value){
          let standardArr=[], customArr=[]
          if(value){
            value.forEach(element => {
              if(element.classType == 1){
                standardArr.push(element)
              }else{
                customArr.push(element)
              }
            });
          }
          setTabNum({
              standardNum: standardArr.length,
              customNum: customArr.length,
          });
          setContentData({
              standardArr,
              customArr,
          });
          standardRef.current?.setValueFn(standardArr);
          customRef.current?.setValueFn(customArr);
        }
    }, [visible]);
    const handleSub = () => {
        const standardArr = standardRef.current?.getTableData()?.lowestNodes||[];
        const customArr = customRef.current?.getTableData()?.lowestNodes||[];
        onOk([...standardArr, ...customArr]);
    };
    const tab = [
        {
            label: !!tabNum.customNum ? (
                <div>
                    标准分类( 已选 <span style={{ color: 'red' }}>{tabNum.customNum}</span>)
                </div>
            ) : (
                '标准分类'
            ),
            key: '2',
            children: (
                <Content
                    value={contentData.customArr}
                    searchApi={SysClassificationAPI.findAllByParentId}
                    searchParams={{ classType: '0', parentId: 0 }}
                    jumpUrl="goods.sys-classification"
                    title="商品分类"
                    onClose={onClose}
                    change={change}
                    ref={customRef}
                    // ref={(ref) => (customRef.current[data] = ref)}
                    setNum={(e) => setTabNum({ ...tabNum, customNum: e })}
                    tips="暂未设置自定义分类,请设置后再进行选择"
                />
            ),
        },
        {
            label: !!tabNum.standardNum ? (
                <div>
                    自定义分类( 已选 <span style={{ color: 'red' }}>{tabNum.standardNum}</span>)
                </div>
            ) : (
                '自定义分类'
            ),
            key: '1',
            children: (
                <Content
                    value={contentData.standardArr}
                    searchApi={SysClassificationAPI.findAllByParentId}
                    searchParams={{ classType: '1', parentId: 0 }}
                    jumpUrl="goods.sys-classification"
                    title="商品分类"
                    onClose={onClose}
                    ref={standardRef}
                    // ref={(ref) => (standardRef.current[data] = ref)}
                    change={change}
                    setNum={(e) => setTabNum({ ...tabNum, standardNum: e })}
                    tips="暂未设置标准分类,请设置后再进行选择"
                />
            ),
        },
    ];
    return (
        <>
            <DragableModal
                width={1000}
                wrapClassName="modal-5px-padding"
                visible={visible}
                title="选择商品分类"
                centered
                maskClosable={false}
                onCancel={onClose}
                footer={null}
            >
                <div className={styles.category}>
                    <Tabs activeKey={activeKey} onChange={(e) => setActiveKey(e)}>
                        {tab.map((v) => (
                            <Tabs.TabPane key={v.key} tab={v.label} tabKey={v.key}>
                                <div>{v.children}</div>
                            </Tabs.TabPane>
                        ))}
                    </Tabs>
                    <div className={styles.footer}>
                        <div>
                            {/* <Radio.Group value={change} onChange={(e) => setChange(e.target.value)}>
                                <Radio value={1}>叠加选择</Radio>
                                <Radio value={2}>替换选择</Radio>
                            </Radio.Group> */}
                        </div>
                        <div>
                            <Button onClick={onClose}>取消</Button>
                            <Button onClick={handleSub} className={styles.ml20} type="primary">
                                确定
                            </Button>
                        </div>
                    </div>
                </div>
            </DragableModal>
        </>
    );
}; 




export default CategoryModal;

每个Content内容:

import React, { useEffect, useMemo, useState, useContext, forwardRef, useImperativeHandle, useRef } from 'react';
import { Button, Empty, Input, List, Space, Tree } from "antd";
import { SearchOutlined, TagOutlined, CloseOutlined } from '@ant-design/icons';
import { reqAndRespHandle } from "@/utils/tools/resp";
import { getLoginUser } from "@/configs/auth";
import { deepCopy } from "@/utils/tools";
import { TabsActionContext } from '@/layout/func-tabs-ctx';
import { BlueIcon } from '@/app/wechat/wechat-work/employees-live-code/components/choose-employee';
import styles from './index.less'


const Content = forwardRef(
    ({
        searchApi = '',
        searchParams = {},
        jumpUrl = '',
        title = '',
        onClose = () => {},
        change = 1,
        tips = '',
        setNum,
        value={},
    }: any,ref) => {
        const { optTabs } = useContext(TabsActionContext);
        const [defaultData, setDefaultData] = useState([]);
        const [treeData, setTreeData] = useState([]);
        const [inputVal, setInputVal] = useState('');
        const [tableData, setTableData] = useState<any>({ lowestNodes: [], displayNodes: [] });
        const [isSearch, setIsSearch] = useState(false);
        useEffect(() => {
            getData();
        }, [value]);
        useImperativeHandle(ref, () => ({
            getTableData: () => tableData,
            setValueFn,
        }));
        const setValueFn = async(data) => {
            if(data && JSON.stringify(data)){
              setTableData({ lowestNodes: setTreeValue(data), displayNodes: setTreeValue(data) });
            }
        };
        const setTreeValue = (data) => {
            return data.map((item) => {
                const { id } = item;
                return {
                    ...item,
                    key: id,
                };
            });
        };
        const onPressEnter = (e) => {
            if (!e.target.value) {
                setTreeData(defaultData);
            } else {
                const data = deepCopy(defaultData);
                // const filteredTree = filterTree(data, e.target.value);
                const filteredTree = findNodesWithSameName(data, e.target.value);
                setTreeData(filteredTree);
            }
        };
        // 递归遍历函数
        function findNodesWithSameName(arr, content) {
            let result = [];

            for (let i = 0; i < arr.length; i++) {
                let node = arr[i];

                if (node.name === content) {
                    result.push(node);
                }else{
                  if (node.children) {
                      let childNodes = findNodesWithSameName(node.children, content);
                      result = result.concat(childNodes);
                  }
                }

            }

            return result;
        }
        const getData = async () => {
            let params = {
                ...searchParams,
                customerId: getLoginUser().customer?.id,
            };
            const { success, result } = await reqAndRespHandle(searchApi, params);
            if (success) {
                const treeData = createTreeData(result);
                setTreeData(treeData);
                setDefaultData(treeData);
                if(value && JSON.stringify(value) !='{}'){
                  setValueFn(value)
                }
            }
        };
        /**
         * @description: 数据重命名
         * @param {*} data
         */
        const createTreeData = (data) => {
            return data.map((item) => {
                const { id, className, sunList } = item;
                return {
                    ...item,
                    key: id,
                    classType: searchParams?.classType == '1' ? 1 : 2,
                    name: className,
                    title: className,
                    children: sunList ? createTreeData(sunList) : [],
                };
            });
        };
        /**
         * @description: 选择tree
         * @param {*} checkedKeys
         * @param {*} e
         */
        const handleCheckedKeys = (checkedKeys, e) => {
            const { checkedNodesPositions = [], node, checked } = e;
            if (change == 1) {
                // 判断是否是搜索
                if (isSearch) {
                    const allNode = flatten(node);
                    const [lowestNodes, displayNodes] = allNode.reduce(
                        ([lowestNodes, displayNodes], selectedNode) => {
                            let newLowesNodes = lowestNodes;
                            let newDisplayNodes = displayNodes;
                            if (!selectedNode.children.length) {
                                newLowesNodes = [...newLowesNodes, selectedNode];
                            }
                            if (!(checkedKeys as string[]).includes(selectedNode.parentId)) {
                                newDisplayNodes = [...newDisplayNodes, selectedNode];
                            }
                            return [newLowesNodes, newDisplayNodes];
                        },
                        [[], []],
                    );
                    let newCheckedNodes = [];
                    if (checked) {
                        // 当前点击的节点被选中
                        newCheckedNodes = [...tableData.lowestNodes, ...lowestNodes];
                    } else {
                        // 当前点击的节点被取消选中
                        newCheckedNodes = tableData.lowestNodes.filter(
                            (obj1) => !lowestNodes.some((obj2) => obj1.id === obj2.id),
                        );
                    }
                    setNum && setNum(newCheckedNodes.length);
                    setTableData({ lowestNodes: newCheckedNodes, displayNodes: newCheckedNodes });
                } else {
                    const checkedNodes = checkedNodesPositions
                        .sort((a, b) => a.pos.length - b.pos.length)
                        .map(({ node }) => node);
                    const [lowestNodes, displayNodes] = checkedNodes.reduce(
                        ([lowestNodes, displayNodes], selectedNode) => {
                            let newLowesNodes = lowestNodes;
                            let newDisplayNodes = displayNodes;
                            if (!selectedNode.children.length) {
                                newLowesNodes = [...newLowesNodes, selectedNode];
                            }
                            if (!(checkedKeys as string[]).includes(selectedNode.parentId)) {
                                newDisplayNodes = [...newDisplayNodes, selectedNode];
                            }
                            return [newLowesNodes, newDisplayNodes];
                        },
                        [[], []],
                    );
                    setNum && setNum(lowestNodes.length);
                    setTableData({ lowestNodes, displayNodes });
                }
            } else {
                if (!node.children.length) {
                    setNum && setNum(1);
                    setTableData({ lowestNodes: [node], displayNodes: [node] });
                } else {
                    const allNode = flatten(node);
                    const [lowestNodes, displayNodes] = allNode.reduce(
                        ([lowestNodes, displayNodes], selectedNode) => {
                            let newLowesNodes = lowestNodes;
                            let newDisplayNodes = displayNodes;
                            if (!selectedNode.children.length) {
                                newLowesNodes = [...newLowesNodes, selectedNode];
                            }
                            if (!(checkedKeys as string[]).includes(selectedNode.parentId)) {
                                newDisplayNodes = [...newDisplayNodes, selectedNode];
                            }
                            return [newLowesNodes, newDisplayNodes];
                        },
                        [[], []],
                    );
                    setNum && setNum(lowestNodes.length);
                    setTableData({ lowestNodes, displayNodes });
                }
            }
        };
        //  扁平化数组
        const flatten = (node) => {
            return [node, ...(node.children ? node.children.flatMap(flatten) : [])];
        };
        /**
         * @description: 全部删除
         */
        const handleDel = () => {
            setNum(0);
            setTableData({ lowestNodes: [], displayNodes: [] });
        };
        /**
         * 渲染右边List的列表
         * @param item
         * @param isSelected 是否为右边选中的数据
         * @returns
         */
        const renderRigthItem = (item: any) => {
            return (
                <List.Item style={{ paddingRight: 12 }}>
                    <Space>
                        <BlueIcon icon={<TagOutlined />} />
                        <span style={{ color: '#555' }}>{item.name}</span>
                    </Space>
                    <CloseOutlined onClick={() => handleRightDelete(item)} style={{ cursor: 'pointer' }} />
                </List.Item>
            );
        };
        /**
         * 删除右侧选中的数据
         */
        const handleRightDelete = (tag: any) => {
            const data = deepCopy(tableData);
            const lowestNodes = data.lowestNodes.filter((el) => el.id !== tag.id);
            const displayNodes = deleteObjectWithId(data.displayNodes, tag.id);
            setNum && setNum(lowestNodes.length);
            setTableData({ lowestNodes, displayNodes });
        };
        /**
         * @description: 删除并删除children为空的内容
         * @param {*} objArray
         * @param {*} id
         */
        const deleteObjectWithId = (objArray, id) => {
            for (var i = 0; i < objArray.length; i++) {
                var obj = objArray[i];
                if (obj.id === id) {
                    objArray.splice(i, 1); // 删除该内容
                    i--; // 由于删除了一个元素,需要将索引减1
                    if (obj.children && obj.children.length === 0) {
                        deleteObjectWithId(objArray, obj.parentId); // 递归删除parent对象
                    }
                } else if (obj.children) {
                    deleteObjectWithId(obj.children, id); // 递归遍历children数组
                }
            }
            return objArray;
        };
        /**
         * @description: 跳转
         */
        const handleJump = () => {
            optTabs({
                tab: { path: jumpUrl, title },
                action: 'add',
            });
            // 关闭弹窗
            onClose && onClose();
        };
        return (
            <div className={styles.content}>
                <div className={styles.left}>
                    <div>
                        <Input
                            onPressEnter={(e) => {
                                setIsSearch(!!e.target.value ? true : false);
                                onPressEnter(e);
                            }}
                            onChange={(e) => setInputVal(e.target.value)}
                            prefix={<SearchOutlined />}
                            placeholder="搜索分类"
                        ></Input>
                    </div>
                    <div className={styles.leftContent}>
                        {treeData.length > 0 ? (
                            <Tree
                                checkedKeys={tableData.lowestNodes.map(({ key }) => key)}
                                checkable
                                selectable={false}
                                treeData={treeData}
                                onCheck={handleCheckedKeys}
                                className={styles.leftTree}
                            ></Tree>
                        ) : (
                            <div className={styles.leftNone}>
                                <>
                                    {isSearch && inputVal && treeData.length == 0 && <Empty description={false} />}
                                    {!isSearch && !inputVal && treeData.length == 0 && (
                                        <div className={styles.leftNoneContent}>
                                            <div>
                                                <Empty description={false} />
                                            </div>
                                            <div className={styles.mt20}>{tips}</div>
                                            <div>
                                                <Button
                                                    className={styles.mt20}
                                                    type="primary"
                                                    onClick={() => handleJump()}
                                                >
                                                    前往设置
                                                </Button>
                                            </div>
                                        </div>
                                    )}
                                </>
                            </div>
                        )}
                    </div>
                </div>
                <div className={styles.right}>
                    <div className={styles.rightTop}>
                        <div>已选择</div>
                        <div onClick={handleDel} className={styles.rightTopDel}>
                            清空
                        </div>
                    </div>
                    <div className={styles.rightContent}>
                        <>
                            {tableData.lowestNodes.length > 0 ? (
                                <>
                                    <List
                                        className={styles.rightTable}
                                        bordered
                                        dataSource={tableData.lowestNodes}
                                        renderItem={renderRigthItem}
                                    />
                                </>
                            ) : (
                                <Empty />
                            )}
                        </>
                    </div>
                </div>
            </div>
        );
    },
);

export default Content;

displayNodes内容我这里没啥用,但是写了就没删