Vue3 示例,演示 **多层机构树**(无限级)(check/uncheck) 三种状态:

- 未选(空心)  
- 半选(实心灰)  
- 全选(实心蓝)  

支持  
1. 点击节点 = 选中/取消本节点;  
2. 父节点自动根据子节点汇总状态;  
3. 父节点点击可一次性全选/全取消子树;  
4. 外部可拿到「已选节点 id 集合」。

--------------------------------------------------
一、数据结构约定(后端直接返回即可)

```ts
interface OrgNode {
  id: string
  name: string
  children?: OrgNode[]
}
```

--------------------------------------------------
二、核心思路(递归 + 组合式 API)

1. 每个节点维护 **checked**(boolean)+ **indeterminate**(半选)。  
2. 每次点击只做两件事:  
   a. 设置本节点新状态;  
   b. 向下同步整棵子树;  
   c. 向上冒泡,让每一级父节点重新汇总。  
3. 汇总算法:  
   - 所有子 **全选** → 父 = 全选  
   - 至少一个 **选中/半选** → 父 = 半选  
   - 其余 → 父 = 未选

--------------------------------------------------
三、代码(Vue3 + `<script setup>`)

```vue
<!-- OrgTree.vue -->
<template>
  <ul class="org-tree">
    <OrgNode
      v-for="n in data"
      :key="n.id"
      :node="n"
      :parent="null"
      @change="bubbleChange"/>
  </ul>
</template>

<script setup lang="ts">
import type { OrgNode } from './type'
import OrgNode from './OrgNode.vue'

defineProps<{ data: OrgNode[] }>()

/* 只是透传事件,真正处理在子节点内部 */
const bubbleChange = () => { /* 根节点无父,无需处理 */ }
</script>
```

```vue
<!-- OrgNode.vue -->
<template>
  <li class="node">
    <span
      class="checkbox"
      :class="{
        checked: node.checked,
        indeterminate: node.indeterminate
      }"
      @click="toggle"></span>

    <span class="name">{{ node.name }}</span>

    <ul v-if="node.children?.length">
      <OrgNode
        v-for="c in node.children"
        :key="c.id"
        :node="c"
        :parent="node"
        @change="bubbleChange"/>
    </ul>
  </li>
</template>

<script setup lang="ts">
import type { OrgNode } from './type'
import { computed } from 'vue'

interface Props {
  node: OrgNode
  parent: OrgNode | null
}
const props = defineProps<Props>()
const emit = defineEmits<{ change: [] }>()

/* 点击:本节点状态翻转 + 同步子树 + 向上冒泡 */
function toggle () {
  const next = !props.node.checked
  setSelfAndChildren(props.node, next)
  bubbleUp()
  emit('change')
}

/* 向下同步整棵子树 */
function setSelfAndChildren (n: OrgNode, val: boolean) {
  n.checked = val
  n.indeterminate = false
  n.children?.forEach(c => setSelfAndChildren(c, val))
}

/* 向上冒泡,让父节点重新计算自身状态 */
function bubbleUp () {
  if (!props.parent) return
  const p = props.parent
  const total = p.children!.length
  const checked = p.children!.filter(c => c.checked).length
  const indeterminate = p.children!.some(c => c.checked || c.indeterminate)

  if (checked === total) {            // 全选
    p.checked = true
    p.indeterminate = false
  } else if (checked === 0 && !indeterminate) { // 全不选
    p.checked = false
    p.indeterminate = false
  } else {                            // 半选
    p.checked = false
    p.indeterminate = true
  }
  /* 继续向上 */
  bubbleUp()
}

/* 初次挂载时,根据子节点计算一次自身状态(数据来自后端可省略) */
bubbleUp()
</script>

<style scoped>
.node{line-height:28px}
.checkbox{
  display:inline-block;width:16px;height:16px;
  border:1px solid #ccc;border-radius:2px;margin-right:6px;
  cursor:pointer;position:relative;
}
.checkbox.checked{background:#4096ff;border-color:#4096ff;}
.checkbox.indeterminate{background:#999;border-color:#999;}
.checkbox.checked::after,
.checkbox.indeterminate::after{
  content:'';position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);
  width:8px;height:2px;background:#fff;
}
</style>
```

--------------------------------------------------
四、使用示例

```vue
<!-- App.vue -->
<template>
  <h3>机构树(多层选中)</h3>
  <OrgTree :data="treeData"/>
  <pre>{{ checkedKeys }}</pre>
</template>

<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import OrgTree from './components/OrgTree.vue'
import type { OrgNode } from './components/type'

const treeData = ref<OrgNode[]>([
  {
    id: '1',
    name: '总部',
    checked: false,
    indeterminate: false,
    children: [
      { id: '11', name: '财务部', checked: false, indeterminate: false },
      { id: '12', name: '技术部', checked: false, indeterminate: false,
        children: [
          { id: '121', name: '前端组', checked: false, indeterminate: false },
          { id: '122', name: '后端组', checked: false, indeterminate: false }
        ]
      }
    ]
  }
])

/* 任意节点变化后,收集所有已选 id */
function collectKeys (nodes: OrgNode[]): string[] {
  const keys: string[] = []
  const walk = (n: OrgNode) => {
    if (n.checked) keys.push(n.id)
    n.children?.forEach(walk)
  }
  nodes.forEach(walk)
  return keys
}

const checkedKeys = computed(() => collectKeys(treeData.value))
</script>
```

--------------------------------------------------
五、关键点小结

1. 每个节点只需多存两个布尔量:`checked` + `indeterminate`。  
2. 点击 → 先整棵树同步,再逐层向上汇总,算法 O(n)。  
3. 样式用纯 CSS 即可区分三种状态;图标可换成 `element-plus` 的 `el-checkbox`。  
4. 若数据来自后端,首次只需把 `checked/indeterminate` 初始化为 `false`,进入页面后跑一次 `bubbleUp()` 即可保持父子一致。  

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐