根据上一篇目标一,进一步实现树的穿梭框
头部搜索功能有点问题,处理bug 在文末,咨询可私
主要内容:
基于ant-design树的穿梭框,实现穿梭后右侧是已选树,(当前antd右侧只有一个层级)
理想的树的穿梭框:
左边是完整的树,右边是已选的树,左边已选穿梭到右边,左边已选的消失,右边增加已选,右边也可以勾选然后穿梭回去左边,左边出现右边消失。
目标1:右边是已选的树,左边已选穿梭到右边,左边树不变可以继续操作,右边只可以看结果
目标2:右边是已选的树,左边已选穿梭到右边,左边已选的消失掉,右边可以选择穿梭回去
主要核心办法:左边不展示已被穿梭的数据(不改变左边的原数组,采用隐藏的方式,已经被穿梭的或者子集全部被穿梭的不展示)
目标2:目前效果
步骤一:把目标一的搜索进行优化
改动代码:
onSearch={(dir, val) => {
if (dir === 'left') { // 左边搜索
if (val !== '') {
const newDeptList = this.onsearchDataSource(dataSource, val);
this.setState({ newDataSource: newDeptList });
} else {
this.setState({ newDataSource: dataSource });// 左边没搜索时候的原始数据
}
} else { //右边搜索
if (val !== '') {
const newDeptList = this.onsearchDataSource(rightDataSource, val);
this.setState({ newRightDataSource: newDeptList });
} else {
this.setState({ newRightDataSource: rightDataSource }); // 右边没搜索时候的原始数据
}
}
}}
// 树搜索函数
onsearchDataSource(dataSource, val) {
// 递归查找存在的父级
const deepFilter = (i, val) => {
return (
i.children.filter((o) => {
if (o.label.indexOf(val) > -1) {
return true;
}
if (o.children && o.children.length > 0) {
return deepFilter(o, val);
}
}).length > 0
);
};
const filterMenu = (list, val) => {
return list
.filter((item) => {
if (item.label.indexOf(val) > -1) {
return true;
}
if (item.children && item.children.length > 0) {
return deepFilter(item, val);
}
return false;
})
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = item.children.filter((res) => res.label.indexOf(val) > -1);
filterMenu(item.children, val);
}
return item;
});
};
return filterMenu(dataSource, val);
}
步骤二:把目标onCheck函数进行更新
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
if (_.length > 0) {
this.setState({ currTargetKeys: lodash.union(targetKeys, checkedChildKeys) });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
// 增加随机数 为了可以触发向右穿梭的按钮 触发后删除随机数
onItemSelect('DEL' + Math.random(), true);
} else {
// 加上如果勾来勾去又没勾的情况
this.setState({ currTargetKeys: targetKeys });
}
}}
左边的tree :
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...currTargetKeys];
return newDataSource.length > 0 ? (
<Tree
blockNode
showLine
defaultExpandParent={true}
disabled={disabled}
checkable
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
if (_.length > 0) {
this.setState({ currTargetKeys: lodash.union(targetKeys, checkedChildKeys) });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
// 增加随机数 为了可以触发向右穿梭的按钮 触发后删除随机数
onItemSelect('DEL' + Math.random(), true);
} else {
this.setState({ currTargetKeys: targetKeys });
}
}}>
{generateTree(newDataSource, targetKeys, searchValue, disabled, 'left')}
</Tree>
) : (
<div></div>
);
}
右边的tree :
// 右边从纯展示变成了可更改 所以增加已选的操作
if (direction === 'right') {
const checkedKeys = [...rightCurrTargetKeys];
return newRightDataSource.length > 0 ? (
<Tree
blockNode
showLine
checkable
defaultExpandAll={true}
disabled={disabled}
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
// 勾选后更改左侧已选定内容
this.setState({ currTargetKeys: lodash.xor(checkedChildKeys, targetKeys) });
// 设置右侧已勾选框
this.setState({ rightCurrTargetKeys: checkedChildKeys });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
}}>
{generateTree(newRightDataSource, targetKeys, searchValue, false, 'right')}
</Tree>
) : (
<div></div>
);
}
步骤三:generateTree展示的更新
目标一:左边没变
目标二:左边减少上次已穿梭的内容,右边增加新穿梭的内容,
// 不改变左边的原数组,采用隐藏的方式,已经被穿梭的或者子集全部被穿梭的不展示label
const generateTree = (treeNodes = [], checkedKeys = [], searchValue, disabled = false, type = 'left') => {
return treeNodes.map(({ children, ...props }) => {
if (type === 'left') {
// 是否子元素都被选择了 如果是则父不展示了
const isAll =
children.length > 0
? lodash.every(children, (e) => {
return checkedKeys.includes(e.key);
})
: false;
// 自己是否被穿梭了 穿梭了则不展示了 !checkedKeys.includes(props.key)
return !isAll && !checkedKeys.includes(props.key) ? (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over'>{props.label}</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
) : (
''
);
} else {
// 右边的展示内容
return (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over'>
{props.label}
</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
);
}
});
};
TreeTransfer 整个组件代码
import React, { Component } from 'react';
import './index.less';
import { Transfer, Tree, Tooltip } from 'antd';
import lodash from 'lodash';
const { TreeNode } = Tree;
const isChecked = (selectedKeys, eventKey) => {
return selectedKeys.indexOf(eventKey) !== -1;
};
const generateTree = (treeNodes = [], checkedKeys = [], searchValue, disabled = false, type = 'left') => {
return treeNodes.map(({ children, ...props }) => {
if (type === 'left') {
// 是否子元素都被选择了
const isAll =
children.length > 0
? lodash.every(children, (e) => {
return checkedKeys.includes(e.key);
})
: false;
return !isAll && !checkedKeys.includes(props.key) ? (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over'>{props.label}</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
) : (
''
);
} else {
return (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over' style={{ color: props.label.indexOf(searchValue) >= 0 ? 'red' : '' }}>
{props.label}
</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
);
}
});
};
class TreeTransfer extends Component {
constructor(props, context) {
super(props, context);
this.stores = this.props.UserMgtMod;
this.state = {
searchValue: null,
transferDataSource: [],
currTargetKeys: [],
rightCurrTargetKeys: [],
defaultExpandAll: true,
newDataSource: [],
newRightDataSource: [],
};
}
componentDidMount() {
const { dataSource, targetKeys, rightDataSource } = this.props;
this.flatten(dataSource);
this.setState({
currTargetKeys: targetKeys,
newDataSource: dataSource,
newRightDataSource: rightDataSource,
});
}
UNSAFE_componentWillReceiveProps(nextprops) {
const { dataSource, targetKeys, rightDataSource } = nextprops;
this.setState({
currTargetKeys: targetKeys,
newDataSource: dataSource,
newRightDataSource: rightDataSource,
});
}
flatten(list = []) {
const dataSource = this.state.transferDataSource;
list.forEach((item) => {
dataSource.push(item);
this.setState({ transferDataSource: dataSource });
this.flatten(item.children);
});
}
onChange(e) {
this.setState({
searchValue: e.target.value,
});
}
onsearchDataSource(dataSource, val) {
// 递归查找存在的父级
const deepFilter = (i, val) => {
return (
i.children.filter((o) => {
if (o.label.indexOf(val) > -1) {
return true;
}
if (o.children && o.children.length > 0) {
return deepFilter(o, val);
}
}).length > 0
);
};
const filterMenu = (list, val) => {
return list
.filter((item) => {
if (item.label.indexOf(val) > -1) {
return true;
}
if (item.children && item.children.length > 0) {
return deepFilter(item, val);
}
return false;
})
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = item.children.filter((res) => res.label.indexOf(val) > -1);
filterMenu(item.children, val);
}
return item;
});
};
return filterMenu(dataSource, val);
}
render() {
const { dataSource, targetKeys, rightDataSource, disabled, ...restProps } = this.props;
const { transferDataSource, searchValue, currTargetKeys, rightCurrTargetKeys, newDataSource, newRightDataSource } = this.state;
return (
<div>
<Transfer
{...restProps}
disabled={disabled}
targetKeys={currTargetKeys}
dataSource={transferDataSource}
className='tree-transfer'
render={(item) => item.label}
showSearch
showSelectAll={false}
onSearch={(dir, val) => {
if (dir === 'left') {
if (val !== '') {
const newDeptList = this.onsearchDataSource(dataSource, val);
this.setState({ newDataSource: newDeptList });
} else {
this.setState({ newDataSource: dataSource });
}
} else {
if (val !== '') {
const newDeptList = this.onsearchDataSource(rightDataSource, val);
this.setState({ newRightDataSource: newDeptList });
} else {
this.setState({ newRightDataSource: rightDataSource });
}
}
}}>
{({ direction, onItemSelect, selectedKeys }) => {
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...currTargetKeys];
return newDataSource.length > 0 ? (
<Tree
blockNode
showLine
defaultExpandParent={true}
disabled={disabled}
checkable
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
if (_.length > 0) {
this.setState({ currTargetKeys: lodash.union(targetKeys, checkedChildKeys) });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
// 增加随机数 为了可以触发向右穿梭的按钮 触发后删除随机数
onItemSelect('DEL' + Math.random(), true);
} else {
this.setState({ currTargetKeys: targetKeys });
}
}}>
{generateTree(newDataSource, targetKeys, searchValue, disabled, 'left')}
</Tree>
) : (
<div></div>
);
}
if (direction === 'right') {
const checkedKeys = [...rightCurrTargetKeys];
return newRightDataSource.length > 0 ? (
<Tree
blockNode
showLine
checkable
defaultExpandAll={true}
disabled={disabled}
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
// 勾选后更改左侧已选定内容
this.setState({ currTargetKeys: lodash.xor(checkedChildKeys, targetKeys) });
// 设置右侧已勾选框
this.setState({ rightCurrTargetKeys: checkedChildKeys });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
}}>
{generateTree(newRightDataSource, targetKeys, searchValue, false, 'right')}
</Tree>
) : (
<div></div>
);
}
}}
</Transfer>
</div>
);
}
}
export default TreeTransfer;
组件引用案例:
<Modal
width={1000}
title={'所属角色'}
className='user-modal'
visible={this.state.roleModal}
onOk={() => {
this.setState({ roleModal: false, rightDataSource: [] });
}}
onCancel={() => {
this.setState({ roleModal: false, rightDataSource: [] });
}}>
<Spin spinning={roleLoading}>
{this.state.roleModal && (
<TreeTransfer
disabled={'' + title === 'detail' ? true : false}
dataSource={roleList} // 原始树结构
rightDataSource={rightDataSource}
targetKeys={roleTargetKeys}
onChange={this.handleChange.bind(this)}
/>
)}
</Spin>
</Modal>
关键函数
handleChange = (newTargetKeys) => {
const targetKeys = newTargetKeys.reduce((arr, e) => {
// 增加随机数 为了可以触发向右穿梭的按钮 触发后删除随机数
if (e.indexOf('DEL') < 0) {
arr.push(e);
}
return arr;
}, []);
// 递归查找是否存在包含选中key的父级
const deepFilter = (i, arr) => {
return (
i.children.filter((o) => {
if (arr.includes(o.key)) {
return true;
}
if (o.children && o.children.length > 0) {
return deepFilter(o, arr);
}
}).length > 0
);
};
// 找到一个存在key的父级 然后遍历获取层级内容
const filterMenu = (list, arr) => {
return list
.filter((item) => {
if (arr.includes(item.key)) {
return true;
}
if (item.children && item.children.length > 0) {
return deepFilter(item, arr);
}
return false;
})
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = filterMenu(item.children, arr);
}
return item;
});
};
if (this.state.roleModal) {
const { roleList } = toJS(this.stores.state);
this.setState({ roleTargetKeys: [...new Set(targetKeys)] }); // 去重
const rightDataSource = filterMenu(roleList, targetKeys); // 查询组装已选树
this.stores.saveInfoModal({ roleIds: targetKeys });
this.props.form.setFieldsValue({ roleIds: targetKeys });
this.setState({ rightDataSource: rightDataSource || [] });
}
};
触发方法:
<div style={{ display: 'flex' }}>
<YxButton
type='primary'
style={{ marginRight: '10px' }}
onClick={() => {
this.setState({ roleModal: true });
setTimeout(() => {
this.handleChange(this.state.roleTargetKeys);
}, 100);
}}>
{'' + title === 'detail' ? '查看' : '请选择'}
</YxButton>
已选{this.state.roleTargetKeys.length}项
</div>
目标二就完成了
处理已勾选的显示值bug
初始化
this.state = {
rightCheckLength: 0, // 记录右边已勾
leftCheckLength: 0, // 记录左边已勾
};
}
重置
UNSAFE_componentWillReceiveProps(nextprops) {
// 穿梭到右边更新后重置
this.setState({
leftCheckLength: 0,
rightCheckLength: 0,
});
}
自行定义title
<Transfer
// 自行定义title
titles={[`${leftCheckLength}/${dataSourceLength.length - targetKeys.length}项`, `${rightCheckLength}/${targetKeys.length}项`]}
>
设置勾选
// 左边
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
// 设置勾选
this.setState({ leftCheckLength: checkedChildKeys.length });
// 右边
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
this.setState({ currTargetKeys: lodash.xor(checkedChildKeys, targetKeys) });
完整代码:
import React, { Component } from 'react';
import './index.less';
import { Transfer, Tree, Tooltip } from 'antd';
import lodash from 'lodash';
const { TreeNode } = Tree;
const isChecked = (selectedKeys, eventKey) => {
return selectedKeys.indexOf(eventKey) !== -1;
};
const generateTree = (treeNodes = [], checkedKeys = [], searchValue, disabled = false, type = 'left') => {
return treeNodes.map(({ children, ...props }) => {
if (type === 'left') {
// 是否子元素都被选择了
const isAll =
children && children.length > 0
? lodash.every(children, (e) => {
return checkedKeys.includes(e.key);
})
: false;
return !isAll && !checkedKeys.includes(props.key) ? (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over'>{props.label}</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
) : (
''
);
} else {
return (
<TreeNode
{...props}
disabled={disabled}
key={props.key}
title={
<Tooltip placement='topLeft' title={props.label}>
<span className='title-over' style={{ color: props.label.indexOf(searchValue) >= 0 ? 'red' : '' }}>
{props.label}
</span>
</Tooltip>
}>
{generateTree(children, checkedKeys, searchValue, disabled, type)}
</TreeNode>
);
}
});
};
class TreeTransfer extends Component {
constructor(props, context) {
super(props, context);
this.stores = this.props.UserMgtMod;
this.state = {
searchValue: null,
transferDataSource: [],
currTargetKeys: [],
rightCurrTargetKeys: [],
defaultExpandAll: true,
newDataSource: [],
newRightDataSource: [],
rightCheckLength: 0,
leftCheckLength: 0,
dataSourceLength: 0,
};
}
componentDidMount() {
const { dataSource, targetKeys, rightDataSource } = this.props;
this.flatten(dataSource);
this.setState({
currTargetKeys: targetKeys,
newDataSource: dataSource,
newRightDataSource: rightDataSource,
});
this.getChildrenLength(dataSource);
}
UNSAFE_componentWillReceiveProps(nextprops) {
const { dataSource, targetKeys, rightDataSource } = nextprops;
this.setState({
leftCheckLength: 0,
rightCheckLength: 0,
});
// this.flatten(nextprops.dataSource);
this.setState({
currTargetKeys: targetKeys,
newDataSource: dataSource,
newRightDataSource: rightDataSource,
});
}
// 获取子数量 用在勾选是左上角的显示
getChildrenLength(dataSource) {
const getTail = (item) => (item.children && item.children.length > 0 ? item.children.map((m) => getTail(m)) : [item]);
const result = lodash.flattenDeep(dataSource.map((m) => getTail(m)));
this.setState({ dataSourceLength: result.length });
}
flatten(list = []) {
const dataSource = this.state.transferDataSource;
list.forEach((item) => {
dataSource.push(item);
this.setState({ transferDataSource: dataSource });
this.flatten(item.children);
});
}
onChange(e) {
this.setState({
searchValue: e.target.value,
});
}
onsearchDataSource(dataSource, val) {
// 递归查找存在的父级
const deepFilter = (i, val) => {
return (
i.children.filter((o) => {
if (o.label.indexOf(val) > -1) {
return true;
}
if (o.children && o.children.length > 0) {
return deepFilter(o, val);
}
}).length > 0
);
};
const filterMenu = (list, val) => {
return list
.filter((item) => {
if (item.label.indexOf(val) > -1) {
return true;
}
if (item.children && item.children.length > 0) {
return deepFilter(item, val);
}
return false;
})
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = item.children.filter((res) => res.label.indexOf(val) > -1);
filterMenu(item.children, val);
}
return item;
});
};
return filterMenu(dataSource, val);
}
render() {
const { dataSource, targetKeys, rightDataSource, disabled, ...restProps } = this.props;
const { leftCheckLength, rightCheckLength, transferDataSource, searchValue, currTargetKeys, rightCurrTargetKeys, newDataSource, newRightDataSource, dataSourceLength } = this.state;
return (
<div>
<Transfer
{...restProps}
disabled={disabled}
targetKeys={currTargetKeys}
dataSource={transferDataSource}
className='tree-transfer'
render={(item) => item.label}
showSearch
titles={[`${leftCheckLength}/${dataSourceLength - targetKeys.length}项`, `${rightCheckLength}/${targetKeys.length}项`]}
showSelectAll={false}
onSearch={(dir, val) => {
if (dir === 'left') {
if (val !== '') {
const newDeptList = this.onsearchDataSource(dataSource, val);
this.setState({ newDataSource: newDeptList });
} else {
this.setState({ newDataSource: dataSource });
}
} else {
if (val !== '') {
const newDeptList = this.onsearchDataSource(rightDataSource, val);
this.setState({ newRightDataSource: newDeptList });
} else {
this.setState({ newRightDataSource: rightDataSource });
}
}
}}>
{({ direction, onItemSelect, selectedKeys }) => {
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...currTargetKeys];
return newDataSource.length > 0 ? (
<Tree
blockNode
showLine
defaultExpandParent={true}
disabled={disabled}
checkable
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
this.setState({ leftCheckLength: checkedChildKeys.length });
if (_.length > 0) {
this.setState({ currTargetKeys: lodash.union(targetKeys, checkedChildKeys) });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
// 增加随机数 为了可以触发向右穿梭的按钮 触发后删除随机数
onItemSelect('DEL' + Math.random(), true);
} else {
this.setState({ currTargetKeys: targetKeys });
}
}}>
{generateTree(newDataSource, targetKeys, searchValue, disabled, 'left')}
</Tree>
) : (
<div></div>
);
}
if (direction === 'right') {
const checkedKeys = [...rightCurrTargetKeys];
return newRightDataSource.length > 0 ? (
<Tree
blockNode
showLine
checkable
defaultExpandAll={true}
disabled={disabled}
checkedKeys={checkedKeys}
onCheck={(_, even) => {
const {
checkedNodes,
node: {
props: { eventKey },
},
} = even;
// 筛选出最底层的子集 集合
const checkedChildKeys = checkedNodes.reduce((arr, e) => {
if (e.props.children.length <= 0) {
arr.push(e.key);
}
return arr;
}, []);
this.setState({ currTargetKeys: lodash.xor(checkedChildKeys, targetKeys) });
this.setState({ rightCheckLength: checkedChildKeys.length, rightCurrTargetKeys: checkedChildKeys });
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
}}>
{generateTree(newRightDataSource, targetKeys, searchValue, false, 'right')}
</Tree>
) : (
<div></div>
);
}
}}
</Transfer>
</div>
);
}
}
export default TreeTransfer;