基于Element Tree的组织架构选择组件设计与实现

539 阅读3分钟

一、组件需求背景

产品要做一个树形组织选择组件,既可以选中或取消所有下级,也可以单独选中当前组织。 支持树的搜索等功能

二、功能特点

  1. 支持单选/多选模式
  • 支持搜索过滤

  • 支持展示选择路径

  • 支持清空/删除已选项

  • 支持禁用状态

  • 支持权限控制

  • 支持自定义占位符

  • 支持可折叠标签

三、核心实现

1. 选择器外观实现

看起来就是一个普通的下拉选择器,点击之后又出现弹框。

1737441849174.png

<template>
  <div class="el-select">
    <!-- 多选标签区域 -->
    <div class="el-select__tags" v-if="multiple">
      <!-- 折叠标签模式 -->
      <span v-if="collapseTags">
        <el-tag>{{ tagsList[0] }}</el-tag>
        <el-tag v-if="tagsList.length > 1">
          + {{ tagsList.length - 1 }}
        </el-tag>
      </span>
      
      <!-- 完整标签模式 -->
      <transition-group v-else>
        <el-tag v-for="item in tagsList">
          {{ item }}
        </el-tag>
      </transition-group>
    </div>

    <!-- 输入框区域 -->
    <el-input
      v-model="innerValue"
      readonly
      :placeholder="placeholder"
    >
      <i slot="suffix" class="el-select__caret"></i>
    </el-input>
  </div>
</template>

点击之后出现弹框

1737441858295.png

2. 弹窗选择实现

支持点击节点和勾选节点

<template>
  <el-dialog title="选择组织" width="800px">
    <div class="transfer__body">
      <!-- 左侧树形选择 -->
      <div class="transfer-pane">
        <el-input 
          v-model="keyword"
          placeholder="请输入关键词查询"
        />
            <el-tree
                :data="treeData"
                :props="props"
                :expand-on-click-node="false"
                default-expand-all
                @node-click="handleNodeClick"
                class="JNPF-common-el-tree"
                node-key="id"
                v-loading="loading"
                ref="tree"
                show-checkbox
                check-strictly
                @check="handleCheck" 
                :filter-node-method="filterNode">
                <span class="custom-tree-node" slot-scope="{ node, data }">
                    <i :class="data.icon"></i>
                    <span class="text">{{ node.label }}</span>
                </span>
            </el-tree>
      </div>

      <!-- 右侧已选列表 -->
      <div class="transfer-pane">
        <div v-for="item in selectedData">
          <span>{{ item }}</span>
          <i class="el-icon-delete" @click="removeData"></i>
        </div>
      </div>
    </div>
  </el-dialog>
</template>

3. 树节点选择与联动

支持双边操作 1737441872908.png

methods: {
  // 处理节点选择
  handleCheck(node, {checkedKeys}) {
    const tree = this.$refs.tree;
    // 获取所有子节点
    const children = this.getChildren(node);
    
    if (children.length > 0) {
      if (checkedKeys.includes(node.id)) {
        // 选中父节点时联动选中子节点
        children.forEach(child => {
          tree.setChecked(child.id, true);
          this.handleNodeClick(child);
        });
      } else {
        // 取消父节点时联动取消子节点
        children.forEach(child => {
          tree.setChecked(child.id, false);
          this.removeSelectedNode(child);
        });
      }
    }
  },

  // 获取所有子节点
  getChildren(node) {
    const children = [];
    const traverse = (node) => {
      if (node.children) {
        node.children.forEach(child => {
          children.push(child);
          traverse(child);
        });
      }
    };
    traverse(node);
    return children;
  }
}

4. 搜索过滤功能

1737441894406.png

methods: {
  // 搜索过滤
  filterNode(value, data) {
    if (!value) return true;
    return data.fullName.indexOf(value) !== -1;
  },

  // 递归判断子节点是否包含关键字
  includesChildren(node, keyword) {
    let flag = false;
    const traverse = (node) => {
      if (node.children) {
        node.children.forEach(child => {
          if(child.fullName.includes(keyword)){
            flag = true;
          } else {
            traverse(child);
          }
        });
      }
    };
    traverse(node);
    return flag;
  }
}

5. 数据回显与同步

1737442016937.png

methods: {
  // 设置默认值
  setDefault() {
    if (!this.value?.length) {
      this.clearSelection();
      return;
    }

    // 处理选中数据
    let selectedIds = this.multiple ? this.value : [this.value];
    this.selectedIds = JSON.parse(JSON.stringify(selectedIds));
    
    // 构建显示文本
    let textList = selectedIds.map(item => {
      return item.map(id => {
        const node = this.allList.find(n => n.id === id);
        return node?.fullName;
      }).join('/');
    });

    this.selectedData = textList;
    this.updateInputValue();
  },

  // 更新输入框显示
  updateInputValue() {
    if (this.multiple) {
      this.innerValue = '';
      this.tagsList = this.selectedData.map(text => 
        text.split('/').slice(-1)[0]
      );
    } else {
      this.innerValue = this.selectedData.join(',');
    }
  }
}

四、性能优化

1. 虚拟滚动

对于大量数据的组织树,可以使用虚拟滚动优化性能:

import VirtualScroll from 'vue-virtual-scroll-list'

components: {
  VirtualScroll
},

computed: {
  virtualizedData() {
    return this.treeData.map((node, index) => ({
      index,
      node
    }))
  }
}

2. 懒加载

对于深层次的组织树,采用懒加载方式:

async loadNode(node, resolve) {
  if (node.level === 0) {
    resolve(this.getRootNodes())
    return
  }
  
  const children = await this.loadChildrenNodes(node.data.id)
  resolve(children)
}

3. 搜索防抖

import { debounce } from 'lodash'

created() {
  this.search = debounce(this.search, 300)
}

五、使用示例

<template>
  <com-select-org
    v-model="selectedOrgs"
    :multiple="true"
    :auth="true"
    placeholder="请选择组织"
    @change="handleOrgChange"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedOrgs: []
    }
  },
  methods: {
    handleOrgChange(ids, data) {
      console.log('选中的组织ID:', ids)
      console.log('选中的组织数据:', data)
    }
  }
}
</script>

写在最后

本文详细介绍了Element Tree组件在企业级应用中的高级使用方法,从基础的节点状态控制到复杂的父子联动选择,从简单的数据过滤到完整的节点管理机制。

希望这篇文章能给正在使用Element Tree组件的同学带来帮助!如果觉得文章对你有帮助,欢迎点赞转发,也欢迎在评论区分享你在使用Element Tree时的心得体会!

关注我,带你玩转前端开发,一起探讨更多技术话题!