Skip to content

Tree 树形控件

看不清未来时,就坚持的更久一点

NOTE

在设计之初的时候,考虑的情况不算多,所以用了把树结构打平取巧的方法,后续大概率会重构,但是不会改动 API 的使用,到时候也会把这个组件尚未完成的功能补充

基础用法

一种基础的树形结构,通常它更多的是用于展示

Level one 1
Level one 2
Level one 3
<script setup lang="ts">
interface Tree {
  label: string
  children?: Tree[]
}

const data: Tree[] = [
  {
    label: 'Level one 1',
    children: [
      {
        label: 'Level two 1-1',
        children: [
          {
            label: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 2',
    children: [
      {
        label: 'Level two 2-1',
        children: [
          {
            label: 'Level three 2-1-1'
          }
        ]
      },
      {
        label: 'Level two 2-2',
        children: [
          {
            label: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 3',
    children: [
      {
        label: 'Level two 3-1',
        children: [
          {
            label: 'Level three 3-1-1'
          }
        ]
      },
      {
        label: 'Level two 3-2',
        children: [
          {
            label: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

const handleClick = (data: Tree) => {
  console.log(data)
}
</script>

<template>
  <c-tree :data="data" @node-click="handleClick" />
</template>

可选择

大多数时候,使用树形控件的时候都会添加上这个可选择,为组件添加 show-checkbox 属性即可开启,通常在使用这个功能的时候,我们需要你提供一个响应式的数组,绑定到 v-model:selected-keys 属性上,这个数组会记录你选中的节点的 key

Level one 1
Level one 2
Level one 3
<script setup lang="ts">
import { ref } from 'vue'

interface Tree {
  label: string
  children?: Tree[]
}

const data: Tree[] = [
  {
    label: 'Level one 1',
    children: [
      {
        label: 'Level two 1-1',
        children: [
          {
            label: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 2',
    children: [
      {
        label: 'Level two 2-1',
        children: [
          {
            label: 'Level three 2-1-1'
          }
        ]
      },
      {
        label: 'Level two 2-2',
        children: [
          {
            label: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 3',
    children: [
      {
        label: 'Level two 3-1',
        children: [
          {
            label: 'Level three 3-1-1'
          }
        ]
      },
      {
        label: 'Level two 3-2',
        children: [
          {
            label: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

// 记录选中的key
const checkedKyes = ref([])
</script>

<template>
  <c-tree v-model:selected-keys="checkedKyes" show-checkbox :data="data" />
</template>

禁用复选框

通过给你用于渲染的数据的数据项添加 disabled 属性,即可禁用这个复选框

Level one 1
Level one 2
Level one 3
<script setup lang="ts">
import { ref, watch } from 'vue'

interface Tree {
  label: string
  disabled?: boolean
  children?: Tree[]
}

const data: Tree[] = [
  {
    label: 'Level one 1',
    disabled: true,
    children: [
      {
        label: 'Level two 1-1',
        children: [
          {
            label: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 2',
    children: [
      {
        label: 'Level two 2-1',
        disabled: true,
        children: [
          {
            label: 'Level three 2-1-1'
          }
        ]
      },
      {
        label: 'Level two 2-2',
        children: [
          {
            label: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 3',
    children: [
      {
        label: 'Level two 3-1',
        children: [
          {
            label: 'Level three 3-1-1'
          }
        ]
      },
      {
        label: 'Level two 3-2',
        children: [
          {
            label: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

const checkedKyes = ref([])
</script>

<template>
  <c-tree v-model:selected-keys="checkedKyes" show-checkbox :data="data" />
</template>

异步加载

如果你的节点数据并不是一开始就确定的,需要进行异步加载,那么你可以给组件添加 load 属性,并绑定一个方法,这个方法会在你展开节点时被调用

请注意你需要保证你展开的节点是没有 children 数据的且不是一个叶子节点,在这种情况下,通常需要你手动给这个节点添加一个 isLeaf 属性,并设置为 false,表示不是一个叶子节点

Level one 1
Level one 2
<script setup lang="ts">
import { ref } from 'vue'
import type { TreeOptions } from 'coderjc-ui'

const data: TreeOptions[] = [
  {
    label: 'Level one 1',
    isLeaf: false,
    children: []
  },
  {
    label: 'Level one 2',
    isLeaf: false,
    children: []
  }
]

const checkedKyes = ref([])

let count = 1

const loadNode = (node: TreeOptions, resolve: Function, reject: Function) => {
  if (count > 3) return resolve([])

  setTimeout(() => {
    resolve([
      {
        label: `Level load--${count}`,
        isLeaf: false,
        children: []
      }
    ])
    count++
  }, 2000)
}
</script>

<template>
  <c-tree
    v-model:selected-keys="checkedKyes"
    :load="loadNode"
    show-checkbox
    :data="data"
  />
</template>

默认展开以及默认选中

此功能你必须保证你的 key 值是能够被使用的,通过 default-checked-keys 属性即可设置默认选中的节点,数组里面的值为节点的 key

通过 default-expanded-keys 属性即可设置默认展开的节点,数组里面的值为节点的 key,需要注意的是,这个属性你如果要展开子节点的话,必须保证你的父节点也在这个数组里面, 否则子节点不会被展开,当然,这不是一个很好的处理的,后续会完善

Level one 1
Level one 2
Level one 3
Level two 3-1
Level two 3-2
<script setup lang="ts">
import { ref } from 'vue'

interface Tree {
  label: string
  children?: Tree[]
}

const data: Tree[] = [
  {
    label: 'Level one 1',
    children: [
      {
        label: 'Level two 1-1',
        children: [
          {
            label: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 2',
    children: [
      {
        label: 'Level two 2-1',
        children: [
          {
            label: 'Level three 2-1-1'
          }
        ]
      },
      {
        label: 'Level two 2-2',
        children: [
          {
            label: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 3',
    children: [
      {
        label: 'Level two 3-1',
        children: [
          {
            label: 'Level three 3-1-1'
          }
        ]
      },
      {
        label: 'Level two 3-2',
        children: [
          {
            label: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

const checkedKyes = ref([])

const defaultCheckedKeys = ref(['Level one 1', 'Level one 3'])
const defaultExpandedKeys = ref(['Level one 3'])
</script>

<template>
  <c-tree
    v-model:selected-keys="checkedKyes"
    :default-checked-keys="defaultCheckedKeys"
    :default-expanded-keys="defaultExpandedKeys"
    show-checkbox
    :data="data"
  />
</template>

更改默认的 label、children 以及 key 属性

通过 label-field children-field node-key 属性,你可以更改默认的这三个属性,当然,如果你更改了这三个属性,那么你需要保证你的数据结构中,这三个属性都是存在的

Level one 1
Level one 2
Level one 3
<script setup lang="ts">
import { ref } from 'vue'

interface Tree {
  id: number
  name: string
  childs?: Tree[]
}

const data: Tree[] = [
  {
    id: 1,
    name: 'Level one 1',
    childs: [
      {
        id: 2,
        name: 'Level two 1-1',
        childs: [
          {
            id: 3,
            name: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    id: 4,
    name: 'Level one 2',
    childs: [
      {
        id: 5,
        name: 'Level two 2-1',
        childs: [
          {
            id: 6,
            name: 'Level three 2-1-1'
          }
        ]
      },
      {
        id: 7,
        name: 'Level two 2-2',
        childs: [
          {
            id: 8,
            name: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    id: 9,
    name: 'Level one 3',
    childs: [
      {
        id: 10,
        name: 'Level two 3-1',
        childs: [
          {
            id: 11,
            name: 'Level three 3-1-1'
          }
        ]
      },
      {
        id: 12,
        name: 'Level two 3-2',
        childs: [
          {
            id: 13,
            name: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

const checkedKyes = ref([])
</script>

<template>
  <c-tree
    v-model:selected-keys="checkedKyes"
    label-field="name"
    children-field="childs"
    node-key="id"
    show-checkbox
    :data="data"
  />
</template>

虚拟化树形控件

如果你的数据量非常大,那么可以尝试使用这个功能,它会将你的数据进行分片,将展示的数据量控制在一个合理的范围内,提升性能,为组件添加 virtual 属性即可开启,通过 remain 属性来设置可视区域应该展示多少条数据

这个组件可能相对来说,并不算很成熟,所以你需要保证你的每项数据高度是一致且已知的,而非动态的,每项的高度你可以通过 node-height 属性设置

道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
道生一
<script setup lang="ts">
import { ref, watchEffect } from 'vue'

interface Tree {
  label: string
  children?: Tree[]
}

function genArray(num: number): Array<number> {
  return [...Array(num).keys()]
}

function createData(level = 4, baseKey = ''): any {
  if (!level) return undefined
  return genArray(20).map((_: any, index: any) => {
    const key = '' + baseKey + level + index
    return {
      label: createLabel(level),
      key,
      children: createData(level - 1, key)
    }
  })
}

function createLabel(level: number): string {
  if (level === 4) return '道生一'
  if (level === 3) return '一生二'
  if (level === 2) return '二生三'
  if (level === 1) return '三生万物'
  return ''
}

const data: Tree[] = createData(undefined, 'coder')

const checkedKyes = ref([])
</script>

<template>
  <c-tree
    v-model:selected-keys="checkedKyes"
    virtual
    node-key="key"
    show-checkbox
    :data="data"
  />
</template>

节点内容插槽

如果你不满足于节点简单的文本展示,那么可以使用这个插槽来展示你自己的内容,可以按照作用域插槽的方式,接受原节点数据,比如加一点 emoji

😳 Level one 1 😈
😳 Level one 2 😈
😳 Level one 3 😈
<script setup lang="ts">
import { ref } from 'vue'

interface Tree {
  label: string
  children?: Tree[]
}

const data: Tree[] = [
  {
    label: 'Level one 1',
    children: [
      {
        label: 'Level two 1-1',
        children: [
          {
            label: 'Level three 1-1-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 2',
    children: [
      {
        label: 'Level two 2-1',
        children: [
          {
            label: 'Level three 2-1-1'
          }
        ]
      },
      {
        label: 'Level two 2-2',
        children: [
          {
            label: 'Level three 2-2-1'
          }
        ]
      }
    ]
  },
  {
    label: 'Level one 3',
    children: [
      {
        label: 'Level two 3-1',
        children: [
          {
            label: 'Level three 3-1-1'
          }
        ]
      },
      {
        label: 'Level two 3-2',
        children: [
          {
            label: 'Level three 3-2-1'
          }
        ]
      }
    ]
  }
]

const checkedKyes = ref([])
</script>

<template>
  <c-tree v-model:selected-keys="checkedKyes" show-checkbox :data="data">
    <template #default="{ node }">
      <span style="color: #6c5ce7">😳 {{ node.label }} 😈</span>
    </template>
  </c-tree>
</template>

Tree API

Tree Props

属性名说明类型默认值
data展示数据array--
empty-text内容为空的时候展示的文本string--
node-key指定使用那个属性值作为 key,这个属性的属性值需要保证唯一,默认以 label 为 keystring--
label-field指定节点标签为节点对象的某个属性值stringlabel
children-field指定子树为节点对象的某个属性值stringchildren
disabled是否禁用复选框booleanfalse
load加载子树数据的方法,仅当 lazy 属性为true 时生效function(node, resolve, reject)--
show-checkbox开启复选框模式booleanfalse
v-model:selected-keys复选框模式下必填,记录选中节点的 keyarray--
default-checked-keys设置默认选中的节点,值为节点的 keyarray--
default-expanded-keys设置默认展开的节点,值为节点的 keyarray--
virtual是否开启虚拟化树形控件booleanfalse
cache必须开启 virtual,为了更好的渲染效果预先渲染多少条数据,后续加载中决定上下加载多少条件数据,默认与 remain 保持一致,一般情况下,你不需要改动这个数据number--
remain必须开启 virtual,可视区域应该展示多少条数据number--
node-height节点的高度number30

Tree 事件

事件名说明类型
node-click当节点被点击时触发(node:TreeOptions, evt:MouseEvent) => void

Tree Exposes

名称说明类型
flatenTree打平的树形数据array
getCurrentKeyRawNode根据 key 获取原节点`(key)=>TreeOptions

常见问题

为什么无法选中节点?

请确保你绑定的 v-model:selected-keys 是一个响应式的数组,并且你选中的节点 key 存在于这个数组中

如果我没有指定某个属性为 key 值会怎么处理?

如果你没有通过 node-key 指定,那么我们会用你的 label 作为 key 值,但是一般情况下,我们还是建议你指定一个 key 值

为什么禁用的复选框,最后还是选中了或者被移除了?

这个禁用仅仅是禁用这个复选框无法被主动点击改变而已,但是依然会受到子级状态改变而变化,所以当你想完全禁用某一个节点的时候,请将其子节点也全部禁用

了解真相才能获得真正的自由