Skip to content
On this page

树结构 Tree

基础用法

lockSelect 设置可以锁定树的选中事件,这个可以用来控制树菜单在不同情况下的操作

  • 一级 1
    • 二级 1-1
      • 三级 1-1-1 我是超长字段我是超长字段我是超长字段
      • 三级 1-1-2
    • 二级 1-2
      • 三级 1-2-1
      • 三级 1-2-2
解锁
<template>
  <div flex>
    <div style="width: 300px">
      <b-tree
        :data="data"
        :lock-select="lockSelect"
        title-ellipsis
        @select-change="handleSelect"
      ></b-tree>
    </div>
    <div style="width: 80px">
      <b-switch v-model="lockSelect" size="large">
        <template #open>锁定</template>
        <template #close>解锁</template>
      </b-switch>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    expand: true,
    children: [
      {
        title: '二级 1-1',
        expand: true,
        children: [
          { title: '三级 1-1-1 我是超长字段我是超长字段我是超长字段' },
          { title: '三级 1-1-2' }
        ]
      },
      {
        title: '二级 1-2',
        expand: true,
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  }
])

const lockSelect = ref(false)
function handleSelect(selected, node) {
  console.log(selected, node)
}
</script>
<template>
  <div flex>
    <div style="width: 300px">
      <b-tree
        :data="data"
        :lock-select="lockSelect"
        title-ellipsis
        @select-change="handleSelect"
      ></b-tree>
    </div>
    <div style="width: 80px">
      <b-switch v-model="lockSelect" size="large">
        <template #open>锁定</template>
        <template #close>解锁</template>
      </b-switch>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    expand: true,
    children: [
      {
        title: '二级 1-1',
        expand: true,
        children: [
          { title: '三级 1-1-1 我是超长字段我是超长字段我是超长字段' },
          { title: '三级 1-1-2' }
        ]
      },
      {
        title: '二级 1-2',
        expand: true,
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  }
])

const lockSelect = ref(false)
function handleSelect(selected, node) {
  console.log(selected, node)
}
</script>

额外参数配置

可以配置额外参数设置,比如附加 id,code 等业务参数,也可以指定 title 显示的字段名称,默认为title

暂无数据

<template>
  <div>
    <b-button @click="initData">初始化数据并默认选中前端组</b-button>
    <div flex class="mt-10">
      <div style="width: 300px">
        <b-tree ref="treeRef" :data="data" title-key="text" @select-change="handleSelect"></b-tree>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick } from 'vue'

const data = ref([])

const defaultId = ref('00102')
const treeRef = ref(null)

function initData() {
  data.value = [
    {
      id: '001',
      text: '研发部',
      deptCode: 'yfb',
      status: '1',
      desc: '研发中心',
      parentId: null,
      children: [
        {
          id: '00101',
          text: '后端组',
          deptCode: 'hd',
          status: '1',
          desc: '后端研发中心',
          parentId: '001'
        },
        {
          id: '00102',
          text: '前端组',
          deptCode: 'qd',
          status: '1',
          desc: '前端研发中心',
          parentId: '001'
        },
        {
          id: '00103',
          text: 'UI设计',
          deptCode: 'sj',
          status: '1',
          desc: '交互、ui设计中心',
          parentId: '001'
        },
        {
          id: '00104',
          text: '测试组',
          deptCode: 'cs',
          status: '1',
          desc: '测试组',
          parentId: '001'
        },
        {
          id: '00105',
          text: '运维组',
          deptCode: 'yw',
          status: '1',
          desc: '运维、服务、巡检',
          parentId: '001'
        }
      ]
    },
    {
      id: '002',
      text: '项目部',
      deptCode: 'xmb',
      status: '1',
      desc: '项目服务部',
      parentId: null,
      children: [
        {
          id: '00201',
          text: '开发组',
          deptCode: 'kf',
          status: '1',
          desc: '后端项目开发',
          parentId: '002'
        },
        {
          id: '00202',
          text: '交付服务组',
          deptCode: 'jf',
          status: '1',
          desc: '交付项目,技术服务支持',
          parentId: '002'
        }
      ]
    }
  ]
  nextTick(() => {
    if (!treeRef.value) return

    // 获取树结构的拍平数据,查找当前需要选中的节点值
    const flatState = treeRef.value.getFlatState()
    const current = flatState.find(node => node.node.id === defaultId.value)
    if (current && current) {
      treeRef.value.setSelected([current.nodeKey])
    }
  })
}

function handleSelect(selected, node) {
  console.log(selected, node)
}
</script>
<template>
  <div>
    <b-button @click="initData">初始化数据并默认选中前端组</b-button>
    <div flex class="mt-10">
      <div style="width: 300px">
        <b-tree ref="treeRef" :data="data" title-key="text" @select-change="handleSelect"></b-tree>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick } from 'vue'

const data = ref([])

const defaultId = ref('00102')
const treeRef = ref(null)

function initData() {
  data.value = [
    {
      id: '001',
      text: '研发部',
      deptCode: 'yfb',
      status: '1',
      desc: '研发中心',
      parentId: null,
      children: [
        {
          id: '00101',
          text: '后端组',
          deptCode: 'hd',
          status: '1',
          desc: '后端研发中心',
          parentId: '001'
        },
        {
          id: '00102',
          text: '前端组',
          deptCode: 'qd',
          status: '1',
          desc: '前端研发中心',
          parentId: '001'
        },
        {
          id: '00103',
          text: 'UI设计',
          deptCode: 'sj',
          status: '1',
          desc: '交互、ui设计中心',
          parentId: '001'
        },
        {
          id: '00104',
          text: '测试组',
          deptCode: 'cs',
          status: '1',
          desc: '测试组',
          parentId: '001'
        },
        {
          id: '00105',
          text: '运维组',
          deptCode: 'yw',
          status: '1',
          desc: '运维、服务、巡检',
          parentId: '001'
        }
      ]
    },
    {
      id: '002',
      text: '项目部',
      deptCode: 'xmb',
      status: '1',
      desc: '项目服务部',
      parentId: null,
      children: [
        {
          id: '00201',
          text: '开发组',
          deptCode: 'kf',
          status: '1',
          desc: '后端项目开发',
          parentId: '002'
        },
        {
          id: '00202',
          text: '交付服务组',
          deptCode: 'jf',
          status: '1',
          desc: '交付项目,技术服务支持',
          parentId: '002'
        }
      ]
    }
  ]
  nextTick(() => {
    if (!treeRef.value) return

    // 获取树结构的拍平数据,查找当前需要选中的节点值
    const flatState = treeRef.value.getFlatState()
    const current = flatState.find(node => node.node.id === defaultId.value)
    if (current && current) {
      treeRef.value.setSelected([current.nodeKey])
    }
  })
}

function handleSelect(selected, node) {
  console.log(selected, node)
}
</script>

其他属性

可以设置 show-checkbox 开启勾选,并可以设置数据格式中的默认选中

expand、selected、checked 和 disabled 可以设置展开,选中,勾选和禁用。multiple 开启多选

多选
  • 一级 1
    • 二级 1-1
      • 三级 1-1-1 我是超长字段我是超长字段我是超长字段
      • 三级 1-1-2
    • 二级 1-2
      • 三级 1-2-1
      • 三级 1-2-2
附加图标
  • 导航
    • 导航菜单
    • 图钉
    • 锚点
    • 面包屑
    • 标签页
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">多选</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        :data="data"
        show-checkbox
        multiple
        @select-change="handleSelect"
        @check-change="handleChecked"
      ></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">附加图标</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree :data="data1"></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    expand: true,
    children: [
      {
        title: '二级 1-1',
        expand: true,
        children: [
          { title: '三级 1-1-1 我是超长字段我是超长字段我是超长字段' },
          { title: '三级 1-1-2' }
        ]
      },
      {
        title: '二级 1-2',
        expand: true,
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  }
])
const data1 = ref([
  {
    title: '导航',
    icon: 'apartment',
    expand: true,
    children: [
      { title: '导航菜单', icon: 'menu' },
      { title: '图钉', icon: 'pushpin' },
      { title: '锚点', icon: 'attachment' },
      { title: '面包屑', icon: 'right' },
      { title: '标签页', icon: 'project' }
    ]
  }
])

function handleSelect(selected, node) {
  console.log(selected, node)
}

function handleChecked(checked, node) {
  console.log(checked, node)
}
</script>
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">多选</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        :data="data"
        show-checkbox
        multiple
        @select-change="handleSelect"
        @check-change="handleChecked"
      ></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">附加图标</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree :data="data1"></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    expand: true,
    children: [
      {
        title: '二级 1-1',
        expand: true,
        children: [
          { title: '三级 1-1-1 我是超长字段我是超长字段我是超长字段' },
          { title: '三级 1-1-2' }
        ]
      },
      {
        title: '二级 1-2',
        expand: true,
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  }
])
const data1 = ref([
  {
    title: '导航',
    icon: 'apartment',
    expand: true,
    children: [
      { title: '导航菜单', icon: 'menu' },
      { title: '图钉', icon: 'pushpin' },
      { title: '锚点', icon: 'attachment' },
      { title: '面包屑', icon: 'right' },
      { title: '标签页', icon: 'project' }
    ]
  }
])

function handleSelect(selected, node) {
  console.log(selected, node)
}

function handleChecked(checked, node) {
  console.log(checked, node)
}
</script>

树节点的操作

树结构默认拍平了所有节点数组,并塞入了唯一的 nodeKey 值用于索引节点,这样可以方便快捷的获取和设置树的状态

  • 一级 1
  • 一级 2
<template>
  <div>
    <div class="mb-16">
      <b-button size="small" @click="expandAll">展开全部</b-button>
      <b-button size="small" @click="collapseAll">收起全部</b-button>
      <b-button size="small" @click="setExpand">展开三级</b-button>
      <b-button size="small" @click="checkAll">选择全部</b-button>
      <b-button size="small" @click="uncheckAll">取消全选</b-button>
      <b-button size="small" @click="setChecked">设置选中1-2及以下</b-button>
      <b-button size="small" @click="setSelected">单选 1-2-1</b-button>
      <b-button size="small" @click="clear">清空单选和多选</b-button>
    </div>
    <div style="width: 300px">
      <b-tree ref="treeRef" :data="data" show-checkbox></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    children: [
      {
        title: '二级 1-1',
        children: [{ title: '三级 1-1-1' }, { title: '三级 1-1-2' }]
      },
      {
        title: '二级 1-2',
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  },
  {
    title: '一级 2',
    children: [
      {
        title: '二级 2-1'
      },
      {
        title: '二级 2-2'
      }
    ]
  }
])
const treeRef = ref(null)

function expandAll() {
  treeRef.value.expandAll()
}
function collapseAll() {
  treeRef.value.collapseAll()
}
function checkAll() {
  treeRef.value.checkAll()
}
function uncheckAll() {
  treeRef.value.uncheckAll()
}
function setChecked() {
  // 默认是获取了nodeKey索引,如有特殊判断,如id,则可以自行根据flatState.find所有nodeKey值
  treeRef.value.setChecked([5, 6])
}
function setSelected() {
  treeRef.value.setSelected([5])
}
function clear() {
  treeRef.value.uncheckAll()
  treeRef.value.unselectAll()
}
function setExpand() {
  treeRef.value.setExpand([0, 4])
}
</script>
<template>
  <div>
    <div class="mb-16">
      <b-button size="small" @click="expandAll">展开全部</b-button>
      <b-button size="small" @click="collapseAll">收起全部</b-button>
      <b-button size="small" @click="setExpand">展开三级</b-button>
      <b-button size="small" @click="checkAll">选择全部</b-button>
      <b-button size="small" @click="uncheckAll">取消全选</b-button>
      <b-button size="small" @click="setChecked">设置选中1-2及以下</b-button>
      <b-button size="small" @click="setSelected">单选 1-2-1</b-button>
      <b-button size="small" @click="clear">清空单选和多选</b-button>
    </div>
    <div style="width: 300px">
      <b-tree ref="treeRef" :data="data" show-checkbox></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: '一级 1',
    children: [
      {
        title: '二级 1-1',
        children: [{ title: '三级 1-1-1' }, { title: '三级 1-1-2' }]
      },
      {
        title: '二级 1-2',
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  },
  {
    title: '一级 2',
    children: [
      {
        title: '二级 2-1'
      },
      {
        title: '二级 2-2'
      }
    ]
  }
])
const treeRef = ref(null)

function expandAll() {
  treeRef.value.expandAll()
}
function collapseAll() {
  treeRef.value.collapseAll()
}
function checkAll() {
  treeRef.value.checkAll()
}
function uncheckAll() {
  treeRef.value.uncheckAll()
}
function setChecked() {
  // 默认是获取了nodeKey索引,如有特殊判断,如id,则可以自行根据flatState.find所有nodeKey值
  treeRef.value.setChecked([5, 6])
}
function setSelected() {
  treeRef.value.setSelected([5])
}
function clear() {
  treeRef.value.uncheckAll()
  treeRef.value.unselectAll()
}
function setExpand() {
  treeRef.value.setExpand([0, 4])
}
</script>

异步加载子节点

  • root
  • leaf
<template>
  <div style="width: 300px">
    <b-tree :data="data" show-checkbox :load-data="loadData"></b-tree>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: 'root',
    children: []
  },
  {
    title: 'leaf',
    isLeaf: true,
    children: []
  }
])

function loadData(item, callback) {
  console.log(item)
  setTimeout(() => {
    let data = [
      {
        title: 'have-child',
        loading: false,
        children: []
      },
      {
        title: 'no-child',
        loading: false,
        children: []
      }
    ]
    // 模拟请求数据有子项的时候
    if (item.title === 'have-child') {
      data = [
        {
          title: 'child1',
          loading: false,
          isLeaf: false,
          children: [],
          visible: true
        },
        {
          title: 'leaf node',
          isLeaf: true,
          children: []
        }
      ]
    } else if (item.title === 'no-child') {
      // 按需自行设置是否是叶子节点或者不设置
      item['isLeaf'] = true
      // 不设置会保留箭头可以再次点击
      // data = []
    }

    callback(data)
  }, 1000)
}
</script>
<template>
  <div style="width: 300px">
    <b-tree :data="data" show-checkbox :load-data="loadData"></b-tree>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    title: 'root',
    children: []
  },
  {
    title: 'leaf',
    isLeaf: true,
    children: []
  }
])

function loadData(item, callback) {
  console.log(item)
  setTimeout(() => {
    let data = [
      {
        title: 'have-child',
        loading: false,
        children: []
      },
      {
        title: 'no-child',
        loading: false,
        children: []
      }
    ]
    // 模拟请求数据有子项的时候
    if (item.title === 'have-child') {
      data = [
        {
          title: 'child1',
          loading: false,
          isLeaf: false,
          children: [],
          visible: true
        },
        {
          title: 'leaf node',
          isLeaf: true,
          children: []
        }
      ]
    } else if (item.title === 'no-child') {
      // 按需自行设置是否是叶子节点或者不设置
      item['isLeaf'] = true
      // 不设置会保留箭头可以再次点击
      // data = []
    }

    callback(data)
  }, 1000)
}
</script>

可以筛选

如果树结构层级较多,可配合过滤函数进行过滤筛选

  • 江苏省
  • 河北省
<template>
  <div>
    <b-input
      v-model="query"
      search
      placeholder="输入过滤条件后回车筛选"
      style="width: 230px"
      @search="handleFilter"
    ></b-input>
    <b-divider style="margin: 14px 0"></b-divider>
    <div style="width: 300px">
      <b-tree ref="treeRef" :data="data" :filter-node-method="filterNode" highlight-filter></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '1',
    title: '江苏省',
    children: [
      {
        id: '1-1',
        title: '南京市',
        children: [
          { id: '1-1-1', title: '玄武区' },
          { id: '1-1-2', title: '鼓楼区' },
          { id: '1-1-3', title: '建邺区' },
          { id: '1-1-4', title: '秦淮区' }
        ]
      },
      {
        id: '1-2',
        title: '无锡市',
        children: [
          { id: '1-2-1', title: '锡山区' },
          { id: '1-2-2', title: '惠山区' },
          { id: '1-2-3', title: '滨湖区' },
          { id: '1-2-4', title: '梁溪区' }
        ]
      },
      {
        id: '1-3',
        title: '徐州市',
        children: [
          { id: '1-3-1', title: '鼓楼区' },
          { id: '1-3-2', title: '云龙区' },
          { id: '1-3-3', title: '泉山区' },
          { id: '1-3-4', title: '铜山区' },
          { id: '1-3-5', title: '贾汪区' },
          { id: '1-3-6', title: '沛县' },
          { id: '1-3-7', title: '丰县' },
          { id: '1-3-8', title: '睢宁县' },
          { id: '1-3-9', title: '新沂市' },
          { id: '1-3-10', title: '邳州市' }
        ]
      }
    ]
  },
  {
    id: '2',
    title: '河北省',
    children: [
      {
        id: '2-1',
        title: '石家庄',
        children: [
          { id: '2-1-1', title: '长安区' },
          { id: '2-1-2', title: '新华区' },
          { id: '2-1-3', title: '鼓楼区' }
        ]
      }
    ]
  }
])

const query = ref('')
const treeRef = ref(null)

const handleFilter = value => {
  treeRef.value.filter(value)
}
const filterNode = (value, node) => {
  if (!value) return true
  // return node.title===value.trim()
  return node.title.indexOf(value) !== -1
}
</script>
<template>
  <div>
    <b-input
      v-model="query"
      search
      placeholder="输入过滤条件后回车筛选"
      style="width: 230px"
      @search="handleFilter"
    ></b-input>
    <b-divider style="margin: 14px 0"></b-divider>
    <div style="width: 300px">
      <b-tree ref="treeRef" :data="data" :filter-node-method="filterNode" highlight-filter></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '1',
    title: '江苏省',
    children: [
      {
        id: '1-1',
        title: '南京市',
        children: [
          { id: '1-1-1', title: '玄武区' },
          { id: '1-1-2', title: '鼓楼区' },
          { id: '1-1-3', title: '建邺区' },
          { id: '1-1-4', title: '秦淮区' }
        ]
      },
      {
        id: '1-2',
        title: '无锡市',
        children: [
          { id: '1-2-1', title: '锡山区' },
          { id: '1-2-2', title: '惠山区' },
          { id: '1-2-3', title: '滨湖区' },
          { id: '1-2-4', title: '梁溪区' }
        ]
      },
      {
        id: '1-3',
        title: '徐州市',
        children: [
          { id: '1-3-1', title: '鼓楼区' },
          { id: '1-3-2', title: '云龙区' },
          { id: '1-3-3', title: '泉山区' },
          { id: '1-3-4', title: '铜山区' },
          { id: '1-3-5', title: '贾汪区' },
          { id: '1-3-6', title: '沛县' },
          { id: '1-3-7', title: '丰县' },
          { id: '1-3-8', title: '睢宁县' },
          { id: '1-3-9', title: '新沂市' },
          { id: '1-3-10', title: '邳州市' }
        ]
      }
    ]
  },
  {
    id: '2',
    title: '河北省',
    children: [
      {
        id: '2-1',
        title: '石家庄',
        children: [
          { id: '2-1-1', title: '长安区' },
          { id: '2-1-2', title: '新华区' },
          { id: '2-1-3', title: '鼓楼区' }
        ]
      }
    ]
  }
])

const query = ref('')
const treeRef = ref(null)

const handleFilter = value => {
  treeRef.value.filter(value)
}
const filterNode = (value, node) => {
  if (!value) return true
  // return node.title===value.trim()
  return node.title.indexOf(value) !== -1
}
</script>

拖拽排序

开启draggable可以开启树节点的拖拽功能,此时需要监听几个事件来进行数据处理

基础拖拽排序
  • 一级 1
    • 二级 1-1
      • 三级 1-1-1
      • 三级 1-1-2
    • 二级 1-2
      • 三级 1-2-1
      • 三级 1-2-2
  • 一级 2
    • 二级 2-1
      • 三级 2-1-1
      • 三级 2-1-2
    • 二级 2-2
      • 三级 2-2-1
      • 三级 2-2-2
自定义函数配置
  • 数据维度
    • 国家层级
      • 国家
    • 部门
      • 部门编号
      • 部门名称
  • 数据度量
    • 默认
      • 统计
    • 部门
      • 上级部门
      • 层级
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">基础拖拽排序</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        ref="treeRef"
        :data="data"
        draggable
        default-expand
        @node-drag-start="handleDragStart"
        @node-drag-enter="handleDragEnter"
        @node-drag-leave="handleDragLeave"
        @node-drag-end="handleDragEnd"
        @node-drop="handleDrop"
      ></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">自定义函数配置</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        :allow-drop="allowDrop"
        :allow-drag="allowDrag"
        :data="data1"
        :render="renderContent1"
        draggable
        lock-select
        default-expand
      ></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, h } from 'vue'
import { Message, BDropdown, BDropdownMenu, BDropdownItem } from 'bin-ui-design'

const data = ref([
  {
    title: '一级 1',
    children: [
      {
        title: '二级 1-1',
        children: [{ title: '三级 1-1-1' }, { title: '三级 1-1-2' }]
      },
      {
        title: '二级 1-2',
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  },
  {
    title: '一级 2',
    children: [
      {
        title: '二级 2-1',
        children: [{ title: '三级 2-1-1' }, { title: '三级 2-1-2' }]
      },
      {
        title: '二级 2-2',
        children: [{ title: '三级 2-2-1' }, { title: '三级 2-2-2' }]
      }
    ]
  }
])
const data1 = ref([
  {
    title: '数据维度',
    nodeType: 'root',
    children: [
      {
        title: '国家层级',
        nodeType: 'hierarchy',
        children: [
          {
            field: 'country',
            title: '国家',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          },
          {
            field: 'province',
            title: '省',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          },
          {
            field: 'city',
            title: '市',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          }
        ]
      },
      {
        title: '部门',
        nodeType: 'folder',
        children: [
          {
            field: 'dept_code',
            title: '部门编号',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0002'
          },
          {
            field: 'dept_name',
            title: '部门名称',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0002'
          }
        ]
      }
    ]
  },
  {
    title: '数据度量',
    nodeType: 'root',
    children: [
      {
        title: '默认',
        nodeType: 'folder',
        children: [
          {
            field: 'count',
            title: '统计',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0001'
          }
        ]
      },
      {
        title: '部门',
        nodeType: 'folder',
        children: [
          {
            field: 'parent_dept',
            title: '上级部门',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0002'
          },
          {
            field: 'level',
            title: '层级',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0002'
          }
        ]
      }
    ]
  }
])

const treeRef = ref(null)

function allowDrop(draggingNode, dropNode, type) {
  if (dropNode.nodeType === 'attribute') {
    return type !== 'inner'
  } else {
    return dropNode.nodeType !== 'root'
  }
}

function allowDrag(draggingNode) {
  // 限制拖拽节点
  return draggingNode.nodeType === 'attribute'
}

function renderContent1({ root, node, data }) {
  const iconMap = {
    root: '',
    hierarchy: 'cluster',
    folder: 'folder',
    dimension: 'deploymentunit',
    measure: 'linechart'
  }
  const colorMap = {
    root: '#1089ff',
    hierarchy: '#1089ff',
    folder: '#35495e',
    dimension: '#1089ff',
    measure: '#52c41a'
  }
  const iconType = data.nodeType === 'attribute' ? data.type : data.nodeType
  const inline = [
    h(
      'span',
      {
        class: 't-ellipsis',
        style: { width: 'calc(100% - 24px)' },
        title: `${data.title}-(${data.field ?? ''})`
      },
      [
        h('i', {
          class: ['b-iconfont', `b-icon-${iconMap[iconType]}`],
          style: { fontSize: '16px', marginRight: '4px', color: colorMap[iconType] }
        }),
        data.title
      ]
    ),
    h(
      BDropdown,
      {
        trigger: 'click',
        appendToBody: true,
        placement: 'bottom-start',
        onCommand: name => {
          console.log(name, data)
          Message(`${name} node: [${data.title}]`)
        }
      },
      {
        default: () => h('i', { class: ['b-iconfont', 'b-icon-setting', 'setting-action'] }),
        dropdown: () =>
          h(BDropdownMenu, () => [
            h(BDropdownItem, { name: 'edit' }, () => [
              h('i', { class: 'b-iconfont b-icon-edit-square' }),
              '编辑'
            ]),
            h(BDropdownItem, { name: 'delete' }, () => [
              h('i', { class: 'b-iconfont b-icon-delete' }),
              '删除'
            ])
          ])
      }
    )
  ]
  return h('span', { style: { width: '100%', fontSize: '12px' }, flex: 'main:justify' }, inline)
}

function handleDragStart(node, ev) {
  console.log('drag start', node)
}
function handleDragEnter(draggingNode, dropNode, ev) {
  console.log('tree drag enter: ', dropNode.title)
}
function handleDragLeave(draggingNode, dropNode, ev) {
  console.log('tree drag leave: ', dropNode.title)
}
function handleDragEnd(draggingNode, dropNode, dropType, ev) {
  console.log('tree drag end: ', dropNode && dropNode.title, dropType)
}
function handleDrop(draggingNode, dropNode, dropType, ev) {
  console.log('tree drop: ', dropNode.title, dropType)
}
</script>
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">基础拖拽排序</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        ref="treeRef"
        :data="data"
        draggable
        default-expand
        @node-drag-start="handleDragStart"
        @node-drag-enter="handleDragEnter"
        @node-drag-leave="handleDragLeave"
        @node-drag-end="handleDragEnd"
        @node-drop="handleDrop"
      ></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">自定义函数配置</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree
        :allow-drop="allowDrop"
        :allow-drag="allowDrag"
        :data="data1"
        :render="renderContent1"
        draggable
        lock-select
        default-expand
      ></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, h } from 'vue'
import { Message, BDropdown, BDropdownMenu, BDropdownItem } from 'bin-ui-design'

const data = ref([
  {
    title: '一级 1',
    children: [
      {
        title: '二级 1-1',
        children: [{ title: '三级 1-1-1' }, { title: '三级 1-1-2' }]
      },
      {
        title: '二级 1-2',
        children: [{ title: '三级 1-2-1' }, { title: '三级 1-2-2' }]
      }
    ]
  },
  {
    title: '一级 2',
    children: [
      {
        title: '二级 2-1',
        children: [{ title: '三级 2-1-1' }, { title: '三级 2-1-2' }]
      },
      {
        title: '二级 2-2',
        children: [{ title: '三级 2-2-1' }, { title: '三级 2-2-2' }]
      }
    ]
  }
])
const data1 = ref([
  {
    title: '数据维度',
    nodeType: 'root',
    children: [
      {
        title: '国家层级',
        nodeType: 'hierarchy',
        children: [
          {
            field: 'country',
            title: '国家',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          },
          {
            field: 'province',
            title: '省',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          },
          {
            field: 'city',
            title: '市',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0001'
          }
        ]
      },
      {
        title: '部门',
        nodeType: 'folder',
        children: [
          {
            field: 'dept_code',
            title: '部门编号',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0002'
          },
          {
            field: 'dept_name',
            title: '部门名称',
            dataType: 'STRING',
            type: 'dimension',
            nodeType: 'attribute',
            tableId: '0002'
          }
        ]
      }
    ]
  },
  {
    title: '数据度量',
    nodeType: 'root',
    children: [
      {
        title: '默认',
        nodeType: 'folder',
        children: [
          {
            field: 'count',
            title: '统计',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0001'
          }
        ]
      },
      {
        title: '部门',
        nodeType: 'folder',
        children: [
          {
            field: 'parent_dept',
            title: '上级部门',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0002'
          },
          {
            field: 'level',
            title: '层级',
            dataType: 'NUMBER',
            type: 'measure',
            nodeType: 'attribute',
            tableId: '0002'
          }
        ]
      }
    ]
  }
])

const treeRef = ref(null)

function allowDrop(draggingNode, dropNode, type) {
  if (dropNode.nodeType === 'attribute') {
    return type !== 'inner'
  } else {
    return dropNode.nodeType !== 'root'
  }
}

function allowDrag(draggingNode) {
  // 限制拖拽节点
  return draggingNode.nodeType === 'attribute'
}

function renderContent1({ root, node, data }) {
  const iconMap = {
    root: '',
    hierarchy: 'cluster',
    folder: 'folder',
    dimension: 'deploymentunit',
    measure: 'linechart'
  }
  const colorMap = {
    root: '#1089ff',
    hierarchy: '#1089ff',
    folder: '#35495e',
    dimension: '#1089ff',
    measure: '#52c41a'
  }
  const iconType = data.nodeType === 'attribute' ? data.type : data.nodeType
  const inline = [
    h(
      'span',
      {
        class: 't-ellipsis',
        style: { width: 'calc(100% - 24px)' },
        title: `${data.title}-(${data.field ?? ''})`
      },
      [
        h('i', {
          class: ['b-iconfont', `b-icon-${iconMap[iconType]}`],
          style: { fontSize: '16px', marginRight: '4px', color: colorMap[iconType] }
        }),
        data.title
      ]
    ),
    h(
      BDropdown,
      {
        trigger: 'click',
        appendToBody: true,
        placement: 'bottom-start',
        onCommand: name => {
          console.log(name, data)
          Message(`${name} node: [${data.title}]`)
        }
      },
      {
        default: () => h('i', { class: ['b-iconfont', 'b-icon-setting', 'setting-action'] }),
        dropdown: () =>
          h(BDropdownMenu, () => [
            h(BDropdownItem, { name: 'edit' }, () => [
              h('i', { class: 'b-iconfont b-icon-edit-square' }),
              '编辑'
            ]),
            h(BDropdownItem, { name: 'delete' }, () => [
              h('i', { class: 'b-iconfont b-icon-delete' }),
              '删除'
            ])
          ])
      }
    )
  ]
  return h('span', { style: { width: '100%', fontSize: '12px' }, flex: 'main:justify' }, inline)
}

function handleDragStart(node, ev) {
  console.log('drag start', node)
}
function handleDragEnter(draggingNode, dropNode, ev) {
  console.log('tree drag enter: ', dropNode.title)
}
function handleDragLeave(draggingNode, dropNode, ev) {
  console.log('tree drag leave: ', dropNode.title)
}
function handleDragEnd(draggingNode, dropNode, dropType, ev) {
  console.log('tree drag end: ', dropNode && dropNode.title, dropType)
}
function handleDrop(draggingNode, dropNode, dropType, ev) {
  console.log('tree drop: ', dropNode.title, dropType)
}
</script>

render 函数

使用 render 函数可以设置更多自定义的效果

新增移除
  • parent 1
    • child 1-1
    • child 1-2
下拉菜单
  • 导航
    • 导航菜单
    • 图钉
    • 锚点
    • 面包屑
    • 标签页
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">新增移除</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree ref="tree" :data="data" :render="renderContent"></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">下拉菜单</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree :data="data1" :render="renderContent1"></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, h } from 'vue'
import { Message, BDropdown, BDropdownMenu, BDropdownItem } from 'bin-ui-design'

const data = ref([
  {
    title: 'parent 1',
    expand: true,
    render: ({ root, node, data }) => {
      return h(
        'span',
        {
          style: {
            display: 'inline-flex',
            justifyContent: 'space-between',
            width: '100%'
          }
        },
        [
          h('span', data.title),
          h('i', {
            class: ['b-iconfont', 'b-icon-plus-square-fill'],
            style: { fontSize: '16px', color: '#1089ff', marginRight: '4px' },
            onClick: () => {
              append(data)
            }
          })
        ]
      )
    },
    children: [{ title: 'child 1-1' }, { title: 'child 1-2' }]
  }
])
const data1 = ref([
  {
    title: '导航',
    icon: 'apartment',
    expand: true,
    children: [
      { title: '导航菜单', icon: 'menu' },
      { title: '图钉', icon: 'pushpin' },
      { title: '锚点', icon: 'attachment' },
      { title: '面包屑', icon: 'right' },
      { title: '标签页', icon: 'project' }
    ]
  }
])

function append(node) {
  const children = node.children || []
  node.expand = true
  children.push({ title: 'new node' })
  node.children = children
  data.value = [...data.value]
}
function remove(root, node, data) {
  console.log(root, node, data)
  const parentKey = root.find(el => el === node).parent
  const parent = root.find(el => el.nodeKey === parentKey).node
  const index = parent.children.indexOf(data)
  parent.children.splice(index, 1)
}

function renderContent({ root, node, data }) {
  return h(
    'span',
    {
      style: { display: 'inline-flex', justifyContent: 'space-between', width: '100%' }
    },
    [
      h('span', { class: 't-ellipsis', style: { width: 'calc(100% - 36px)' } }, data.title),
      h('span', { style: { width: '36px' } }, [
        h('i', {
          class: ['b-iconfont', 'b-icon-plus-square-fill'],
          style: { fontSize: '16px', color: '#5d6d7e' },
          onClick: e => {
            e.stopPropagation()
            append(data)
          }
        }),
        h('i', {
          class: ['b-iconfont', 'b-icon-minus-square-fill'],
          style: { fontSize: '16px', color: '#f5222d' },
          onClick: e => {
            e.stopPropagation()
            remove(root, node, data)
          }
        })
      ])
    ]
  )
}

function renderContent1({ root, node, data }) {
  const inline = [
    h(
      'span',
      {
        class: 't-ellipsis',
        style: { width: 'calc(100% - 24px)' },
        title: `${data.title}`
      },
      [
        h('i', {
          class: ['b-iconfont', `b-icon-${data.icon}`],
          style: { marginRight: '4px' }
        }),
        data.title
      ]
    ),
    h(
      BDropdown,
      {
        trigger: 'click',
        appendToBody: true,
        placement: 'bottom-start',
        onCommand: name => {
          console.log(name, data)
          Message(`${name} node: [${data.title}]`)
        }
      },
      {
        default: () => h('i', { class: ['b-iconfont', 'b-icon-setting', 'setting-action'] }),
        dropdown: () =>
          h(BDropdownMenu, () => [
            h(BDropdownItem, { name: 'edit' }, () => [
              h('i', { class: 'b-iconfont b-icon-edit-square' }),
              '编辑'
            ]),
            h(BDropdownItem, { name: 'delete' }, () => [
              h('i', { class: 'b-iconfont b-icon-delete' }),
              '删除'
            ])
          ])
      }
    )
  ]
  return h('span', { style: { width: '100%', fontSize: '12px' }, flex: 'main:justify' }, inline)
}
</script>
<template>
  <div flex>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">新增移除</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree ref="tree" :data="data" :render="renderContent"></b-tree>
    </div>
    <div class="p10" style="width: 300px; border-right: 1px solid #eeeeee">
      <b-tag type="primary">下拉菜单</b-tag>
      <b-divider style="margin: 8px 0"></b-divider>
      <b-tree :data="data1" :render="renderContent1"></b-tree>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, h } from 'vue'
import { Message, BDropdown, BDropdownMenu, BDropdownItem } from 'bin-ui-design'

const data = ref([
  {
    title: 'parent 1',
    expand: true,
    render: ({ root, node, data }) => {
      return h(
        'span',
        {
          style: {
            display: 'inline-flex',
            justifyContent: 'space-between',
            width: '100%'
          }
        },
        [
          h('span', data.title),
          h('i', {
            class: ['b-iconfont', 'b-icon-plus-square-fill'],
            style: { fontSize: '16px', color: '#1089ff', marginRight: '4px' },
            onClick: () => {
              append(data)
            }
          })
        ]
      )
    },
    children: [{ title: 'child 1-1' }, { title: 'child 1-2' }]
  }
])
const data1 = ref([
  {
    title: '导航',
    icon: 'apartment',
    expand: true,
    children: [
      { title: '导航菜单', icon: 'menu' },
      { title: '图钉', icon: 'pushpin' },
      { title: '锚点', icon: 'attachment' },
      { title: '面包屑', icon: 'right' },
      { title: '标签页', icon: 'project' }
    ]
  }
])

function append(node) {
  const children = node.children || []
  node.expand = true
  children.push({ title: 'new node' })
  node.children = children
  data.value = [...data.value]
}
function remove(root, node, data) {
  console.log(root, node, data)
  const parentKey = root.find(el => el === node).parent
  const parent = root.find(el => el.nodeKey === parentKey).node
  const index = parent.children.indexOf(data)
  parent.children.splice(index, 1)
}

function renderContent({ root, node, data }) {
  return h(
    'span',
    {
      style: { display: 'inline-flex', justifyContent: 'space-between', width: '100%' }
    },
    [
      h('span', { class: 't-ellipsis', style: { width: 'calc(100% - 36px)' } }, data.title),
      h('span', { style: { width: '36px' } }, [
        h('i', {
          class: ['b-iconfont', 'b-icon-plus-square-fill'],
          style: { fontSize: '16px', color: '#5d6d7e' },
          onClick: e => {
            e.stopPropagation()
            append(data)
          }
        }),
        h('i', {
          class: ['b-iconfont', 'b-icon-minus-square-fill'],
          style: { fontSize: '16px', color: '#f5222d' },
          onClick: e => {
            e.stopPropagation()
            remove(root, node, data)
          }
        })
      ])
    ]
  )
}

function renderContent1({ root, node, data }) {
  const inline = [
    h(
      'span',
      {
        class: 't-ellipsis',
        style: { width: 'calc(100% - 24px)' },
        title: `${data.title}`
      },
      [
        h('i', {
          class: ['b-iconfont', `b-icon-${data.icon}`],
          style: { marginRight: '4px' }
        }),
        data.title
      ]
    ),
    h(
      BDropdown,
      {
        trigger: 'click',
        appendToBody: true,
        placement: 'bottom-start',
        onCommand: name => {
          console.log(name, data)
          Message(`${name} node: [${data.title}]`)
        }
      },
      {
        default: () => h('i', { class: ['b-iconfont', 'b-icon-setting', 'setting-action'] }),
        dropdown: () =>
          h(BDropdownMenu, () => [
            h(BDropdownItem, { name: 'edit' }, () => [
              h('i', { class: 'b-iconfont b-icon-edit-square' }),
              '编辑'
            ]),
            h(BDropdownItem, { name: 'delete' }, () => [
              h('i', { class: 'b-iconfont b-icon-delete' }),
              '删除'
            ])
          ])
      }
    )
  ]
  return h('span', { style: { width: '100%', fontSize: '12px' }, flex: 'main:justify' }, inline)
}
</script>

树选择

由于树选择场景较多,这里也额外封装了一个树形选择器以供表单进行节点选择

currentId:00102
currentNode:{}
checked:[]
<template>
  <div>
    <div flex>
      <div flex>
        <b-tree-select
          v-model="currentId"
          style="width: 300px"
          :data="data"
          title-key="text"
          @change="handleChange"
        ></b-tree-select>
        <b-button @click="defaultSelect">设置默认选中前端组</b-button>
      </div>
      <div flex class="ml-16">
        <b-tree-select
          v-model="currentId"
          v-model:checked="checked"
          style="width: 300px"
          :data="data"
          title-key="text"
          show-checkbox
          @change="handleChange"
        ></b-tree-select>
        <b-button @click="setChecked">设置默认勾选项目部</b-button>
      </div>
    </div>

    <div class="pt-8 pb-8">
      <b-button @click="clear">清空选择</b-button>
    </div>
    <div>currentId:{{ currentId }}</div>
    <div>currentNode:{{ currentNode }}</div>
    <div>checked:{{ checked }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '001',
    text: '研发部',
    deptCode: 'yfb',
    status: '1',
    desc: '研发中心',
    parentId: null,
    children: [
      {
        id: '00101',
        text: '后端组',
        deptCode: 'hd',
        status: '1',
        desc: '后端研发中心',
        parentId: '001'
      },
      {
        id: '00102',
        text: '前端组',
        deptCode: 'qd',
        status: '1',
        desc: '前端研发中心',
        parentId: '001'
      },
      {
        id: '00103',
        text: 'UI设计',
        deptCode: 'sj',
        status: '1',
        desc: '交互、ui设计中心',
        parentId: '001'
      },
      {
        id: '00104',
        text: '测试组',
        deptCode: 'cs',
        status: '1',
        desc: '测试组',
        parentId: '001'
      },
      {
        id: '00105',
        text: '运维组',
        deptCode: 'yw',
        status: '1',
        desc: '运维、服务、巡检',
        parentId: '001'
      }
    ]
  },
  {
    id: '002',
    text: '项目部',
    deptCode: 'xmb',
    status: '1',
    desc: '项目服务部',
    parentId: null,
    children: [
      {
        id: '00201',
        text: '开发组',
        deptCode: 'kf',
        status: '1',
        desc: '后端项目开发',
        parentId: '002'
      },
      {
        id: '00202',
        text: '交付服务组',
        deptCode: 'jf',
        status: '1',
        desc: '交付项目,技术服务支持',
        parentId: '002'
      }
    ]
  }
])

const currentId = ref('00102')
const currentNode = ref({})
const checked = ref([])

function defaultSelect() {
  currentId.value = '00102'
}
function handleChange(val, node) {
  currentNode.value = node ? { id: node.id, text: node.text } : {}
}
function setChecked() {
  checked.value = ['002', '00201', '00202']
}
function clear() {
  currentId.value = ''
  currentNode.value = {}
  checked.value = []
}
</script>
<template>
  <div>
    <div flex>
      <div flex>
        <b-tree-select
          v-model="currentId"
          style="width: 300px"
          :data="data"
          title-key="text"
          @change="handleChange"
        ></b-tree-select>
        <b-button @click="defaultSelect">设置默认选中前端组</b-button>
      </div>
      <div flex class="ml-16">
        <b-tree-select
          v-model="currentId"
          v-model:checked="checked"
          style="width: 300px"
          :data="data"
          title-key="text"
          show-checkbox
          @change="handleChange"
        ></b-tree-select>
        <b-button @click="setChecked">设置默认勾选项目部</b-button>
      </div>
    </div>

    <div class="pt-8 pb-8">
      <b-button @click="clear">清空选择</b-button>
    </div>
    <div>currentId:{{ currentId }}</div>
    <div>currentNode:{{ currentNode }}</div>
    <div>checked:{{ checked }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '001',
    text: '研发部',
    deptCode: 'yfb',
    status: '1',
    desc: '研发中心',
    parentId: null,
    children: [
      {
        id: '00101',
        text: '后端组',
        deptCode: 'hd',
        status: '1',
        desc: '后端研发中心',
        parentId: '001'
      },
      {
        id: '00102',
        text: '前端组',
        deptCode: 'qd',
        status: '1',
        desc: '前端研发中心',
        parentId: '001'
      },
      {
        id: '00103',
        text: 'UI设计',
        deptCode: 'sj',
        status: '1',
        desc: '交互、ui设计中心',
        parentId: '001'
      },
      {
        id: '00104',
        text: '测试组',
        deptCode: 'cs',
        status: '1',
        desc: '测试组',
        parentId: '001'
      },
      {
        id: '00105',
        text: '运维组',
        deptCode: 'yw',
        status: '1',
        desc: '运维、服务、巡检',
        parentId: '001'
      }
    ]
  },
  {
    id: '002',
    text: '项目部',
    deptCode: 'xmb',
    status: '1',
    desc: '项目服务部',
    parentId: null,
    children: [
      {
        id: '00201',
        text: '开发组',
        deptCode: 'kf',
        status: '1',
        desc: '后端项目开发',
        parentId: '002'
      },
      {
        id: '00202',
        text: '交付服务组',
        deptCode: 'jf',
        status: '1',
        desc: '交付项目,技术服务支持',
        parentId: '002'
      }
    ]
  }
])

const currentId = ref('00102')
const currentNode = ref({})
const checked = ref([])

function defaultSelect() {
  currentId.value = '00102'
}
function handleChange(val, node) {
  currentNode.value = node ? { id: node.id, text: node.text } : {}
}
function setChecked() {
  checked.value = ['002', '00201', '00202']
}
function clear() {
  currentId.value = ''
  currentNode.value = {}
  checked.value = []
}
</script>

树校验

可以结合form表单进行校验提示。

currentId:00102
currentNode:{}
<template>
  <div>
    <div flex>
      <b-form ref="ruleFormRef" :model="obj" label-width="85px" :rules="ruleValidate">
        <b-form-item prop="currentId" label="树结构">
          <b-tree-select
            v-model="obj.currentId"
            style="width: 300px"
            :data="data"
            title-key="text"
            clearable
            @change="handleChange"
          ></b-tree-select>
        </b-form-item>
        <b-form-item>
          <b-button type="primary" @click="submitForm">提交</b-button>
          <b-button @click="resetForm">重置</b-button>
        </b-form-item>
      </b-form>
    </div>
    <div>currentId:{{ obj.currentId }}</div>
    <div>currentNode:{{ currentNode }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '001',
    text: '研发部',
    deptCode: 'yfb',
    status: '1',
    desc: '研发中心',
    parentId: null,
    children: [
      {
        id: '00101',
        text: '后端组',
        deptCode: 'hd',
        status: '1',
        desc: '后端研发中心',
        parentId: '001'
      },
      {
        id: '00102',
        text: '前端组',
        deptCode: 'qd',
        status: '1',
        desc: '前端研发中心',
        parentId: '001'
      },
      {
        id: '00103',
        text: 'UI设计',
        deptCode: 'sj',
        status: '1',
        desc: '交互、ui设计中心',
        parentId: '001'
      },
      {
        id: '00104',
        text: '测试组',
        deptCode: 'cs',
        status: '1',
        desc: '测试组',
        parentId: '001'
      },
      {
        id: '00105',
        text: '运维组',
        deptCode: 'yw',
        status: '1',
        desc: '运维、服务、巡检',
        parentId: '001'
      }
    ]
  },
  {
    id: '002',
    text: '项目部',
    deptCode: 'xmb',
    status: '1',
    desc: '项目服务部',
    parentId: null,
    children: [
      {
        id: '00201',
        text: '开发组',
        deptCode: 'kf',
        status: '1',
        desc: '后端项目开发',
        parentId: '002'
      },
      {
        id: '00202',
        text: '交付服务组',
        deptCode: 'jf',
        status: '1',
        desc: '交付项目,技术服务支持',
        parentId: '002'
      }
    ]
  }
])

const obj = ref({
  currentId: '00102'
})
const ruleFormRef = ref(null)
const currentNode = ref({})
const ruleValidate = {
  currentId: [{ required: true, message: '树不能为空', trigger: 'change' }]
}

function handleChange(val, node) {
  // console.log(val, node)
  currentNode.value = node ? { id: node.id, text: node.text } : {}
}

function submitForm() {
  ruleFormRef.value.validate(valid => {
    if (valid) {
      alert('submit!')
    } else {
      console.log('error submit!!')
      return false
    }
  })
}
function resetForm() {
  ruleFormRef.value.resetFields()
}
</script>
<template>
  <div>
    <div flex>
      <b-form ref="ruleFormRef" :model="obj" label-width="85px" :rules="ruleValidate">
        <b-form-item prop="currentId" label="树结构">
          <b-tree-select
            v-model="obj.currentId"
            style="width: 300px"
            :data="data"
            title-key="text"
            clearable
            @change="handleChange"
          ></b-tree-select>
        </b-form-item>
        <b-form-item>
          <b-button type="primary" @click="submitForm">提交</b-button>
          <b-button @click="resetForm">重置</b-button>
        </b-form-item>
      </b-form>
    </div>
    <div>currentId:{{ obj.currentId }}</div>
    <div>currentNode:{{ currentNode }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
  {
    id: '001',
    text: '研发部',
    deptCode: 'yfb',
    status: '1',
    desc: '研发中心',
    parentId: null,
    children: [
      {
        id: '00101',
        text: '后端组',
        deptCode: 'hd',
        status: '1',
        desc: '后端研发中心',
        parentId: '001'
      },
      {
        id: '00102',
        text: '前端组',
        deptCode: 'qd',
        status: '1',
        desc: '前端研发中心',
        parentId: '001'
      },
      {
        id: '00103',
        text: 'UI设计',
        deptCode: 'sj',
        status: '1',
        desc: '交互、ui设计中心',
        parentId: '001'
      },
      {
        id: '00104',
        text: '测试组',
        deptCode: 'cs',
        status: '1',
        desc: '测试组',
        parentId: '001'
      },
      {
        id: '00105',
        text: '运维组',
        deptCode: 'yw',
        status: '1',
        desc: '运维、服务、巡检',
        parentId: '001'
      }
    ]
  },
  {
    id: '002',
    text: '项目部',
    deptCode: 'xmb',
    status: '1',
    desc: '项目服务部',
    parentId: null,
    children: [
      {
        id: '00201',
        text: '开发组',
        deptCode: 'kf',
        status: '1',
        desc: '后端项目开发',
        parentId: '002'
      },
      {
        id: '00202',
        text: '交付服务组',
        deptCode: 'jf',
        status: '1',
        desc: '交付项目,技术服务支持',
        parentId: '002'
      }
    ]
  }
])

const obj = ref({
  currentId: '00102'
})
const ruleFormRef = ref(null)
const currentNode = ref({})
const ruleValidate = {
  currentId: [{ required: true, message: '树不能为空', trigger: 'change' }]
}

function handleChange(val, node) {
  // console.log(val, node)
  currentNode.value = node ? { id: node.id, text: node.text } : {}
}

function submitForm() {
  ruleFormRef.value.validate(valid => {
    if (valid) {
      alert('submit!')
    } else {
      console.log('error submit!!')
      return false
    }
  })
}
function resetForm() {
  ruleFormRef.value.resetFields()
}
</script>

Big-Tree 超大数据

如果需要渲染超大数据量的属性结构,需要使用扩展组件<b-big-tree>,组件内部 api 基本复用树结构所有属性,对树形结构进行优化,利用可视区域位置来进行过滤筛选节点并进行操作, 但为了性能的提示必然要牺牲部分体验,因此大数据量的树结构不提供动画展开缩放效果

默认节点高度为28px,可通过visible-count指定树可是区域显示的节点个数,默认为 15 个 即420px因为是针对可视区域的滚动优化,因此必须指定容器高度

数据条数:

暂无数据

<template>
  <div>
    <div style="margin-bottom: 8px">
      <b-space>
        数据条数:
        <b-input-number v-model="number" style="width: 120px"></b-input-number>
        <b-button @click="init">渲染</b-button>
        <b-input
          v-model="query"
          search
          placeholder="输入过滤条件后回车筛选"
          style="width: 230px"
          @search="handleFilter"
        ></b-input>
        <b-button-group>
          <b-button @click="expandAll">展开所有</b-button>
          <b-button @click="collapseAll">收起所有</b-button>
        </b-button-group>
      </b-space>
    </div>
    <b-divider style="margin: 14px 0"></b-divider>
    <b-big-tree
      ref="treeRef"
      :data="treedata"
      :visible-count="10"
      :filter-node-method="filterNode"
      @select-change="handleSelected"
    ></b-big-tree>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const treedata = ref([])

const number = ref(5000)
const query = ref('')
const treeRef = ref(null)

let maxNode = 10000 // 最大的节点数
const childNodesNumber = [2, 5] // 子节点数
const maxLevel = 3 // 最大嵌套层级
const childRate = 0.4 // 拥有子节点的概率
const label = '节点' // 节点label
let index = 0
const data = []

const randomInteger = function (min, max) {
  let result = min - 0.5 + Math.random() * (max - min + 1)
  result = Math.round(result)
  return result
}

const generateId = function () {
  ++index
  return Math.random().toString().slice(3) * 1
}

const generateNode = function () {
  const id = generateId()
  return {
    id: id,
    title: `${label}_${index}-id:${id}`
  }
}
const generateChild = function (tree, level = 1) {
  if (index > maxNode - 1) return
  tree.children = []
  const childNumber = randomInteger(childNodesNumber[0], childNodesNumber[1])
  for (let i = 0; i < childNumber; i++) {
    if (index > maxNode - 1) break
    const obj = generateNode()

    if (Math.random() < childRate && level < maxLevel) {
      generateChild(obj, ++level)
    }
    tree.children.push(obj)
  }
}

const generate = function (number) {
  // eslint-disable-next-line no-const-assign
  maxNode = number
  // eslint-disable-next-line no-unmodified-loop-condition
  while (index < maxNode) {
    let obj = generateNode()
    generateChild(obj)
    data.push(obj)
  }
  return data
}
function init() {
  treedata.value = generate(number.value)
}
function handleSelected(allSelected, node) {
  console.log(allSelected, node)
}
function expandAll() {
  treeRef.value.expandAll()
}
function collapseAll() {
  treeRef.value.collapseAll()
}
function handleFilter(value) {
  treeRef.value.filter(value)
}
function filterNode(value, node) {
  if (!value) return true
  // return node.title===value.trim()
  return node.title.indexOf(value) !== -1
}
</script>
<template>
  <div>
    <div style="margin-bottom: 8px">
      <b-space>
        数据条数:
        <b-input-number v-model="number" style="width: 120px"></b-input-number>
        <b-button @click="init">渲染</b-button>
        <b-input
          v-model="query"
          search
          placeholder="输入过滤条件后回车筛选"
          style="width: 230px"
          @search="handleFilter"
        ></b-input>
        <b-button-group>
          <b-button @click="expandAll">展开所有</b-button>
          <b-button @click="collapseAll">收起所有</b-button>
        </b-button-group>
      </b-space>
    </div>
    <b-divider style="margin: 14px 0"></b-divider>
    <b-big-tree
      ref="treeRef"
      :data="treedata"
      :visible-count="10"
      :filter-node-method="filterNode"
      @select-change="handleSelected"
    ></b-big-tree>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const treedata = ref([])

const number = ref(5000)
const query = ref('')
const treeRef = ref(null)

let maxNode = 10000 // 最大的节点数
const childNodesNumber = [2, 5] // 子节点数
const maxLevel = 3 // 最大嵌套层级
const childRate = 0.4 // 拥有子节点的概率
const label = '节点' // 节点label
let index = 0
const data = []

const randomInteger = function (min, max) {
  let result = min - 0.5 + Math.random() * (max - min + 1)
  result = Math.round(result)
  return result
}

const generateId = function () {
  ++index
  return Math.random().toString().slice(3) * 1
}

const generateNode = function () {
  const id = generateId()
  return {
    id: id,
    title: `${label}_${index}-id:${id}`
  }
}
const generateChild = function (tree, level = 1) {
  if (index > maxNode - 1) return
  tree.children = []
  const childNumber = randomInteger(childNodesNumber[0], childNodesNumber[1])
  for (let i = 0; i < childNumber; i++) {
    if (index > maxNode - 1) break
    const obj = generateNode()

    if (Math.random() < childRate && level < maxLevel) {
      generateChild(obj, ++level)
    }
    tree.children.push(obj)
  }
}

const generate = function (number) {
  // eslint-disable-next-line no-const-assign
  maxNode = number
  // eslint-disable-next-line no-unmodified-loop-condition
  while (index < maxNode) {
    let obj = generateNode()
    generateChild(obj)
    data.push(obj)
  }
  return data
}
function init() {
  treedata.value = generate(number.value)
}
function handleSelected(allSelected, node) {
  console.log(allSelected, node)
}
function expandAll() {
  treeRef.value.expandAll()
}
function collapseAll() {
  treeRef.value.collapseAll()
}
function handleFilter(value) {
  treeRef.value.filter(value)
}
function filterNode(value, node) {
  if (!value) return true
  // return node.title===value.trim()
  return node.title.indexOf(value) !== -1
}
</script>

Props

参数说明类型可选值默认值
data可嵌套的节点属性的数组,生成 tree 的数据Array[]
multiple是否支持多选Booleanfalse
show-checkbox是否显示多选框Booleanfalse
empty-text没有数据时的提示String暂无数据
load-data异步加载数据的方法,见示例Function
title-key定义 title 键,默认 titleStringtitle
children-key定义子节点键,默认 children 为子节点 keyStringchildren
check-strictly复选框的情况下,是否严格的遵循父子不互相关联的做法Booleanfalse
check-directly开启后,在 show-checkbox 模式下,select 的交互也将转为 checkBooleanfalse
lock-select锁定树选择,再部分业务中常用,比如开启弹窗后禁用树的选中操作Booleanfalse
title-ellipsis是否开启标题超长省略Booleantrue
draggable是否开启拖拽节点功能Booleantrue
allow-drag判断节点能否被拖拽 如果返回 false ,节点不能被拖动Function(node)
allow-drop拖拽时判定目标节点能否成为拖动目标位置。 返回false拖动节点不能被拖放到目标节点。type 参数有三种情况:'prev'、'inner'和 'next',表示放置在目标节点前、插入目标节点和放置在目标节点后Function(draggingNode, dropNode, type)
filter-node-method筛选过滤树节点函数Function
highlight-filter高亮搜索条件文字Booleantrue
timeout刷新频率(<b-big-tree>扩展组件可用)Number17
itemHeight节点高度 (<b-big-tree>扩展组件可用)Number28
visibleCount显示区域个数(<b-big-tree>扩展组件可用)Number15

Events

事件名说明返回值
select-change点击树节点时触发当前已选中的节点数组、当前项、flatState
check-change点击复选框时触发当前已勾选节点的数组、当前项、包含半选的节点数组、flatState
toggle-expand展开和收起子列表时触发当前节点的数据
node-drag-start拖拽节点开始事件被拖拽节点对应的 Node、event
node-drag-enter拖拽节点进入事件被拖拽节点对应的 Node、当前进入节点对应的 Node、event
node-drag-leave拖拽节点移除事件被拖拽节点对应的 Node、所离开节点对应的 Node、event
node-drag-over拖拽节点 over 事件被拖拽节点对应的 Node、当前进入节点对应的 Node、event
node-drag-end拖拽节点结束事件被拖拽节点对应的 Node、当前进入节点对应的 Node、放置节点类型 dropType、event
node-drag拖拽节点结束事件被拖拽节点对应的 Node、当前进入节点对应的 Node、放置节点类型 dropType、event

Methods

事件名说明返回值
getFlatState拍平的数组 buffer,包含层级关系及索引 无
getCheckedNodes获取被勾选的节点
getSelectedNodes获取被选中的节点
getCheckedAndIndeterminateNodes获取选中及半选节点
filter树节点过滤函数,必须设置 filter-node-method 过滤匹配函数
setChecked设置 node 勾选 ,参数为 keys:nodeKey 数组, flag:勾选状态,默认 true
setSelected设置 node 单选 ,参数为 keys:nodeKey 数组, flag:勾选状态,默认 true,expandParent:是否展开祖先节点,默认 true
setExpand设置 node 展开 ,参数为 keys:nodeKey 数组, flag:勾选状态,默认 true
expandAll展开所有
collapseAll收起所有
checkAll勾选所有
unselectAll取消所有单选
uncheckAll取消全部勾选

Children

属性说明类型默认值
icon节点显示 iconString
title节点显示标题String
expand是否展开直子节点Booleanfalse
disabled禁用Booleanfalse
disableCheckbox禁用单选框Booleanfalse
selected是否选中子节点Booleanfalse
checked是否勾选(如果勾选,子节点也会全部勾选)Booleanfalse
visible是否显示节点(可通过操作节点 visible 属性隐藏节点)Booleanfalse
children子节点属性数组,可以设置 tree 组件 children-key 属性来自定义Array
loading可设置为异步加载节点,需配合 loadData 设置加载函数Boolean
isLeaf是否为叶子节点,可控制是否显示箭头,可配合异步加载实现Boolean