Ant+React树形穿梭框实现(支持搜索和全选)

1.效果图

image.png

2.官方组件的示例不是很全面,而且右边不是树形的;直接上源码,我的是ts的语法,参考的原文章是js语法

//组件代码
import React, { useState,forwardRef,useImperativeHandle } from 'react'
import { Transfer, Tree,Modal } from 'antd'
import { DataNode } from 'antd/es/tree'
import './chooseUserTransfer.less'
const AuthorizationModal = (props: any, ref: any) => {
    const [targetKeys, setTargetKeys] = useState<any>([])
    const [rightTreeData, setRightTreeData] = useState([])
    const [treeData,setTreeData]=useState([])
    const generateTree = (treeNodes: DataNode[] = [], checkedKeys: string[] = []): DataNode[] =>
        treeNodes.map(({ children, ...props }:any) => ({
            ...props,
            disabled: checkedKeys.includes(props.key as string),
            children: generateTree(children, checkedKeys),
        }))
 
    const dealCheckboxSeleted = ({ node, onItemSelect, onItemSelectAll }:any, direction:any) => {
        let {
            checked,
            halfCheckedKeys,
            node: { key, children },
        } = node
        // 勾选的是父节点
        if (children?.length > 0) {
            let keys: any[] = []
            let temp = []
            if (direction === 'left') {
                let state = false
                if (rightTreeData.length > 0) {
                    rightTreeData?.map((item:any) => {
                        if (item?.childCompanies?.length > 0 && (item.key == key)) {
                            temp = item.childCompanies.filter((v:any) => !item.childCompanies.some(((t:any) => t.key === v.key)))
                            temp?.forEach((child:any) => {
                                keys.push(child.key)
                            })
                        } else {
                            state = true
                        }
                    })
                } else {
                    state = true
                }
                if (state) {
                    children?.forEach((child:any) => {
                        keys.push(child.key)
                    })
                }
                onItemSelectAll([...keys, key], checked)
            }
            if (direction === 'right') {
                children?.forEach((child:any) => {
                    keys.push(child.key)
                })
                onItemSelectAll([...keys], checked)
            }
        } else {
            // 勾选的是子节点
            if (!checked) {
                // 查找该元素的父元素
                let parentKeys = []
                parentKeys = [halfCheckedKeys?.[0]] || []
                if (parentKeys[0] == undefined) {
                    // 当一级下的二级全部取消勾选,一级也取消勾选
                    treeData.forEach((tree:any) => {
                        if (tree.children) {
                            tree.children?.forEach((child:any) => {
                                if (child?.key === key) {
                                    parentKeys.push(tree?.key)
                                }
                            })
                        }
                    })
                }
                onItemSelectAll([...parentKeys, key], checked)
            } else {
                let parentKey:any =''
                treeData.forEach((tree:any) => {
                    if (tree?.children) {
                        tree.children?.forEach((child:any) => {
                            if (child?.key === key) {
                                parentKey = tree?.key
                            }
                        })
                    }
                })
 
                if (!halfCheckedKeys?.includes(parentKey) && parentKey != '') {
                    onItemSelectAll([key, parentKey], checked)
                } else {
                    onItemSelect(key, checked)
                }
            }
        }
    }
    
    // modal组件
    const [type, setType] = useState(1);//1选择用户2选择用户组
    const [isModalOpen, setIsModalOpen] = useState(false);
    const handleOk = () => {
        let result:any = []
        rightTreeData.forEach((ele:any)=>{
          result.push({
            key:ele.key,
            title:ele.title,
          })
          ele.children.forEach((eles:any)=>{
            result.push({
              key:eles.key,
              title:eles.title,
            })
          })
        })
        result=result.filter((ele: any) =>targetKeys.includes(ele.key)&&ele.key.split('_').length>2)
        props.onChildData(type, result)
        setIsModalOpen(false);
      };
      const handleCancel = () => {
        setIsModalOpen(false);
      };
      useImperativeHandle(ref, () => ({
        // changeVal 就是暴露给父组件的方法
        // changeVal: () => {
        //     setIsModalOpen(true);
    
        // }
        showModal
    
      }));
      
    const showModal = (type: number, data: any, rightData: []) => {
        setIsModalOpen(true);
        setType(type)
        setTreeData(data)
        let keyArr:any=[]
        rightData.forEach((ele:any)=>{
            keyArr.push(ele.key)
            ele.children.forEach((eles:any)=>{
                keyArr.push(eles.key)
            })
        })
        setTargetKeys(keyArr)
        setRightTreeData(rightData)
    };
    const TreeTransfer = ({ dataSource, targetKeys, ...restProps }:any) => {
        const transferDataSource:any = []
        const dataSourceData = dataSource
        let rightTreeDataResult = [...rightTreeData]
        function flatten(list = []) {
            list.forEach((item:any) => {
                transferDataSource.push(item)
                flatten(item.children)
            })
        }
        flatten(dataSource)
        return ( 
            <Transfer
                {...restProps}
                targetKeys={targetKeys}
                dataSource={transferDataSource}
                className="tree-transfer"
                showSearch
                showSelectAll={true}
                render={item => item.title}
                rowKey={record => record.key}
                // 搜索功能逻辑
                onSearch={(dir, val) => {
                    let data = (dir === 'left' ? dataSourceData : rightTreeData)
                    // 1.先遍历二级,过滤出搜索对应的数据;
                    // 2.如果二级有数据,过滤出二级的companyName和一级的companyName
                    // 3.最后把一级不符合搜索对应的值过滤
                    const newDeptList = data?.map((item:any) => {
                        item = Object.assign({}, item)
                        if (item.children) {
                            item.children = item.children?.filter((res:any) => (res.title.indexOf(val) > -1))
                        }
                        return item
                    }).filter((item:any) => {
                        if (item.children?.length > 0 || val.length == 0) {
                            item = Object.assign({}, item)
                            item.children?.filter((e:any) => (
                                e.title.indexOf(val) > -1 ? '' : item.title.indexOf(val) > -1
                            ))
                        } else {
                            item = item.title.indexOf(val) > -1
                        }
                        return item
                    })
                    if (dir === 'left') {
                        dataSource = newDeptList
                    }
                    if (dir === 'right') {
                        rightTreeDataResult = newDeptList
                    }
                }}
            >
                {({ direction, onItemSelect, onItemSelectAll, selectedKeys }) => {
                    if (direction === 'left') {
                        const checkedKeys = [...selectedKeys, ...targetKeys]
                        return (
                            <Tree
                                blockNode
                                checkable
                                checkedKeys={checkedKeys}
                                defaultExpandedKeys={checkedKeys}
                                treeData={generateTree(dataSource, targetKeys)}
                                fieldNames={{ title: 'title', key: 'key', children: 'children' }}
                                onCheck={(_, node) => {
                                    dealCheckboxSeleted({ node, onItemSelect, onItemSelectAll }, direction)
                                }}
                                onSelect={(_, node) => {
                                    dealCheckboxSeleted({ node, onItemSelect, onItemSelectAll }, direction)
                                }}
                            />
                        )
                    }
                    if (direction === 'right') {
                        //defaultExpandAll 默认展开全部
                        const checkedKeys = [...selectedKeys]
                        return (
                            <Tree
                                blockNode
                                checkable
                                checkedKeys={checkedKeys}
                                treeData={rightTreeDataResult}
                                fieldNames={{ title: 'title', key: 'key', children: 'children' }}
                                onCheck={(_, node) => {
                                    dealCheckboxSeleted({ node, onItemSelect, onItemSelectAll }, direction)
                                }}
                                onSelect={(_, node) => {
                                    dealCheckboxSeleted({ node, onItemSelect, onItemSelectAll }, direction)
                                }}
                            />
                        )
                    }
                }}
            </Transfer>
        )
    }
 
    /**
     * 改变右边tree数据
     * @param {*右边tree需要处理的keys集合} keys 
     * @param {*0-删除以上的keys 1-新增以上的keys} type
     */
    const getRightTreeData = (keys:any, type:any) => {
        let arr:any = [...rightTreeData]
        if (keys?.length > 0) {
            keys.forEach((key:any) => {
                treeData.forEach((data:any) => {
                    if (key === data.key) {
                        // 勾选的是父节点,查看右侧是否有勾选对象
                        let index = arr.findIndex((i:any) => {
                            return i.key === key
                        })
                        if (type === 1) {
                            if (index === -1) {
                                arr.push(data)
                            } else if (index > -1 && arr?.[index]?.children?.length < data?.children?.length) {
                                // 先选择子项再勾选该父级时,传过来的keys是 ['0-1-0','0-1'],此时第一次循环已经将该父级放到arr中,
                                // 再遍历0-1时,需要先删除再将全部的children复制
                                arr.splice(index, 1)
                                arr.push(data)
                            }
                        } else if (type === 0) {
                            if (index > -1) {
                                arr.splice(index, 1)
                            }
                        }
                    } else {
                        // 勾选的是子节点
                        // 左侧数据处理
                        let selectedParentKey = '' //选定的父项id
                        let selectedObj:any = {}       //选定对象
                        if (data?.children?.length > 0) {
                            data.children.forEach((child:any) => {
                                if (key === child.key) {
                                    selectedParentKey = data.key
                                    selectedObj = child
                                }
                            })
                        }
                        // 右侧数据处理
                        if (Object.keys(selectedObj)?.length > 0) {
                            let newData:any = {}
                            // 查看右侧是否有选中子项的父项
                            let index = arr.findIndex((item:any) => {
                                return item.key === selectedParentKey
                            })
                            if (index > -1) {
                                // 右侧已有选中子项的父项,selectedIndex查看右侧子项是否有勾选对象
                                let oldChildArr = [...arr[index].children]
                                let selectedIndex = oldChildArr?.findIndex(o => {
                                    return o.key === selectedObj.key
                                })
                                if (selectedIndex === -1 && type === 1) {
                                    arr[index].children.push(selectedObj)
                                }
                                if (selectedIndex > -1 && type === 0) {
                                    arr[index].children.splice(selectedIndex, 1)
                                    if (arr[index].children?.length === 0) {
                                        arr.splice(index, 1)
                                    }
                                }
                            } else {
                                // 右侧没有选中子项的父项
                                if (type === 1) {
                                    newData = { ...data }
                                    newData.children = []
                                    newData.children.push(selectedObj)
                                    arr.push(newData)
                                } else if (type === 0) {
                                    arr = []
                                }
                            }
                        }
                    }
                })
            })
            setRightTreeData(arr)
        }
    }
 
    // 左右移动按钮
    const onChange = (keys:any, direction:any, moveKeys:any) => {
        let changeArrType = 1 // 0-删除  1-新增
        if (direction == 'left') {
            changeArrType = 0
            if (keys.length > 0) {
                treeData.forEach((item:any) => {
                    let index = keys.indexOf(item.key)
                    if (index > -1 && item.children?.length > 0) {
                        item.children?.forEach((v:any) => {
                            if (moveKeys.includes(v.key)) {
                                keys.splice(index, 1)
                            }
                        })
                    }
                })
            }
        }
        setTargetKeys(keys)
        let keysList = changeArrType === 1 ? keys : moveKeys
        getRightTreeData(keysList, changeArrType)
    };
 
    return  <Modal width={600} title={type == 1 ? '选择用户' : '选择设备'} open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
       <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} /> </Modal>
}
 
export default forwardRef(AuthorizationModal)

3.如何引入使用代码

import ChooseUserTreeTransfer from "../../components/Common/Add/chooseUserTransfer"
//方法
const childRefUser = useRef<any>(null)
const eventClick=()=>{
 childRefUser.current.showModal()
}
const getChildDataUser = (type: number, data: any) => {
}
//组件使用
 <ChooseUserTreeTransfer ref={childRefUser} onChildData={getChildDataUser}></ChooseUserTreeTransfer>

备注
参考地址:https://blog.csdn.net/weixin_49581008/article/details/128953065

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容