vue3.x 使用vue3-tree-org实现组织架构图 + 自定义模版内容 - 附完整示例
2025年11月17日 14:08
组织树形结构架构图,如果是vue2项目,请移步www.cnblogs.com/10ve/p/1257…
本文主要讲解在vue3项目中使用,废话不多说,直接上代码。
实际完成效果图
![]()
官方文档:sangtian152.github.io/vue3-tree-o…
![]()
安装
npm i vue3-tree-org -S
# or
yarn add vue3-tree-org
安装版本号
"vue3-tree-org": "^4.2.2",
全局使用共有两种方法:
- main.js直接使用:
import { createApp } from 'vue'
import vue3TreeOrg from 'vue3-tree-org';
import "vue3-tree-org/lib/vue3-tree-org.css";
const app = createApp(App)
app.use(vue3TreeOrg)
app.mount('#app')
- main.js封装使用(推荐):
import { createApp } from 'vue';
import App from './App.vue';
import router, { setupRouter } from '@/router';
import { setupStore } from '@/store';
import { setupDirectives } from '@/directives';
import setupPlugins from '@/plugins';
// 引入动画
import 'animate.css/animate.min.css';
import 'animate.css/animate.compat.css';
import '@/styles/common/base.scss';
import '@/styles/common/element_edit_after.scss';
import '@/styles/common/el-button.scss';
async function appInit() {
const app = createApp(App);
// 挂载状态管理
setupStore(app);
// 挂载路由
setupRouter(app);
// 挂载插件
setupPlugins(app);
// 自定义指令
setupDirectives(app);
// 路由准备就绪后挂载APP实例
await router.isReady();
// 挂载到页面
app.mount('#app', true);
}
void appInit();
3. plugins文件下的treeOrg.ts
import { App } from 'vue'
import vue3TreeOrg from 'vue3-tree-org';
import "vue3-tree-org/lib/vue3-tree-org.css";
export function setupTreeOrg(app: App) {
app.use(vue3TreeOrg)
}
整体文件对应图
![]()
如果不需要自定义内容,可以这样使用
<template>
<div class="tree-wrap" style="height: 400px">
<div class="search-box">
<span>搜索:</span>
<input type="text" v-model="keyword" placeholder="请输入搜索内容" @keydown.enter="filter" />
</div>
<vue3-tree-org
ref="treeRef"
:data="data"
:horizontal="horizontal"
:collapsable="collapsable"
:label-style="style"
:node-draggable="true"
:scalable="false"
:only-one-node="onlyOneNode"
:default-expand-level="1"
:filter-node-method="filterNodeMethod"
:clone-node-drag="cloneNodeDrag"
@on-restore="restore"
@on-contextmenu="onMenus"
@on-node-click="onNodeClick"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const treeRef = ref()
const data = ref({
id: 1,
label: 'xxx科技有限公司',
children: [
{
id: 2,
pid: 1,
label: '产品研发部',
style: { color: '#fff', background: '#108ffe' },
children: [
{ id: 6, pid: 2, label: '禁止编辑节点', disabled: true },
{ id: 8, pid: 2, label: '禁止拖拽节点', noDragging: true },
{ id: 10, pid: 2, label: '测试' }
]
},
{
id: 3,
pid: 1,
label: '客服部',
children: [
{ id: 11, pid: 3, label: '客服一部' },
{ id: 12, pid: 3, label: '客服二部' }
]
},
{ id: 4, pid: 1, label: '业务部' }
]
})
const keyword = ref('')
const horizontal = ref(false)
const collapsable = ref(true)
const onlyOneNode = ref(true)
const cloneNodeDrag = ref(true)
const expandAll = ref(true)
const style = ref({
background: '#fff',
color: '#5e6d82'
})
const onMenus = ({ node, command }) => {
console.log(node, command)
}
const restore = () => {
console.log('restore')
}
const filter = () => {
treeRef.value.filter(keyword.value)
}
const filterNodeMethod = (value, data) => {
console.log(value, data)
if (!value) return true
return data.label.indexOf(value) !== -1
}
const onNodeClick = (e, data) => {
ElMessage.info(data.label)
}
const expandChange = () => {
toggleExpand(data.value, expandAll.value)
}
</script>
<style lang="scss" scoped>
.tree-wrap {
position: relative;
padding-top: 52px;
}
.search-box {
padding: 8px 15px;
position: absolute;
top: 0;
left: 0;
input {
width: 200px;
height: 32px;
border: 1px solid #ddd;
outline: none;
border-radius: 5px;
padding-left: 10px;
}
}
.tree-org-node__text {
text-align: left;
font-size: 14px;
.custom-content {
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid currentColor;
}
}
效果图为:
![]()
如果需要自定义,可以这样使用
<template v-slot="{node}">
<div class="tree-org-node__text node-label">
<div class="custom-content">自定义内容</div>
<div>节点ID:{{node.id}}</div>
<div>节点名称:{{node.label}}</div>
</div>
</template>
注意:
- 这样只能只能取id和label
- 如果你有其他的,如createTime,gross这些额外的字段,在使用node.createTime,或node.gross,将不会生效,你需要使用$$data字段进行解析
- 由来$$data::render-content函数进行渲染打印,你会得到:
<template>
<vue3-tree-org
:render-content="renderContent"
>
</vue3-tree-org>
</template>
const renderContent = (h: any, node: any) => {
console.log(node, '11111111111111111')
}
![]()
- 此时你就可以这样使用:
<template v-slot="{node}">
<div class="tree-org-node__text node-label">
<div class="custom-content">自定义内容</div>
<div>节点ID:{{node.$$data.id}}</div>
<div>节点名称:{{node.$$data.label}}</div>
<div>节点时间:{{node.$$data.createTime}}</div>
<div>节点增长:{{node.$$data.gross}}</div>
</div>
</template>
注意:如果你在使用renderContent函数进行数据渲染打印时,控制台无输出,请暂时删掉template模版内所有内容后重试,原因如官网所示:
![]()
项目中使用(完整代码)
<template>
<div class="tree-wrap">
<vue3-tree-org
center
ref="treeRef"
:data="treeData"
:horizontal="horizontal"
:collapsable="collapsable"
:label-style="style"
:node-draggable="true"
:scalable="scalable"
:only-one-node="onlyOneNode"
:default-expand-level="1"
:clone-node-drag="cloneNodeDrag"
:before-drag-end="beforeDragEnd"
>
<!-- 自定义节点内容,实现可配置 -->
<template v-slot="{node}">
<div class="tree-org-node__text">
<div class="mb12">提煤计划号:{{ node.$$data.no || '--' }}</div>
<div class="mb12 myb-cursor-pointer">
<span class="no-cursor-pointer">转发张数:{{node.$$data.num || 0}}</span>
<span class="ml15" @click="getClick('4', node.$$data.id)">接单张数:<span class="c409eff">{{node.$$data.receive || 0}}</span></span>
<span class="ml15" @click="getClick('7', node.$$data.id)">过空张数:<span class="c409eff">{{node.$$data.tare || 0}}</span></span>
<span class="ml15" @click="getClick('8', node.$$data.id)">过重张数:<span class="c409eff">{{node.$$data.gross || 0}}</span></span>
<span class="ml15" @click="getClick('9', node.$$data.id)">作废张数:<span class="c409eff">{{node.$$data.cancel || 0}}</span></span>
</div>
<div class="mb5 box">
<span class="myb-ellipsis-1">转出方:{{ node.$$data.partyBname || '--' }}</span>
<span class="ml15">转出时间:{{ formatTime(node.$$data.createTime, node.$$data.partyBname) }}</span>
</div>
<div class="mb5 box">
<span class="myb-ellipsis-1">转入方:{{ node.$$data.preName || '--' }}</span>
<span class="ml15">转入时间:{{ formatTime(node.$$data.createTime, node.$$data.preName) }}</span>
</div>
</div>
</template>
<!-- 节点展开数量 -->
<!-- <template v-slot:expand="{node}">
<div>{{node.children.length}}</div>
</template> -->
</vue3-tree-org>
</div>
</template>
<script setup lang="ts">
import moment from 'moment'
import { ref, onBeforeMount, h } from 'vue'
import { coalPlanTreeDetail, statByCoalPlan } from "@/service-api/coalDeliveryNote";
const props = defineProps({
coalPlanId: {
// 提煤计划id
type: [String, Number],
default: "",
},
});
const treeData = ref({})
const scalable = ref(false) // 是否可缩放
const horizontal = ref(false) // 是否水平布局
const collapsable = ref(true) // 是否可折叠
const onlyOneNode = ref(true) // 是否仅拖动当前节点,如果true,仅拖动当前节点,子节点自动添加到当前节点父节点,如果false,则当前节点及子节点一起拖动
const cloneNodeDrag = ref(true) // 是否拷贝节点拖拽
// tree整体样式配置
const style = ref({
background: '#fff',
color: '#606266'
})
// 递归获取所有节点ID
const getAllNodeIds = (node: any): number[] => {
let ids: number[] = [];
if (node.id) {
ids.push(node.id);
}
if (node.children && node.children.length > 0) {
node.children.forEach((child: any) => {
ids = ids.concat(getAllNodeIds(child));
});
}
return ids;
};
// 递归更新节点数据
const updateNodeData = (node: any, statDataMap: Map<number, any>) => {
if (node.id !== undefined && statDataMap.has(node.id)) {
const statData = statDataMap.get(node.id);
node.receive = statData.receive;
node.tare = statData.tare;
node.gross = statData.gross;
node.cancel = statData.cancel;
}
if (node.children && node.children.length > 0) {
node.children.forEach((child: any) => {
updateNodeData(child, statDataMap);
});
}
};
const getTreeData = async () => {
const { data } = await coalPlanTreeDetail({
// id: props.coalPlanId
id: 35
});
// 注意:因为本项目中的接单张数,过空张数,过重张数,作废张数,是在接口请求之后,在同步请求statByCoalPlan接口,将数据同步到节点数据中,如果你的项目部需要这步骤,则直接用下面代码:
if (data) {
// 获取所有节点ID
const allIds = getAllNodeIds(data);
const statDataMap = new Map<number, any>();
// 并行请求所有统计数据
const promises = allIds.map(id => statByCoalPlan({ coalPlanId: id }));
const results = await Promise.all(promises);
// 将结果存入映射
results.forEach((result, index) => {
if (result.data) {
statDataMap.set(allIds[index], result.data);
}
});
// 更新节点数据
updateNodeData(data, statDataMap);
treeData.value = data;
}
// 如果不需要这个步骤,则直接使用下面代码:
treeData.value = data;
};
const beforeDragEnd = (node: any, targetNode: any) => {
return new Promise<void>((resolve, reject) => {
if (!targetNode) reject()
if (node.id === targetNode.id) {
reject()
} else {
resolve()
}
})
};
const emit = defineEmits(['handleSwitchTab'])
const getClick = (type: string, id: string) => {
emit('handleSwitchTab', type, id)
};
const formatTime = (time: string, name: string) => {
return time && name ? moment(time).format("YYYY-MM-DD HH:mm:ss") : '--';
};
onBeforeMount(() => {
getTreeData();
});
</script>
<style lang="scss" scoped>
.tree-wrap {
height: 500px;
position: relative;
:deep(.zm-tree-org) {
padding: 15px 0 0 0;
.zoom-container {
overflow: auto; // 在允许视图滚动的同时,影藏未满足滚动时的滚动槽
.tree-org>.tree-org-node {
padding: 3px 0 10px 0; // tree-org 组件节点间距调整
.tree-org-node__children {
display: flex;
}
}
.tree-org-node {
flex-shrink: 0; // 防止盒子被压缩
.tree-org-node__text {
text-align: left;
.myb-ellipsis-1 {
max-width: 370px;
display: inline-block;
}
.no-cursor-pointer {
cursor: default;
}
}
}
}
}
}
// 影藏放大图标
:deep(.zoom-out) {
display: none;
}
// 影藏缩小图标
:deep(.zoom-in) {
display: none;
}
</style>
END...