手写Tree-Transfer 穿梭树的组件封装

580 阅读1分钟

需求

image.png

左侧为树形组件,使用el-transfer无法满足需求,所以自己实现了一个transfer穿梭组件;

最终效果

image.png

HTML代码:

<div class="my-transfer" id="transfer">
         <!-- 左边 -->
         <div class="my-transfer-panel">
            <p class="my-transfer-panel__header">
              <span>未分配</span>
            </p>
            <div class="my-transfer-panel__body"  v-loading="loadingTree">
               <!-- 搜索框 -->
               <el-select
                  v-model="leftFilter"
                  filterable
                  remote
                  clearable
                  reserve-keyword
                  placeholder="请输入内容按回车键(Enter)搜索"
                  @change="changeSelect"
                  :loading="loading"
                  :remote-method="remoteMethod"
                  class="my-transfer-panel__filter"
                  style="width:80%"
                >
                <template #prefix>
                  <el-icon><el-icon-search/></el-icon>
                </template>
                  <el-option
                    v-for="item in leftOptions"
                    :key="item.userId"
                    :label="item.realName"
                    :value="{value:item.userId,item:item}"
                    :disabled='item.userId === leftFilter.userId'
                  />
                </el-select>
               <!-- 树结构 -->
               <el-tree 
                    ref="tree"
                    :load="loadNode"
                    highlight-current
                    node-key="key"
                    @check-change="handleCheckChange"
                    lazy
                    :props="{label: 'label',children: 'children',isLeaf: 'leaf',disabled:'disabled'}" 
                    show-checkbox />
            </div>
         </div>
         <!-- 中间按钮 -->
         <div class="my-transfer__buttons">
            <el-button type="primary" style="margin-bottom:30px" :disabled="rightCheckListKey.length < 1 "  @click="handleTransfer('toLeft')">
              <template #default>
                <span style="display: inline-flex;align-items: center;">
                  <el-icon ><el-icon-arrow-left /></el-icon>

                </span>
              </template>
            </el-button>
            <el-button type="primary" style="margin-left:0;" :disabled="leftCheckMapKey.size < 1"  @click="handleTransfer()">
              <template #default>
                <span style="display: inline-flex;align-items: center;">
              <!--       <span style="padding-right:5px">Toright</span>  -->
                    <el-icon ><el-icon-arrow-right /></el-icon>
                </span>
              </template>
            </el-button>
         </div>
         <!-- 右边 -->
         <div class="my-transfer-panel">
          <p class="my-transfer-panel__header">
              <el-checkbox v-model="rightAllCheckbox" @change="changeRightAllCheckbox">
                <template #default>
                    已分配
                    <span>{{rightList.length}}</span>
                </template>
              </el-checkbox>
            </p>
            <div class="my-transfer-panel__body">
               <!-- 远程 搜索框 -->
               <el-input v-model="userName" prefix-icon="el-icon-search" clearable @clear="resetQuery" @keyup.enter="getList"  class="my-transfer-panel__filter" placeholder="请输入搜索内容" />
               <!-- 按钮选择组 -->
               <el-checkbox-group v-model="rightCheckListKey" class="my-transfer-panel__list">
                  <el-checkbox v-for="(i,index) in rightList" :key="index" 
                      :label="i.key" class="my-transfer-panel__item">
                    {{i.label}}
                  </el-checkbox>
              </el-checkbox-group>
            </div>
         </div>
  </div>
<script>
import { defineComponent, reactive, toRefs,onMounted,getCurrentInstance } from "vue";

export default defineComponent({
  setup() {
    const { proxy } = getCurrentInstance();
    const state = reactive({

      // 存放左侧选中数据
      leftCheckMapKey:new Map,
      // 远程搜索字段
      leftFilter:{userId:undefined},
      // 远程搜索 选项
      leftOptions:[],
      // 右侧列表数据
      rightList:[],
      // 右侧列表数据 map格式
      rightMapList:new Map,
      // 右侧选中 数据列表
      rightCheckListKey:[],
      // 右侧全选 数据列表
      rightAllCheckbox:false,

    });
    onMounted(() => {
      getList()
      
    })
   
    /* 数据穿梭 */
    function handleTransfer(str){
      // 移动到左侧
      if (str === 'toLeft') {
        let transData = []
        console.log(state.rightMapList);
        // 选中的key
        state.rightCheckListKey.forEach(i =>{
          // 右侧全部数据map 
          if (state.rightMapList.has(i)) {
            // 改变右侧数据列表
            state.rightList = state.rightList.filter(v=>v.key != i)
            let item = this.rightMapList.get(i)
            transData.push(
              { key: item.key,
                label: item.label,
              })
          }
        })
        // 更新树结构数据
        proxy.$nextTick(()=>{
            for (let index = 0; index < transData.length; index++) {
              this.$refs['tree'].append(transData[index],transData[index].key)    
            }
        }) 
      // 清空 右侧选择的 key 完成移动
       this.rightCheckListKey = []
       state.rightAllCheckbox = false
      }else{
        // 获取 左侧树 选中的数据
        let data = [...this.leftCheckMapKey.values()]
        // 从树中 移除该数据
        proxy.$nextTick(()=>{
          for (let index = 0; index < data.length; index++) {
            proxy._.refs['tree'].remove(data[index])  
            // 放入右侧数据列表  
            state.rightList.push(data[index])
            state.rightMapList.set(data[index].key,data[index])
          }
        })
        // 清空左侧选择的 key
       state.leftCheckMapKey.clear()
       proxy._.refs['tree'].setCheckedKeys([])
      }
    }
    /* 左侧树形框 */
    /* 树形结构 数据懒加载 */
    async function loadNode(node, resolve){
      if (!node) {
        return;
      }
      let arr = []
      for (let i = 1; i <= 4; i++) {
        arr.push({
          key: `${i}${node.level}`,
          label: `Option ${i}${node.level}`,
          disabled: i % 4 === 0,
          leaf:node.level === 1
        })
      }
      resolve(arr);
    }
    /* 树节点 复选框被点击 */
    function handleCheckChange(val){
      // 判断是否是子节点
      if (val.leaf) {
        // 当前 key 已存在 删除
        if (state.leftCheckMapKey.size > 0 && state.leftCheckMapKey.has(val.key)) {
          state.leftCheckMapKey.delete(val.key)
        } else {
          // key 不存在 添加
          state.leftCheckMapKey.set(val.key,val)
        }
      } 
    }
     /* 远程搜索请求 */
     async function remoteMethod(){
     /*   let res = await this.$API.role.permission.unallocatedUserList.get({userName:str,roleId:this.queryParams.roleId}); */
       /* this.leftOptions = res.rows */
    }
    /* 远程搜索完成 选择数据 */
    async function changeSelect(val){
      if (val.item) {
        let data = {
        key:val.item.userId,
        userName:val.item.userName,
      }
      //将 选择的数据 放入 右侧列表
      state.rightList.push(data)
    }

    }
    /* 右侧 */
    /** 请求右侧数据 */
    async function getList() {
        // 接口请求 数据
        for (let i = 1; i <= 4; i++) {
          state.rightList.push({
            key: `${i}`,
            label: `Option ${i}`,
            disabled: i % 4 === 0,
          })
      }
      // 数据转存map,方便取值
      state.rightList.forEach(i=>{state.rightMapList.set(i.key,i)})
    }
    /* 全选 */
    function changeRightAllCheckbox(val){
      if (val) {
        // 全选
        state.rightList.forEach(item =>{
          if (!state.rightCheckListKey.includes(item.key)) {
            state.rightCheckListKey.push(item.key)
          }
        })
        
      } else {
        // 取消 全选
        state.rightCheckListKey = []
      }
    }
    return {
      ...toRefs(state),loadNode,remoteMethod,changeSelect,getList,handleCheckChange,
      changeRightAllCheckbox,handleTransfer
    };
  }
})
</script>

<style lang="scss" scoped>
   .my-transfer{
    display: flex;
    align-items: center;
    width: fill-available;
    width: -webkit-fill-available;
    font-size: var(--el-font-size-base);
  }
  .my-transfer__buttons{
    display: flex;
    align-items: center;
    flex-direction: row;
    justify-content: center;
    flex-wrap: wrap;
    width: 85px;
  }
  .my-transfer-panel{
    width: auto;
   
    background: var(--el-bg-color-overlay);
    display: inline-block;
    text-align: left; 

    vertical-align: middle;
    max-height: 100%;
    box-sizing: border-box;
    position: relative;
  }

  .my-transfer-panel__header{
    display: flex;
    align-items: center;
    height:40px;
    background: var(--el-fill-color-light);
    margin: 0;
    padding-left: 15px;
    border: 1px solid var(--el-border-color-lighter);
    border-top-left-radius: var(--el-border-radius-base);
    border-top-right-radius: var(--el-border-radius-base);
    box-sizing: border-box;
    color: var(--el-color-black);
    .el-checkbox{
      position: relative;
      display: flex;
      width: 100%;
      align-items: center;
      :deep(.el-checkbox__label){
        font-size: 16px;
        color: var(--el-text-color-primary);
        font-weight: 400;
      }

    }
   
    .el-checkbox .el-checkbox__label span{
        position: absolute;
        right: 15px;
        top: 50%;
        transform: translate3d(0,-50%,0);
        color: var(--el-text-color-secondary);
        font-size: 12px;
        font-weight: 400;
    }

  }
  .my-transfer-panel__body{
    border-bottom: 1px solid var(--el-border-color-lighter);
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    height: 500px;
    border-left: 1px solid var(--el-border-color-lighter);
    border-right: 1px solid var(--el-border-color-lighter);
    overflow: overlay;
    .my-transfer-panel__filter{
      text-align: center;
      margin: 15px;
      box-sizing: border-box;
      width: 100%;
      width:fill-available;
      width: -moz-available;
      width: -webkit-fill-available;
    }
    .my-transfer-panel__list{
      height: calc(100% - 32px - 30px);
      padding-top: 0;
      margin: 0;
      padding: 6px 0;
      list-style: none;
      overflow: auto;
      box-sizing: border-box;
    }
    .my-transfer-panel__item{
      height:30px;
      line-height: 30px;
      padding-left: 15px;
      display: block!important;

    }
  }
</style>

组件功能还可以在完善,奈何水平有限🤣🤣🤣只能完成简单的操作,右侧内容穿梭到左侧的树时,树结构无法及时渲染视图,有清楚怎么解决的大佬请指教指教~~~ 谢谢谢谢谢谢谢谢啦😘