Antd Desgin 穿梭框
高级用法-树穿梭框组件
默认用法
右侧并不是树组件,只是一个单纯的目标源
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和问题
- 给右边添加了初始值以后,初始值不能移动到左边
- 当在右侧搜索框搜索子项,全选父类后,其余的子类也跟随父类一起被添加到右侧穿梭框内
- 当二级子元素下还有
*
层子元素时,代码逻辑并未处理
普通用法
/* 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