HTML
<template>
<div class='tree-transfer'>
<div class="left-tree">
<div class="tree-tit">{{leftTit || '左侧栏'}}</div>
<div class="list">
<el-tree
ref="treeRefL"
v-if="reLoad"
:data="leftData"
show-checkbox
default-expand-all
:node-key="nodeKey"
highlight-current
:props="defaultProps"
/>
</div>
</div>
<div class="btn-div">
<el-button :icon="Back" type="primary" :disabled="disabled" @click="toLeft()" />
<el-button :icon="Right" type="primary" :disabled="disabled" @click="toRight()" />
</div>
<div class="right-tree">
<div class="tree-tit">{{rightTit || '右侧栏'}}</div>
<div class="list">
<el-tree
ref="treeRefR"
v-if="reLoad"
:data="rightData"
show-checkbox
default-expand-all
:node-key="nodeKey"
highlight-current
:props="defaultProps"
/>
</div>
</div>
</div>
</template>
穿梭框控制逻辑
import { ref, nextTick, defineExpose } from 'vue'
import { Right, Back } from '@element-plus/icons-vue';
const props = defineProps({
nodeKey: String,
fromData: Array,
toData: Array,
defaultProps: {},
leftTit: String,
rightTit: String,
disabled: {
type: Boolean,
default: false
}
})
//定义emit
const emit = defineEmits(['checkVal'])
const treeRefL = ref([])
const treeRefR = ref([])
const leftData = ref([])
const rightData = ref([])
const reLoad = ref(true)
//右侧数据
const toData = ref([])
// 右侧需要移除的数据
const removeData = ref([])
defineExpose({
/**
* 清空数据
*/
clearData() {
toData.value = []
},
/**
* 初始化数据
*/
initData() {
const originalLeft = JSON.parse(JSON.stringify(props.fromData))
const originalRight = JSON.parse(JSON.stringify(props.fromData))
if (props.toData.length > 0) {
leftData.value = sortData(originalLeft, props.toData, 'left')
rightData.value = sortData(originalRight, props.toData, 'right')
}else{
leftData.value = originalLeft
rightData.value = []
}
}
})
//方法
//去右边
const toRight = () =>{
// 将勾选中的数据保存到toData中
const checkNodes = treeRefL.value.getCheckedNodes(false, false)
const newArr = toData.value.concat(checkNodes)
const obj = {};
const peon = newArr.reduce((cur, next) => {
obj[next[props.nodeKey]] ? "" : obj[next[props.nodeKey]] = true && cur.push(next);
return cur;
},[]) //设置cur默认类型为数组,并且初始值为空的数组
toData.value = peon
reLoad.value = false
const originalLeft = JSON.parse(JSON.stringify(props.fromData))
const originalRight = JSON.parse(JSON.stringify(props.fromData))
// 抽离出选中数据中的id
const ids = extractId(toData.value)
// 重新整理两侧树中数据
leftData.value = sortData(originalLeft, ids, 'left')
rightData.value = sortData(originalRight, ids, 'right')
nextTick(() => {
reLoad.value = true
})
checkVal()
}
//去左边
const toLeft = () =>{
// 将勾选中的数据保存到toData中
const checkNodes = treeRefR.value.getCheckedNodes(false, false)
const newArr = removeData.value.concat(checkNodes)
const obj = {};
const peon = newArr.reduce((cur, next) => {
obj[next[props.nodeKey]] ? "" : obj[next[props.nodeKey]] = true && cur.push(next);
return cur;
},[]) //设置cur默认类型为数组,并且初始值为空的数组
const dataNeedRemove = peon
reLoad.value = false
const originalLeft = JSON.parse(JSON.stringify(props.fromData))
const originalRight = JSON.parse(JSON.stringify(props.fromData))
// 抽离出选中数据中的id
const idsNeedRemove = extractId(dataNeedRemove)
// 删除相同id
const oldData = removeId(toData.value, idsNeedRemove)
toData.value = oldData
// 右侧列表需要保留的数据的id
const ids = extractId(oldData)
// 重新整理两侧树中数据
leftData.value = sortData(originalLeft, ids, 'left')
rightData.value = sortData(originalRight, ids, 'right')
nextTick(() => {
reLoad.value = true
})
checkVal()
}
/**
* 将tree中的整理进行整理,判断数据是否再tree中显示
* @param data tree数据
* @param condition 被选中的数据
* @param leftRight 整理左侧tree中的数据还是整理右侧tree中的数据
*/
const sortData = (data: any, condition: Array<string>, leftRight: string) => {
if(leftRight === 'left'){
const result = [];
for (const item of data) {
// 判断item的id是否在condition中,如果不在,说明不需要删除
if (!condition.includes(item.id)) {
// 如果item有children属性,递归调用本函数,传入item的children和condition
if (item.children) {
item.children = sortData(item.children, condition, leftRight);
}
// 如果item的children为空数组,删除item的children属性
if (item.children && item.children.length === 0) {
delete item.children;
}
result.push(item);
}
}
return result;
}else{
const result = [];
for (const item of data) {
// 如果item的id在condition中,说明该数据需要保留
if (condition.includes(item.id)) {
result.push(item);
} else {
// 否则,判断item是否有children属性
if (item.children) {
const subResult = sortData(item.children, condition, leftRight);
// 如果返回的结果数组不为空,说明有符合条件的子数据
if (subResult.length > 0) {
// 将item的children属性更新为返回的结果数组
item.children = subResult;
result.push(item);
}
}
}
}
return result;
}
}
/**
* 如果新数组中的id再旧数组中存在则删除原始数组中的id
* @param oldIds 原始id
* @param newIds 新id
*/
const removeId = (data: any, newIds: Array<string>) => {
const ids = []
for (const item of data) {
if(!newIds.includes(item.id)){
ids.push(item)
}
}
return ids
}
/**
* 将id从备选中的数据取出
* @param arr tree中被选中的数据
*/
const extractId = (arr: any) => {
const newArr = []
for(const i in arr){
newArr.push(arr[i].id)
}
return newArr
}
//返回父组件
const checkVal = () =>{
emit('checkVal', toData.value)
}
CSS
.tree-transfer{
width: 100%;
display: flex;
justify-content: space-between;
.left-tree,.right-tree{
flex-grow: 1;
width: calc((100% - 60px) / 2);
.tree-tit{
margin-bottom: 10px;
}
.list{
overflow: auto;
height: 300px;
border: 1px solid #ddd;
border-radius: 4px;
.item{
padding: 0 10px;
font-size: 14px;
line-height: 26px;
cursor: pointer;
&.active{
background: #b9d7fa;
}
}
.item-checkbox{
height: 26px;
padding: 0 10px;
font-size: 14px;
line-height: 26px;
&>.el-checkbox{
height: 26px;
}
}
}
}
.btn-div{
width: 120px;
flex-shrink: 0;
display: flex;
// flex-direction: column;
align-items: center;
justify-content: center;
}
.el-checkbox__input.is-disabled .el-checkbox__inner{
display: none;
}
}
父组件调用
<tree-transfer ref="treeTransfer" :nodeKey="'id'" :fromData="menuList" :toData="ruleForm.menuIds"
:defaultProps="transferProps" :leftTit="'可选菜单'" :rightTit="'已选菜单'" @checkVal="checkVal"/>
/**
* 将选中菜单存入表单
* @param val 子组件穿梭框返回
*/
const checkVal = (val: any) => {
const arr = []
for(const i in val){
arr.push(val[i].id)
}
ruleForm.menuIds = arr
}
穿梭框参数文档
属性
列表
参数说明
字段 | 说明 | 类型 | 是否必传 |
---|
nodeKey | 树中项目对应的唯一id值 | string | true |
fromData | 菜单树 ( 必须包含id name ) | array[object] | true |
toData | 已经选中的值 | array[string] | true |
defaultProps | 列表的列宽 | object | true |
leftTit | 左侧菜单名称 | string | false |
rightTit | 右侧菜单名称 | string | false |
disabled | 是否禁用穿梭框的左右按钮 | boolean | false |
dataLabel 中的 fromData说明
字段 | 说明 | 类型 | 是否必传 |
---|
id | 唯一id值 | string | true |
name | id对应展示渲染值 | string | true |
children | 树的子层级 | string | false |
dataLabel 中的 defaultProps说明
字段 | 说明 | 类型 | 是否必传 |
---|
label | 指定节点标签为节点对象的某个属性值 | string, function(data, node) | true |
children | 指定子树为节点对象的某个属性值 | string | true |
disabled | 指定节点选择框是否禁用为节点对象的某个属性值 | string, function(data, node) | true |
Expose
字段 | 说明 | 类型 |
---|
initData | 初始化穿梭框中的数据 | function |
clearData | 清空穿梭框中的数据 | function |