vue+element实现树形结构多选及单选

2,984 阅读2分钟

vue+element+el-tree实现树形下拉可支持单选/多选/选择父类亲测可用,后续上图片暂时先上代码借鉴于[](github.com/yujinpan/el…) 但是这个不支持点击复选框单选和选择有子类的功能,我这边略微改动了一下。

话不多说直接上代码

/**单选*/
<selectTree
    size="small"
    :props="props"
    :data="treeList"
    v-model="value"
    node-key="id"
    show-checkbox
    check-strictly
    default-expand-all
/>

/**多选*/
<selectTree
    size="small"
    :props="props"
    :data="treeList"
    v-model="value"
    node-key="id"
    show-checkbox
    check-strictly
    default-expand-all
    multiple
/>

export default {
    data() {
        return {
          treeList: [{
          label: '一级 1',
          children: [{
            label: '二级 1-1',
            children: [{
              label: '三级 1-1-1'
            }]
          }]
        }, {
          label: '一级 2',
          children: [{
            label: '二级 2-1',
            children: [{
              label: '三级 2-1-1'
            }]
          }, {
            label: '二级 2-2',
            children: [{
              label: '三级 2-2-1'
            }]
          }]
        }, {
          label: '一级 3',
          children: [{
            label: '二级 3-1',
            children: [{
              label: '三级 3-1-1'
            }]
          }, {
            label: '二级 3-2',
            children: [{
              label: '三级 3-2-1'
            }]
          }]
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
        }
    }
}

components代码部分

<template>
  <div class="el-select-tree">
    <el-popover
      ref="elPopover"
      v-model="visible"
      transition="el-zoom-in-top"
      popper-class="el-select-tree__popover"
      trigger="click"
      :disabled="disabled"
      :placement="placement"
      :width="popoverWidth"
      @after-enter="handleScroll()"
    >
      <!-- scrollbar wrap -->
      <el-scrollbar
        wrap-class="el-select-dropdown__wrap"
        view-class="el-select-dropdown__list"
        ref="scrollbar"
      >
        <el-tree
          ref="elTree"
          class="el-select-tree__list"
          :default-expanded-keys="defaultExpandedKeys"
          :show-checkbox="showCheckbox"
          :expand-on-click-node="multiple"
          :style="{ 'min-width': minWidth + 'px' }"
          @node-click="nodeClick"
          @check-change="checkChange"
          @transitionend.native="$refs.elPopover.updatePopper()"
          :data="data"
          :props="props"
          :node-key="propsValue"
          :default-expand-all="defaultExpandAll"
          :check-strictly="checkStrictly"
          :lazy="lazy"
          :load="load"
          :icon-class="iconClass"
          :indent="indent"
          :accordion="accordion"
          :filter-node-method="filterNodeMethod"
          :auto-expand-parent="autoExpandParent"
          :render-content="renderContent"
          :render-after-expand="renderAfterExpand"
        >
          <div
            class="el-select-tree__item"
            slot-scope="{ data }"
            :class="treeItemClass(data)"
          >
            {{ data[propsLabel] }}
          </div>
        </el-tree>
      </el-scrollbar>
      <!-- trigger input -->
      <el-input
        v-model="selectedLabel"
        ref="reference"
        slot="reference"
        readonly
        :validate-event="false"
        :size="size"
        :class="{
          'is-active': visible,
          'is-selected': selectedLabel,
          'is-clearable': clearable
        }"
        :disabled="disabled"
        :placeholder="placeholder"
      >
        <i
          v-if="clearable"
          @click.stop="clear()"
          slot="suffix"
          class="el-input__icon el-input__icon-close el-icon-circle-close"
        ></i>
        <i
          slot="suffix"
          class="el-input__icon el-input__icon-arrow-down el-icon-arrow-down"
        ></i>
      </el-input>
    </el-popover>
  </div>
</template>

<script>
  import Vue from 'vue';
  import Emitter from 'element-ui/lib/mixins/emitter';
  import {
    addResizeListener,
    removeResizeListener
  } from 'element-ui/lib/utils/resize-event';

  export default {
    name: 'ElSelectTree',
    mixins: [Emitter],
    model: {
      prop: 'value',
      event: 'change'
    },
    props: {
      // [el-tree] forwarding parameters https://element.eleme.io/#/zh-CN/component/tree#attributes
      data: {
        type: Array,
        default() {
          return [];
        }
      },
      props: {
        type: Object,
        default() {
          return {
            value: 'value',
            label: 'label',
            children: 'children',
            disabled: 'disabled',
            isLeaf: 'isLeaf'
          };
        }
      },
      checkStrictly: Boolean,
      showCheckbox: Boolean,
      nodeKey: String,
      defaultExpandAll: Boolean,
      lazy: Boolean,
      load: Function,
      iconClass: String,
      indent: Number,
      accordion: Boolean,
      filterNodeMethod: Function,
      autoExpandParent: {
        type: Boolean,
        default: true
      },
      renderContent: Function,
      renderAfterExpand: Boolean,
      // [el-tree] forwarding parameters end
      clearable: Boolean,
      placeholder: {
        type: String,
        default: '请选择'
      },
      placement: {
        type: String,
        default: 'bottom-start'
      },
      size: {
        type: String,
        default: Vue.prototype.$ELEMENT ? Vue.prototype.$ELEMENT.size : ''
      },
      disabled: Boolean,
      multiple: Boolean,
      value: {
        type: [Number, String, Array],
        default: ''
      },
      popoverWidth: Number
    },
    computed: {
      propsValue() {
        return this.nodeKey || this.props.value || 'value';
      },

      propsLabel() {
        return this.props.label || 'label';
      },

      propsIsLeaf() {
        return this.props.isLeaf || 'isLeaf';
      },

      defaultExpandedKeys() {
        return Array.isArray(this.value)
          ? this.value
          : this.value || this.value === 0
            ? [this.value]
            : [];
      }
    },

    data() {
      return {
        visible: false,
        selectedLabel: '',
        minWidth: 0
      };
    },

    methods: {
      valueChange(value, node) {
        this.$emit('change', value, node);
      },

      clear() {
        this.visible = false;
        if (this.multiple) {
          this.valueChange([]);
          this.$nextTick(() => {
            this.$refs.elTree.setCheckedKeys([]);
          });
        } else {
          this.valueChange('');
        }
        this.$emit('clear');
      },

      // 触发滚动条的重置
      handleScroll() {
        this.$refs.scrollbar && this.$refs.scrollbar.handleScroll();
      },

      nodeClick(data, node, component) {
        const children = data[this.props.children];
        const value = data[this.propsValue];
        if (
          this.showCheckbox ||
          ((children && children.length) ||
            (this.lazy && !data[this.propsIsLeaf])) &&
          !this.checkStrictly
        ) {
          component.handleExpandIconClick();
        } else if (!this.multiple && !data.disabled) {
          if (value !== this.value) {
            this.valueChange(value, data);
            this.selectedLabel = data[this.propsLabel];
          }
          this.visible = false;
        }
      },

      checkChange(data, checked) {
        if (this.multiple) {
          //多选处理
          const elTree = this.$refs.elTree;
          const leafOnly = !this.checkStrictly;
          const keys = elTree.getCheckedKeys(leafOnly);
          const nodes = elTree.getCheckedNodes(leafOnly);
          this.valueChange(keys, nodes);
          this.setMultipleSelectedLabel();
        }
        else {
          //显示checkbox,但不多选处理
          if (checked) {
            const value = data[this.propsValue];
            this.valueChange(value, data);
            this.$refs.elTree.setCheckedNodes([data]);
            this.visible = false;
          }
        }
      },

      setSelected() {
        this.$nextTick(() => {
          const elTree = this.$refs.elTree;
          if (this.multiple) {
            elTree.setCheckedKeys(this.value);
            this.setMultipleSelectedLabel();
          } else {
            const selectedNode = elTree.getNode(this.value);
            this.selectedLabel = selectedNode
              ? selectedNode.data[this.propsLabel]
              : '';
          }
        });
      },

      setMultipleSelectedLabel() {
        const elTree = this.$refs.elTree;
        const selectedNodes = elTree.getCheckedNodes(!this.checkStrictly);
        this.selectedLabel = selectedNodes
          .map((item) => item[this.propsLabel])
          .join(',');
      },

      treeItemClass(data) {
        return {
          'is-selected': this.multiple
            ? false
            : data[this.propsValue] === this.value,
          'is-disabled': data.disabled
        };
      },

      handleResize() {
        // set the `tree` default `min-width`
        // border's width is 2px
        this.minWidth = this.$el.clientWidth - 2;
      }
    },

    watch: {
      value() {
        this.setSelected();
        // trigger parent `el-form-item` validate event
        this.dispatch('ElFormItem', 'el.form.change');
      },
      data() {
        this.setSelected();
      }
    },

    created() {
      if (this.multiple && !Array.isArray(this.value)) {
        throw new Error(
          '[el-select-tree] props `value` must be Array if use multiple!'
        );
      }
    },

    mounted() {
      this.setSelected();
      addResizeListener(this.$el, this.handleResize);
    },

    beforeDestroy() {
      if (this.$el && this.handleResize) {
        removeResizeListener(this.$el, this.handleResize);
      }
    }
  };
</script>
<style lang="scss" scoped>
  @import './common-variables';
  ::v-deep :focus {
    outline: 0;
  }
  .el-select-tree {
    display: inline-block;
    .el-input__icon {
      cursor: pointer;
      transition: transform 0.3s;
      &-close {
        display: none;
      }
    }
    .el-input__inner {
      cursor: pointer;
      padding-right: 30px;
    }
    .el-input {
      &:hover:not(.is-disabled) {
        .el-input__inner {
          border-color: $--input-border-color-hover;
        }
        &.is-selected.is-clearable {
          .el-input__icon {
            &-close {
              display: inline-block;
            }
            &-arrow-down {
              display: none;
            }
          }
        }
      }
      &.is-active {
        .el-input__icon-arrow-down {
          transform: rotate(-180deg);
        }
        .el-input__inner {
          border-color: $--button-primary-border-color;
        }
      }
    }
    &__popover {
      padding: 0 !important;
      // extends el-select-dropdown - start
      border: $--select-dropdown-border !important;
      border-radius: $--border-radius-base !important;
      // extends el-select-dropdown - end
      .popper__arrow {
        left: 35px !important;
      }
      .el-tree-node__expand-icon.is-leaf {
        cursor: pointer;
      }
    }
    &__list {
      overflow-y: auto;
      // scroll style - start
      &::-webkit-scrollbar-track-piece {
        background: #d3dce6;
      }
      &::-webkit-scrollbar {
        width: 4px;
      }
      &::-webkit-scrollbar-thumb {
        background: #99a9bf;
      }
      // scroll style - end
    }
    &__item {
      position: relative;
      white-space: nowrap;
      padding-right: $spacing-medium;
      &.is-selected {
        color: $--select-option-selected-font-color;
        font-weight: bolder;
      }
      &.is-disabled {
        color: $--font-color-disabled-base;
        cursor: not-allowed;
      }
    }
  }
</style>

common-variables.scss

// 导入 element-ui 的默认样式变量
@import 'node_modules/element-ui/packages/theme-chalk/src/common/var.scss';

// 基准颜色
$color-blue: rgba(157, 184, 233, 1);
$color-primary: $--color-primary;
$color-white: $--color-white;
$color-black: $--color-black;
$color-success: $--color-success;
$color-warning: $--color-warning;
$color-danger: $--color-danger;
$color-info: $--color-info;

// 文本颜色
$color-text-primary: $--color-text-primary;
$color-text-regular: $--color-text-regular;
$color-text-secondary: $--color-text-secondary;
$color-text-placeholder: $--color-text-placeholder;

// 边框颜色
$border-color-base: $--border-color-base;
$border-color-light: $--border-color-light;
$border-color-lighter: $--border-color-lighter;
$border-color-extra-light: $--border-color-extra-light;

// 背景颜色
$background-color-base: $--background-color-base;

// 图标颜色
$icon-color: $--icon-color;
$icon-color-base: $color-info;

// 边框样式
$border-width-base: $--border-width-base;
$border-style-base: $--border-style-base;
$border-color-hover: $--color-text-placeholder;
$border-base: $--border-width-base $--border-style-base $--border-color-base;
$border-radius-base: $--border-radius-base;
$border-radius-small: $--border-radius-small;
$border-radius-circle: $--border-radius-circle;
$border-radius-zero: $--border-radius-zero;

// 投影样式
$box-shadow-default: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
$box-shadow-base: $--box-shadow-base;
$box-shadow-dark: $--box-shadow-dark;
$box-shadow-light: $--box-shadow-light;

// 字体大小
$font-size-extra-small: $--font-size-extra-small; // 12px
$font-size-small: $--font-size-small; // 13px
$font-size-base: $--font-size-base; // 14px
$font-size-medium: $--font-size-medium; // 16px
$font-size-large: $--font-size-large; // 18px
$font-size-extra-large: $--font-size-extra-large; // 20px

// 字体颜色
$font-color: #444;

// 间距大小
$spacing-base: 10px;
$spacing-medium: 20px;
$spacing-large: 30px;

// 过渡效果
$transition-base: all 0.3s;