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>