Skip to content

虚拟树组件

一.定义树中Props

组件的基本应用,传入数据即可根据数据渲染树组件

<z-tree :data="data"></z-tree>
1

定义树中所需的基本props属性

// 树的数据类型
export type Key = string | number
export interface TreeOption {
  key?: Key
  label?: string
  isLeaf?: boolean
  children?: TreeOption[]
  [k: string]: unknown
}
export const treeProps = {
  defaultExpandedKeys: {
    // 1.要展开的key
    type: Array as PropType<Key[]>,
    default: () => []
  },
  keyField: {
    // 2.key字段的别名
    type: String,
    default: 'key'
  },
  labelField: {
    // 3.label字段的别名
    type: String,
    default: 'label'
  },
  childrenField: {
    // 4.children字段的别名
    type: String,
    default: 'children'
  },
  data: {
    // 5.所有数据
    type: Array as PropType<TreeOption[]>,
    default: () => []
  }
} as const
export type TreeProps = Partial<ExtractPropTypes<typeof treeProps>>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

我们需要对用户传入的数据进行格式化后在使用。格式化后的props是这个酱紫的~

export interface TreeNode extends Required<TreeOption>{
  level: number // 层级
  children: Array<TreeNode> // 儿子数组
  rawNode: TreeOption // 原始的node
}
1
2
3
4
5
import { withInstall } from '@zi-shui/utils/with-install'
import _Tree from './src/tree.vue'
const Tree = withInstall(_Tree) // 生成带有install方法的组件
export default Tree // 导出tree组件
declare module 'vue' {
  export interface GlobalComponents {
    ZTree: typeof Tree
  }
}
export * from './src/tree'
1
2
3
4
5
6
7
8
9
10

二.数据格式化和组件渲染

1.创建渲染数据

function createData(level = 4, parentKey = ''): TreeOption[] {
  if (!level) return []
  const arr = new Array(6 - level).fill(0)
  return arr.map((_, idx: number) => {
    const key = parentKey + level + idx
    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 = ref<TreeOption[]>(createData())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

2.封装获取属性方法

<script lang="ts" setup>
import { createNamespace } from '@zi-shui/utils/create'
import { computed} from 'vue'
import { treeProps, TreeOption, TreeNode, Key } from './tree'
const bem = createNamespace('tree')
defineOptions({
  name: 'ZTree'
})
const props = defineProps(treeProps)
// 1)封装获取方法
function createTreeOptions(keyField: string, childrenField: string,labelField:string) {
  return {
    getKey<T>(node:T):Key {
      return node[keyField]
    },
    getChildren<T>(node:T):T[] {
      return node[childrenField]
    },
    getLabel<T>(node:T):string{
      return node[labelField]
    }
  }
}
const treeOptions = createTreeOptions(props.keyField, props.childrenField,props.labelField)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

3.数据格式化

function createTree(data: TreeOption[], parent: TreeNode | null = null) {
  function traversal(data: TreeOption[], parent: TreeNode | null): TreeNode[] {
    return data.map(node => {
      let children = treeOptions.getChildren(node) || [] // 获得所有的孩子
      const childrenLen = children.length || 0
      const treeNode: TreeNode = {
        key: treeOptions.getKey(node),
        label: treeOptions.getLabel(node),
        level: parent ? parent.level + 1 : 0,
        isLeaf: node.isLeaf ?? childrenLen == 0,
        children: [],
        rawNode: node
      }
      if (childrenLen > 0) {
        treeNode.children = traversal(children, treeNode)
      }
      return treeNode
    })
  }
  const result: TreeNode[] = traversal(data, parent)
  return result
}
const tree = ref<Array<TreeNode>>([]) // tree数据列表
watch(
  () => props.data, // 监控data变化,重新创建树
  (data: TreeOption[]) => {
    tree.value = createTree(data)
    console.log(tree.value)
  },
  { immediate: true }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

4.根据expandedKeys拍平数组

const expandedKeySet = ref<Set<Key>>(new Set(props.defaultExpandedKeys))
const flattenTree = computed(() => {
  const expandedKeys = expandedKeySet.value // 需要展开的key
  const flattenNodes: TreeNode[] = []
  const nodes = tree.value || []
  const stack: TreeNode[] = [] // 存放节点的
  for (let i = nodes.length - 1; i >= 0; --i) {
    stack.push(nodes[i]) // 节点2 节点1
  }
  while (stack.length) {
    const node = stack.pop(); // 拿到节点1
    if (!node) continue
    flattenNodes.push(node); // 将节点1入队列
    if (expandedKeys.has(node.key)) { // 如果需要展开
      const children = node.children
      if (children) {
        const length = children.length; // 将节点1的儿子  child3 child2 child1入栈
        for (let i = length - 1; i >= 0; --i) {
          stack.push(children[i])
        }
      }
    }
  }
  return flattenNodes
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

5.渲染Tree组件

<div :class="bem.b()">
    <z-tree-node
      v-for="node in flattenTree"
      :node="node"
      :expanded="isExpanded(node)"
    ></z-tree-node>
</div>
<script>
// ...
function isExpanded(node: TreeNode): boolean {
  return expandedKeySet.value.has(node.key)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

三.抽离TreeNode组件

tree-node组件中需要使用展开图标,这里采用tsx编写内置图标组件

import { h, defineComponent } from 'vue'
export default defineComponent({
  name: 'Switcher',
  render () {
    return (
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
        <path d="M12 8l10 8l-10 8z" />
      </svg>
    )
  }
})
1
2
3
4
5
6
7
8
9
10
11

需要再vite中配置@vitejs/plugin-vue-jsx插件,才可以正确解析tsx语法

1.定义TreeNodeProps

export const treeNodeProps = {
  node:{
    type: Object as PropType<TreeNode>,
    required:true
  },
  expanded:{
    type:Boolean,
    default:false
  }
} as const
export type TreeNodeProps = Partial<ExtractPropTypes<typeof treeNodeProps>>
1
2
3
4
5
6
7
8
9
10
11
<template>
  <div :class="bem.b()">
    <div :class="bem.e('content')">
      <span
        :class="[
          bem.e('expand-icon'),
          bem.is('leaf', node.isLeaf),
          { expanded: !node.isLeaf && expanded }
        ]"
      >
        <z-icon>
          <Switcher></Switcher>
        </z-icon>
      </span>
      <span> {{ node.label }}</span>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { createNamespace } from '@zi-shui/utils/create'
import { PropType, Ref, ref, toRef, watch } from 'vue'
import { TreeNode, treeNodeProps } from './tree'
import ZIcon from '@zi-shui/components/icon'
import Switcher from './icon/Switcher'
const bem = createNamespace('tree-node')
const props = defineProps(treeNodeProps)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

2.编写基本样式

@use 'mixins/mixins' as *;
@use 'common/var' as *;
@include b('tree') {
  display: inline-block;
  width: 100%;
}
@include b('tree-node') {
  padding: 5px 0;
  font-size:14px;
  &:hover {
    background-color:#f5f7fa;
  }
  @include e(expand-icon){
    display: inline-block;
    cursor: pointer;
    transform: rotate(0deg);
    transition: transform 0.1s ease-in-out;
    &.expanded{
      transform:rotate(90deg);
    }
    &.is-leaf{
      fill:transparent;
      cursor: default;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

3.展开收缩功能

定义触发切换的事件

export const treeNodeEvents = {
  toggle:(node:TreeNode)=> node
}
1
2
3
<div
      :class="bem.e('content')"
>
      <span
        :class="[
          bem.e('expand-icon'),
          bem.is('leaf', node.isLeaf),
          { expanded: !node.isLeaf && expanded }
        ]"
      >
        <z-icon>
          <Switcher @click.stop="handleExpandIconClick(node)"></Switcher>
        </z-icon>
      </span>
      <span> {{ node.label }}</span>
    </div>
<script>
const emit = defineEmits(treeNodeEvents)
const handleExpandIconClick = (node:TreeNode)=>{
  emit('toggle',node); // 触发toggle事件
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

组件监听toggle事件

<z-tree-node
      v-for="node in flattenTree"
      :node="node"
      :expanded="isExpanded(node)"
      @toggle="toggleExpand"
></z-tree-node>
<script>
function collapse(node: TreeNode) {
  expandedKeySet.value.delete(node.key)
}
function expand(node: TreeNode) {
  const keySet = expandedKeySet.value
  keySet.add(node.key)
}
function toggleExpand(node: TreeNode) {
  const expandedKeys = expandedKeySet.value
  if (expandedKeys.has(node.key)) {
    collapse(node)
  } else {
    expand(node)
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4.增加indent值

根据层级实现缩进

<div :class="bem.e('content')" :style="{paddingLeft:`${(node.level)*16 +'px'}`}">
  <span>
    <z-icon @click.stop="handleExpandIconClick(node)">
      <Switcher></Switcher>
    </z-icon>
  </span>
  <span> {{ node.label }}</span>
</div>
1
2
3
4
5
6
7
8

四.树组件异步加载

1.构建异步数据

<script>
function createData() {
  return [
    {
      label: nextLabel(),
      key: 1,
      isLeaf: false
    },
    {
      label: nextLabel(),
      key: 2,
      isLeaf: false
    }
  ]
}
function nextLabel(currentLabel?: string): string {
  if (!currentLabel) return 'Out of Tao, One is born'
  if (currentLabel === 'Out of Tao, One is born') return 'Out of One, Two'
  if (currentLabel === 'Out of One, Two') return 'Out of Two, Three'
  if (currentLabel === 'Out of Two, Three') {
    return 'Out of Three, the created universe'
  }
  if (currentLabel === 'Out of Three, the created universe') {
    return 'Out of Tao, One is born'
  }
  return ''
}

const data = ref<TreeOption[]>(createData())
const handleLoad = (node: TreeOption) => {
  // 每次实现懒加载时,会触发此方法,将当前点击的node传入
  return new Promise<TreeOption[]>((resolve, reject) => {
    setTimeout(() => {
      resolve([
        {
          label: nextLabel(node.label),
          key: node.key + nextLabel(node.label),
          isLeaf: false
        }
      ])
    },2000)
  })
}
</script>
<template>
  <z-tree :data="data" :on-load="handleLoad"></z-tree>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
  onLoad:Function as PropType<(node:TreeOption)=>Promise<TreeOption[]>>
}
1
2
3

2.实现触发加载

const loadingKeysRef = ref(new Set<Key>()) // 存储正在加载的key
function triggerLoading(node){
  if (!node.children.length && !node.isLeaf) { // 需要异步加载
    const loadingKeys = loadingKeysRef.value
    const { onLoad } = props // 有onLoad方法
    if (!loadingKeys.has(node.key)) { // 防止重复加载
      loadingKeys.add(node.key) // 添加为正在加载
      if (onLoad) { // 调用用户提供的加载方法
        onLoad(node.rawNode).then((children:TreeOption[]) => {
          node.rawNode.children = children;
          node.children = createTree(children,node); // 格式化后绑定children属性
          loadingKeys.delete(node.key); // 加载完毕移除key
        })
      }
    }
  }
}
function expand(node: TreeNode) {
  const keySet = expandedKeySet.value
  keySet.add(node.key)
  triggerLoading(node);// 展开时触发加载逻辑
}
function toggleExpand(node: TreeNode) {
  const expandedKeys = expandedKeySet.value
  if (expandedKeys.has(node.key) && !loadingKeysRef.value.has(node.key)) {
    collapse(node) // 如果现在是正在加载中,则不进行收起操作
  } else {
    expand(node)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

3.loading图标实现

import { h, defineComponent } from 'vue'

export default defineComponent({
  name: 'Loading',
  render () {
    return (
      <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 1024 1024"
      class="loading"
    >
      <path
        d="M512 1024c-69.1 0-136.2-13.5-199.3-40.2C251.7 958 197 921 150 874c-47-47-84-101.7-109.8-162.7C13.5 648.2 0 581.1 0 512c0-19.9 16.1-36 36-36s36 16.1 36 36c0 59.4 11.6 117 34.6 171.3c22.2 52.4 53.9 99.5 94.3 139.9c40.4 40.4 87.5 72.2 139.9 94.3C395 940.4 452.6 952 512 952c59.4 0 117-11.6 171.3-34.6c52.4-22.2 99.5-53.9 139.9-94.3c40.4-40.4 72.2-87.5 94.3-139.9C940.4 629 952 571.4 952 512c0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9a437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.2C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7c26.7 63.1 40.2 130.2 40.2 199.3s-13.5 136.2-40.2 199.3C958 772.3 921 827 874 874c-47 47-101.8 83.9-162.7 109.7c-63.1 26.8-130.2 40.3-199.3 40.3z"
        fill="currentColor"
      ></path>
    </svg>
    )
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

将loadingKeys列表传入

<z-tree-node
      v-for="node in flattenTree"
      :node="node"
      :expanded="isExpanded(node)"
      @toggle="toggleExpand"
      :loadingKeys="loadingKeysRef"
></z-tree-node>
1
2
3
4
5
6
7
export const treeNodeProps = {
  loadingKeys:{
    type:Object as PropType<Set<Key>>
  }
} as const
1
2
3
4
5

根据传入的loadingKeys判断是否需要显示loading图标

<z-icon>
  <Switcher v-if="!isLoading"></Switcher>
  <Loading v-else></Loading>
</z-icon>
<script>
const isLoading = computed(() => {
  return props.loadingKeys?.has(props.node.key)
})
</script>
1
2
3
4
5
6
7
8
9

五.实现禁用、多选节点

1.计算选中列表

<z-tree :data="data" selectable v-model:selected-keys="value"></z-tree>
1
export const treeProps = {
  multiple: Boolean,
  selectable:{
    type:Boolean,
    default:true,
  }, 
  selectedKeys: Array as PropType<Key[]>,
} as const
export const treeEvents = {
  'update:selectedKeys':(keys:Key[])=> keys
} 
1
2
3
4
5
6
7
8
9
10
11
const emit = defineEmits(treeEvents)
const selectedKeys = ref<Key[]>([]) // 选中的key列表
watch(
  () => props.selectedKeys, // 监控selectedKeys
  value => {
    if (value != undefined) {
      selectedKeys.value = value
    }
  },
  { immediate: true }
)
function handleSelect(node: TreeNode) {
  let keys = Array.from(selectedKeys.value);
  if (!props.selectable) {
    // 如果不支持选中
    return
  }
  if (props.multiple) { // 支持多选
     const index = keys.findIndex(key=>key === node.key)
     if(index > -1){
      keys.splice(index,1);
     }else{
      keys.push(node.key)
     }
  } else {
    if (keys.includes(node.key)) {
      // 如果选中的包含则清空
      keys = []
    } else {
      keys = [node.key]
    }
  }
  emit('update:selectedKeys',keys)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<z-tree-node
   :selectKeys="selectedKeys"
   @select="handleSelect"
></z-tree-node>
1
2
3
4
export const treeNodeProps = {
  // ...
  selectKeys:{
    type:Array as PropType<Key[]>
  }
} as const
export const treeNodeEvents = {
   select:(node:TreeNode)=> node,
}
1
2
3
4
5
6
7
8
9

2.实现选中状态

<div :class="[bem.b(),bem.is('selected',isSelected)]">
    <div
      :class="bem.e('content')"
      :style="{ paddingLeft: `${node.level * 16 + 'px'}` }"
    >
      <!-- ... -->
      </span>
      <span @click="handleContentClick(node)" :class="bem.e('label')"> {{ node.label }}</span>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
const isSelected = computed(() => { // 判断是否选中
  return props.selectKeys?.includes(props.node.key)
})
const handleContentClick =(node: TreeNode)=>{ // 内容点击触发选择
  emit('select', node)
}
1
2
3
4
5
6
@include b('tree-node') {
  @include when(selected){
    background-color:#e7f5ee;
  }
  @include e(content){
    display: flex;
  }
  @include e(label){
    cursor: pointer;
    flex:1
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

3.禁用节点

const data = ref<TreeOption[]>([
  {
    key: '0',
    label: '0',
    children: [
      {
        key: '0-0',
        label: '0-0'
      },
      {
        disabled: true,
        key: '0-1',
        label: '0-1',
        children: [
          {
            label: '0-1-0',
            key: '0-1-0'
          },
          {
            label: '0-1-1',
            key: '0-1-1'
          }
        ]
      }
    ]
  }
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const treeNode: TreeNode = {
  key: treeOptions.getKey(node),
  label: treeOptions.getLabel(node),
  level: parent ? parent.level + 1 : 0,
  isLeaf: node.isLeaf ?? childrenLen == 0,
  children: [],
  disabled:!!node.disabled, // 添加disabled属性
  rawNode: node
}
1
2
3
4
5
6
7
8
9
<div :class="[bem.b(),bem.is('selected',isSelected),bem.is('disabled',node.disabled)]">
	<span @click="handleContentClick(node)" :class="[bem.e('label')]"> {{ node.label }}</span>
</div>
1
2
3
@include b('tree-node') {
  &:not(.is-disabled){
    .z-tree-node__label{
      cursor: pointer;
      flex:1
    }
  }
  &.is-disabled{
    .z-tree-node__label{
      cursor: not-allowed;
      flex:1;
      color:#cdcdcd;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在选中节点时判断,节点是否为禁用状态

const handleContentClick =(node: TreeNode)=>{
  if(node.disabled) return;
  emit('select', node)
}
1
2
3
4

4.自定义节点内容

// 创建上下文对象,提供注入实现
export interface TreeContext {
  slots: SetupContext['slots'] // 插槽属性
}
export const treeInjectionKey: InjectionKey<TreeContext> = Symbol()
1
2
3
4
5
provide(treeInjectionKey, {
  slots: useSlots() // 提供slots属性
});
1
2
3
<span @click="handleContentClick(node)" :class="bem.e('label')">
    <ZTreeNodeContent :node="node"></ZTreeNodeContent>
</span>
1
2
3
export const treeNodeContentProps = {
  node: {
    type: Object as PropType<TreeNode>,
    required: true
  }
} as const
1
2
3
4
5
6
import { defineComponent, inject } from "vue"
import { treeInjectionKey, treeNodeContentProps } from "./tree"

export default defineComponent({
  name:'ZTreeNodeContent',
  props: treeNodeContentProps,
  setup(props) {
    const tree = inject(treeInjectionKey)
    return () => {
      const node = props.node
      return tree?.slots.default
        ? tree.slots.default({ node })
        : node?.label
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

六.可选择的树

1.基本功能

<z-tree
    :data="data"
    multiple
    selectable
    v-model:selected-keys="value"
    show-checkbox
>
</z-tree>
1
2
3
4
5
6
7
8
export const treeProps = {
  showCheckbox: {
    type: Boolean,
    default: false
  },
  defaultCheckedKeys:{
    type: Array as PropType<Key[]>,
    default: () => []
  }
} as const
1
2
3
4
5
6
7
8
9
10

在父组件中将属性传递下去~

<z-tree-node
      v-for="node in flattenTree"
      :node="node"
      :expanded="isExpanded(node)"
      @toggle="toggleExpand"
      :loadingKeys="loadingKeysRef"
      :selectKeys="selectedKeys"
      @select="handleSelect"
      :show-checkbox="showCheckbox"
></z-tree-node>
1
2
3
4
5
6
7
8
9
10
export const treeNodeProps = {
  showCheckbox:{
    type:Boolean,
    required:true
  },
  checked:Boolean,
  disabled:Boolean,
  indeterminate:Boolean
} as const
1
2
3
4
5
6
7
8
9
 <div :class="[bem.b(),bem.is('selected',isSelected),,bem.is('disabled',node.disabled)]">
    <div
      :class="bem.e('content')"
      :style="{ paddingLeft: `${node.level * 16 + 'px'}` }"
    >
      <span>
        <input type="checkbox" v-if="showCheckbox">
      </span>
      <span @click="handleContentClick(node)" :class="bem.e('label')">
        <ZTreeNodeContent :node="node"></ZTreeNodeContent>
      </span>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13

2.封装checkbox组件

import { ExtractPropTypes, PropType } from "vue";
export const checkboxProps = {
  modelValue:{
    type:[Boolean,Number,String] as PropType<boolean | number | string>
  },
  label:{
    type:[Boolean,Number,String] as PropType<boolean | number | string>
  },
  indeterminate: Boolean,
  disabled: Boolean,
} as const

export type CheckboxProps = Partial<ExtractPropTypes<typeof checkboxProps>>
export const checkboxEmits = {
  change: (value:boolean) => typeof value === 'boolean',
  'update:modelValue':(value:boolean | number | string ) => value
}
export type CheckboxEmits = typeof checkboxEmits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <label :class="bem.b()">
    <span :class="bem.e('input')">
      <input
        type="checkbox"
        :value="label"
        :disabled="disabled"
        v-model="model"
        :checked="isChecked"
        @change="handleChange"
        ref="inputRef"
      />
    </span>
    <!-- 没有默认 有label -->
    <span v-if="$slots.default || label" :class="bem.e('label')">
      <slot></slot>
      <template v-if="!$slots.default">{{ label }}</template>
    </span>
  </label>
</template>

<script lang="ts" setup>
import { createNamespace } from '@zi-shui/utils/create'
import { computed, onMounted, ref, watch } from 'vue'
import { checkboxEmits, CheckboxProps, checkboxProps } from './checkbox'
const bem = createNamespace('checkbox')
defineOptions({
  name: 'ZCheckbox'
})
const emit = defineEmits(checkboxEmits)
const props = defineProps(checkboxProps)
const useModel = (props: CheckboxProps) => {
  const model = computed<string | number | boolean>({
    get() {
      return props.modelValue!
    },
    set(val) {
      emit('update:modelValue', val)
    }
  })
  return model
}
const useCheckboxStatus = (props: CheckboxProps, model) => {
  const isChecked = computed(() => {
    const value = model.value
    return value
  })
  return isChecked
}
const useEvent = () => {
  // checkbox修改事件
  function handleChange(e: Event) {
    const target = e.target as HTMLInputElement
    const value = target.checked ? true : false // 获取checked属性,触发修改逻辑
    emit('change', value)
  }
  return handleChange
}
function useCheckbox(props: CheckboxProps) {
  // 1.实现用于双向绑定的model属性
  const model = useModel(props)
  const isChecked = useCheckboxStatus(props, model)
  const handleChange = useEvent()
  return {
    model,
    isChecked,
    handleChange
  }
}
const { model, isChecked, handleChange } = useCheckbox(props)

const inputRef = ref<HTMLInputElement>()
function indeterminate(val) {
  inputRef.value!.indeterminate = val
}
watch(() => props.indeterminate, indeterminate)
onMounted(() => {
  // 默认加载完毕后
  indeterminate(props.indeterminate)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

3.指定tree-node中属性

<z-tree-node
      v-for="node in flattenTree"
      :node="node"
      :expanded="isExpanded(node)"
      @toggle="toggleExpand"
      :loadingKeys="loadingKeysRef"
      :selectKeys="selectedKeys"
      @select="handleSelect"
      :show-checkbox="showCheckbox"
      :checked="isChecked(node)"
      :disabled="isDisabled(node)"
      :indeterminate="isIndeterminate(node)"
></z-tree-node>

<script>
// 稍后更新选中集合
const checkedKeySet = ref<Set<Key>>(new Set(props.defaultCheckedKeys))
function isChecked(node:TreeNode){
   return checkedKeySet.value.has(node.key)
}
function isDisabled(node:TreeNode){
  return !!node.disabled
}
// 稍后更新半选集合
const indeterminateKeySet = ref<Set<Key>>(new Set())
function isIndeterminate(node:TreeNode){
  return true
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

4.checkbox接收属性

<Checkbox
  :indeterminate="indeterminate"
  :model-value="checked"
  :disabled="disabled"
  @change="handleCheckChange"
></Checkbox>
1
2
3
4
5
6
const handleCheckChange = (value: boolean) => {
   emit('check',props.node,value)
}
1
2
3
export const treeNodeEvents = {
  toggle:(node:TreeNode)=> node,
  select:(node:TreeNode)=> node,
  check: (node: TreeNode,value:boolean) => node
}
1
2
3
4
5
function toggleCheckbox(node: TreeNode, isChecked: boolean) {
  toggle(node,isChecked); // 控制孩子切换
  updateCheckedKeys(node);
}
1
2
3
4

这里需要构建panrentKey

const treeNode: TreeNode = {
        key: treeOptions.getKey(node),
        label: treeOptions.getLabel(node),
        level: parent ? parent.level + 1 : 0,
        isLeaf: node.isLeaf ?? childrenLen == 0,
        children: [],
        disabled:!!node.disabled, // 添加disabled属性
        rawNode: node,
        parentKey:parent?.key!,
}
1
2
3
4
5
6
7
8
9
10
function toggle(node:TreeNode,isChecked:boolean) {
    let checkKeys = checkedKeySet.value
    if(isChecked){
      indeterminateKeySet.value.delete(node.key)
    }
    checkKeys[isChecked ? 'add' : 'delete'](node.key)
    const children = node.children
    if (children) {
      children.forEach(childNode => {
        if (!childNode.disabled) {
          toggle(childNode,isChecked);
        }
      })
    }
}
function updateCheckedKeys(node:TreeNode){
    if(node.parentKey){ //有父key 存在
       let parentNode = flattenTree.value.find(item=> item.key === node.parentKey);// 找到父节点
       if(parentNode){
            let allChecked = true;
            let hasChecked = false;
            let nodes = parentNode.children; // 获取孩子节点
            for(let node of nodes){
              if(checkedKeySet.value.has(node.key)){ // 孩子被选中
                hasChecked = true;
              }else if(indeterminateKeySet.value.has(node.key)){ // 孩子是半选
                allChecked = false;
                hasChecked = true;
              }else{
                allChecked = false;
              }
            }

            if(allChecked){
              checkedKeySet.value.add(parentNode.key)
              indeterminateKeySet.value.delete(parentNode.key)
            }else if(hasChecked){
              indeterminateKeySet.value.add(parentNode.key)
                checkedKeySet.value.delete(parentNode.key)
            }else{
              checkedKeySet.value.delete(parentNode.key)
              indeterminateKeySet.value.delete(parentNode.key)
            }
            updateCheckedKeys(parentNode); // 自己搞定再看父级
       }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

七.虚拟滚动组件

import { withInstall } from '@zi-shui/utils/withInstall'
import _vertual from './src/virtual'

const VirtualList = withInstall(_vertual) // 生成带有install方法的组件

export default VirtualList // 导出Icon组件

declare module 'vue' {
  export interface GlobalComponents {
    ZVirtualList: typeof VirtualList
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
 <z-virtual-list :items="flattenTree">
      <template #default="{ node,idx }">
        <z-tree-node
          :node="node"
          :idx="node.key"
          :expanded="isExpanded(node)"
          :loadingKeys="loadingKeysRef"
          :selectKeys="selectedKeys"
          :show-checkbox="showCheckbox"
          :checked="isChecked(node)"
          :disabled="isDisabled(node)"
          :indeterminate="isIndeterminate(node)"
          @check="toggleCheckbox"
           @toggle="toggleExpand"
          @select="handleSelect"
        ></z-tree-node>
      </template>
 </z-virtual-list>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createNamespace } from "@zi-shui/utils/create";
import { computed, defineComponent, h, onMounted, reactive, ref } from "vue";

export default defineComponent({
  name:'ZVirtualList',
  props:{
    size:{
      type:Number,
      default:30
    },
    remain:{
      type:Number,
      default:8
    },
    items:{
      type:Array,
      default:()=>[]
    }
  },
  setup(props,{slots}){
    
    const bem = createNamespace('vl');

    const wrapperRef = ref<HTMLElement>()
    const barRef = ref<HTMLElement>();

    const state = reactive({
        start:0, // 从哪里开始
        end:props.remain
    });
    const offset = ref(0);

    const prev = computed(()=>{
      return Math.min(state.start,props.remain)      
    })
    const next = computed(()=>{
      return Math.min(props.remain,props.items.length - state.end)      
    })
    const visibleData = computed(()=>{
      return props.items.slice(state.start - prev.value,state.end + next.value);
    })

    const handleScroll = ()=>{
      // 算出来 当前滚过去几个了,当前从第几个显示
      let scrollTop =  wrapperRef.value!.scrollTop;
      state.start = Math.floor(scrollTop / props.size);
      state.end = state.start + props.remain;
      offset.value =state.start * props.size - props.size * prev.value; // 让可视区域调整
    }
    onMounted(()=>{
      wrapperRef.value!.style.height = props.size * props.remain  + 'px';
      barRef.value!.style.height = props.items.length * props.size + 'px'
    })
    return ()=>{
      return <div class={bem.b()} ref={wrapperRef} onScroll={handleScroll} >
        <div class={bem.e('scroll-bar')} ref={barRef}></div>
        <div class={bem.e('scroll-list')} style={{transform:`translate3d(0,${offset.value}px,0)`,color:'red'}}> 
        {
          visibleData.value.map( (node,idx) => slots.default?.({node,idx}))
        }
         </div>
      </div>
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@use 'mixins/mixins' as *;
@include b('vl') {
  overflow-y:scroll;
  position: relative;
  border:2px solid #ddd;
  @include e('scroll-list') {
    position: absolute;
    top:0;
    left:0;
    width: 100%;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

Released under the MIT License.