Skip to content

TreeSelect 树形选择

概述

TreeSelect 树形选择组件是一个结合了树形结构和选择器功能的复合组件。它允许用户在树形数据结构中进行单选或多选操作,特别适用于具有层级关系的数据选择场景。

主要特性

  • 树形结构展示:支持多层级数据的树形展示
  • 单选/多选模式:支持单选和多选两种模式
  • 搜索过滤:内置搜索功能,快速定位目标节点
  • 懒加载:支持异步加载子节点数据
  • 自定义渲染:支持自定义节点内容和样式
  • 禁用控制:可以禁用特定节点或整个组件
  • 表单集成:与 el-form 完美集成

适用场景

  • 组织架构选择
  • 地区/城市选择
  • 分类目录选择
  • 权限管理
  • 文件目录选择
  • 菜单配置

学习目标

基础知识

  • 掌握 TreeSelect 组件的基本概念和使用场景
  • 学会基础的树形选择功能实现
  • 了解树形数据结构的配置方法
  • 掌握单选和多选模式的使用

进阶技能

  • 学会搜索过滤功能的配置
  • 掌握懒加载的实现方法
  • 了解自定义节点渲染
  • 学会禁用状态的控制

实战应用

  • 能够构建完整的组织架构选择器
  • 掌握地区选择功能的实现
  • 了解性能优化和用户体验提升
  • 学会与其他组件的集成使用

基础用法

基本树形选择

最简单的树形选择功能:

vue
<template>
  <div>
    <h4>基础树形选择</h4>
    <el-tree-select
      v-model="value"
      :data="data"
      :render-after-expand="false"
      :show-checkbox="false"
      placeholder="请选择"
      @change="handleChange"
    />
    <p>选择的值:{{ value }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const value = ref('')

const data = [
  {
    id: 1,
    label: '一级 1',
    children: [
      {
        id: 4,
        label: '二级 1-1',
        children: [
          {
            id: 9,
            label: '三级 1-1-1'
          },
          {
            id: 10,
            label: '三级 1-1-2'
          }
        ]
      }
    ]
  },
  {
    id: 2,
    label: '一级 2',
    children: [
      {
        id: 5,
        label: '二级 2-1'
      },
      {
        id: 6,
        label: '二级 2-2'
      }
    ]
  },
  {
    id: 3,
    label: '一级 3',
    children: [
      {
        id: 7,
        label: '二级 3-1'
      },
      {
        id: 8,
        label: '二级 3-2'
      }
    ]
  }
]

const handleChange = (val) => {
  console.log('选择变化:', val)
}
</script>

多选模式

启用多选功能:

vue
<template>
  <div>
    <h4>多选模式</h4>
    <el-tree-select
      v-model="value"
      :data="data"
      multiple
      :show-checkbox="true"
      placeholder="请选择(可多选)"
      @change="handleChange"
    />
    <p>选择的值:{{ value }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const value = ref([])

const data = [
  {
    id: 1,
    label: '技术部',
    children: [
      {
        id: 11,
        label: '前端组',
        children: [
          { id: 111, label: 'Vue 开发' },
          { id: 112, label: 'React 开发' }
        ]
      },
      {
        id: 12,
        label: '后端组',
        children: [
          { id: 121, label: 'Java 开发' },
          { id: 122, label: 'Python 开发' }
        ]
      }
    ]
  },
  {
    id: 2,
    label: '产品部',
    children: [
      { id: 21, label: '产品经理' },
      { id: 22, label: 'UI 设计师' }
    ]
  },
  {
    id: 3,
    label: '运营部',
    children: [
      { id: 31, label: '内容运营' },
      { id: 32, label: '用户运营' }
    ]
  }
]

const handleChange = (val) => {
  console.log('多选变化:', val)
}
</script>

可搜索

添加搜索功能:

vue
<template>
  <div>
    <h4>可搜索的树形选择</h4>
    
    <div class="search-example">
      <el-tree-select
        v-model="selectedValue"
        :data="searchData"
        filterable
        :filter-node-method="filterNode"
        placeholder="输入关键字搜索"
        @change="handleSearchChange"
        style="width: 300px"
      />
    </div>
    
    <div class="search-info">
      <p>选择的值:{{ selectedValue }}</p>
      <p>选择的标签:{{ getSelectedLabel() }}</p>
    </div>
    
    <div class="search-tips">
      <h5>搜索提示:</h5>
      <ul>
        <li>可以搜索:北京、上海、广州、深圳等城市</li>
        <li>可以搜索:华北、华东、华南等地区</li>
        <li>搜索不区分大小写</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const selectedValue = ref('')

const searchData = [
  {
    id: 'north',
    label: '华北地区',
    children: [
      {
        id: 'beijing',
        label: '北京市',
        children: [
          { id: 'chaoyang', label: '朝阳区' },
          { id: 'haidian', label: '海淀区' },
          { id: 'dongcheng', label: '东城区' },
          { id: 'xicheng', label: '西城区' }
        ]
      },
      {
        id: 'tianjin',
        label: '天津市',
        children: [
          { id: 'heping', label: '和平区' },
          { id: 'hexi', label: '河西区' },
          { id: 'nankai', label: '南开区' }
        ]
      },
      {
        id: 'hebei',
        label: '河北省',
        children: [
          { id: 'shijiazhuang', label: '石家庄市' },
          { id: 'tangshan', label: '唐山市' },
          { id: 'qinhuangdao', label: '秦皇岛市' }
        ]
      }
    ]
  },
  {
    id: 'east',
    label: '华东地区',
    children: [
      {
        id: 'shanghai',
        label: '上海市',
        children: [
          { id: 'huangpu', label: '黄浦区' },
          { id: 'xuhui', label: '徐汇区' },
          { id: 'changning', label: '长宁区' },
          { id: 'jingan', label: '静安区' }
        ]
      },
      {
        id: 'jiangsu',
        label: '江苏省',
        children: [
          { id: 'nanjing', label: '南京市' },
          { id: 'suzhou', label: '苏州市' },
          { id: 'wuxi', label: '无锡市' }
        ]
      },
      {
        id: 'zhejiang',
        label: '浙江省',
        children: [
          { id: 'hangzhou', label: '杭州市' },
          { id: 'ningbo', label: '宁波市' },
          { id: 'wenzhou', label: '温州市' }
        ]
      }
    ]
  },
  {
    id: 'south',
    label: '华南地区',
    children: [
      {
        id: 'guangdong',
        label: '广东省',
        children: [
          { id: 'guangzhou', label: '广州市' },
          { id: 'shenzhen', label: '深圳市' },
          { id: 'dongguan', label: '东莞市' },
          { id: 'foshan', label: '佛山市' }
        ]
      },
      {
        id: 'fujian',
        label: '福建省',
        children: [
          { id: 'fuzhou', label: '福州市' },
          { id: 'xiamen', label: '厦门市' },
          { id: 'quanzhou', label: '泉州市' }
        ]
      }
    ]
  }
]

const filterNode = (value, data) => {
  if (!value) return true
  return data.label.toLowerCase().includes(value.toLowerCase())
}

const handleSearchChange = (val) => {
  console.log('搜索选择变化:', val)
}

const getSelectedLabel = () => {
  if (!selectedValue.value) return '未选择'
  
  const findLabel = (nodes, targetId) => {
    for (const node of nodes) {
      if (node.id === targetId) {
        return node.label
      }
      if (node.children) {
        const result = findLabel(node.children, targetId)
        if (result) return result
      }
    }
    return null
  }
  
  return findLabel(searchData, selectedValue.value) || '未找到'
}
</script>

<style scoped>
.search-example {
  margin-bottom: 16px;
}

.search-info {
  padding: 12px;
  background-color: #f5f7fa;
  border-radius: 4px;
  margin-bottom: 16px;
}

.search-info p {
  margin: 4px 0;
  color: #606266;
}

.search-tips {
  padding: 12px;
  background-color: #f0f9ff;
  border-radius: 4px;
  border-left: 4px solid #409eff;
}

.search-tips h5 {
  margin: 0 0 8px 0;
  color: #303133;
}

.search-tips ul {
  margin: 0;
  padding-left: 20px;
}

.search-tips li {
  margin: 4px 0;
  color: #606266;
}
</style>

懒加载

异步加载子节点数据:

vue
<template>
  <div>
    <h4>懒加载树形选择</h4>
    
    <div class="lazy-example">
      <el-tree-select
        v-model="lazyValue"
        :data="lazyData"
        :props="lazyProps"
        lazy
        :load="loadNode"
        placeholder="选择部门(懒加载)"
        @change="handleLazyChange"
        style="width: 300px"
      />
    </div>
    
    <div class="lazy-info">
      <p>选择的值:{{ lazyValue }}</p>
      <p>加载状态:{{ loading ? '加载中...' : '空闲' }}</p>
    </div>
    
    <div class="lazy-log">
      <h5>加载日志:</h5>
      <div class="log-content">
        <div v-for="(log, index) in loadLogs" :key="index" class="log-item">
          <span class="log-time">{{ log.time }}</span>
          <span class="log-message">{{ log.message }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const lazyValue = ref('')
const loading = ref(false)
const loadLogs = ref([])

const lazyData = [
  {
    id: 1,
    label: '总公司',
    isLeaf: false
  }
]

const lazyProps = {
  value: 'id',
  label: 'label',
  children: 'children',
  isLeaf: 'isLeaf'
}

const addLog = (message) => {
  const now = new Date()
  const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
  loadLogs.value.unshift({ time, message })
  
  // 只保留最近10条日志
  if (loadLogs.value.length > 10) {
    loadLogs.value = loadLogs.value.slice(0, 10)
  }
}

const loadNode = (node, resolve) => {
  loading.value = true
  addLog(`开始加载节点: ${node.label || '根节点'}`)
  
  // 模拟异步加载
  setTimeout(() => {
    let children = []
    
    if (node.level === 0) {
      // 根节点加载一级部门
      children = [
        { id: 11, label: '技术部', isLeaf: false },
        { id: 12, label: '产品部', isLeaf: false },
        { id: 13, label: '运营部', isLeaf: false },
        { id: 14, label: '财务部', isLeaf: true },
        { id: 15, label: '人事部', isLeaf: true }
      ]
    } else if (node.level === 1) {
      // 一级部门加载二级部门
      if (node.data.id === 11) {
        // 技术部
        children = [
          { id: 111, label: '前端组', isLeaf: false },
          { id: 112, label: '后端组', isLeaf: false },
          { id: 113, label: '测试组', isLeaf: true },
          { id: 114, label: '运维组', isLeaf: true }
        ]
      } else if (node.data.id === 12) {
        // 产品部
        children = [
          { id: 121, label: '产品策划', isLeaf: true },
          { id: 122, label: 'UI设计', isLeaf: true },
          { id: 123, label: '用户研究', isLeaf: true }
        ]
      } else if (node.data.id === 13) {
        // 运营部
        children = [
          { id: 131, label: '内容运营', isLeaf: true },
          { id: 132, label: '用户运营', isLeaf: true },
          { id: 133, label: '活动运营', isLeaf: true }
        ]
      }
    } else if (node.level === 2) {
      // 二级部门加载具体岗位
      if (node.data.id === 111) {
        // 前端组
        children = [
          { id: 1111, label: 'Vue 开发工程师', isLeaf: true },
          { id: 1112, label: 'React 开发工程师', isLeaf: true },
          { id: 1113, label: '前端架构师', isLeaf: true }
        ]
      } else if (node.data.id === 112) {
        // 后端组
        children = [
          { id: 1121, label: 'Java 开发工程师', isLeaf: true },
          { id: 1122, label: 'Python 开发工程师', isLeaf: true },
          { id: 1123, label: '后端架构师', isLeaf: true }
        ]
      }
    }
    
    loading.value = false
    addLog(`加载完成: ${node.label || '根节点'}, 子节点数量: ${children.length}`)
    resolve(children)
  }, 1000 + Math.random() * 1000) // 随机延迟1-2秒
}

const handleLazyChange = (val) => {
  console.log('懒加载选择变化:', val)
  addLog(`选择了节点: ${val}`)
}
</script>

<style scoped>
.lazy-example {
  margin-bottom: 16px;
}

.lazy-info {
  padding: 12px;
  background-color: #f5f7fa;
  border-radius: 4px;
  margin-bottom: 16px;
}

.lazy-info p {
  margin: 4px 0;
  color: #606266;
}

.lazy-log {
  padding: 12px;
  background-color: #fafafa;
  border-radius: 4px;
  border: 1px solid #e4e7ed;
}

.lazy-log h5 {
  margin: 0 0 8px 0;
  color: #303133;
}

.log-content {
  max-height: 200px;
  overflow-y: auto;
}

.log-item {
  display: flex;
  margin: 4px 0;
  font-size: 12px;
}

.log-time {
  color: #909399;
  margin-right: 8px;
  min-width: 60px;
}

.log-message {
  color: #606266;
}
</style>

禁用状态

设置禁用状态:

vue
<template>
  <div>
    <h4>禁用状态</h4>
    
    <div class="disabled-example">
      <div class="control-section">
        <el-button @click="toggleDisabled">{{ globalDisabled ? '启用' : '禁用' }}组件</el-button>
        <el-button @click="toggleNodeDisabled">切换节点禁用状态</el-button>
        <el-button @click="resetSelection">重置选择</el-button>
      </div>
      
      <div class="tree-section">
        <div class="tree-item">
          <label>全局禁用:</label>
          <el-tree-select
            v-model="disabledValue1"
            :data="disabledData"
            :disabled="globalDisabled"
            placeholder="全局禁用状态"
            style="width: 250px"
          />
        </div>
        
        <div class="tree-item">
          <label>节点禁用:</label>
          <el-tree-select
            v-model="disabledValue2"
            :data="disabledData"
            placeholder="部分节点禁用"
            style="width: 250px"
          />
        </div>
        
        <div class="tree-item">
          <label>多选禁用:</label>
          <el-tree-select
            v-model="disabledValue3"
            :data="disabledData"
            multiple
            :show-checkbox="true"
            :disabled="globalDisabled"
            placeholder="多选禁用状态"
            style="width: 250px"
          />
        </div>
      </div>
      
      <div class="status-info">
        <h5>选择状态:</h5>
        <p>全局禁用选择:{{ disabledValue1 || '未选择' }}</p>
        <p>节点禁用选择:{{ disabledValue2 || '未选择' }}</p>
        <p>多选禁用选择:{{ Array.isArray(disabledValue3) ? disabledValue3.join(', ') : '未选择' }}</p>
        <p>全局禁用状态:{{ globalDisabled ? '已禁用' : '已启用' }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const globalDisabled = ref(false)
const disabledValue1 = ref('')
const disabledValue2 = ref('')
const disabledValue3 = ref([])

const disabledData = ref([
  {
    id: 1,
    label: '可选择的部门',
    children: [
      {
        id: 11,
        label: '技术部',
        children: [
          { id: 111, label: '前端组' },
          { id: 112, label: '后端组(禁用)', disabled: true },
          { id: 113, label: '测试组' }
        ]
      },
      {
        id: 12,
        label: '产品部(禁用)',
        disabled: true,
        children: [
          { id: 121, label: '产品经理' },
          { id: 122, label: 'UI设计师' }
        ]
      }
    ]
  },
  {
    id: 2,
    label: '运营部',
    children: [
      { id: 21, label: '内容运营' },
      { id: 22, label: '用户运营(禁用)', disabled: true },
      { id: 23, label: '活动运营' }
    ]
  },
  {
    id: 3,
    label: '财务部(禁用)',
    disabled: true,
    children: [
      { id: 31, label: '会计' },
      { id: 32, label: '出纳' }
    ]
  }
])

const toggleDisabled = () => {
  globalDisabled.value = !globalDisabled.value
}

const toggleNodeDisabled = () => {
  // 切换某些节点的禁用状态
  const toggleNodeState = (nodes) => {
    nodes.forEach(node => {
      if (node.id === 112 || node.id === 22) {
        node.disabled = !node.disabled
      }
      if (node.children) {
        toggleNodeState(node.children)
      }
    })
  }
  
  toggleNodeState(disabledData.value)
}

const resetSelection = () => {
  disabledValue1.value = ''
  disabledValue2.value = ''
  disabledValue3.value = []
}
</script>

<style scoped>
.disabled-example {
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 16px;
}

.control-section {
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid #e4e7ed;
}

.tree-section {
  margin-bottom: 16px;
}

.tree-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}

.tree-item label {
  min-width: 100px;
  margin-right: 12px;
  font-weight: 500;
  color: #303133;
}

.status-info {
  padding: 12px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.status-info h5 {
  margin: 0 0 8px 0;
  color: #303133;
}

.status-info p {
  margin: 4px 0;
  color: #606266;
}
</style>

在表单中使用

与 el-form 一起使用:

vue
<template>
  <div>
    <h4>表单中的树形选择</h4>
    
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <el-form-item label="员工姓名" prop="name">
        <el-input v-model="form.name" placeholder="请输入员工姓名" />
      </el-form-item>
      
      <el-form-item label="所属部门" prop="department">
        <el-tree-select
          v-model="form.department"
          :data="departmentData"
          placeholder="请选择所属部门"
          style="width: 100%"
          @change="handleDepartmentChange"
        />
      </el-form-item>
      
      <el-form-item label="负责项目" prop="projects">
        <el-tree-select
          v-model="form.projects"
          :data="projectData"
          multiple
          :show-checkbox="true"
          placeholder="请选择负责的项目(可多选)"
          style="width: 100%"
          @change="handleProjectsChange"
        />
      </el-form-item>
      
      <el-form-item label="权限范围" prop="permissions">
        <el-tree-select
          v-model="form.permissions"
          :data="permissionData"
          multiple
          :show-checkbox="true"
          :check-strictly="true"
          placeholder="请选择权限范围"
          style="width: 100%"
          @change="handlePermissionsChange"
        />
      </el-form-item>
      
      <el-form-item label="工作地点" prop="location">
        <el-tree-select
          v-model="form.location"
          :data="locationData"
          filterable
          placeholder="请选择工作地点"
          style="width: 100%"
          @change="handleLocationChange"
        />
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="submitForm">提交</el-button>
        <el-button @click="resetForm">重置</el-button>
        <el-button @click="fillTestData">填充测试数据</el-button>
      </el-form-item>
    </el-form>
    
    <div v-if="submittedData" class="submitted-data">
      <h4>提交的数据:</h4>
      <div class="form-result">
        <p><strong>员工姓名:</strong>{{ submittedData.name }}</p>
        <p><strong>所属部门:</strong>{{ getDepartmentName(submittedData.department) }}</p>
        <p><strong>负责项目:</strong>{{ getProjectNames(submittedData.projects) }}</p>
        <p><strong>权限范围:</strong>{{ getPermissionNames(submittedData.permissions) }}</p>
        <p><strong>工作地点:</strong>{{ getLocationName(submittedData.location) }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'

const formRef = ref()
const submittedData = ref(null)

const form = reactive({
  name: '',
  department: '',
  projects: [],
  permissions: [],
  location: ''
})

const rules = {
  name: [
    { required: true, message: '请输入员工姓名', trigger: 'blur' }
  ],
  department: [
    { required: true, message: '请选择所属部门', trigger: 'change' }
  ],
  projects: [
    { required: true, message: '请选择负责的项目', trigger: 'change' }
  ],
  location: [
    { required: true, message: '请选择工作地点', trigger: 'change' }
  ]
}

const departmentData = [
  {
    id: 'tech',
    label: '技术部',
    children: [
      { id: 'frontend', label: '前端组' },
      { id: 'backend', label: '后端组' },
      { id: 'mobile', label: '移动端组' },
      { id: 'test', label: '测试组' }
    ]
  },
  {
    id: 'product',
    label: '产品部',
    children: [
      { id: 'pm', label: '产品经理' },
      { id: 'ui', label: 'UI设计师' },
      { id: 'ue', label: 'UE设计师' }
    ]
  },
  {
    id: 'operation',
    label: '运营部',
    children: [
      { id: 'content', label: '内容运营' },
      { id: 'user', label: '用户运营' },
      { id: 'activity', label: '活动运营' }
    ]
  }
]

const projectData = [
  {
    id: 'web',
    label: 'Web项目',
    children: [
      { id: 'admin', label: '管理后台' },
      { id: 'portal', label: '门户网站' },
      { id: 'ecommerce', label: '电商平台' }
    ]
  },
  {
    id: 'mobile',
    label: '移动端项目',
    children: [
      { id: 'ios', label: 'iOS应用' },
      { id: 'android', label: 'Android应用' },
      { id: 'miniprogram', label: '小程序' }
    ]
  },
  {
    id: 'internal',
    label: '内部系统',
    children: [
      { id: 'hr', label: '人事系统' },
      { id: 'finance', label: '财务系统' },
      { id: 'crm', label: 'CRM系统' }
    ]
  }
]

const permissionData = [
  {
    id: 'system',
    label: '系统管理',
    children: [
      { id: 'user_manage', label: '用户管理' },
      { id: 'role_manage', label: '角色管理' },
      { id: 'permission_manage', label: '权限管理' }
    ]
  },
  {
    id: 'business',
    label: '业务管理',
    children: [
      { id: 'order_manage', label: '订单管理' },
      { id: 'product_manage', label: '商品管理' },
      { id: 'customer_manage', label: '客户管理' }
    ]
  },
  {
    id: 'report',
    label: '报表统计',
    children: [
      { id: 'sales_report', label: '销售报表' },
      { id: 'user_report', label: '用户报表' },
      { id: 'finance_report', label: '财务报表' }
    ]
  }
]

const locationData = [
  {
    id: 'beijing',
    label: '北京',
    children: [
      { id: 'chaoyang', label: '朝阳区' },
      { id: 'haidian', label: '海淀区' },
      { id: 'dongcheng', label: '东城区' }
    ]
  },
  {
    id: 'shanghai',
    label: '上海',
    children: [
      { id: 'huangpu', label: '黄浦区' },
      { id: 'xuhui', label: '徐汇区' },
      { id: 'changning', label: '长宁区' }
    ]
  },
  {
    id: 'guangzhou',
    label: '广州',
    children: [
      { id: 'tianhe', label: '天河区' },
      { id: 'yuexiu', label: '越秀区' },
      { id: 'haizhu', label: '海珠区' }
    ]
  }
]

const handleDepartmentChange = (val) => {
  console.log('部门变化:', val)
}

const handleProjectsChange = (val) => {
  console.log('项目变化:', val)
}

const handlePermissionsChange = (val) => {
  console.log('权限变化:', val)
}

const handleLocationChange = (val) => {
  console.log('地点变化:', val)
}

const submitForm = async () => {
  try {
    await formRef.value.validate()
    submittedData.value = { ...form }
    ElMessage.success('提交成功')
  } catch (error) {
    ElMessage.error('请检查表单内容')
  }
}

const resetForm = () => {
  formRef.value.resetFields()
  submittedData.value = null
}

const fillTestData = () => {
  form.name = '张三'
  form.department = 'frontend'
  form.projects = ['admin', 'portal']
  form.permissions = ['user_manage', 'order_manage']
  form.location = 'chaoyang'
  ElMessage.success('已填充测试数据')
}

// 辅助函数:获取名称
const findLabelById = (data, id) => {
  for (const item of data) {
    if (item.id === id) {
      return item.label
    }
    if (item.children) {
      const result = findLabelById(item.children, id)
      if (result) return result
    }
  }
  return id
}

const getDepartmentName = (id) => {
  return findLabelById(departmentData, id)
}

const getProjectNames = (ids) => {
  if (!Array.isArray(ids) || ids.length === 0) return '未选择'
  return ids.map(id => findLabelById(projectData, id)).join(', ')
}

const getPermissionNames = (ids) => {
  if (!Array.isArray(ids) || ids.length === 0) return '未选择'
  return ids.map(id => findLabelById(permissionData, id)).join(', ')
}

const getLocationName = (id) => {
  return findLabelById(locationData, id)
}
</script>

<style scoped>
.submitted-data {
  margin-top: 20px;
  padding: 16px;
  background-color: #f0f9ff;
  border-radius: 6px;
  border-left: 4px solid #409eff;
}

.form-result p {
  margin: 8px 0;
  color: #606266;
}

.form-result strong {
  color: #303133;
}
</style>

实际应用示例

组织架构选择器

vue
<template>
  <div class="org-selector">
    <h3>组织架构管理系统</h3>
    
    <div class="selector-container">
      <div class="selector-section">
        <h4>员工分配</h4>
        <el-form :model="orgForm" :rules="orgRules" ref="orgFormRef" label-width="120px">
          <el-form-item label="员工姓名" prop="employeeName">
            <el-input v-model="orgForm.employeeName" placeholder="请输入员工姓名" />
          </el-form-item>
          
          <el-form-item label="目标部门" prop="targetDepartment">
            <el-tree-select
              v-model="orgForm.targetDepartment"
              :data="organizationData"
              :props="orgProps"
              filterable
              placeholder="请选择目标部门"
              style="width: 100%"
              @change="handleDepartmentSelect"
            />
          </el-form-item>
          
          <el-form-item label="职位级别" prop="level">
            <el-select v-model="orgForm.level" placeholder="请选择职位级别">
              <el-option label="实习生" value="intern" />
              <el-option label="初级" value="junior" />
              <el-option label="中级" value="middle" />
              <el-option label="高级" value="senior" />
              <el-option label="专家" value="expert" />
              <el-option label="管理者" value="manager" />
            </el-select>
          </el-form-item>
          
          <el-form-item label="汇报对象" prop="reportTo">
            <el-tree-select
              v-model="orgForm.reportTo"
              :data="managerData"
              :props="orgProps"
              filterable
              placeholder="请选择汇报对象"
              style="width: 100%"
              :disabled="!orgForm.targetDepartment"
            />
          </el-form-item>
          
          <el-form-item>
            <el-button type="primary" @click="assignEmployee">分配员工</el-button>
            <el-button @click="resetOrgForm">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
      
      <div class="org-chart">
        <h4>组织架构图</h4>
        <div class="chart-container">
          <el-tree
            :data="organizationData"
            :props="orgProps"
            :expand-on-click-node="false"
            default-expand-all
            class="org-tree"
          >
            <template #default="{ node, data }">
              <div class="org-node">
                <div class="node-info">
                  <span class="node-name">{{ data.name }}</span>
                  <span class="node-type">{{ getNodeTypeText(data.type) }}</span>
                </div>
                <div class="node-stats">
                  <span class="employee-count">{{ data.employeeCount || 0 }}人</span>
                  <span class="manager" v-if="data.manager">负责人: {{ data.manager }}</span>
                </div>
              </div>
            </template>
          </el-tree>
        </div>
      </div>
    </div>
    
    <div v-if="selectedDepartmentInfo" class="department-info">
      <h4>部门详情</h4>
      <div class="info-content">
        <div class="info-item">
          <label>部门名称:</label>
          <span>{{ selectedDepartmentInfo.name }}</span>
        </div>
        <div class="info-item">
          <label>部门类型:</label>
          <span>{{ getNodeTypeText(selectedDepartmentInfo.type) }}</span>
        </div>
        <div class="info-item">
          <label>员工数量:</label>
          <span>{{ selectedDepartmentInfo.employeeCount || 0 }}人</span>
        </div>
        <div class="info-item">
          <label>部门负责人:</label>
          <span>{{ selectedDepartmentInfo.manager || '未设置' }}</span>
        </div>
        <div class="info-item">
          <label>部门描述:</label>
          <span>{{ selectedDepartmentInfo.description || '暂无描述' }}</span>
        </div>
      </div>
    </div>
    
    <div v-if="assignmentHistory.length > 0" class="assignment-history">
      <h4>分配历史</h4>
      <el-table :data="assignmentHistory" style="width: 100%">
        <el-table-column prop="time" label="时间" width="180" />
        <el-table-column prop="employeeName" label="员工姓名" width="120" />
        <el-table-column prop="departmentName" label="目标部门" width="150" />
        <el-table-column prop="level" label="职位级别" width="100" />
        <el-table-column prop="reportTo" label="汇报对象" width="120" />
        <el-table-column label="操作" width="100">
          <template #default="scope">
            <el-button size="small" @click="viewAssignment(scope.row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'

const orgFormRef = ref()
const assignmentHistory = ref([])

const orgForm = reactive({
  employeeName: '',
  targetDepartment: '',
  level: '',
  reportTo: ''
})

const orgRules = {
  employeeName: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
  targetDepartment: [{ required: true, message: '请选择目标部门', trigger: 'change' }],
  level: [{ required: true, message: '请选择职位级别', trigger: 'change' }]
}

const orgProps = {
  children: 'children',
  label: 'name',
  value: 'id'
}

const organizationData = ref([
  {
    id: 'company',
    name: '科技有限公司',
    type: 'company',
    employeeCount: 150,
    manager: '张总',
    description: '一家专注于互联网技术的创新公司',
    children: [
      {
        id: 'tech',
        name: '技术部',
        type: 'department',
        employeeCount: 60,
        manager: '李技术总监',
        description: '负责公司所有技术产品的研发',
        children: [
          {
            id: 'frontend',
            name: '前端组',
            type: 'team',
            employeeCount: 15,
            manager: '王前端组长',
            description: '负责Web和移动端前端开发'
          },
          {
            id: 'backend',
            name: '后端组',
            type: 'team',
            employeeCount: 20,
            manager: '赵后端组长',
            description: '负责服务端和API开发'
          },
          {
            id: 'mobile',
            name: '移动端组',
            type: 'team',
            employeeCount: 12,
            manager: '钱移动端组长',
            description: '负责iOS和Android应用开发'
          },
          {
            id: 'test',
            name: '测试组',
            type: 'team',
            employeeCount: 8,
            manager: '孙测试组长',
            description: '负责产品质量保证和测试'
          },
          {
            id: 'devops',
            name: '运维组',
            type: 'team',
            employeeCount: 5,
            manager: '周运维组长',
            description: '负责系统运维和部署'
          }
        ]
      },
      {
        id: 'product',
        name: '产品部',
        type: 'department',
        employeeCount: 25,
        manager: '吴产品总监',
        description: '负责产品规划和设计',
        children: [
          {
            id: 'pm',
            name: '产品经理组',
            type: 'team',
            employeeCount: 8,
            manager: '郑产品经理',
            description: '负责产品需求分析和规划'
          },
          {
            id: 'ui',
            name: 'UI设计组',
            type: 'team',
            employeeCount: 10,
            manager: '冯UI组长',
            description: '负责用户界面设计'
          },
          {
            id: 'ue',
            name: 'UE设计组',
            type: 'team',
            employeeCount: 7,
            manager: '陈UE组长',
            description: '负责用户体验设计'
          }
        ]
      },
      {
        id: 'operation',
        name: '运营部',
        type: 'department',
        employeeCount: 30,
        manager: '褚运营总监',
        description: '负责产品运营和市场推广',
        children: [
          {
            id: 'content',
            name: '内容运营组',
            type: 'team',
            employeeCount: 12,
            manager: '卫内容组长',
            description: '负责内容策划和制作'
          },
          {
            id: 'user',
            name: '用户运营组',
            type: 'team',
            employeeCount: 10,
            manager: '蒋用户组长',
            description: '负责用户增长和留存'
          },
          {
            id: 'activity',
            name: '活动运营组',
            type: 'team',
            employeeCount: 8,
            manager: '沈活动组长',
            description: '负责活动策划和执行'
          }
        ]
      },
      {
        id: 'hr',
        name: '人事部',
        type: 'department',
        employeeCount: 15,
        manager: '韩人事总监',
        description: '负责人力资源管理',
        children: [
          {
            id: 'recruit',
            name: '招聘组',
            type: 'team',
            employeeCount: 6,
            manager: '杨招聘组长',
            description: '负责人才招聘'
          },
          {
            id: 'training',
            name: '培训组',
            type: 'team',
            employeeCount: 5,
            manager: '朱培训组长',
            description: '负责员工培训和发展'
          },
          {
            id: 'admin',
            name: '行政组',
            type: 'team',
            employeeCount: 4,
            manager: '秦行政组长',
            description: '负责行政事务管理'
          }
        ]
      },
      {
        id: 'finance',
        name: '财务部',
        type: 'department',
        employeeCount: 20,
        manager: '尤财务总监',
        description: '负责财务管理和会计',
        children: [
          {
            id: 'accounting',
            name: '会计组',
            type: 'team',
            employeeCount: 12,
            manager: '许会计组长',
            description: '负责日常会计工作'
          },
          {
            id: 'audit',
            name: '审计组',
            type: 'team',
            employeeCount: 8,
            manager: '何审计组长',
            description: '负责内部审计工作'
          }
        ]
      }
    ]
  }
])

const managerData = computed(() => {
  // 根据选择的部门动态生成管理者列表
  if (!orgForm.targetDepartment) return []
  
  const findDepartment = (data, targetId) => {
    for (const item of data) {
      if (item.id === targetId) {
        return item
      }
      if (item.children) {
        const result = findDepartment(item.children, targetId)
        if (result) return result
      }
    }
    return null
  }
  
  const department = findDepartment(organizationData.value, orgForm.targetDepartment)
  if (!department) return []
  
  // 生成该部门的管理者选项
  const managers = []
  if (department.manager) {
    managers.push({
      id: department.id + '_manager',
      name: department.manager
    })
  }
  
  // 添加上级部门的管理者
  const findParent = (data, targetId, parent = null) => {
    for (const item of data) {
      if (item.id === targetId) {
        return parent
      }
      if (item.children) {
        const result = findParent(item.children, targetId, item)
        if (result) return result
      }
    }
    return null
  }
  
  const parentDepartment = findParent(organizationData.value, orgForm.targetDepartment)
  if (parentDepartment && parentDepartment.manager) {
    managers.push({
      id: parentDepartment.id + '_manager',
      name: parentDepartment.manager
    })
  }
  
  return managers
})

const selectedDepartmentInfo = computed(() => {
  if (!orgForm.targetDepartment) return null
  
  const findDepartment = (data, targetId) => {
    for (const item of data) {
      if (item.id === targetId) {
        return item
      }
      if (item.children) {
        const result = findDepartment(item.children, targetId)
        if (result) return result
      }
    }
    return null
  }
  
  return findDepartment(organizationData.value, orgForm.targetDepartment)
})

const getNodeTypeText = (type) => {
  const typeMap = {
    company: '公司',
    department: '部门',
    team: '小组'
  }
  return typeMap[type] || type
}

const handleDepartmentSelect = (val) => {
  console.log('选择部门:', val)
  // 清空汇报对象选择
  orgForm.reportTo = ''
}

const assignEmployee = async () => {
  try {
    await orgFormRef.value.validate()
    
    // 添加到分配历史
    const assignment = {
      time: new Date().toLocaleString(),
      employeeName: orgForm.employeeName,
      departmentName: selectedDepartmentInfo.value?.name || '',
      level: orgForm.level,
      reportTo: managerData.value.find(m => m.id === orgForm.reportTo)?.name || ''
    }
    
    assignmentHistory.value.unshift(assignment)
    
    ElMessage.success('员工分配成功')
    resetOrgForm()
  } catch (error) {
    ElMessage.error('请检查表单内容')
  }
}

const resetOrgForm = () => {
  orgFormRef.value.resetFields()
}

const viewAssignment = (assignment) => {
  ElMessage.info(`查看分配记录:${assignment.employeeName}`)
}
</script>

<style scoped>
.org-selector {
  max-width: 1200px;
  padding: 20px;
}

.selector-container {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}

.selector-section {
  flex: 1;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 16px;
}

.org-chart {
  flex: 1;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 16px;
}

.chart-container {
  max-height: 400px;
  overflow-y: auto;
}

.org-tree {
  background: transparent;
}

.org-node {
  display: flex;
  flex-direction: column;
  padding: 8px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  background-color: #fafafa;
  margin: 2px 0;
}

.node-info {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.node-name {
  font-weight: 500;
  color: #303133;
}

.node-type {
  font-size: 12px;
  color: #909399;
  background-color: #e4e7ed;
  padding: 2px 6px;
  border-radius: 2px;
}

.node-stats {
  display: flex;
  gap: 12px;
  font-size: 12px;
  color: #606266;
}

.employee-count {
  color: #409eff;
  font-weight: 500;
}

.department-info {
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 16px;
  margin-bottom: 20px;
  background-color: #f5f7fa;
}

.info-content {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 12px;
}

.info-item {
  display: flex;
  align-items: center;
}

.info-item label {
  min-width: 100px;
  font-weight: 500;
  color: #303133;
  margin-right: 8px;
}

.info-item span {
  color: #606266;
}

.assignment-history {
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  padding: 16px;
}
</style>

API

TreeSelect Attributes

属性名说明类型默认值
model-value / v-model绑定值string / number / array
data展示数据array
multiple是否多选booleanfalse
disabled是否禁用booleanfalse
size输入框尺寸string
clearable是否可以清空选项booleanfalse
collapse-tags多选时是否将选中值按文字的形式展示booleanfalse
collapse-tags-tooltip当鼠标悬停于折叠标签的文本时,是否显示所有选中的标签booleanfalse
multiple-limit多选时用户最多可以选择的项目数,为 0 则不限制number0
nameselect input 的 name 属性string
effectTooltip 主题,内置了 dark / light 两种主题stringlight
autocompleteselect input 的 autocomplete 属性stringoff
placeholder占位符string请选择
filterable是否可搜索booleanfalse
allow-create是否允许用户创建新条目booleanfalse
filter-node-method对树节点进行筛选时执行的方法function
remote是否为远程搜索booleanfalse
remote-method远程搜索方法function
loading是否正在从远程获取数据booleanfalse
loading-text远程加载时显示的文字string加载中
no-match-text搜索条件无匹配时显示的文字string无匹配数据
no-data-text选项为空时显示的文字string无数据
props配置选项object
node-key每个树节点用来作为唯一标识的属性string
default-expand-all是否默认展开所有节点booleanfalse
expand-on-click-node是否在点击节点的时候展开或者收缩节点booleantrue
check-on-click-node是否在点击节点的时候选中节点booleanfalse
auto-expand-parent展开子节点的时候是否自动展开父节点booleantrue
default-expanded-keys默认展开的节点的 key 的数组array
show-checkbox节点是否可被选择booleanfalse
check-strictly在显示复选框的情况下,是否严格的遵循父子不互相关联的做法booleanfalse
default-checked-keys默认勾选的节点的 key 的数组array
current-node-key当前选中的节点string / number
filter-method对数据进行筛选时使用的方法function
accordion是否每次只打开一个同级树节点展开booleanfalse
indent相邻级节点间的水平缩进,单位为像素number18
icon自定义树节点的图标string / Component
lazy是否懒加载子节点,需与 load 方法结合使用booleanfalse
load加载子树数据的方法,仅当 lazy 属性为true 时生效function
render-after-expand是否在第一次展开某个树节点后才渲染其子节点booleantrue
default-expand-all是否默认展开所有节点booleanfalse
highlight-current是否高亮当前选中节点booleanfalse

TreeSelect Events

事件名说明回调参数
change选中值发生变化时触发目前的选中值
visible-change下拉框出现/隐藏时触发出现则为 true,隐藏则为 false
remove-tag多选模式下移除tag时触发移除的tag值
clear可清空的单选模式下用户点击清空按钮时触发
blur当 input 失去焦点时触发(event: Event)
focus当 input 获得焦点时触发(event: Event)
node-click节点被点击时的回调共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身
node-contextmenu当某一节点被鼠标右键点击时会触发该事件共四个参数,依次为:event、传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身
node-expand节点被展开时触发的事件共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身
node-collapse节点被关闭时触发的事件共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身
check-change节点选中状态发生变化时的回调共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
check当复选框被点击的时候触发共两个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、树目前的选中状态对象
current-change当前选中节点变化时触发的事件共两个参数,依次为:当前节点的数据,当前节点的 Node 对象

TreeSelect Slots

插槽名说明参数
default自定义树节点的内容
prefixSelect 组件头部内容
empty无选项时的列表

TreeSelect Methods

方法名说明参数
filter对树节点进行筛选操作接收一个任意类型的参数,该参数会在 filter-node-method 中作为第一个参数
updateKeyChildren通过 keys 设置节点子元素,使用此方法必须设置 node-key 属性(key, data) 接收两个参数,1. 节点 key 2. 节点数据的数组
getCheckedNodes若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组(leafOnly, includeHalfChecked) 接收两个 boolean 类型的参数,1. 是否只是叶子节点,默认值为 false 2. 是否包含半选节点,默认值为 false
setCheckedNodes设置目前勾选的节点,使用此方法必须设置 node-key 属性(nodes) 接收勾选节点数据的数组
getCheckedKeys若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组(leafOnly) 接收一个 boolean 类型的参数,若为 true 则仅返回被选中的叶子节点的 keys,默认值为 false
setCheckedKeys通过 keys 设置目前勾选的节点,使用此方法必须设置 node-key 属性(keys, leafOnly) 接收两个参数,1. 勾选节点的 key 的数组 2. boolean 类型,若为 true 则仅设置叶子节点的选中状态,默认值为 false
setChecked通过 key / data 设置某个节点的勾选状态,使用此方法必须设置 node-key 属性(key/data, checked, deep) 接收三个参数,1. 勾选节点的 key 或者 data 2. boolean 类型,节点是否选中 3. boolean 类型,是否设置子节点 ,默认为 false
getHalfCheckedNodes若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点所组成的数组
getHalfCheckedKeys若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点的 key 所组成的数组
getCurrentKey获取当前被焦点选中的节点的 key,使用此方法必须设置 node-key 属性
getCurrentNode获取当前被焦点选中的节点的 data,若没有节点被选中则返回 null
setCurrentKey通过 key 设置某个节点的当前选中状态,使用此方法必须设置 node-key 属性(key) 待被选节点的 key,若为 null 则取消当前高亮的节点
setCurrentNode通过 node 设置某个节点的当前选中状态,使用此方法必须设置 node-key 属性(node) 待被选节点的 node
getNode根据 data 或者 key 拿到 Tree 组件中的 node(data) 要获得 node 的 key 或者 data
remove删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性(data) 要删除的节点的 data 或者 node
append为 Tree 中的一个节点追加一个子节点,使用此方法必须设置 node-key 属性(data, parentNode) 接收两个参数,1. 要追加的子节点的 data 2. 子节点的 parent 的 data、key 或者 node
insertBefore为 Tree 的一个节点的前面增加一个节点,使用此方法必须设置 node-key 属性(data, refNode) 接收两个参数,1. 要增加的节点的 data 2. 要增加的节点的位置参考节点的 data、key 或者 node
insertAfter为 Tree 的一个节点的后面增加一个节点,使用此方法必须设置 node-key 属性(data, refNode) 接收两个参数,1. 要增加的节点的 data 2. 要增加的节点的位置参考节点的 data、key 或者 node

Props 配置

属性说明类型默认值
value指定节点标签为节点对象的某个属性值string, function(data, node)
label指定节点标签为节点对象的某个属性值string, function(data, node)
children指定子树为节点对象的某个属性值string
disabled指定节点选择框是否禁用为节点对象的某个属性值string, function(data, node)
isLeaf指定节点是否为叶子节点,仅在指定了 lazy 属性的情况下生效string, function(data, node)
class自定义节点类名string, function(data, node)

最佳实践

数据结构设计

  1. 统一的数据格式

    • 保持数据结构的一致性
    • 使用标准的 id、label、children 字段
    • 为每个节点提供唯一标识
  2. 合理的层级深度

    • 避免过深的层级结构(建议不超过5层)
    • 考虑用户的认知负担
    • 必要时使用懒加载减少初始数据量
  3. 性能优化

    • 大数据量时使用懒加载
    • 合理使用 render-after-expand
    • 避免在数据中包含不必要的字段

用户体验优化

  1. 搜索功能

    • 为大数据量场景提供搜索功能
    • 实现智能的搜索匹配算法
    • 提供搜索结果的高亮显示
  2. 加载状态

    • 为懒加载提供明确的加载提示
    • 处理加载失败的情况
    • 提供重试机制
  3. 错误处理

    • 优雅处理数据加载错误
    • 提供有意义的错误提示
    • 实现降级方案

表单集成

  1. 验证规则

    • 设置合适的必填验证
    • 提供清晰的错误提示
    • 考虑多选时的数量限制
  2. 数据绑定

    • 正确处理单选和多选的数据格式
    • 实现数据的双向绑定
    • 处理默认值的设置
  3. 联动效果

    • 实现级联选择的联动
    • 处理依赖关系的更新
    • 保持数据的一致性

常见问题

数据相关问题

Q: 如何处理动态数据更新?

A: 可以通过响应式数据更新来实现:

javascript
// 更新整个数据
data.value = newData

// 更新特定节点
const updateNode = (nodes, targetId, newData) => {
  for (let node of nodes) {
    if (node.id === targetId) {
      Object.assign(node, newData)
      return true
    }
    if (node.children && updateNode(node.children, targetId, newData)) {
      return true
    }
  }
  return false
}

Q: 如何实现节点的动态添加和删除?

A: 使用组件提供的方法:

javascript
// 添加节点
treeSelectRef.value.append(newNodeData, parentNode)

// 删除节点
treeSelectRef.value.remove(nodeData)

// 插入节点
treeSelectRef.value.insertBefore(newNodeData, referenceNode)

选择相关问题

Q: 如何实现父子节点的关联选择?

A: 通过 check-strictly 属性控制:

vue
<!-- 父子关联 -->
<el-tree-select
  v-model="value"
  :data="data"
  multiple
  :show-checkbox="true"
  :check-strictly="false"
/>

<!-- 父子不关联 -->
<el-tree-select
  v-model="value"
  :data="data"
  multiple
  :show-checkbox="true"
  :check-strictly="true"
/>

Q: 如何获取选中节点的完整路径?

A: 可以通过递归查找实现:

javascript
const getNodePath = (nodes, targetId, path = []) => {
  for (let node of nodes) {
    const currentPath = [...path, node]
    if (node.id === targetId) {
      return currentPath
    }
    if (node.children) {
      const result = getNodePath(node.children, targetId, currentPath)
      if (result) return result
    }
  }
  return null
}

性能相关问题

Q: 大数据量时如何优化性能?

A: 采用以下策略:

  1. 使用懒加载
  2. 启用虚拟滚动(如果支持)
  3. 减少不必要的数据字段
  4. 使用 render-after-expand
  5. 合理设置 default-expand-all

Q: 如何处理搜索性能问题?

A: 实现防抖搜索:

javascript
import { debounce } from 'lodash-es'

const debouncedFilter = debounce((value) => {
  treeSelectRef.value.filter(value)
}, 300)

样式相关问题

Q: 如何自定义节点样式?

A: 使用插槽和 CSS:

vue
<el-tree-select v-model="value" :data="data">
  <template #default="{ node, data }">
    <span class="custom-tree-node">
      <span class="node-label">{{ data.label }}</span>
      <span class="node-count" v-if="data.count">({{ data.count }})</span>
    </span>
  </template>
</el-tree-select>

<style>
.custom-tree-node {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}

.node-count {
  color: #999;
  font-size: 12px;
}
</style>

实际应用示例

组织架构选择器

这个示例展示了如何创建一个完整的组织架构选择器,支持搜索、懒加载和权限控制。

vue
<template>
  <div class="org-selector">
    <h4>组织架构选择器</h4>
    
    <div class="selector-controls">
      <el-button @click="expandAll">展开全部</el-button>
      <el-button @click="collapseAll">收起全部</el-button>
      <el-button @click="getSelectedInfo">获取选择信息</el-button>
      <el-button @click="clearSelection">清空选择</el-button>
    </div>
    
    <div class="selector-container">
      <el-tree-select
        ref="orgTreeRef"
        v-model="selectedOrg"
        :data="orgData"
        :props="orgProps"
        multiple
        :show-checkbox="true"
        filterable
        :filter-node-method="filterOrgNode"
        lazy
        :load="loadOrgNode"
        placeholder="请选择组织架构"
        style="width: 100%"
        @change="handleOrgChange"
        @node-click="handleNodeClick"
      >
        <template #default="{ node, data }">
          <div class="org-node">
            <el-icon v-if="data.type === 'company'" class="node-icon company">
              <OfficeBuilding />
            </el-icon>
            <el-icon v-else-if="data.type === 'department'" class="node-icon department">
              <Folder />
            </el-icon>
            <el-icon v-else class="node-icon person">
              <User />
            </el-icon>
            <span class="node-label">{{ data.label }}</span>
            <span v-if="data.count" class="node-count">({{ data.count }})</span>
            <el-tag v-if="data.disabled" size="small" type="info">禁用</el-tag>
          </div>
        </template>
      </el-tree-select>
    </div>
    
    <div v-if="selectedInfo" class="selection-info">
      <h5>选择信息:</h5>
      <div class="info-content">
        <p><strong>选中数量:</strong>{{ selectedInfo.count }}</p>
        <p><strong>公司数量:</strong>{{ selectedInfo.companies }}</p>
        <p><strong>部门数量:</strong>{{ selectedInfo.departments }}</p>
        <p><strong>人员数量:</strong>{{ selectedInfo.persons }}</p>
        <div class="selected-list">
          <h6>选中项目:</h6>
          <el-tag
            v-for="item in selectedInfo.items"
            :key="item.id"
            :type="getTagType(item.type)"
            class="selected-tag"
          >
            {{ item.label }}
          </el-tag>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { OfficeBuilding, Folder, User } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const orgTreeRef = ref()
const selectedOrg = ref([])
const selectedInfo = ref(null)

const orgData = ref([
  {
    id: 'company-1',
    label: '总公司',
    type: 'company',
    isLeaf: false,
    count: 1250
  }
])

const orgProps = {
  value: 'id',
  label: 'label',
  children: 'children',
  isLeaf: 'isLeaf',
  disabled: 'disabled'
}

const loadOrgNode = (node, resolve) => {
  setTimeout(() => {
    let children = []
    
    if (node.level === 0) {
      // 加载一级部门
      children = [
        { id: 'dept-tech', label: '技术部', type: 'department', isLeaf: false, count: 45 },
        { id: 'dept-product', label: '产品部', type: 'department', isLeaf: false, count: 23 },
        { id: 'dept-sales', label: '销售部', type: 'department', isLeaf: false, count: 67 },
        { id: 'dept-hr', label: '人事部', type: 'department', isLeaf: false, count: 12 },
        { id: 'dept-finance', label: '财务部', type: 'department', isLeaf: true, count: 8, disabled: true }
      ]
    } else if (node.level === 1) {
      // 加载二级部门或人员
      if (node.data.id === 'dept-tech') {
        children = [
          { id: 'team-frontend', label: '前端组', type: 'department', isLeaf: false, count: 15 },
          { id: 'team-backend', label: '后端组', type: 'department', isLeaf: false, count: 18 },
          { id: 'team-mobile', label: '移动端组', type: 'department', isLeaf: false, count: 8 },
          { id: 'team-test', label: '测试组', type: 'department', isLeaf: false, count: 4 }
        ]
      } else if (node.data.id === 'dept-product') {
        children = [
          { id: 'person-pm1', label: '张三 - 产品经理', type: 'person', isLeaf: true },
          { id: 'person-pm2', label: '李四 - 产品经理', type: 'person', isLeaf: true },
          { id: 'person-ui1', label: '王五 - UI设计师', type: 'person', isLeaf: true },
          { id: 'person-ui2', label: '赵六 - UI设计师', type: 'person', isLeaf: true }
        ]
      }
    } else if (node.level === 2) {
      // 加载具体人员
      if (node.data.id === 'team-frontend') {
        children = [
          { id: 'person-fe1', label: '前端工程师 - 小明', type: 'person', isLeaf: true },
          { id: 'person-fe2', label: '前端工程师 - 小红', type: 'person', isLeaf: true },
          { id: 'person-fe3', label: '前端架构师 - 老王', type: 'person', isLeaf: true }
        ]
      }
    }
    
    resolve(children)
  }, 800)
}

const filterOrgNode = (value, data) => {
  if (!value) return true
  return data.label.toLowerCase().includes(value.toLowerCase())
}

const handleOrgChange = (value) => {
  console.log('组织选择变化:', value)
  getSelectedInfo()
}

const handleNodeClick = (data, node) => {
  console.log('节点点击:', data, node)
}

const expandAll = () => {
  // 展开所有节点的实现
  ElMessage.success('已展开全部节点')
}

const collapseAll = () => {
  // 收起所有节点的实现
  ElMessage.success('已收起全部节点')
}

const getSelectedInfo = () => {
  if (!selectedOrg.value.length) {
    selectedInfo.value = null
    return
  }
  
  // 获取选中节点的详细信息
  const checkedNodes = orgTreeRef.value?.getCheckedNodes() || []
  
  const info = {
    count: checkedNodes.length,
    companies: checkedNodes.filter(node => node.type === 'company').length,
    departments: checkedNodes.filter(node => node.type === 'department').length,
    persons: checkedNodes.filter(node => node.type === 'person').length,
    items: checkedNodes.map(node => ({
      id: node.id,
      label: node.label,
      type: node.type
    }))
  }
  
  selectedInfo.value = info
}

const clearSelection = () => {
  selectedOrg.value = []
  selectedInfo.value = null
  ElMessage.success('已清空选择')
}

const getTagType = (type) => {
  switch (type) {
    case 'company': return 'danger'
    case 'department': return 'warning'
    case 'person': return 'success'
    default: return ''
  }
}
</script>

<style scoped>
.org-selector {
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
}

.selector-controls {
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid #e4e7ed;
}

.selector-container {
  margin-bottom: 16px;
}

.org-node {
  display: flex;
  align-items: center;
  width: 100%;
}

.node-icon {
  margin-right: 6px;
  font-size: 14px;
}

.node-icon.company {
  color: #e6a23c;
}

.node-icon.department {
  color: #409eff;
}

.node-icon.person {
  color: #67c23a;
}

.node-label {
  flex: 1;
  margin-right: 8px;
}

.node-count {
  color: #909399;
  font-size: 12px;
  margin-right: 8px;
}

.selection-info {
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.selection-info h5 {
  margin: 0 0 12px 0;
  color: #303133;
}

.info-content p {
  margin: 6px 0;
  color: #606266;
}

.selected-list {
  margin-top: 12px;
}

.selected-list h6 {
  margin: 0 0 8px 0;
  color: #303133;
}

.selected-tag {
  margin: 2px 4px 2px 0;
}
</style>

地区选择器

这个示例展示了如何创建一个地区选择器,支持省市区三级联动。

vue
<template>
  <div class="region-selector">
    <h4>地区选择器</h4>
    
    <div class="selector-modes">
      <el-radio-group v-model="selectorMode" @change="handleModeChange">
        <el-radio-button label="single">单选模式</el-radio-button>
        <el-radio-button label="multiple">多选模式</el-radio-button>
      </el-radio-group>
    </div>
    
    <div class="region-container">
      <el-tree-select
        v-model="selectedRegion"
        :data="regionData"
        :props="regionProps"
        :multiple="selectorMode === 'multiple'"
        :show-checkbox="selectorMode === 'multiple'"
        filterable
        :filter-node-method="filterRegionNode"
        placeholder="请选择地区"
        style="width: 100%"
        @change="handleRegionChange"
      >
        <template #default="{ node, data }">
          <div class="region-node">
            <el-icon v-if="data.level === 'province'" class="region-icon province">
              <Location />
            </el-icon>
            <el-icon v-else-if="data.level === 'city'" class="region-icon city">
              <OfficeBuilding />
            </el-icon>
            <el-icon v-else class="region-icon district">
              <MapLocation />
            </el-icon>
            <span class="region-label">{{ data.label }}</span>
            <span v-if="data.code" class="region-code">[{{ data.code }}]</span>
          </div>
        </template>
      </el-tree-select>
    </div>
    
    <div v-if="regionInfo" class="region-info">
      <h5>选择信息:</h5>
      <div class="info-grid">
        <div class="info-item">
          <label>选择模式:</label>
          <span>{{ selectorMode === 'single' ? '单选' : '多选' }}</span>
        </div>
        <div class="info-item">
          <label>选中值:</label>
          <span>{{ formatSelectedValue() }}</span>
        </div>
        <div class="info-item">
          <label>完整路径:</label>
          <span>{{ regionInfo.fullPath }}</span>
        </div>
        <div class="info-item">
          <label>行政级别:</label>
          <span>{{ regionInfo.level }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'
import { Location, OfficeBuilding, MapLocation } from '@element-plus/icons-vue'

const selectorMode = ref('single')
const selectedRegion = ref('')
const regionInfo = ref(null)

const regionProps = {
  value: 'code',
  label: 'label',
  children: 'children'
}

const regionData = ref([
  {
    code: '110000',
    label: '北京市',
    level: 'province',
    children: [
      {
        code: '110100',
        label: '北京市',
        level: 'city',
        children: [
          { code: '110101', label: '东城区', level: 'district' },
          { code: '110102', label: '西城区', level: 'district' },
          { code: '110105', label: '朝阳区', level: 'district' },
          { code: '110106', label: '丰台区', level: 'district' },
          { code: '110107', label: '石景山区', level: 'district' },
          { code: '110108', label: '海淀区', level: 'district' },
          { code: '110109', label: '门头沟区', level: 'district' },
          { code: '110111', label: '房山区', level: 'district' },
          { code: '110112', label: '通州区', level: 'district' },
          { code: '110113', label: '顺义区', level: 'district' },
          { code: '110114', label: '昌平区', level: 'district' },
          { code: '110115', label: '大兴区', level: 'district' },
          { code: '110116', label: '怀柔区', level: 'district' },
          { code: '110117', label: '平谷区', level: 'district' },
          { code: '110118', label: '密云区', level: 'district' },
          { code: '110119', label: '延庆区', level: 'district' }
        ]
      }
    ]
  },
  {
    code: '310000',
    label: '上海市',
    level: 'province',
    children: [
      {
        code: '310100',
        label: '上海市',
        level: 'city',
        children: [
          { code: '310101', label: '黄浦区', level: 'district' },
          { code: '310104', label: '徐汇区', level: 'district' },
          { code: '310105', label: '长宁区', level: 'district' },
          { code: '310106', label: '静安区', level: 'district' },
          { code: '310107', label: '普陀区', level: 'district' },
          { code: '310109', label: '虹口区', level: 'district' },
          { code: '310110', label: '杨浦区', level: 'district' },
          { code: '310112', label: '闵行区', level: 'district' },
          { code: '310113', label: '宝山区', level: 'district' },
          { code: '310114', label: '嘉定区', level: 'district' },
          { code: '310115', label: '浦东新区', level: 'district' },
          { code: '310116', label: '金山区', level: 'district' },
          { code: '310117', label: '松江区', level: 'district' },
          { code: '310118', label: '青浦区', level: 'district' },
          { code: '310120', label: '奉贤区', level: 'district' },
          { code: '310151', label: '崇明区', level: 'district' }
        ]
      }
    ]
  },
  {
    code: '440000',
    label: '广东省',
    level: 'province',
    children: [
      {
        code: '440100',
        label: '广州市',
        level: 'city',
        children: [
          { code: '440103', label: '荔湾区', level: 'district' },
          { code: '440104', label: '越秀区', level: 'district' },
          { code: '440105', label: '海珠区', level: 'district' },
          { code: '440106', label: '天河区', level: 'district' },
          { code: '440111', label: '白云区', level: 'district' },
          { code: '440112', label: '黄埔区', level: 'district' },
          { code: '440113', label: '番禺区', level: 'district' },
          { code: '440114', label: '花都区', level: 'district' },
          { code: '440115', label: '南沙区', level: 'district' },
          { code: '440117', label: '从化区', level: 'district' },
          { code: '440118', label: '增城区', level: 'district' }
        ]
      },
      {
        code: '440300',
        label: '深圳市',
        level: 'city',
        children: [
          { code: '440303', label: '罗湖区', level: 'district' },
          { code: '440304', label: '福田区', level: 'district' },
          { code: '440305', label: '南山区', level: 'district' },
          { code: '440306', label: '宝安区', level: 'district' },
          { code: '440307', label: '龙岗区', level: 'district' },
          { code: '440308', label: '盐田区', level: 'district' },
          { code: '440309', label: '龙华区', level: 'district' },
          { code: '440310', label: '坪山区', level: 'district' },
          { code: '440311', label: '光明区', level: 'district' }
        ]
      }
    ]
  }
])

const filterRegionNode = (value, data) => {
  if (!value) return true
  return data.label.toLowerCase().includes(value.toLowerCase()) ||
         (data.code && data.code.includes(value))
}

const handleModeChange = (mode) => {
  selectedRegion.value = mode === 'multiple' ? [] : ''
  regionInfo.value = null
}

const handleRegionChange = (value) => {
  console.log('地区选择变化:', value)
  updateRegionInfo(value)
}

const updateRegionInfo = (value) => {
  if (!value || (Array.isArray(value) && value.length === 0)) {
    regionInfo.value = null
    return
  }
  
  const targetCode = Array.isArray(value) ? value[0] : value
  const nodeInfo = findNodeByCode(regionData.value, targetCode)
  
  if (nodeInfo) {
    regionInfo.value = {
      fullPath: nodeInfo.path.map(node => node.label).join(' / '),
      level: getLevelText(nodeInfo.node.level)
    }
  }
}

const findNodeByCode = (nodes, targetCode, path = []) => {
  for (const node of nodes) {
    const currentPath = [...path, node]
    if (node.code === targetCode) {
      return { node, path: currentPath }
    }
    if (node.children) {
      const result = findNodeByCode(node.children, targetCode, currentPath)
      if (result) return result
    }
  }
  return null
}

const getLevelText = (level) => {
  const levelMap = {
    province: '省/直辖市',
    city: '市',
    district: '区/县'
  }
  return levelMap[level] || level
}

const formatSelectedValue = () => {
  if (!selectedRegion.value) return '未选择'
  if (Array.isArray(selectedRegion.value)) {
    return selectedRegion.value.length > 0 ? selectedRegion.value.join(', ') : '未选择'
  }
  return selectedRegion.value
}
</script>

<style scoped>
.region-selector {
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
}

.selector-modes {
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid #e4e7ed;
}

.region-container {
  margin-bottom: 16px;
}

.region-node {
  display: flex;
  align-items: center;
  width: 100%;
}

.region-icon {
  margin-right: 6px;
  font-size: 14px;
}

.region-icon.province {
  color: #e6a23c;
}

.region-icon.city {
  color: #409eff;
}

.region-icon.district {
  color: #67c23a;
}

.region-label {
  flex: 1;
  margin-right: 8px;
}

.region-code {
  color: #909399;
  font-size: 12px;
}

.region-info {
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.region-info h5 {
  margin: 0 0 12px 0;
  color: #303133;
}

.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.info-item {
  display: flex;
}

.info-item label {
  min-width: 80px;
  font-weight: 500;
  color: #303133;
  margin-right: 8px;
}

.info-item span {
  color: #606266;
}
</style>

总结

TreeSelect 树形选择组件是 Element Plus 中功能强大的表单组件之一,具有以下特点:

  • 功能丰富:支持单选、多选、搜索、懒加载等多种功能
  • 数据灵活:支持树形数据结构,适用于层级关系的数据选择
  • 性能优化:提供懒加载、渲染优化等性能优化方案
  • 用户体验:内置搜索、过滤、禁用等用户体验优化
  • 高度定制:支持自定义节点内容、样式和行为
  • 表单集成:与 el-form 完美集成,支持验证和数据绑定

TreeSelect 特别适用于组织架构选择、地区选择、分类目录选择等具有层级关系的数据选择场景。合理使用 TreeSelect 可以大大提升用户在处理层级数据时的操作效率和体验。

参考资料

Element Plus Study Guide