由于饿了么组件库没有树形穿梭框,我在网上找了很多的博客发现一个通过el-tree自己封装的组件代码。但是我实际使用了一下vue3 + element-plus 自定义树形穿梭框-CSDN博客这个代码有些地方有问题然后没有css代码。所以我就进行了问题的修正和css代码的添加。不多说直接上完整代码。
<!-- eslint-disable vue/no-unused-vars -->
<template>
<div class="treeTransfer">
<!-- 左边 -->
<div class="leftTree">
<div class="list">
<div class="left_lowline">
<el-checkbox
v-model="checkedLeft" :disabled="leftTreeData.length < 1" label="" size="large"
style="margin-right: 12px" @change="leftAllCheck" />
<p class="left_title">{{ props.title[0] }}</p>
</div>
<!-- 搜索 -->
<div class="left_input">
<el-input
v-model="leftSearchText" class="w-50" placeholder="请输入通道或目录名称" clearable
prefix-icon="el-icon-search" @clear="onSearchLeft" @keyup.enter="onSearchLeft"
@change="onSearchLeft" />
</div>
<div style="overflow: auto; flex: 1;margin-top: 10px;">
<el-tree
ref="leftTreeRef" :filter-node-method="filterNode" :data="leftTreeData" show-checkbox
:node-key="props.nodeKey" :props="props.defaultProps" v-slot="{ node, data }" accordion
@check="onCheckLeft" default-expand-all>
<div>
{{ data.corgName }}
</div>
<div>
{{ data.label }}
</div>
</el-tree>
</div>
</div>
</div>
<!-- 中间按钮 -->
<div class="btnDiv">
<div class="mg10">
<el-button @click="toRight()" size="default" type="primary" :icon="ArrowRight" :disabled="leftOperation.length < 1"/>
</div>
<div class="mg10">
<el-button @click="toLeft()" size="default" type="primary" :icon="ArrowLeft" :disabled="rightOperation.length < 1"/>
</div>
</div>
<!-- 右边 -->
<div class="rightTree">
<div class="list">
<div class="left_lowline">
<el-checkbox
v-model="checkedRight" :disabled="rightTreeData.length < 1" label="" size="large"
style="margin-right: 12px" @change="rightAllCheck" />
<p class="left_title">{{ props.title[1] }}</p>
</div>
<!-- 搜索 -->
<div class="left_input">
<el-input
v-model="rightSearchText" class="w-50" placeholder="请输入已挂载的通道或目录名称" clearable
prefix-icon="el-icon-search" @clear="onSearchRight" @keyup.enter="onSearchRight"
@change="onSearchRight" />
</div>
<div style="overflow: auto; flex: 1;margin-top: 10px;">
<el-tree
:filter-node-method="filterNode"
ref="rightTreeRef" :data="rightTreeData" show-checkbox :node-key="props.nodeKey"
:props="props.defaultProps" v-slot="{ node, data }" accordion @check="onCheckRight" default-expand-all>
<div>
{{ data.corgName }}
</div>
<div>
{{ data.label }}
</div>
</el-tree>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
//@ts-nocheck 解决ts类型报红
import { ref, onMounted, watch } from 'vue'
import lodash from 'lodash'
const props = defineProps(['nodeKey', 'fromData', 'toData', 'defaultProps', 'title'])
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
const checkedLeft = ref(false)
const checkedRight = ref(false)
const leftSearchText = ref('')
const rightSearchText = ref('')
const leftOperation = ref<any[]>([])
const rightOperation = ref<any[]>([])
// 定义emit
const emits = defineEmits(['change'])
const leftTreeRef = ref()
const rightTreeRef = ref()
// 左侧数据
const leftTreeData = ref([])
// 右侧数据
const rightTreeData = ref([])
watch(
props,
(newVal) => {
leftTreeData.value = lodash.cloneDeep(newVal.fromData)
rightTreeData.value = lodash.cloneDeep(newVal.toData)
},
{ immediate: true }
)
watch(leftSearchText, (val) => {
leftTreeRef.value?.filter(val)
})
watch(rightSearchText, (val) => {
rightTreeRef.value?.filter(val)
})
defineExpose({
leftTreeData,
rightTreeData
})
onMounted(() => {
leftSearchText.value = ''
rightSearchText.value = ''
})
const removeChecked = (rightList, allCheckedKeys) => {
rightList.forEach((e, index) => {
if (allCheckedKeys.indexOf(e.id) == -1) {
rightList.splice(index, 1)
}
if (e.children) {
removeChecked(e.children, allCheckedKeys)
}
})
return rightList
}
const recursionTree = (parents, allCheckedKeys) => {
parents.forEach((e) => {
const list = lodash.filter(e.children, (child) => allCheckedKeys.indexOf(child.id) >= 0)
e.children = list
if (e.children.length !== 0) {
recursionTree(e.children, allCheckedKeys)
}
})
return parents
}
const handleData = (rightNodes, leftTree, allCheckedKeys) => {
rightNodes.forEach((item) => {
if (item.children) {
let parent = leftTree.getNode(item.pid)
let node = item
if (!leftTree.getNode(item.id) && allCheckedKeys.indexOf(item.id) > 0) {
const copyNode = lodash.cloneDeep(node)
copyNode.children = []
leftTree.append(copyNode, parent)
}
handleData(item.children, leftTree, allCheckedKeys)
} else {
let parent = leftTree.getNode(item.pid)
let node = item
if (!leftTree.getNode(item.id) && allCheckedKeys.indexOf(item.id) > 0) {
leftTree.append(node, parent)
}
}
})
}
// // 去右边
const toRight = async () => {
const leftTree = leftTreeRef.value
checkedLeft.value = false
if (!leftTree) {
return
}
const leftNodes = leftTree.getCheckedNodes(false, true)
const parents = leftNodes.filter((el) => el.children && el.children.length > 0 && el.pid == '0')
const checkedKeys = leftTree.getCheckedKeys(false)
const halfCheckedKeys = leftTree.getHalfCheckedKeys()
const allCheckedKeys = halfCheckedKeys.concat(checkedKeys)
const rightTree = rightTreeRef.value
if (!rightTree) {
let rightList = parents.map((parent) => {
const obj = lodash.omit(parent, ['children'])
obj.children = lodash.filter(
parent.children,
(child) => allCheckedKeys.indexOf(child.id) >= 0
)
return obj
})
let copyRightList = lodash.cloneDeep(rightList)
removeChecked(copyRightList, allCheckedKeys)
rightTreeData.value.push(...copyRightList)
} else {
let isExist = false
rightTree.data.forEach((e) => {
if (parents[0].id === (e as any).id) isExist = true
})
if (!isExist) {
const overall = lodash.cloneDeep(parents)
let rightList = await recursionTree(overall, allCheckedKeys)
let copyRightList = lodash.cloneDeep(rightList)
removeChecked(copyRightList, allCheckedKeys)
rightTreeData.value.push(...copyRightList)
} else {
handleData(leftNodes, rightTree, allCheckedKeys)
}
}
// 移过去之后要删除原本的值
leftNodes.forEach((node) => {
leftTree.setChecked(node, false, false)
if (checkedKeys.indexOf(node.id) >= 0) {
leftTree.remove(node)
}
})
leftOperation.value = formatTree(leftTree.getCheckedNodes(false) || [], 'pid', 'id')
// emits('change', leftTreeData.value, rightTreeData.value)
emits('change', checkedKeys)
setTimeout(() => {
rightTree?.setCheckedNodes(leftNodes)
rightOperation.value = formatTree(rightTree.getCheckedNodes(false) || [], 'pid', 'id')
if (lodash.isEqual(rightOperation.value, rightTreeData.value)) {
checkedRight.value = true
} else {
checkedRight.value = false
}
}, 500)
}
// // 去左边
const toLeft = async () => {
checkedRight.value = false
const rightTree = rightTreeRef.value
if (!rightTree) {
return
}
const rightNodes = rightTree.getCheckedNodes(false, true)
const checkedKeys = rightTree.getCheckedKeys(false)
const halfCheckedKeys = rightTree.getHalfCheckedKeys()
const allCheckedKeys = halfCheckedKeys.concat(checkedKeys)
const parents = lodash.filter(
rightNodes,
(item) => item.children && item.children.length > 0 && item.pid == '0'
)
const leftTree = leftTreeData.value.length ? leftTreeRef.value : null
if (!leftTree) {
let leftList = parents.map((parent) => {
const obj = lodash.omit(parent, ['children'])
obj.children = lodash.filter(
parent.children,
(child) => allCheckedKeys.indexOf(child.id) >= 0
)
return obj
})
let copyLeftList = lodash.cloneDeep(leftList)
removeChecked(copyLeftList, allCheckedKeys)
leftTreeData.value.push(...copyLeftList)
} else {
let isExist = false
leftTree.data.forEach((e) => {
if (parents[0].id === (e as any).id) isExist = true
})
if (!isExist) {
const overall = lodash.cloneDeep(parents)
let leftList = await recursionTree(overall, allCheckedKeys)
let copyLeftList = lodash.cloneDeep(leftList)
removeChecked(copyLeftList, allCheckedKeys)
rightTreeData.value.push(...copyLeftList)
} else {
handleData(rightNodes, leftTree, allCheckedKeys)
}
}
console.log(leftTreeData.value, 'slf-leftTreeData')
rightNodes.forEach((node) => {
rightTree.setChecked(node, false, false)
if (checkedKeys.indexOf(node.id) >= 0) {
rightTree.remove(node)
}
})
rightOperation.value = formatTree(rightTree.getCheckedNodes(false) || [], 'pid', 'id')
// emits('change', leftTreeData.value, rightTreeData.value)
emits('change', checkedKeys)
}
//左侧选中(移除原本值用)
const onCheckLeft = () => {
leftOperation.value = formatTree(leftTreeRef.value?.getCheckedNodes(false) || [], 'pid', 'id')
if (lodash.isEqual(leftOperation.value, leftTreeData.value)) {
checkedLeft.value = true
} else {
checkedLeft.value = false
}
}
// 右侧选中
const onCheckRight = () => {
rightOperation.value = formatTree(rightTreeRef.value!.getCheckedNodes(false) || [], 'pid', 'id')
if (lodash.isEqual(rightOperation.value, rightTreeData.value)) {
checkedRight.value = true
} else {
checkedRight.value = false
}
}
const formatTree = (tree: any[], parentKey: string = 'pid', idKey: string = 'id') => {
// 格式化选择的树:清除全选下面的子节点
let swap,
parentIds: string[] = []
// 先找出有children的id集合,再把所有的数据做对比,只要pid和其中一个对上,就把该数据删除;
tree.forEach((item, index) => {
if (item.children) {
parentIds.push(item[idKey])
}
})
swap = tree.filter((item, index) => {
if (parentIds.indexOf(item[parentKey]) == -1) {
return item
}
})
return swap
}
const leftAllCheck = () => {
const leftTree = leftTreeRef.value
if (checkedLeft.value) {
leftTree?.setCheckedNodes(leftTreeData.value)
checkedLeft.value = true
} else {
leftTree?.setCheckedNodes([])
checkedLeft.value = false
}
onCheckLeft()
}
const onSearchLeft = () => {
leftTreeRef.value?.filter(leftSearchText.value)
}
const rightAllCheck = () => {
const rightTree = rightTreeRef.value
if (checkedRight.value) {
rightTree?.setCheckedNodes(rightTreeData.value)
checkedRight.value = true
} else {
rightTree?.setCheckedNodes([])
checkedRight.value = false
}
onCheckRight()
}
const onSearchRight = () => {
rightTreeRef.value?.filter(rightSearchText.value)
}
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
</script>
<style lang="scss" scoped>
.treeTransfer{
display: flex;
height: 400px;
width: 100%;
.btnDiv{
flex:1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.mg10{
padding: 5px;
:deep(svg) , :deep(.el-icon) {
height: 2em !important;
width: 2em !important;
}
}
}
.leftTree{
width: 45%;
border: 1px solid rgb(182, 182, 182);
box-sizing: border-box;
padding: 5px 10px;
.list{
height: 100%;
display: flex;
flex-direction: column;
}
}
.rightTree{
border: 1px solid rgb(182, 182, 182);
box-sizing: border-box;
padding: 5px 10px;
width: 45%;
.list{
height: 100%;
display: flex;
flex-direction: column;
}
}
.left_lowline{
display: flex;
align-items: center;
}
.right_lowline{
display: flex;
align-items: center;
}
}
</style>
用法如下:
<TreeTransfer
style="margin-top: 20px;"
ref="treeTransferRef"
node-key="id"
:fromData="fromData"
:toData="toData"
:defaultProps="transferProps"
:title="['请选择需要移动的通道或目录', '已挂载的通道或目录']"/>
let treeTransferRef = ref(); // 树形穿梭框
let fromData = ref([
{
id: "1",
pid: 0, //自定义pid的参数名,默认为"pid" 必填:false
label: "一级 1",
children: [
{
id: '1-2-1',
pid: '1-2',
children: [],
label: '二级 1-2-1'
},
{
id: '1-2-2',
pid: '1-2',
children: [],
label: '二级 1-2-2'
},
{
id: '1-2-3',
pid: '1-2',
children: [],
label: '二级 1-2-3'
}
]
},
]); // 树形数据
let toData = ref([]); // 选中的ids数据
const transferProps = ref({
label: 'label',
children: 'children',
disabled: 'disabled',
});