antd tree 拖拽

425 阅读4分钟

antd tree 拖动

2024-08-1614.01.08-ezgif.com-optimize.gif

说明

上面gif中拖拽接口有问题,导致刷新数据,位置出错,功能无问题
项目使用React,使用antd的tree。功能有:拖拽,右键新增/编辑/删除,末级没有新增功能

右键功能

  • 方法1:
    使用Tree 组件的titleRender属性
const titleRender = (nodeData: any) => {
let isLast = false;
if (nodeData?.children == 0 || nodeData?.children == undefined) {
        isLast = true;
}

return (
        <div
                onClick={(e) => onSelect(e, nodeData)} // 处理点击事件
                // onContextMenu={()=>setVisible(true)}
                // onContextMenu={(e) => handleContextMenu(e, node)} // 处理右键菜单事件
        >
                <Dropdown
                        onVisibleChange={(visible) => setVisible(visible)}
                        overlay={TreeMenu(nodeData, isLast)}
                        trigger={['contextMenu']}
                >
                        <div>{nodeData.title}</div>
                </Dropdown>
        </div>
    );
};

问题:我点击右键使用Dropdown,点击后弹出弹窗功能,阻止冒泡后无法关闭Dropdown

  • 方法2: 使用TreehandleRightClick方法
const [pageX, setPageX] = useState(0);
const [pageY, setPageY] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [nodeData, setNodeData] = useState<any>();

const handleRightClick = ({ event, node }: any) => {
        event.stopPropagation();
        setPageX(event.pageX);
        setPageY(event.pageY);
        setShowMenu(true);
        setNodeData(node);
};

const renderMenu = () => {
    if (pageX && pageY) {
            let isLast = false;
            if (nodeData?.children == 0 || nodeData?.children == undefined) {
                    isLast = true;
            }
            return (
                    <div
                        tabIndex={-1}
                        style={{
                                display: showMenu ? 'inherit' : 'none',
                                position: 'fixed',
                                left: pageX - 16,
                                top: pageY + 8,
                        }}
                        ref={dropdownElement}
                        onBlur={(e) => {
                                e.stopPropagation();
                                setShowMenu(false);
                        }}
                    >
                        {TreeMenu(nodeData, isLast)}
                    </div>
            );
    }
    return null;
};

const TreeMenu = (data, type) => {
    return (
    <div>
            <Menu>
                    <Menu.Item>
                            <AddTree obj={data} customerId={props?.searchParams?.customerId} handleRefresh={handleRefresh}>
                                    <div style={{ padding: '5px 10px' }}>编辑</div>
                            </AddTree>
                    </Menu.Item>
                    {!type && (
                            <Menu.Item>
                                    <AddTree
                                            obj={{ parentId: data?.value }}
                                            customerId={props?.searchParams?.customerId}
                                            handleRefresh={handleRefresh}
                                    >
                                            <div
                                                    style={{ padding: '5px 10px' }}
                                                    // onClick={(e) => handleTree('add', data, e)}
                                            >
                                                    新增子级
                                            </div>
                                    </AddTree>
                            </Menu.Item>
                    )}
                    <Menu.Item>
                            <div style={{ padding: '5px 10px' }} onClick={(e) => handleTree('del', data, e)}>
                                    删除
                            </div>
                    </Menu.Item>
            </Menu>
    </div>
    );
    };

点击其他内容关闭右击后的Menu内容

const modalRef = useRef<any>(null)
// 处理点击事件,判断点击是否在弹窗外部
const handleClickOutside = (event) => {
    if (modalRef.current && !modalRef.current.contains(event.target)) {
            closeModal();
    }
};
const closeModal = ()=>{
     setShowMenu(false)
}
// 添加和移除事件监听器
useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => {
                document.removeEventListener('mousedown', handleClickOutside);
        };
}, []);
        
<div className={styles.content} ref={modalRef}>
    <Tree>
        xxxxx
    </Tree>
</div>

样式问题

公司项目比较久,使用的antd版本是4.x版本。2个项目一个4.16.6,一个4.22.8。
4.16.6版本无拖拽最左侧icon。

完整代码

import React, { useEffect, useRef, useState } from "react";
import styles from './tree.less'
import AddTree from "@/app/merchandise/view/new-21-12/merchant-goods/merchant-products/addTree";
import { DownOutlined, DragOutlined, UpOutlined } from "@ant-design/icons";
import { deepCopy } from "@/utils/tools";
import { Dropdown, Menu, message, Modal, Tree } from "antd";
import { DataNode } from "antd/es/tree";
import { reqAndRespHandle } from "@/utils/resp";
import { SysClassificationAPI } from "@/app/merchandise/api/sys-classification";
import { getLoginUser } from "@/common/auth";
import 'antd/dist/antd.css';


    const TreeContent = (props)=>{
    const [expandedKeys, setExpandedKeys] = useState([]);
    const [visible, setVisible] = useState(false);
    const [treeData, setTreeData] = useState([]);
    const [selectedKeys, setSelectedKeys] = useState([]);
    const [pageX, setPageX] = useState(0);
    const [pageY, setPageY] = useState(0);
    const [showMenu, setShowMenu] = useState(false);
    const dropdownElement: React.RefObject<HTMLDivElement> = useRef(null);
    const [nodeData, setNodeData] = useState<any>();
    const modalRef = useRef<any>(null)
    useEffect(() => {
            if (!!props?.searchParams?.customerId) {
                    getListData(props?.searchParams?.customerId);
            }
    }, [props?.searchParams?.customerId]);
    // 处理点击事件,判断点击是否在弹窗外部
    const handleClickOutside = (event) => {
            if (modalRef.current && !modalRef.current.contains(event.target)) {
                    closeModal();
            }
    };
    const closeModal = ()=>{
            setShowMenu(false)
    }
    // 添加和移除事件监听器
    React.useEffect(() => {
            document.addEventListener('mousedown', handleClickOutside);
            return () => {
                    document.removeEventListener('mousedown', handleClickOutside);
            };
    }, []);
    const getListData = async (id) => {
            const { success, result } = await reqAndRespHandle(SysClassificationAPI.findAllByParentId, {
                    customerId: id,
                    // classType: '0',
                    parentId: '0',
            });
            if (success) {
                    const item = renameSubProperties(
                            renameSubProperties(renameSubProperties(result, 'sunList', 'children'), 'className', 'title'),
                            'id',
                            'value',
                    );
                    const newItem = traverseArray(item, 'key', 'value');
                    setTreeData(newItem);
            }
    };
    const traverseArray = (arr, keyName, valueKey) => {
            arr.forEach((item) => {
                    if (Array.isArray(item)) {
                            traverseArray(item, keyName, valueKey);
                    } else if (typeof item === 'object') {
                            const value = item[valueKey];
                            if (value !== undefined) {
                                    item[keyName] = value;
                            }
                            traverseArray(Object.values(item), keyName, valueKey);
                    }
            });
            return arr;
    };
    /**
     * @description: 递归修改参数名
     * @param {*} obj 内容
     * @param {*} oldName 原参数名
     * @param {*} newName 新参数名
     */
    const renameSubProperties = (obj, oldName, newName) => {
            if (typeof obj !== 'object' || obj === null) {
                    return obj;
            }
            let hasRename = false;
            const newObj = Array.isArray(obj)
                    ? obj.map((item) => {
                                    if (typeof item === 'object' && item !== null) {
                                            const itemNew = renameSubProperties(item, oldName, newName);
                                            if (itemNew !== item) {
                                                    hasRename = true;
                                            }
                                            return itemNew;
                                    } else {
                                            return item;
                                    }
                      })
                    : Object.entries(obj).reduce((acc, [key, value]: any) => {
                                    const newKey = !!value.length && key.toLowerCase() === oldName.toLowerCase() ? newName : key;
                                    const newValue =
                                            typeof value === 'object' && value !== null
                                                    ? renameSubProperties(value, oldName, newName)
                                                    : value;
                                    if (newValue !== value || newKey !== key) {
                                            hasRename = true;
                                            acc[newKey] = newValue;
                                    } else {
                                            acc[key] = value;
                                    }
                                    return acc;
                      }, {});
            // 判断是否需要再次递归处理子集
            if (hasRename) {
                    return newObj;
            } else {
                    return newObj;
            }
    };

    const onDragEnter = (info) => {};
    const onDrop = async (info) => {
            const dragNode = info.dragNode;
            const dropNode = info.node;
            if (dropNode.parentId != dragNode.parentId && dropNode.key != dragNode.parentId) {
                    // console.log('提示---不可以不同父级进行拖拽');
                    message.error('不可以不同父级进行拖拽');
                    return;
            }

            let clodata = deepCopy(treeData);
            const loopSplice = async (data) => {
                    let cIndex = data.findIndex((o: any) => o.key == dragNode.key);
                    let cItem = data.find((o: any) => o.key == dragNode.key);
                    //   console.log(cIndex, "===cIndex==");
                    //   console.log(info.dropPosition, "===info.dropPosition==");
                    //   console.log(cIndex >= info.dropPosition ? info.dropPosition : info.dropPosition - 1, "===info.dropPosition  tow==");
                    data.splice(cIndex, 1);
                    if (!info.dropToGap || info.dropPosition == -1) {
                            data.splice(0, 0, cItem);
                    } else {
                            data.splice(cIndex >= info.dropPosition ? info.dropPosition : info.dropPosition - 1, 0, cItem);
                    }
                    const { success } = await reqAndRespHandle(SysClassificationAPI.changeClassSort, {
                            customerId: props?.searchParams?.customerId,
                            id: dropNode.value,
                            oldSort: cIndex + 1,
                            newSort: info?.dropPosition,
                    });
                    if (success) {
                            // getListData(props?.searchParams?.customerId);
                            message.success('操作成功');
                    }
            };
            const loop = (data) => {
                    if (dragNode.parentId == 0) {
                            loopSplice(data);
                            return;
                    }
                    for (const iterator of data) {
                            if (iterator.key == dragNode.parentId) {
                                    loopSplice(iterator.children);
                                    return;
                            }
                            if (iterator?.children) loop(iterator?.children);
                    }
            };
            loop(clodata);
            setTreeData(() => clodata);
    };
    const handleTree = (type, data, e) => {
            e?.stopPropagation();
            if (type == 'del') {
                    Modal.confirm({
                            title: '删除',
                            content: '确定删除?',
                            onOk: async () => {
                                    const { success } = await reqAndRespHandle(SysClassificationAPI.delClass, {
                                            customerId: props?.searchParams?.customerId,
                                            id: data.key,
                                    });
                                    if (success) {
                                            message.success('操作成功');
                                            getListData(props?.searchParams?.customerId);
                                    }
                            },
                    });
            }
    };
    const onSelect = (key, { node }) => {
            const isLeaf = !node.children || node.children.length === 0;
            if (isLeaf) {
                    setSelectedKeys(key);
                    props.onSelect(key[0]);
            } else {
                    setSelectedKeys([]);
            }
    };
    // const onSelect = (e, data) => {
    // 	e?.stopPropagation();
    // 	if (!data?.haveChildren) {
    // 		if (data.key == selectedKeys[0]) {
    // 			setSelectedKeys([]);
    // 			props.onSelect(undefined);
    // 		} else {
    // 			setSelectedKeys([data?.key]);
    // 			props.onSelect(data?.key);
    // 		}
    // 	} else {
    // 		setSelectedKeys([]);
    // 	}
    // };
    const TreeMenu = (data, type) => {
            return (
                    <div>
                            <Menu>
                                    <Menu.Item>
                                            <AddTree obj={data} customerId={props?.searchParams?.customerId} handleRefresh={handleRefresh}>
                                                    <div style={{ padding: '5px 10px' }}>编辑</div>
                                            </AddTree>
                                    </Menu.Item>
                                    {!type && (
                                            <Menu.Item>
                                                    <AddTree
                                                            obj={{ parentId: data?.value }}
                                                            customerId={props?.searchParams?.customerId}
                                                            handleRefresh={handleRefresh}
                                                    >
                                                            <div
                                                                    style={{ padding: '5px 10px' }}
                                                                    // onClick={(e) => handleTree('add', data, e)}
                                                            >
                                                                    新增子级
                                                            </div>
                                                    </AddTree>
                                            </Menu.Item>
                                    )}
                                    <Menu.Item>
                                            <div style={{ padding: '5px 10px' }} onClick={(e) => handleTree('del', data, e)}>
                                                    删除
                                            </div>
                                    </Menu.Item>
                            </Menu>
                    </div>
            );
    };
    const titleRender = (nodeData: any) => {
            let isLast = false;
            if (nodeData?.children == 0 || nodeData?.children == undefined) {
                    isLast = true;
            }

            return (
                    <div
                            onClick={(e) => onSelect(e, nodeData)} // 处理点击事件
                            // onContextMenu={()=>setVisible(true)}
                            // onContextMenu={(e) => handleContextMenu(e, node)} // 处理右键菜单事件
                    >
                            <Dropdown
                                    onVisibleChange={(visible) => setVisible(visible)}
                                    overlay={TreeMenu(nodeData, isLast)}
                                    trigger={['contextMenu']}
                            >
                                    <div>{nodeData.title}</div>
                            </Dropdown>
                    </div>
            );
    };
    const handleRefresh = () => {
            getListData(props?.searchParams?.customerId);
    };
    const handleRightClick = ({ event, node }: any) => {
            event.stopPropagation();
            setPageX(event.pageX);
            setPageY(event.pageY);
            setShowMenu(true);
            setNodeData(node);
    };
    const renderMenu = () => {
            if (pageX && pageY) {
                    let isLast = false;
                    if (nodeData?.children == 0 || nodeData?.children == undefined) {
                            isLast = true;
                    }
                    return (
                            <div
                                    tabIndex={-1}
                                    style={{
                                            display: showMenu ? 'inherit' : 'none',
                                            position: 'fixed',
                                            left: pageX - 16,
                                            top: pageY + 8,
                                    }}
                                    ref={dropdownElement}
                                    onBlur={(e) => {
                                            e.stopPropagation();
                                            setShowMenu(false);
                                    }}
                            >
                                    {TreeMenu(nodeData, isLast)}
                            </div>
                    );
            }
            return null;
    };
    return (
            <div className={styles.content} ref={modalRef}>
                    <div className={styles.container}>
                            <Tree
                                    // className="draggable-tree"
                                    selectedKeys={selectedKeys}
                                    // expandedKeys={expandedKeys}
                                    draggable
                                    blockNode
                                    onDragEnter={onDragEnter}
                                    onDrop={onDrop}
                                    // titleRender={titleRender}
                                    treeData={treeData}
                                    onRightClick={handleRightClick}
                                    // loadData={onLoadData}
                                    onSelect={onSelect}
                            />
                    </div>
                    <AddTree customerId={props?.searchParams?.customerId} handleRefresh={handleRefresh}>
                            <div className={styles.footer}>新增顶级分类</div>
                    </AddTree>
                    {renderMenu()}
            </div>
    );
    }


export default TreeContent;