antd tree 拖动
说明
上面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:
使用
Tree中handleRightClick方法
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;