vue3 基于 element-plus 扩展树形穿梭框
# vue3 基于 element-plus 扩展树形穿梭框
# 代码
<template>
<div class="tree-transfer el-transfer">
<!-- 左侧树形面板 -->
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">
<el-checkbox v-model="leftAllChecked" :indeterminate="leftIndeterminate" @change="handleLeftAllChange">{{ leftPanelTitle }}</el-checkbox>
</p>
<div class="el-transfer-panel__body">
<el-tree ref="leftTree" :data="leftData" draggable :props="treeProps" :node-key="nodeKey" show-checkbox :allow-drop="handleAllowDrop" @check="handleTreeCheck('left', ...arguments)" />
</div>
</div>
<!-- 穿梭按钮 -->
<div class="el-transfer__buttons">
<div>
<el-button type="primary" @click="transferToLeft">
<el-icon><Back /></el-icon>
</el-button>
</div>
<div>
<el-button type="primary" @click="transferToRight">
<el-icon><Right /></el-icon>
</el-button>
</div>
</div>
<!-- 右侧树形面板 -->
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">
{{ rightPanelTitle }}
</p>
<div class="el-transfer-panel__body">
<el-tree ref="rightTree" :data="rightData" draggable :props="treeProps" :node-key="nodeKey" show-checkbox :allow-drop="handleAllowDrop" @node-drop="handleDrop" @check="handleTreeCheck('right', ...arguments)" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted, defineEmits,defineExpose,watch } from "vue";
import { Search, Right, Back } from "@element-plus/icons-vue";
const props = defineProps({
// 右侧列表key值数组
modelValue: { type: Array, default: () => [] },
// 数据源
data: {
type: Array,
required: true,
default: () => {
return [];
},
},
// leftDefaultChecked: { type: Array, default: () => [] },
// 默认选中
rightDefaultChecked: { type: Array, default: () => [] },
// 左侧标题
leftPanelTitle:{type:String, default:'选择'},
// 右侧标题
rightPanelTitle:{type:String, default:'已选'},
// draggable:{type:Boolean, default:false},
// 树形结构Key值
nodeKey: { type: String, default: "id" },
// 树形结构扩展配置
treeProps: {
type: Object,
default: () => ({ label: "label", children: "children" }),
},
});
const emit = defineEmits(['update:modelValue','getCheckData']);
// 状态管理
const leftTree = ref(null);
const rightTree = ref(null);
const leftChecked = ref([]);
const leftShowKeyList = ref([])
const rightChecked = ref([]);
const rightShowKeyList = ref([])
const leftAllChecked = ref(false)
// 计算属性
const leftList = ref([])
const hasCheck = ref(false)
// const rightData = computed(() => props.data.filter((item) => props.modelValue.includes(item[props.nodeKey])));
const rightData = ref([]);
// const leftData = computed(() => props.data.filter((item) => !props.modelValue.includes(item[props.nodeKey])));
const leftData = computed(() => hasCheck.value ? leftList : props.data );
let dataObj = {}
// 获取所有数据键值对
const getDataObj = (arr,level = 1,parentKey= null )=>{
let curLev = level
for (let index = 0; index < arr.length; index++) {
const item = arr[index];
const key = item[props.nodeKey]
item.level = curLev
item.parentKey = parentKey
if(item[props.treeProps.children] && Array.isArray(item[props.treeProps.children]) && item[props.treeProps.children].length > 0){
getDataObj(item[props.treeProps.children],curLev+1,key)
item[props.treeProps.children] = []
}
dataObj[props.nodeKey] = item
}
}
// 获取所有数据nodeKey
const getAllKey = function(arr){
let list = []
if(arr && Array.isArray(arr)){
arr.forEach(item => {
if(item[props.nodeKey]){
list.push(item[props.nodeKey])
}
if(item[props.treeProps.children] && Array.isArray(item[props.treeProps.children])){
let arr2 = getAllKey(item[props.treeProps.children])
list = [...list,...arr2]
}
});
}
return list
}
const allKeyList = computed(()=> getAllKey(props.data))
// 点击
const handleTreeCheck = (type,v)=>{
if(type == 'left'){
const list = leftTree.value.getCheckedKeys()
if(list.length >= leftShowKeyList.value.length){
leftAllChecked.value = true
}else {
leftAllChecked.value = false
}
}
}
// 递归查询展示数据
const checkSelect =(arr = [],selectList)=>{
let list = []
if(Array.isArray(arr)){
const aList = JSON.parse(JSON.stringify(arr))
aList.forEach(item => {
if(item[props.treeProps.children] && Array.isArray(item[props.treeProps.children])){
item[props.treeProps.children] = checkSelect(item[props.treeProps.children],selectList)
}else {
item[props.treeProps.children] = []
}
if(selectList.includes(item[props.nodeKey]) || item[props.treeProps.children].length > 0){
list.push(item)
}
})
}
return list
}
// 获取右侧展示数据
const getRightData = ()=>{
const list = checkSelect(props.data,rightShowKeyList.value,0)
return list
}
// 获取左侧展示数据
const getLeftData = ()=>{
const list = checkSelect(props.data,leftShowKeyList.value,1)
return list
}
// 增加右侧选中
const arrConcat = (arr1,arr2)=>{
const deepArr = JSON.parse(JSON.stringify(arr1))
const list = deepArr
for (let index2 = 0; index2 < arr2.length; index2++) {
const item2 = arr2[index2];
const key2 = item2[props.nodeKey]
const fIndex = list.findIndex(item => item[props.nodeKey] == key2)
if(fIndex>=0){
const item1 = list[fIndex]
if(item1[props.treeProps.children] && Array.isArray(item1[props.treeProps.children]) && item1[props.treeProps.children].length > 0 &&
item2[props.treeProps.children] && Array.isArray(item2[props.treeProps.children]) && item2[props.treeProps.children].length > 0
){
item1[props.treeProps.children] = arrConcat(item1[props.treeProps.children],item2[props.treeProps.children])
}
}else {
list.push(item2)
}
}
return list
}
// 取消右侧选中
const arrSplice = (arr1,arr2)=>{
const deepArr1 = JSON.parse(JSON.stringify(arr1))
let list = deepArr1
for (let index1 = 0; index1 < list.length; index1++) {
const item1 = list[index1];
const key1 = item1[props.nodeKey]
const fIndex = arr2.findIndex(item => item[props.nodeKey] == key1)
if(fIndex>=0){
const item2 = arr2[fIndex]
if(item1[props.treeProps.children] && Array.isArray(item1[props.treeProps.children]) && item1[props.treeProps.children].length > 0 &&
item2[props.treeProps.children] && Array.isArray(item2[props.treeProps.children]) && item2[props.treeProps.children].length > 0
){
item1[props.treeProps.children] = arrSplice(item1[props.treeProps.children],item2[props.treeProps.children])
}
}else {
list[index1] = ''
}
}
list = list.filter(it => it)
return list
}
const dataInit = (type)=>{
const list1 = rightData.value
const list2 = getRightData()
if(type==1){
// 左边选中后添加到右边
rightData.value = arrConcat(list1,list2)
}else if(type ==2){
// 右边取消选中后减少显示数据
rightData.value = arrSplice(list1,list2)
}else {
}
}
// 确认选择
const transferToRight = ()=>{
const list = leftTree.value.getCheckedNodes()
if(list.length > 0){
leftChecked.value = list.map(item => {
return item.id
})
rightShowKeyList.value = [...rightShowKeyList.value,...leftChecked.value]
leftShowKeyList.value = allKeyList.value.filter(item => !rightShowKeyList.value.includes(item))
if(leftChecked.value.length > 0){
hasCheck.value = true
}
leftAllChecked.value = false
leftTree.value.setCheckedKeys([])
nextTick(()=>{
leftChecked.value = []
leftList.value = getLeftData()
dataInit(1)
// rightData.value = getRightData()
changeCheck()
})
}
}
// 取消已选
const transferToLeft = ()=>{
const list = rightTree.value.getCheckedNodes()
if(list.length > 0){
rightChecked.value = list.map(item => {
return item.id
})
leftShowKeyList.value = [...leftShowKeyList.value,...rightChecked.value]
rightShowKeyList.value = allKeyList.value.filter(item => !leftShowKeyList.value.includes(item))
if(rightChecked.value.length > 0){
hasCheck.value = true
}else {
hasCheck.value = false
}
nextTick(()=>{
leftList.value = getLeftData()
rightChecked.value = []
dataInit(2)
// rightData.value = getRightData()
changeCheck()
})
}
}
const handleLeftAllChange = (v)=> {
const list = leftTree.value.getCheckedKeys()
if(list.length < leftShowKeyList.value.length){
leftTree.value.setCheckedKeys([...leftShowKeyList.value])
}else {
leftTree.value.setCheckedKeys([])
}
}
const init = ()=>{
const rightDefaultKey = getAllKey(props.rightDefaultChecked)
if(rightDefaultKey.length > 0){
rightChecked.value = rightDefaultKey
rightShowKeyList.value = [...rightChecked.value]
leftShowKeyList.value = allKeyList.value.filter(item => !rightShowKeyList.value.includes(item))
hasCheck.value = true
nextTick(()=>{
leftList.value = getLeftData()
if(rightDefaultKey.length > 0){
rightData.value = props.rightDefaultChecked
}
changeCheck()
})
}else {
hasCheck.value = false
leftList.value = []
rightData.value = []
leftAllChecked.value = false
rightChecked.value = []
leftChecked.value = []
leftShowKeyList.value = allKeyList.value
rightShowKeyList.value = []
}
}
// 判断是否允许拖拽
const handleAllowDrop = (draggingNode,targetNode,type) => {
// 获取拖拽节点和目标节点的父节点
const draggingParent = draggingNode.parent?.data?.id || null; // 根节点的父节点为 null
const targetParent = targetNode.parent?.data?.id || null;
// 若父节点不同,禁止拖拽到其他父节点下
if (draggingParent !== targetParent || type === 'inner') return false;
// 其他自定义规则(如同级节点限制等)
return true;
};
// 拖拽成功
const handleDrop = ()=>{
nextTick(()=>{
changeCheck()
})
}
const changeCheck = ()=>{
const dataKeys = rightShowKeyList.value
emit('update:modelValue',dataKeys)
const data = getCheckData()
emit('getCheckData',data)
}
const getCheckData = ()=>{
const dataKeys = rightShowKeyList.value
const dataList = rightData.value
return {dataKeys,dataList}
// emit('getCheckData',{dataKeys,dataList})
}
watch(props.data,()=>{
// getDataObj(props.data)
init()
},{deep:true,immediate:true})
// 暴露子组件的方法,以便父组件可以访问
defineExpose({
getCheckData
});
</script>
<style scoped lang="scss">
.tree-transfer {
display: flex;
height: 280px;
/* 穿透修改 Transfer 组件样式 */
:deep(.el-transfer) {
--el-transfer-panel-width: 300px;
--el-transfer-panel-height: 400px;
--el-transfer-panel-header-height: 40px;
}
/* 树形结构样式适配 */
:deep(.el-transfer-panel__body) {
height: calc(100% - 41px);
.el-tree {
height: 100%;
overflow: auto;
:deep(.el-tree-node) {
padding: 4px 0;
}
}
}
/* 按钮组垂直居中 */
.el-transfer__buttons {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 16px;
}
.el-transfer__buttons .el-button {
margin: 8px 0;
}
}
</style>