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

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

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

实际完成效果图

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

安装

csharp 复制代码
npm i vue3-tree-org -S
# or
yarn add vue3-tree-org

安装版本号

json 复制代码
"vue3-tree-org": "^4.2.2",

全局使用共有两种方法:

  1. main.js直接使用:
javascript 复制代码
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封装使用(推荐):
javascript 复制代码
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();
  1. plugins文件下的treeOrg.ts
javascript 复制代码
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)
}

整体文件对应图

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

xml 复制代码
<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;
  }
}

效果图为:

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

xml 复制代码
<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函数进行渲染打印,你会得到:
typescript 复制代码
<template>
    <vue3-tree-org 
        :render-content="renderContent"
    >
    </vue3-tree-org>
</template>

const renderContent = (h: any, node: any) => {
  console.log(node, '11111111111111111')
}
  1. 此时你就可以这样使用:
xml 复制代码
<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模版内所有内容后重试,原因如官网所示:

项目中使用(完整代码)

xml 复制代码
<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...

相关推荐
却尘10 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare10 小时前
浅浅看一下设计模式
前端
Lee川10 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix10 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人10 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl10 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人11 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼11 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空11 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust