普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月17日首页

vue3.x 使用vue3-tree-org实现组织架构图 + 自定义模版内容 - 附完整示例

作者 bug爱好者
2025年11月17日 14:08

组织树形结构架构图,如果是vue2项目,请移步www.cnblogs.com/10ve/p/1257…

本文主要讲解在vue3项目中使用,废话不多说,直接上代码。

实际完成效果图

a106a8797916fda0c2faf2501698f655.png

官方文档:sangtian152.github.io/vue3-tree-o…

image.png

安装

npm i vue3-tree-org -S
# or
yarn add vue3-tree-org

安装版本号

"vue3-tree-org": "^4.2.2",

全局使用共有两种方法:

  1. 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')
  1. 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)
}

整体文件对应图

image.png

如果不需要自定义内容,可以这样使用


<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;
  }
}

效果图为:

image.png

如果需要自定义,可以这样使用

<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>

注意:

  1. 这样只能只能取id和label
  2. 如果你有其他的,如createTime,gross这些额外的字段,在使用node.createTime,或node.gross,将不会生效,你需要使用$$data字段进行解析
  3. 由来$$data::render-content函数进行渲染打印,你会得到:
<template>
    <vue3-tree-org 
        :render-content="renderContent"
    >
    </vue3-tree-org>
</template>

const renderContent = (h: any, node: any) => {
  console.log(node, '11111111111111111')
}

image.png

  1. 此时你就可以这样使用:
<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模版内所有内容后重试,原因如官网所示:

image.png

项目中使用(完整代码)

<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...

❌
❌