基于Element-ui 封装穿梭框(左侧树 右侧列表,可全选,列表可拖拽)

Element-ui提供的穿梭框只支持列表,根据实际需求自己写了一个左边是树结构,右边是列表结构的穿梭框,(如果需要两边都是树结构的话,需要把右侧的逻辑参考左侧改一改)拖拽使用了vuedraggable插件

效果图

左侧树右侧列表的穿梭框

组件代码

<template>
  <div class="transfer-tree">
    <div class="transfer-panel">
      <div class="transfer-panel-header">
        <el-checkbox
          v-model="leftAllChecked"
          :disabled="!(leftDataList && leftDataList.length)"
          :indeterminate="isIndeterminateLeft"
          @change="handleCheckAllChangeLeft">{{ leftTitle }}</el-checkbox>
      </div>
      <div class="transfer-panel-body">
        <el-tree
          ref="leftTree"
          show-checkbox
          check-on-click-node
          default-expand-all
          :node-key="defaultProps.key"
          :data="leftDataList"
          :props="defaultProps"
          @check="handleCheckLeft">
        </el-tree>
      </div>
    </div>
    <div class="transfer-buttons">
      <el-button
        class="mb8"
        size="mini"
        icon="el-icon-arrow-left"
        :disabled="!(rightCheckedList && rightCheckedList.length)"
        @click="handleLeftChange"></el-button>
      <el-button
        type="primary"
        size="mini"
        icon="el-icon-arrow-right"
        :disabled="!(leftCheckedList && leftCheckedList.length)"
        @click="handleRightChange"></el-button>
    </div>
    <div class="transfer-panel">
      <div class="transfer-panel-header">
        <el-checkbox
          v-model="rightAllChecked"
          :disabled="!(rightDataList && rightDataList.length)"
          :indeterminate="isIndeterminateRight"
          @change="handleCheckAllChangeRight">{{ rightTitle }}</el-checkbox>
          <!-- 右侧数据量/限制最大可保存数据量 -->
        <span class="transfer-panel-ratio">{{ rightDataList.length }}/{{ maxLimitCount }}</span>
      </div>
      <div class="transfer-panel-body">
        <el-checkbox-group
          v-if="rightDataList && rightDataList.length"
          v-model="rightCheckedKeyList"
          @change="handleCheckRight">
          <draggable
            v-model="rightDataList"
            chosenClass="chosen"
            forceFallback="true"
            animation="200"
            @start="drag = true"
            @end="drag = false"
            @update="handleOrder">
            <transition-group>
              <el-checkbox
                v-for="(item, index) in rightDataList"
                :key="`right_${item[defaultProps.key]}_${index}`"
                :label="item[defaultProps.key]"
                >{{ item[defaultProps.label]}}
                <img
                  src="@/assets/drag_icon.svg"
                  alt="拖拽排序"
                  width="40"
                  height="15" /></el-checkbox>
            </transition-group>
          </draggable>
        </el-checkbox-group>
        <el-empty description="暂无数据" v-else></el-empty>
      </div>
    </div>
  </div>
</template>

<script>
import { number } from 'echarts';
import draggable from 'vuedraggable';

export default {
  name: '',
  components: {
    draggable,
  },
  props: {
    // tree的默认结构
    defaultProps: {
      type: Object,
      required: true,
      default: () => ({
        children: 'children',
        label: 'label',
        key: 'key',
        parentKey: 'parent', // 这个属性不是 tree组件需要的,是子节点数据中记录父节点标识的属性
      }),
    },
    // left 原始数据
    leftOriginalList: {
      type: Array,
      default: () => [],
    },
    // right 原始数据
    rightOriginalList: {
      type: Array,
      default: () => [],
    },
    // 最大可保存数据量
    maxLimitCount: {
      type: Number,
      default: 0,
    },
    // left 标题
    leftTitle: {
      type: String,
      default: '可选项',
    },
    // right 标题
    rightTitle: {
      type: String,
      default: '已选项',
    },
  },
  data() {
    return {
      leftAllChecked: false, // left 全选checkbox
      leftDataList: [], // left 所有数据
      leftCheckedList: [], // left 选中的数据
      isIndeterminateLeft: false,
      rightAllChecked: false, // right 全选checkbox
      rightDataList: [], // right 所有数据
      rightCheckedList: [], // right 选中的数据 =>rightCheckedKeyList对应的 对象数组
      rightCheckedKeyList: [], // right 选中的 key list => 绑定在 el-checkbox-group上的 list
      isIndeterminateRight: false,
      drag: false,
    };
  },
  // 初始化
  watch: {
    leftOriginalList: {
      immediate: true,
      deep: true,
      handler(newVal) {
        this.leftDataList = JSON.parse(JSON.stringify(newVal));
        this.leftCheckedList = [];
        this.leftAllChecked = false;
        this.isIndeterminateLeft = false;
      },
    },
    rightOriginalList: {
      immediate: true,
      deep: true,
      handler(newVal) {
        this.rightDataList = JSON.parse(JSON.stringify(newVal));
        this.rightCheckedList = [];
        this.rightCheckedKeyList = [];
        this.rightAllChecked = false;
        this.isIndeterminateRight = false;
      },
    },
  },
  computed: {
    // left 所有子节点数据的数量
    leftDataTotal() {
      let count = 0;
      this.leftDataList.forEach((v) => {
        if (v[this.defaultProps.children]) {
          count += v[this.defaultProps.children].length;
        }
      });
      return count;
    },
  },
  methods: {
    // 选择——left
    handleCheckLeft(val, { checkedNodes }) {
      // 包含了父节点
      const checkedCount = checkedNodes.length;
      const totalNodeCount = this.leftDataTotal + this.leftDataList.length;
      this.leftAllChecked = checkedCount === totalNodeCount;
      this.isIndeterminateLeft = checkedCount > 0 && checkedCount < totalNodeCount;
      // 手动剔除父节点
      this.leftCheckedList = checkedNodes.filter((v) => (!v[this.defaultProps.children]));
    },
    // 选择——right
    handleCheckRight(val) {
      const checkedCount = val.length;
      this.rightAllChecked = checkedCount === this.rightDataList.length;
      this.isIndeterminateRight = checkedCount > 0 && checkedCount < this.rightDataList.length;
      // 手动组织对象数组
      this.rightCheckedList = this.rightDataList.filter((v) => (val.includes(v[this.defaultProps.key])));
    },
    // 全选——left
    handleCheckAllChangeLeft(val) {
      this.isIndeterminateLeft = false;
      const checkedNodes = [];
      if (val) {
        this.leftDataList.forEach((v) => {
          checkedNodes.push(v);
          if (v[this.defaultProps.children]) {
            v[this.defaultProps.children].forEach((child) => { checkedNodes.push(child); });
          }
        });
      }
      // 手动赋checkedlist值
      this.leftCheckedList = checkedNodes.filter((v) => (!v[this.defaultProps.children]));
      this.$refs.leftTree.setCheckedNodes(checkedNodes);
    },
    // 全选——right
    handleCheckAllChangeRight(val) {
      this.isIndeterminateRight = false;
      this.rightCheckedKeyList = val ? this.rightDataList.map((v) => (v[this.defaultProps.key])) : [];
      // 手动赋checkedlist值
      this.rightCheckedList = val ? this.rightDataList.map((v) => (v)) : [];
    },
    // 传递 right => left
    handleLeftChange() {
      // left +
      const leftDataMap = {};
      this.leftDataList.forEach((v) => {
        leftDataMap[v[this.defaultProps.key]] = v[this.defaultProps.children] || [];
      });
      this.rightCheckedList.forEach((v) => {
        leftDataMap[v[this.defaultProps.parentKey]].push(v);
      });
      // right -
      this.rightDataList = this.rightDataList.filter((v) => !(this.rightCheckedKeyList.includes(v[this.defaultProps.key])));
      // 清空选中数组
      this.rightCheckedList = [];
      this.rightCheckedKeyList = [];
      // right 全选 => 直接取消
      this.rightAllChecked = false;
      this.isIndeterminateRight = false;
      // left 全选 => 原先没有选中/半选中=>不动,原先全选=>半选中 => 重新渲染一次 tree组件选中
      if (this.leftAllChecked && !this.isIndeterminateLeft) {
        this.leftAllChecked = false;
        this.isIndeterminateLeft = true;
      }
      // 先清空再重置,直接重置的话,父节点的状态会有问题
      this.$refs.leftTree.setCheckedNodes([]);
      this.$nextTick(() => {
        this.$refs.leftTree.setCheckedNodes(this.leftCheckedList);
      });
      // 传递当前数据分布
      this.$emit('change', {
        left: this.leftDataList,
        right: this.rightDataList,
      });
    },
    // 传递 left => right
    handleRightChange() {
      // right +
      this.rightDataList.push(...this.leftCheckedList);
      // left -
      const { key, children } = this.defaultProps;
      const checkedKeys = this.leftCheckedList.map((v) => (v[key]));
      this.leftDataList.forEach((v) => {
        if (v[children]) {
          v[children] = v[children].filter((child) => !checkedKeys.includes(child[key]));
        }
      });
      // 清空选中数组
      this.leftCheckedList = [];
      // 清空 tree组件选中
      this.$refs.leftTree.setCheckedNodes([]);
      // left 全选 => 直接取消
      this.leftAllChecked = false;
      this.isIndeterminateLeft = false;
      // right 全选 => 原先没有选中/半选中=>不动,原先全选=>半选中
      if (this.rightAllChecked && !this.isIndeterminateRight) {
        this.rightAllChecked = false;
        this.isIndeterminateRight = true;
      }
      // 传递当前数据分布
      this.$emit('change', {
        left: this.leftDataList,
        right: this.rightDataList,
      });
    },
    handleOrder() {
      // 传递当前数据分布
      this.$emit('change', {
        left: this.leftDataList,
        right: this.rightDataList,
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.transfer-tree {
  display: flex;
  width: 100%;
  .transfer-panel {
    width: 100%;
    height: 100%;
    border-radius: 4px;
    border: 1px solid $color-border;
    .transfer-panel-header {
      display: flex;
      justify-content: space-between;
      height: 30px;
      line-height: 30px;
      border-radius: 3px 3px 0px 0px;
      padding: 0 12px;
      ::v-deep .el-checkbox {
        .el-checkbox__label {
          color: $color-text;
          font-size: 14px;
          padding-left: 8px;
        }
      }
      .transfer-panel-ratio {
        font-size: 12px;
        color: $color-text;
      }
    }
    .transfer-panel-body {
      height: 200px;
      padding: 12px 12px 0 12px;
      border-top: 1px solid $color-border;
      overflow: auto;
      .transfer-panel-filter {
        float: right;
        width: 170px;
        .el-checkbox__label {
          color: $color-text;
          font-size: 12px;
          padding-left: 8px;
        }
        .el-input__inner {
          height: 26px;
          border: none;
        }
      }
      ::v-deep .el-tree {
        color: $color-text;
        margin-bottom: 4px;
        .el-tree-node__content {
          height: 22px;
          margin-bottom: 8px;
          .el-tree-node__label {
            font-size: 12px;
          }
        }
        .el-tree-node__children {
          .el-tree-node__content {
            padding-left: 12px!important;
          }
        }
        .el-tree-node__expand-icon {
          margin-left: -6px;
        }
      }
      ::v-deep .el-checkbox-group {
        margin-bottom: 4px;
        .el-checkbox {
          display: block;
          line-height: 22px;
          color: $color-text;
          margin-bottom: 8px;
          width: 100%;
          .el-checkbox__label {
            width: calc(100% - 5px);
            position: relative;
            font-size: 12px;
            padding-left: 8px;
            img{
              position: absolute;
              right: 0;
              top: 2px;
            }
          }
        }
      }
    }
  }
  .transfer-buttons {
    display: flex;
    justify-content: center;
    flex-flow: column;
    margin: 0 12px;
    .el-button {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 32px;
      height: 24px;
      padding: 0;
      margin-left: 0;
    }
  }
}
::v-deep .el-empty {
  height: 60px;
  padding: 0;
  .el-empty__image {
    display: none;
  }
  .el-empty__description {
    margin: 0;
  }
}
</style>

父组件调用

<template>
  <div>
  	<TransferTreeList
        :defaultProps="{ children: 'list', label: 'name', key: 'id', parentKey: 'classify' }"
        :leftOriginalList="unselectedList"
        :rightOriginalList="selectedList"
        :maxLimitCount="10"
        @change="handelSelectedChange" />
  </div>
</template>

<script>
import TransferTreeList from '@/components/TransferTreeList';

export default {
  name: '',
  components: {
    TransferTreeList,
  },
  data() {
    return {
      unselectedList: [ // 未被选中的选项
        {
          id: 'classify1',
          name: '分类1',
          list: [
            { id: 'kpi1-1', name: '选项1-1', classify: 'classify1' },
            { id: 'kpi1-3', name: '选项1-3', classify: 'classify1' },
          ],
        },
        {
          id: 'classify2',
          name: '分类2',
          list: [
            { id: 'kpi2-1', name: '选项2-1', classify: 'classify2' },
            { id: 'kpi2-3', name: '选项2-3', classify: 'classify2' },
          ],
        },
      ],
      selectedList: [ // 被选中的选项(选项内部要有父节点标识)
        { id: 'kpi2-2', name: '选项2-2', classify: 'classify2' },
        { id: 'kpi1-2', name: '选项1-2', classify: 'classify1' },
      ],
    };
  },
  methods: {
    handelSelectedChange(data) {
      console.log('最新数据', data)
    },
  },
};
</script>
  • 6
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值