vue2用elementUI做单选下拉树

发布于:2025-07-27 ⋅ 阅读:(13) ⋅ 点赞:(0)

1. 需求

elementUI的form表单如果实现一个下拉树的需求,也不想用级联的功能,就得自己手动搓代码了。

2. 代码

组件代码

<template>
  <div ref="treeSelect" class="tree-select">
    <div class="tree-select-dom" @click="handleIconClick">
      <el-input
        v-model="inputValue"
        :placeholder="placeholder"
        :clearable="clearable"
        :disabled="disabled"
        readonly
        @click.native="handleInputClick"
        @clear="clearSelection"
      >
        <i
          slot="suffix"
          :class="[
          	'el-input__icon', showPopover ? 'el-icon-arrow-up' : 'el-icon-arrow-down'
          ]"
        />
      </el-input>
    </div>

    <el-popover
      ref="popover"
      v-model="showPopover"
      v-click-outside="handleClickOutside"
      placement="bottom-start"
      trigger="manual"
      :width="popoverWidth"
      popper-class="tree-select-popover"
    >
      <div class="popover-content">
        <div class="tree-filter-input">
          <el-input
            v-if="filterable"
            v-model="filterText"
            placeholder="输入关键字过滤"
            prefix-icon="el-icon-search"
            size="small"
          />
        </div>

        <el-tree
          ref="tree"
          class="tree-select-tree"
          :data="data"
          :props="defaultProps"
          :highlight-current="true"
          :filter-node-method="filterNode"
          :node-key="nodeKey"
          :default-expand-all="defaultExpandAll"
          :expand-on-click-node="false"
          @node-click="handleNodeClick"
        >
          <div slot-scope="{ node }" class="custom-tree-node">
            <span :class="['tree-node-label', { 'is-leaf': node.isLeaf }]">
              <i v-if="!node.isLeaf" class="el-icon-folder" />
              <i v-else class="el-icon-document" />
              {{ node.label }}
            </span>
          </div>
        </el-tree>
      </div>
    </el-popover>
  </div>
</template>
export default {
  name: 'TreeSelect',
  directives: {
    // 自定义指令:点击外部区域关闭下拉框
    'click-outside': {
      bind: function(el, binding, vnode) {
        el.clickOutsideEvent = function(event) {
          if (!(el === event.target || el.contains(event.target))) {
            vnode.context[binding.expression](event);
          }
        };
        document.body.addEventListener('click', el.clickOutsideEvent);
      },
      unbind: function(el) {
        document.body.removeEventListener('click', el.clickOutsideEvent);
      }
    }
  },
  props: {
    // 树形数据
    data: {
      type: Array,
      default: () => []
    },
    // 树节点配置
    props: {
      type: Object,
      default: () => ({})
    },
    // 选中的节点值
    value: {
      type: [String, Number],
      default: null
    },
    // 占位文本
    placeholder: {
      type: String,
      default: '请选择'
    },
    // 是否可清空
    clearable: {
      type: Boolean,
      default: true
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false
    },
    // 是否可搜索
    filterable: {
      type: Boolean,
      default: false
    },
    // 是否默认展开所有节点
    defaultExpandAll: {
      type: Boolean,
      default: false
    },
    // 节点唯一标识字段
    nodeKey: {
      type: String,
      default: 'id'
    },
    // 弹出框宽度
    popoverWidth: {
      type: [String, Number],
      default: 300
    }
  },
  data() {
    return {
      showPopover: false,
      filterText: '',
      currentValue: null,
      currentLabel: '',
      defaultProps: {
        children: 'children',
        label: 'label'
      }
    };
  },
  computed: {
    inputValue() {
      return this.currentLabel || '';
    }
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal) {
        this.currentValue = newVal;
        if (newVal) {
          this.setCurrentLabel();
        } else {
          this.currentLabel = '';
        }
      }
    },
    props: {
      immediate: true,
      handler(newProps) {
        this.defaultProps = Object.assign({}, this.defaultProps, newProps);
      }
    },
    filterText(val) {
      this.$refs.tree.filter(val);
    }
  },
  methods: {
    // 处理输入框点击
    handleInputClick() {
      if (this.disabled) return;
      this.togglePopover();
    },

    // 处理图标点击
    handleIconClick(e) {
      e.stopPropagation();
      if (this.disabled) return;
      this.togglePopover();
    },

    // 切换下拉框显示状态
    togglePopover() {
      this.showPopover = !this.showPopover;
      if (this.showPopover) {
        this.$nextTick(() => {
          if (this.currentValue) {
            this.$refs.tree.setCurrentKey(this.currentValue);
          }
        });
      }
    },

    // 节点点击事件
    handleNodeClick(data, node) {
      if (node.isLeaf) {
        this.currentValue = data[this.nodeKey];
        this.currentLabel = node.label;
        this.$emit('input', this.currentValue);
        this.$emit('change', this.currentValue, node);
        this.showPopover = false;
      } else {
        // 非叶子节点点击展开/折叠
        node.expanded = !node.expanded;
      }
    },

    // 设置当前显示的标签
    setCurrentLabel() {
      if (!this.data.length || !this.currentValue) return;

      const findNode = (data, value) => {
        for (const node of data) {
          if (node[this.nodeKey] === value) {
            return node;
          }
          if (node[this.defaultProps.children] && node[this.defaultProps.children].length) {
            const result = findNode(node[this.defaultProps.children], value);
            if (result) return result;
          }
        }
        return null;
      };

      const node = findNode(this.data, this.currentValue);
      if (node) {
        this.currentLabel = node[this.defaultProps.label];
      }
    },

    // 清空选择
    clearSelection() {
      this.currentValue = null;
      this.currentLabel = '';
      this.$emit('input', null);
      this.$emit('change', null);
      this.showPopover = false;
    },

    // 过滤节点
    filterNode(value, data, node) {
      if (!value) return true;
      return node.label && node.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
    },

    // 处理点击外部区域
    handleClickOutside() {
      this.showPopover = false;
    },

    // 获取当前选中的节点
    getCurrentNode() {
      return this.$refs.tree.getCurrentNode();
    }
  }
};
<style lang="less" scoped>
.tree-select {
  position: relative;
  display: inline-block;
  width: 100%;
}
.tree-select-dom{
    position: relative;

    &::after{
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        content: '';
        cursor: pointer;
    }
}
.tree-select .el-input {
  cursor: pointer;
}

.tree-select .el-input__icon {
  transition: transform 0.3s;
  cursor: pointer;
}

.popover-content {
  padding: 10px;
  max-height: 300px;
  overflow-y: auto;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.tree-filter-input {
  margin-bottom: 10px;
}

.custom-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  font-size: 14px;
  padding: 5px 0;
}

.tree-node-label {
  display: flex;
  align-items: center;
  transition: all 0.2s;
}

.tree-node-label:hover {
  color: #3498db;
}

.tree-node-label i {
  margin-right: 8px;
}

.tree-node-label.is-leaf i {
  color: #2ecc71;
}
</style>

<style lang="less">
.tree-select-popover{
    background-color: #4e5470;
    padding: 0;
    .popover-content{
        background-color: #4e5470;

        display: flex;
        flex-direction: column;

        .tree-filter-input{
            flex-shrink: 0;
        }
    }
    .tree-select-tree{
        flex: 1;
        overflow-x: hidden;
        overflow-y: auto;
    }

    .el-input__inner{
        padding-left: 30px;
    }
}
</style>

3. 使用

<template>
	<TreeSingleSelect
	    v-model="selectedValue1"
        :data="treeData"
        :prop="{
	        children: 'subitems',
        	label: 'name'
        }"
        placeholder="请选择部门"
        node-key="id"
        filterable
        :default-expand-all="true"
        class="demo-component"
   />
</template>
import TreeSingleSelect from '@/components/treeSelect/TreeSingleSelect.vue';

export default {
	components: {
		TreeSingleSelect 
	},
	data() {
		return {
			selectedValue1: '',
			treeData: [
            {
              id: 1,
              label: '集团总部',
              children: [
                {
                  id: 101,
                  label: '总裁办公室',
                  children: [
                    { id: 1011, label: '秘书处' },
                    { id: 1012, label: '行政科' }
                  ]
                },
                {
                  id: 102,
                  label: '人力资源部',
                  children: [
                    { id: 1021, label: '招聘组' },
                    { id: 1022, label: '培训组' },
                    { id: 1023, label: '薪酬组' }
                  ]
                }
              ]
            }
          ]
		}
	}
}

4. 效果

在这里插入图片描述

求关注
在这里插入图片描述

网站公告

今日签到

点亮在社区的每一天
去签到