my-form/src/common/TreeSelect.vue

<template>
  <BasePicker class="my-tree-select"
              :disabled="readonly || disabled"
              ref="picker"
              popper-class="my-tree-select__popover"
              ref-class="my-tree-select__ref">
    <template v-slot:reference>
      <TagInput v-if="multiple"
                :placeholder="placeholder"
                :size="size"
                :value="tags"
                :disabled="disabled"
                :readonly="readonly"
                :collapse-tags="collapseTags"
                :closable="closable"
                :allow-create="false"
                @remove="handleRemove"
                icon="el-icon-arrow-down" 
                > 
                </TagInput>
                <!-- :value="checked ? checked[keyMap.label]:''" -->
      <ElInput v-else
               :value="singleSelValShow()"
               :placeholder="placeholder"
               :size="size"
               :disabled="disabled"
               :readonly="readonly" 
               >
                <i v-show="!clearable || (!checked && clearable)" slot="suffix" class="el-input__icon el-icon-arrow-down" ></i>
                <i v-show="checked && clearable" slot="suffix" class="el-input__icon el-icon-error" style="cursor:pointer" @click.stop="clearClickHandle"></i>
               </ElInput>
    </template>

    <div class="my-tree-select__content">
      <ElInput v-if="filter"
               class="my-tree-select__filter"
               v-model="query"
               size="small"
               clearable
               placeholder="请输入筛选关键字"></ElInput>
      <Tree class="my-tree-select__tree is-line"
            ref="tree"
            v-bind="tree"
            :highlight-current="!multiple"
            :show-checkbox="multiple"
            @current-change="handleCurrentChange"
            @check-change="handleCheckChange"
            :node-key="keyMap.value"
            :filter-node-method="filterNodeMethod"
            :current-node-key="currentNodeKey"
            :default-checked-keys="defaultCheckedKeys"
            :data="optionsTree"
            :style="treeStyle">
        <template v-slot="{node, data}">
            <span :class="{'is-disabled':node[keyMap.disabled] || (onlyLeaf && !node.isLeaf)}">
              <slot :node="node"
                    :data="data">{{node[keyMap.label]}}</slot></span>
        </template>
      </Tree>
    </div>

  </BasePicker>
</template>

<script>
  /**
   * 下拉树组件
   * @module $ui/components/my-form/src/common/TreeSelect
   */
  import {Input, Tree} from 'element-ui'
  import BasePicker from './BasePicker'
  import TagInput from './TagInput'
  import {create as createTree, treeRevert, findPath} from '$ui/utils/tree'
  import {cloneDeep, isEqual} from '$ui/utils/util'

  /**
   * 属性参数
   * @member props
   * @property {string|number|array} [value] 值,多选为数组,支持双向绑定
   * @property {array} [options] 选项数组 [{id, parentId, label, value}]
   * @property {Object} [keyMap] 字段映射 {id, parentId, label, value, disabled}
   * @property {boolean} [multiple] 开启多选
   * @property {boolean} [clearable] 是否可以删除
   * @property {string} [size] 尺寸,可选值:'medium', 'small', 'mini', ''
   * @property {boolean} [disabled]  禁用
   * @property {boolean} [readonly] 只读
   * @property {number} [minWidth=300] 弹出层的最小宽度
   * @property {number} [maxHeight] 弹出层的最大高度
   * @property {boolean|function} [filter] 筛选函数, false即不开启筛选功能
   * @property {*} [root=null] 树的根节点,parentId的值
   * @property {boolean} [collapseTags]  折叠标签,多选有效
   * @property {boolean} [closable] 允许关闭标签,多选有效
   * @property {boolean} [useOriginOpts] 是否使用原options作为参数, 默认false
   * @property {object} [tree] el-tree的其他参数
   *
   */
  export default { 
    components: {
      ElInput: Input,
      TagInput,
      Tree,
      BasePicker
    },
    props: {
      // 值
      value: [String, Number, Array],
      // 选项数据
      options: {
        type: Array,
        default() {
          return []
        }
      },
      // 字段映射
      keyMap: {
        type: Object,
        default() {
          return {
            id: 'id',
            parentId: 'parentId',
            label: 'label',
            disabled: 'disabled',
            value: 'value'
          }
        }
      },
      // 开启多选
      multiple: Boolean,
      // 可以删除
      clearable: Boolean,
      placeholder: String,
      // 尺寸
      size: {
        type: String,
        default: '',
        validator(val) {
          return ['medium', 'small', 'mini', ''].includes(val)
        }
      },
      // 禁用
      disabled: Boolean,
      // 只读
      readonly: Boolean,

      // 筛选函数
      filter: {
        type: [Boolean, Function],
        default: true
      },
      // 树的根节点,parentId的值
      root: {
        type: [String, Number, Boolean],
        default: null
      },
      // 是否保留根节点
      withRoot: {
        type: Boolean,
        default: false
      },
      // 折叠标签,多选有效
      collapseTags: Boolean,
      // 允许关闭标签,多选有效
      closable: {
        type: Boolean,
        default: true
      },
      // 树其他参数
      tree: {
        type: Object,
        default() {
          return {}
        }
      },
      // 弹出层的最大高度
      maxHeight: {
        type: Number,
        default: 300
      },
      onlyLeaf: {
        type: Boolean,
        default: false
      },
      // 是否使用原options作为参数
      useOriginOpts: {
        type: Boolean,
        default: false
      },
      // 是否显示选中完整路径
      showFull: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        checked: null,
        query: '',
        optionsProxy: []
      }
    },
    computed: {
      treeStyle() {
        return {
          maxHeight: `${this.maxHeight}px`
        }
      },
      optionsTree() {
        if (this.useOriginOpts) {
          return cloneDeep(this.optionsProxy)
        } else {
          const {id, parentId} = this.keyMap
          return createTree(this.optionsProxy || [], this.root, id, parentId, this.withRoot)
        }
      },
      currentNodeKey() {
        if (this.multiple) {
          return
        }
        return this.checked ? this.checked[this.keyMap.value] : undefined
      },
      defaultCheckedKeys() {
        if (!this.multiple) return
        const {value} = this.keyMap
        return this.checked ? this.checked.map(n => n[value]) : []
      },
      filterNodeMethod() {
        const {label} = this.keyMap
        return typeof this.filter === 'function'
          ? this.filter
          : (value, data) => {
            if (!value) return true;
            return data[label].indexOf(value) !== -1;
          }
      },
      tags() {
        const {label, value} = this.keyMap
        return (this.checked || []).map(n => {
          let output = ''
          if (this.showFull) {
            const path = this.getCheckedPath(n[value])
            output = path && path.length ? path.map((item) => {
              return item[label]
            }).join('/') : n[label]
          } else {
            output = n[label]
          }
         return output
        })
      }
    },
    watch: {
      options: {
        immediate: true,
        handler(val) {
          if (this.useOriginOpts) {
            setTimeout(() => {
              this.optionsProxy = val
            }, 300)
          } else {
            this.optionsProxy = val
          }
        }
      },
      optionsTree: {
        immediate: true,
        handler(val) {
          if (this.value) {
            this.resetValueByOpts(this.value)
          }
        }
      },
      value: {
        immediate: true,
        handler(val) {
          this.resetValueByOpts(val)
        }
      },
      checked(val) {
        this.$emit('change', val)
      },
      query(val) {
        this.$refs.tree.filter(val)
      }
    },
    methods: {
      // 将value 进行回填,需要使用到options的数据,因此如果options的数据变化,也要执行一次。
      resetValueByOpts(val) {
        const {value} = this.keyMap
        if (isEqual(val, this.checked)) return
        // 若使用原装options 需要先将原装树转化为一维数组
        const _opts = this.useOriginOpts ? treeRevert(this.optionsProxy) : this.optionsProxy 
        if (this.multiple) {
          const vals = val ? [].concat(val) : [] 
          this.checked = _opts.filter(item => {
            return vals.includes(item[value])
          })
          // 重置tree组件
          this.$refs.tree && this.$refs.tree.setCheckedNodes(this.checked)
        } else {
          this.checked = _opts.find(item => {
            return val === item[value]
          })
        } 
      },
      handleCurrentChange(item, node) {
        if (this.multiple || item.disabled || this.readonly || this.disabled) return
        if (this.onlyLeaf && !node.isLeaf) return
        setTimeout(() => {
          this.checked = item
        }, 100)
        this.$emit('input', item[this.keyMap.value])
        this.$nextTick(() => {
          this.$refs.picker.visible = false
        })
      },
      handleCheckChange() {
        // 修复由于tree checkbox 变化导致频繁触发‘change’事件 的bug
        if (!this._timer) {
          this._timer = setTimeout(() => {
            this.checked = this.$refs.tree.getCheckedNodes()
            
            const {value} = this.keyMap
            let result
            if (this.onlyLeaf) {
              result = this.checked.filter((item) => {
                return !item.children
              }).map(n => n[value])
            } else {
              result = this.checked.map(n => n[value])
            }
            
            this.$emit('input', result)
            this._timer = null
          }, 100)
        }
      },
      handleRemove(index) {
        if (this.checked) {
          this.checked.splice(index, 1)
          const {value} = this.keyMap
          const keys = this.checked.map(n => n[value])
          this.$refs.tree.setCheckedNodes(keys)
          this.$emit('input', keys)
          this.$refs.picker.visible = false
        }
      },
      clearClickHandle() {
        this.checked = null
        this.$emit('input', '')
      },
      singleSelValShow() {
        if (this.checked) {
          if (this.showFull) { 
            const path = this.getCheckedPath(this.checked[this.keyMap.value])
            const output = path && path.length ? path.map((item) => {
              return item[this.keyMap.label]
            }).join('/') : this.checked[this.keyMap.label]
            return output
          } else { 
            return this.checked[this.keyMap.label]
          }
        } else {
          return ''
        }
      },
      getCheckedPath(value) {
        const treeChildrenProp = this.tree.props && this.tree.props.children || 'children'
        const path = findPath(this.optionsTree, (item, index, data) => {
          return item[this.keyMap.value] === value
        }, treeChildrenProp)
        return path
      }
    }
  }
</script>