Vue3 示例,演示 **多层机构树**(无限级)(check/uncheck) 三种状态:
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()` 即可保持父子一致。
更多推荐


所有评论(0)