react的tree弹窗封装
怎么又双叒叕在写,这次比上次的好多了,上次的搜索的时候是展开拼接上级的,现在做成了精确查找,然后展示子集。点击查看以前写的内容➡️可搜索并左右切换的穿梭框
需求/思路
封装弹窗,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内容我这里没啥用,但是写了就没删