HTML
<template>
<div>
<div class="v-transfer">
<!-- 左侧框 -->
<div class="v-transfer-item v-transfer-left">
<div class="header">
<el-checkbox v-model="checkedLeft" :disabled="leftTreeData.length < 1" label="" size="large"
style="margin-right: 12px" @change="leftAllCheck" />
{{ leftTitle }}
</div>
<div class="content">
<!-- 搜索框 -->
<div class="seacrh-box">
<el-input v-model.trem="leftFilterText" placeholder="请输入" clearable @clear="onSearchLeft"
@keyup.enter="onSearchLeft" @change="onSearchLeft"></el-input>
</div>
<!-- 数据区域 -->
<div class="tree-box">
<div class="empty-box" v-if="leftTreeData.length < 1">暂无数据</div>
<el-tree class="tree" v-else ref="leftTreeRef" :data="leftTreeData" show-checkbox :node-key="'id'"
:filter-node-method="filterNode" :props="props.defaultProps" @check="onCheckLeft">
<template #default="{ data }">
<span style="padding-left: 4px; font-size: 16px">{{ data.title }}</span>
</template></el-tree>
</div>
</div>
</div>
<!-- 按钮区域 -->
<div class="v-transfer-middle">
<div class="middle-icon">
<div :disabled="leftOperation.length < 1" @click="addHandle">
<ArrowRight style="width: 1em; height: 1em" />
</div>
<div :disabled="rightOperation.length < 1" @click="delHandle">
<ArrowLeft style="width: 1em; height: 1em" />
</div>
</div>
</div>
<!-- 右侧框 -->
<div class="v-transfer-item v-transfer-right">
<div class="header">
<el-checkbox v-model="checkedRight" :disabled="rightTreeData.length < 1" label="" size="large"
style="margin-right: 12px" @change="rightAllCheck" />{{ rightTitle }}
</div>
<div class="content">
<!-- 搜索框 -->
<div class="seacrh-box">
<el-input v-model.trem="rightFilterText" placeholder="请输入" clearable @clear="onSearchRight"
@keyup.enter="onSearchRight" @change="onSearchRight"></el-input>
</div>
<!-- 数据区域 -->
<div class="tree-box">
<div class="empty-box" v-if="rightTreeData.length < 1">暂无数据</div>
<el-tree class="tree" v-else ref="rightTreeRef" :filter-node-method="filterNode" :data="rightTreeData"
show-checkbox :node-key="'id'" :props="props.defaultProps" @check="onCheckRight">
<template #default="{ data }">
<span style="padding-left: 4px; font-size: 16px">{{ data.title }}</span>
</template>
</el-tree>
</div>
</div>
</div>
</div>
<div class="bottomBtn">
<el-button type="primary" style="background-color:#0068DA;" @click="roleDialogSubmit">确认授权</el-button>
</div>
</div>
</template>
JS
<script setup lang="ts" name="TreeTransfer">
import { onMounted, ref, reactive, watch, nextTick } from 'vue'
import { ElTree, ElMessage, ElMessageBox } from 'element-plus'
import { grant } from '@/api/roleMangement/index'
import type Node from 'element-plus/es/components/tree/src/model/node'
import lodash from 'lodash'
import { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
import { Right } from '@element-plus/icons-vue'
import { treeToList } from '@/utils/util'
interface treeNode {
className: string
parentId: string
id: string
children?: treeNode
}
const props = defineProps({
leftTitle: {
type: String,
default: () => {
return ''
}
},
rightTitle: {
type: String,
default: () => {
return ''
}
},
leftTree: {
type: Array,
default: () => {
return []
}
},
rightTree: {
type: Array,
default: () => {
return []
}
},
defaultProps: {
type: Object,
default: () => {
return {
children: 'children',
label: 'title'
}
}
},
nodeKey: {
type: String,
default: 'id'
},
leafsArr: {
type: Array,
default: () => {
return []
}
}
})
const checkedLeft = ref(false)
const checkedRight = ref(false)
// 抛出事件
const emits = defineEmits(['change', 'delete', 'add'])
const leftFilterText = ref<string>('')
const leftTreeData = ref<any[]>([])
const leftDefaultCheckedKeys = ref<any[]>([])
const leftDefaultExpandedKeys = ref<any[]>([])
const leftOperation = ref<any[]>([])
const leftTreeRef = ref<InstanceType<typeof ElTree>>()
const rightFilterText = ref<string>('')
const rightTreeData = ref<any[]>([])
const rightDefaultCheckedKeys = ref<any[]>([])
const rightDefaultExpandedKeys = ref<any[]>([])
const rightOperation = ref<any[]>([])
const rightTreeRef = ref<InstanceType<typeof ElTree>>()
const removeNodes = ref<any[]>([])
interface Tree {
id: number
label: string
children?: Tree[]
}
watch(
props,
newVal => {
leftTreeData.value = lodash.cloneDeep(newVal.leftTree)
rightTreeData.value = lodash.cloneDeep(newVal.rightTree)
},
{ immediate: true }
)
watch(rightFilterText, val => {
rightTreeRef.value!.filter(val)
})
watch(leftFilterText, val => {
leftTreeRef.value!.filter(val)
})
defineExpose({
leftTreeData,
rightTreeData
})
onMounted(() => {
leftFilterText.value = ''
rightFilterText.value = ''
})
const formatTree = (tree: any[], parentKey: string = 'parentId', idKey: string = 'id') => {
// 格式化选择的树:清除全选下面的子节点
let swap,
parentIds: string[] = []
// 先找出有children的id集合,再把所有的数据做对比,只要parentId和其中一个对上,就把该数据删除;
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 onCheckLeft = () => {
leftOperation.value = formatTree(
leftTreeRef.value!.getCheckedNodes(false) || [],
'parentId',
'id'
)
}
// 左侧搜素
// const onSearchLeft = () => {
// leftTreeRef.value!.filter(leftFilterText.value)
// }
// 左侧过滤
// const filterLeftNode = (value: string, data: Tree, node: treeNode) => {
// if (!value) return true
// return chodeNode(value, data, node)
// }
// 右侧选中
const onCheckRight = () => {
rightOperation.value = formatTree(
rightTreeRef.value!.getCheckedNodes(false) || [],
'parentId',
'id'
)
}
// 右侧搜素
const onSearchRight = () => {
rightTreeRef.value!.filter(rightFilterText.value)
}
// 左
const onSearchLeft = () => {
leftTreeRef.value!.filter(leftFilterText.value)
}
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
const leftAllCheck = () => {
const leftTree = leftTreeRef.value
if (checkedLeft.value) {
leftTree?.setCheckedNodes(leftTreeData.value)
checkedLeft.value = true
} else {
leftTree?.setCheckedNodes([])
checkedLeft.value = false
}
}
const rightAllCheck = () => {
const rightTree = rightTreeRef.value
if (checkedRight.value) {
rightTree?.setCheckedNodes(rightTreeData.value)
checkedRight.value = true
} else {
rightTree?.setCheckedNodes([])
checkedRight.value = false
}
}
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 addHandle = 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.parentId == '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) || [], 'parentId', 'id')
emits('change', leftTreeData.value, rightTreeData.value)
emits('add')
}
// 左移
const delHandle = 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.parentId == '0'
)
const leftTree = leftTreeRef.value
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)
}
}
rightNodes.forEach(node => {
rightTree.setChecked(node, false, false)
if (checkedKeys.indexOf(node.id) >= 0) {
rightTree.remove(node)
}
})
rightOperation.value = formatTree(rightTree.getCheckedNodes(false) || [], 'parentId', 'id')
emits('change', leftTreeData.value, rightTreeData.value)
emits('delete')
}
const handleData = (rightNodes, leftTree, allCheckedKeys) => {
rightNodes.forEach(item => {
if (item.children) {
let parent = leftTree.getNode(item.parentId)
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.parentId)
let node = item
if (!leftTree.getNode(item.id) && allCheckedKeys.indexOf(item.id) > 0) {
leftTree.append(node, parent)
}
}
})
}
const roleDialogSubmit = () => {
const rightList = treeToList(rightTreeData.value)
grant({ roleIds: [props.nodeKey], menuIds: rightList.map(item => (item).id) }).then(
res => {
ElMessage({
type: 'success',
message: '授权成功!'
})
}
)
}
</script>
CSS
<style lang="scss">
.v-transfer {
width: 100%;
height: 100%;
.tree {
.ep-tree__empty-block {
text-align: center;
color: #e5e6eb;
position: relative;
&::before {
content: '';
background-image: url(../../assets/img/empty.png);
background-size: cover;
width: 160px;
height: 172px;
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.ep-tree__empty-text {
position: absolute;
top: 192px;
left: 50%;
transform: translateX(-50%);
}
}
}
}
.bottomBtn {
text-align: center;
}
</style>
<style lang="scss" scoped>
.v-transfer {
display: flex;
justify-content: flex-start;
font-size: 16px;
&-item {
border: 1px solid #e5e6eb;
border-radius: 2px;
min-height: 200px;
display: flex;
flex-direction: column;
.header {
background: #f7f8fa;
height: 40px;
display: flex;
align-items: center;
// justify-content: center;
padding: 0 16px;
border-bottom: 1px solid #e5e6eb;
}
.content {
background-color: #fff;
flex: 1;
padding: 16px;
.seacrh-box {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 16px;
.ep-button {
margin-left: 16px;
}
}
.tree-box {
height: 570px;
overflow-y: auto;
}
}
}
&-left {
width: 40%;
height: calc(100% - 60px);
}
&-middle {
width: 20%;
height: calc(100% - 60px);
text-align: center;
margin: auto 0;
position: relative;
// .ep-button {
// margin-top: 10px;
// }
.middle-icon {
position: absolute;
top: 38%;
left: 100px;
div {
width: 40px;
height: 40px;
background: #409eff;
border-radius: 50%;
color: #fff;
cursor: pointer;
&:first-child {
margin-bottom: 40px;
}
svg {
margin-top: 12px;
}
}
}
}
&-right {
width: 40%;
height: calc(100% - 60px);
}
.empty-box {
text-align: center;
color: #86909c;
margin-top: 40px;
}
}
</style>
引用
<TreeTransfer
ref="treeTransferRef"
:nodeKey="roleId"
:left-tree="leftTree"
:right-tree="rightTree"
:default-props="props"
:leafsArr="leafsArr"
:leftTitle="'左侧标题'"
:rightTitle="'右侧标题'"
/>
效果
很简陋,待优化