【Ant-Desgin-React 穿梭框】表格穿梭框,树穿梭框逻辑代码(只能包含不含子类的子类)

高级用法-树穿梭框组件

默认用法

右侧并不是树组件,只是一个单纯的目标源 Id 数组

/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
import React, { useState } from 'react';
import { theme, Transfer, Tree } from 'antd';
// Customize Table Transfer
const isChecked = (selectedKeys, eventKey) => selectedKeys.includes(eventKey);
const generateTree = (treeNodes = [], checkedKeys = []) =>
  treeNodes.map(({ children, ...props }) => ({
    ...props,
    disabled: checkedKeys.includes(props.key),
    children: generateTree(children, checkedKeys),
  }));
const TreeTransfer = ({ dataSource, targetKeys = [], ...restProps }) => {
  const { token } = theme.useToken();
  const transferDataSource = [];
  function flatten(list = []) {
    list.forEach((item) => {
      transferDataSource.push(item);
      flatten(item.children);
    });
  }
  flatten(dataSource);
  return (
    <Transfer
      {...restProps}
      targetKeys={targetKeys}
      dataSource={transferDataSource}
      className="tree-transfer"
      render={(item) => item.title}
      showSelectAll={false}
    >
      {({ direction, onItemSelect, selectedKeys }) => {
        if (direction === 'left') {
          const checkedKeys = [...selectedKeys, ...targetKeys];
          return (
            <div
              style={{
                padding: token.paddingXS,
              }}
            >
              <Tree
                blockNode
                checkable
                defaultExpandAll
                checkedKeys={checkedKeys}
                treeData={generateTree(dataSource, targetKeys)}
                onCheck={(_, { node: { key } }) => {
                  onItemSelect(key, !isChecked(checkedKeys, key));
                }}
                onSelect={(_, { node: { key } }) => {
                  onItemSelect(key, !isChecked(checkedKeys, key));
                }}
              />
            </div>
          );
        }
      }}
    </Transfer>
  );
};
const treeData = [
  {
    key: '0-0',
    title: '0-0',
  },
  {
    key: '0-1',
    title: '0-1',
    children: [
      {
        key: '0-1-0',
        title: '0-1-0',
      },
      {
        key: '0-1-1',
        title: '0-1-1',
      },
    ],
  },
  {
    key: '0-2',
    title: '0-2',
  },
  {
    key: '0-3',
    title: '0-3',
  },
  {
    key: '0-4',
    title: '0-4',
  },
];
const NewTransfer = () => {
  const [targetKeys, setTargetKeys] = useState([]);
  const onChange = (keys) => {
    setTargetKeys(keys);
  };
  return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
};
export default NewTransfer;

在这里插入图片描述

改良后

TreeTransfer.jsx 树穿梭框组件

/* eslint-disable no-undef */
/* eslint-disable react/prop-types */
/* eslint-disable no-unused-vars */
import React, { useState } from 'react'
import { Transfer, Tree } from 'antd'

const UserTransfer = () => {
  const [targetKeys, setTargetKeys] = useState([])
  const [rightTreeData, setRightTreeData] = useState([])
  const treeData = [
    {
      key: '0-0',
      title: '0-0'
    },
    {
      key: '0-1',
      title: '0-1',
      children: [
        {
          key: '0-1-0',
          title: '0-1-0'
        },
        {
          key: '0-1-1',
          title: '0-1-1'
        }
      ]
    },
    {
      key: '0-2',
      title: '0-2'
    },
    {
      key: '0-3',
      title: '0-3'
    },
    {
      key: '0-4',
      title: '0-4'
    }
  ]
  
  // generateTree函数用于生成一颗新的树
  // treeNodes参数是原始树节点数组,checkedKeys是需要被标记为禁用的节点的key的集合
  const generateTree = (treeNodes = [], checkedKeys = []) =>
    treeNodes.map(({ children, ...props }) => ({
      // 对每个节点及其子节点应用map函数
      ...props, // 拷贝当前节点的所有属性(除children)
      disabled: checkedKeys.includes(props.key), // 判断当前节点的key是否在checkedKeys中,若在则设置disabled为true
      children: generateTree(children, checkedKeys) // 递归处理所有子节点
    }))

  const dealCheckboxSeleted = ({ node, onItemSelect, onItemSelectAll }, direction) => {
    let {
      checked,
      halfCheckedKeys,
      node: { key, children }
    } = node
    // 勾选的是父节点
    if (children?.length > 0) {
      let keys = []
      let temp = []
      if (direction === 'left') {
        let state = false
        if (rightTreeData.length > 0) {
          // rightTreeData?.map(item => {
          //   if (item.childCompanies?.length > 0 && item.key == key) {
          //     temp = childCompanies.filter(v => !item.childCompanies.some(t => t.key === v.key))
          //     temp?.forEach(child => {
          //       keys.push(child.key)
          //     })
          //   } else {
          //     state = true
          //   }
          // })
        } else {
          state = true
        }
        if (state) {
          children?.forEach(child => {
            keys.push(child.key)
          })
        }
        onItemSelectAll([...keys, key], checked)
      }
      if (direction === 'right') {
        children?.forEach(child => {
          keys.push(child.key)
        })
        onItemSelectAll([...keys], checked)
      }
    } else {
      // 勾选的是子节点
      if (!checked) {
        // 查找该元素的父元素
        let parentKeys = []
        parentKeys = [halfCheckedKeys?.[0]] || []
        if (parentKeys[0] == undefined) {
          // 当一级下的二级全部取消勾选,一级也取消勾选
          treeData.forEach(tree => {
            if (tree.children) {
              tree.children?.forEach(child => {
                if (child?.key === key) {
                  parentKeys.push(tree?.key)
                }
              })
            }
          })
        }
        onItemSelectAll([...parentKeys, key], checked)
      } else {
        let parentKey = ''
        treeData.forEach(tree => {
          if (tree?.children) {
            tree.children?.forEach(child => {
              if (child?.key === key) {
                parentKey = tree?.key
              }
            })
          }
        })

        if (!halfCheckedKeys?.includes(parentKey) && parentKey != '') {
          onItemSelectAll([key, parentKey], checked)
        } else {
          onItemSelect(key, checked)
        }
      }
    }
  }

  const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
    const transferDataSource = []
    const dataSourceData = dataSource
    let test = [...rightTreeData]
    function flatten(list = []) {
      list.forEach(item => {
        transferDataSource.push(item)
        flatten(item.children)
      })
    }
    flatten(dataSource)


    console.log(transferDataSource, test, "test为右侧树结构数据(带父元素)");
    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 => {
              item = Object.assign({}, item)
              if (item.children) {
                item.children = item.children?.filter(res => res.title.indexOf(val) > -1)
              }
              return item
            })
            .filter(item => {
              if (item.children?.length > 0 || val.length == 0) {
                item = Object.assign({}, item)
                item.children?.filter(e => (e.title.indexOf(val) > -1 ? '' : item.title.indexOf(val) > -1))
              } else {
                item = item.title.indexOf(val) > -1
              }
              return item
            })
          console.log(newDeptList, 165)
          if (dir === 'left') {
            dataSource = newDeptList
          }
          if (dir === 'right') {
            test = newDeptList
          }
        }}
      >
        {({ direction, onItemSelect, onItemSelectAll, selectedKeys }) => {
          if (direction === 'left') {
            const checkedKeys = [...selectedKeys, ...targetKeys]
            return (
              <Tree
                blockNode
                checkable
                defaultExpandAll
                checkedKeys={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') {
            const checkedKeys = [...selectedKeys]
            return (
              <Tree
                blockNode
                checkable
                defaultExpandAll
                checkedKeys={checkedKeys}
                treeData={test}
                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, type) => {
    let arr = [...rightTreeData]
    if (keys?.length > 0) {
      keys.forEach(key => {
        treeData.forEach(data => {
          if (key === data.key) {
            // 勾选的是父节点,查看右侧是否有勾选对象
            let index = arr.findIndex(i => {
              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 = {} //选定对象
            if (data?.children?.length > 0) {
              data.children.forEach(child => {
                if (key === child.key) {
                  selectedParentKey = data.key
                  selectedObj = child
                }
              })
            }
            // 右侧数据处理
            if (Object.keys(selectedObj)?.length > 0) {
              let newData = {}
              // 查看右侧是否有选中子项的父项
              let index = arr.findIndex(item => {
                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, direction, moveKeys) => {
    let changeArrType = 1 // 0-删除  1-新增
    if (direction == 'left') {
      changeArrType = 0
      if (keys.length > 0) {
        treeData.forEach(item => {
          let index = keys.indexOf(item.key)
          if (index > -1 && item.children?.length > 0) {
            item.children?.forEach(v => {
              if (moveKeys.includes(v.key)) {
                keys.splice(index, 1)
              }
            })
          }
        })
      }
    }
    setTargetKeys(keys)
    let keysList = changeArrType === 1 ? keys : moveKeys
    getRightTreeData(keysList, changeArrType)
  }

  return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />
}

export default UserTransfer

在这里插入图片描述
Tree 参考文章: https://blog.csdn.net/weixin_49581008/article/details/128953065

改良后的Bug和问题

  1. 给右边添加了初始值以后,初始值不能移动到左边
  2. 当在右侧搜索框搜索子项,全选父类后,其余的子类也跟随父类一起被添加到右侧穿梭框内
  3. 当二级子元素下还有 * 层子元素时,代码逻辑并未处理

普通用法

/* eslint-disable no-unused-vars */
import React, { useEffect, useState } from 'react'
import { Space, Transfer } from 'antd'

// Antd的穿梭框组件Mock数据
const mockData = Array.from({
  length: 20
}).map((_, i) => ({
  key: i.toString(),
  title: `content${i + 1}`,
  description: `description of content${i + 1}`,
  disabled: i % 3 < 1 // 禁用某项
}))

// 筛选出ID数组
const initialTargetKeys = mockData.filter(item => Number(item.key) > 10).map(item => item.key)

const App = () => {
  // 设置目标键数组
  const [targetKeys, setTargetKeys] = useState(initialTargetKeys)
  // 设置选中的键数组
  const [selectedKeys, setSelectedKeys] = useState([])

  useEffect(() => {
    console.log('模拟数据', mockData)
  }, [])

  const onChange = (nextTargetKeys, direction, moveKeys) => {
    console.log('==========Start Change==========')
    console.log('targetKeys:', nextTargetKeys) // 下一次的目标键数组,即移动后的目标列表
    console.log('direction:', direction) // 移动的方向,可以是'left'或'right',表示从左侧列表移动到右侧列表或从右侧列表移动到左侧列表
    console.log('moveKeys:', moveKeys) // 移动的键数组,即移动的项
    console.log('==========End Change==========')
    setTargetKeys(nextTargetKeys)
  }

  const onSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
    console.log('==========Start SelectChange==========')
    console.log('sourceSelectedKeys:', sourceSelectedKeys) // 源列表中选中的键数组
    console.log('targetSelectedKeys:', targetSelectedKeys) // 目标列表中选中的键数组
    console.log('==========End SelectChange==========')
    setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys])
  }

  const onScroll = (direction, e) => {
    console.log('==========Start Scroll==========')
    console.log('direction:', direction) // 滚动的方向,可以是'left'或'right',表示向左滚动或向右滚动
    console.log('target:', e.target) // 滚动事件对象,包含了滚动的相关信息,如滚动的目标等
    console.log('==========End Scroll==========')
  }

  console.log('==========Start Search==========')
  const handleSearch = (dir, value) => {
    // dir 表示搜索框所在的列表,可以是'left'或'right',表示在源列表或目标列表中搜索\
    // value 表示搜索框中的值
    console.log('search:', dir, value)
  }
  console.log('==========End Search==========')

  return (
    <div className="App">
      <Space>
        <Transfer
          dataSource={mockData} // 数据源,即需要在两个列表之间移动的数据列表
          titles={['Source', 'Target']} // 列表的标题,包括源列表和目标列表的标题
          targetKeys={targetKeys} // 目标列表中的键数组,表示当前已经选中的项的键数组
          selectedKeys={selectedKeys} // 当前选中的项的键数组,用于在两个列表之间移动项时的状态管理
          onChange={onChange} // 当目标列表中的键数组改变时触发的事件回调函数
          onSelectChange={onSelectChange} // 当源列表和目标列表中的选中项改变时触发的事件回调函数
          onScroll={onScroll} // 当滚动时触发的事件回调函数
          onSearch={handleSearch} // 当搜索框中的值改变时触发的事件回调函数
          render={item => item.title} // 定义如何渲染每个数据项,返回一个React元素
          oneWay // 是否只允许从左侧列表向右侧列表移动数据,默认为false
          showSearch // 是否显示搜索框,默认为false
          pagination // 是否显示分页,默认为false,一般在大数据量下使用
        />
        {/* 自定义状态 */}
        <Transfer status="error" />
        <Transfer status="warning" showSearch />
      </Space>
    </div>
  )
}

export default App

在这里插入图片描述

高级用法-表格穿梭框组件

/* eslint-disable react/prop-types */
/* eslint-disable no-unused-vars */
import React, { useState } from 'react'
import { Space, Switch, Table, Tag, Transfer } from 'antd'

// leftColumns 表示左侧表格的列,rightColumns表示右侧表格的列,restProps表示其他属性
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }) => (
  // 渲染Transfer组件
  <Transfer {...restProps}>
    {({
      direction, // 数据传输方向(左或右)
      filteredItems, // 经过搜索过滤后的项
      onItemSelect, // 选中项时的回调函数
      onItemSelectAll, // 全选项时的回调函数
      selectedKeys: listSelectedKeys, // 已选中项的键数组
      disabled: listDisabled // 列表是否被禁用的标志
    }) => {
      const columns = direction === 'left' ? leftColumns : rightColumns // 根据传输方向选择表格列
      const rowSelection = {
        getCheckboxProps: () => ({
          disabled: listDisabled // 设置复选框是否禁用
        }),
        onChange(selectedRowKeys) {
          onItemSelectAll(selectedRowKeys, 'replace') // 全选/取消全选时的操作
        },
        selectedRowKeys: listSelectedKeys, // 已选中项的键数组
        selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE] // 表格行选择器
      }
      return (
        <Table
          rowSelection={rowSelection} // 表格行选择器配置
          columns={columns} // 表格列配置
          dataSource={filteredItems} // 数据源
          size="small" // 表格尺寸
          style={{
            pointerEvents: listDisabled ? 'none' : undefined // 根据列表是否禁用设置CSS样式
          }}
          onRow={({ key, disabled: itemDisabled }) => ({
            // 表格行的事件处理函数
            onClick: () => {
              if (itemDisabled || listDisabled) {
                // 如果项被禁用或列表被禁用,则不执行操作
                return
              }
              onItemSelect(key, !listSelectedKeys.includes(key)) // 选中/取消选中项时的操作
            }
          })}
        />
      )
    }}
  </Transfer>
)

const mockTags = ['cat', 'dog', 'bird'] // 模拟标签数据
const mockData = Array.from({
  // 生成模拟数据
  length: 20
}).map((_, i) => ({
  key: i.toString(), // 唯一键
  title: `content${i + 1}`, // 标题
  description: `description of content${i + 1}`, // 描述
  tag: mockTags[i % 3] // 标签
}))
// 表格列配置
const columns = [
  {
    dataIndex: 'title',
    title: 'Name'
  },
  {
    dataIndex: 'tag',
    title: 'Tag',
    render: tag => (
      <Tag
        style={{
          marginInlineEnd: 0
        }}
        color="cyan"
      >
        {tag.toUpperCase()}
      </Tag>
    )
  },
  {
    dataIndex: 'description',
    title: 'Description'
  }
]
const Default = () => {
  const [targetKeys, setTargetKeys] = useState([]) // 目标键数组的状态及其更新函数
  const [disabled, setDisabled] = useState(false) // 禁用状态及其更新函数
  const onChange = nextTargetKeys => {
    // 目标键数组变化时的处理函数
    setTargetKeys(nextTargetKeys) // 更新目标键数组
  }
  const toggleDisabled = checked => {
    // 切换禁用状态的处理函数
    setDisabled(checked) // 更新禁用状态
  }
  return (
    <>
      <TableTransfer // 表格数据传输组件
        dataSource={mockData} // 数据源
        targetKeys={targetKeys} // 目标键数组
        disabled={disabled} // 是否禁用
        showSearch // 是否显示搜索框
        showSelectAll={false} // 是否显示全选按钮
        onChange={onChange} // 目标键数组变化时的回调函数
        filterOption={(
          inputValue,
          item // 自定义搜索过滤函数
        ) => item.title.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1}
        leftColumns={columns} // 左侧表格列配置
        rightColumns={columns} // 右侧表格列配置
      />
      <Space
        style={{
          marginTop: 16
        }}
      >
        {/* 开关组件,用于切换禁用状态 */}
        <Switch unCheckedChildren="disabled" checkedChildren="disabled" checked={disabled} onChange={toggleDisabled} />
      </Space>
    </>
  )
}
export default Default

在这里插入图片描述

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星光菌子

你真是个富哥

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值