特点:① 支持无限层级;② 支持全选/反选,子节点全勾选,父节点自动勾选

代码结构
public/
img/
arrow.svg
folder.svg
src/
views/
warehouse/
index.vue
components/
TreeItem.vue
utils/
staticData.ts
stores/
tree.tssrc/warehouse/index.vue
<template>
<div class="warehouse">
<van-space></van-space>
<div class="checkbox-container">
<span>全选/反选</span>
<input
type="checkbox"
class="checkbox"
:checked="isChecked"
@change="checkAll"
/>
</div>
<section class="warehouse-nav">
<ul>
<tree-item :orgTree="orgTree"></tree-item>
</ul>
<van-divider>到底了~~</van-divider>
</section>
</div>
</template>
<script setup lang="ts">
import { TreeNode } from '@/utils/staticData'
import TreeItem from './components/TreeItem.vue'
import useTreeStore from '@/stores/tree'
defineOptions({
name: 'warehouseComponent',
})
const orgTree = useTreeStore().getTree()
const isChecked = ref(false)
const isAllChecked = computed(() => useTreeStore().isCheckAll)
watch(isAllChecked, (val) => {
isChecked.value = val
})
//全选反选关联
const checkAll = () => {
isChecked.value = !isChecked.value
const tree = useTreeStore().getTree()
useTreeStore().isCheckAll = isChecked.value
const toggleCheckAll = (nodes: TreeNode[]): void => {
nodes.forEach((item) => {
item.checked = isChecked.value
if (item.children) {
toggleCheckAll(item.children)
}
})
}
toggleCheckAll(tree)
const allCheckedIds = useTreeStore().getAllCheckedIds(tree)
useTreeStore().selectedIds = allCheckedIds
}
onMounted(() => {
isChecked.value = useTreeStore().isCheckAll
})
</script>
<style lang="less" scoped>
.warehouse-nav {
margin: 0.4rem 0.8rem;
border-radius: 0.4rem;
background-color: #fff;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: right;
padding: 0.6rem 1.2rem;
}
.checkbox {
width: 1rem;
height: 1rem;
}
</style>
src/warehouse/components/TreeItem.vue
<template>
<li
v-for="(item, index) in orgTree"
:key="item.id"
:class="[
'warehouse-nav-item',
orgTree.length - 1 !== index ? 'van-hairline--bottom' : '',
]"
>
<div class="warehouse-nav-item-inner">
<span @click="toggleShow(item)">
<img
class="arrow-icon"
:class="item.isShow ? 'rotate90' : ''"
:style="{
visibility:
item.children && item.children.length ? 'visible' : 'hidden',
}"
src="/img/arrow.svg"
/>
<img src="/img/folder.svg" class="folder-icon" />{{ item.text }}
</span>
<input
type="checkbox"
class="checkbox"
:data-id="item.id"
@change="handleCheckboxChange(item, $event)"
:checked="item.checked"
/>
</div>
<ul v-show="item.isShow">
<treeItem :orgTree="item.children ? item.children : []" />
</ul>
</li>
</template>
<script setup lang="ts">
import { TreeNode } from '@/utils/staticData'
import useTreeStore from '@/stores/tree'
defineOptions({
name: 'treeItem',
})
withDefaults(
defineProps<{
orgTree: TreeNode[]
}>(),
{}
)
const toggleShow = (item: TreeNode) => {
if (item.children && item.children.length) {
item.isShow = !item.isShow
}
}
/***
* 全选反选
* 支持无限层级联动
* */
const handleCheckboxChange = async (item: TreeNode, event: Event) => {
const target = event.target as HTMLInputElement
item.checked = target.checked
// 递归联动所有子节点勾选状态
const toggleChildren = (node: TreeNode, checked: boolean) => {
if (node.children) {
node.children.forEach((child) => {
child.checked = checked
toggleChildren(child, checked)
})
}
}
toggleChildren(item, target.checked)
// 联动所有父节点勾选状态
await nextTick()
if (item.pid) {
const tree = useTreeStore().getTree()
// 递归查找父节点
const findParent = (nodes: TreeNode[], pid: string): TreeNode | null => {
for (const node of nodes) {
if (node.id === pid) {
return node
}
if (node.children) {
const found = findParent(node.children, pid)
if (found) {
return found
}
}
}
return null
}
// 检查父节点的所有子节点是否都已选中
const allChildrenChecked = (node: TreeNode): boolean => {
if (!node.children || node.children.length === 0) {
return true
}
return node.children.every(child => child.checked && allChildrenChecked(child))
}
let currentParent = findParent(tree, item.pid)
while (currentParent) {
currentParent.checked = allChildrenChecked(currentParent)
if (currentParent.pid) {
currentParent = findParent(tree, currentParent.pid)
} else {
currentParent = null
}
}
}
// 使用store中的函数获取所有选中节点的ID
const allCheckedIds = useTreeStore().getAllCheckedIds(useTreeStore().getTree())
useTreeStore().selectedIds = allCheckedIds
useTreeStore().isCheckAll = hasCheckedAll()
}
const hasCheckedAll = () => {
const tree = useTreeStore().getTree()
// 只要有一个节点(无论层级)未勾选,就视为“未全选”
const allChecked = (nodes: TreeNode[]): boolean =>
nodes.every((n) => n.checked && (!n.children || allChecked(n.children)))
return allChecked(tree)
}
</script>
<style lang="less" scoped>
ul,
li {
list-style: none;
}
.rotate90 {
transform: rotate(90deg);
}
.checkbox {
width: 1rem;
height: 1rem;
}
.warehouse-nav {
&-item {
padding: 0.8rem 0.6rem;
}
.warehouse-nav-item .warehouse-nav-item {
margin-left: 0.9rem;
}
.arrow-icon {
height: 1.2rem;
transition: all 0.2s;
}
.folder-icon {
margin-right: 0.6rem;
height: 1.6rem;
}
}
.warehouse-nav-item-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
src/utils/staticData.ts
export interface TreeNode {
id: string
pid?: string
text: string
children?: TreeNode[]
checked?: boolean // 动态状态,建议在初始化时设置
isShow?: boolean // 动态状态,建议在初始化时设置
level?: number // 可选字段,记录节点层级
}
export const ORG_TREE: TreeNode[] = [
{
id: '1',
pid: null,
text: 'XX集团有限公司',
children: [
// 核心决策机构
{
id: '100',
pid: '1',
text: '董事会',
children: [
{
id: '101',
pid: '100',
text: '董事长办公室',
},
{
id: '102',
pid: '100',
text: '董事会秘书处',
},
],
},
{
id: '200',
pid: '1',
text: '经营管理层',
children: [
{
id: '201',
pid: '200',
text: '总裁办公室',
},
],
},
// 精简的职能部门
{
id: '300',
pid: '1',
text: '管理中心',
children: [
{
id: '301',
pid: '300',
text: '人力资源部',
},
{
id: '302',
pid: '300',
text: '财务部',
},
{
id: '303',
pid: '300',
text: '行政部',
},
{
id: '304',
pid: '300',
text: '法务部',
},
],
},
// 核心业务部门
{
id: '400',
pid: '1',
text: '业务中心',
children: [
{
id: '401',
pid: '400',
text: '产品研发部',
},
{
id: '402',
pid: '400',
text: '生产制造部',
},
{
id: '403',
pid: '400',
text: '市场销售部',
},
{
id: '404',
pid: '400',
text: '客户服务部',
},
],
},
// 简化的分支机构
{
id: '500',
pid: '1',
text: '分支机构',
children: [
{
id: '501',
pid: '500',
text: '北京分公司',
},
{
id: '502',
pid: '500',
text: '上海分公司',
},
{
id: '503',
pid: '500',
text: '广州分公司',
},
{
id: '504',
pid: '500',
text: '深圳分公司',
},
],
},
// 主要子公司
{
id: '600',
pid: '1',
text: '子公司',
children: [
{
id: '601',
pid: '600',
text: 'XX科技有限公司',
},
],
},
],
},
]src/stores/tree.ts
/**
* 树形结构
*/
import { ORG_TREE, TreeNode } from '@/utils/staticData'
export default defineStore('tree', () => {
const isCheckAll = ref(false)
const selectedIds = ref<string[]>([])
// 递归获取所有选中节点的ID
const getAllCheckedIds = (nodes: TreeNode[]): string[] => {
let ids: string[] = []
nodes.forEach((node) => {
if (node.checked) {
ids.push(node.id)
}
if (node.children) {
ids = ids.concat(getAllCheckedIds(node.children))
}
})
return ids
}
function getTree() {
return reactive<TreeNode[]>(ORG_TREE)
}
return {
selectedIds,
isCheckAll,
getTree,
getAllCheckedIds,
}
})
参考https://cn.vuejs.org/examples/#tree

原创文章,如需转载,请注明出处。