vue2--基于zm-tree-org实现公司部门组织架构图

1.安装zm-tree-org

npm i zm-tree-org -S

2.引入使用

import Vue from 'vue';
import ZmTreeOrg from 'zm-tree-org';
import "zm-tree-org/lib/zm-tree-org.css";

Vue.use(ZmTreeOrg);

3.个人需求

组织架构图中:部门可拖动更改所属部门,可增加部门下节点,删除当前部门节点,查看当前部门节点的所有所属节点,编辑当前部门节点,同级部门节点可进行左右换位移动

4.实现效果

基于zm-tree-org实现组织架构图
整体如图所示:
vue2--基于zm-tree-org实现公司部门组织架构图_第1张图片
因为需求没有用插件里面的自定义右键菜单(define-menus)
实现效果如图所示:

5.代码实现

<template>
  <div>
    <div
      id="mainContent"
      style="height: 800px; border:1px solid #eee"
    >
      <zm-tree-org
        ref="treeRefs"
        node-key="id"
        :data="data"
        :horizontal="horizontal"
        :collapsable="collapsable"
        :node-draggable="true"
        :only-one-node="onlyOneNode"
        :clone-node-drag="cloneNodeDrag"
        :tool-bar="toolBar"
        :default-expand-level="5"
        :define-menus="defineMenus"
        :node-delete="handleOnNodeDelete"
      >
       
        <template v-slot="{node}">
          <div class="card-main">
            <div :class="node.isRoot?'top-position top-position-root':'top-position'">
              <div
                v-if="!node.isRoot"
                @click="handleChangeNode($event,node,'before')"
              ><i class="el-icon-arrow-left" />div>
              <div>{{ node.department }}div>
              <div
                v-if="!node.isRoot"
                @click="handleChangeNode($event,node,'after')"
              ><i class="el-icon-arrow-right" />div>
            div>
            
            <div class="p-title">{{ node.operation }}div>
            <div class="p-people">
              <div class="p-peopleName">
                <div
                  v-if="node.name"
                  class="p-cicle"
                >{{ getNameCicle(node.name,node) }}div>
                <template v-else>
                  <div class="p-cicle p-cicle-empty"><span>虚位span><span>以待span>div>
                template>

                <div class="p-name">{{ node.name }}div>
              div>
            div>
            <div :class="node.isRoot ? 'operation-btn operation-root-btn' :'operation-btn'">
              <div @click="onNodeHandleBtn('add',node)">
                <i class="iconfont icon-tianjia" />
              div>
              
              <template v-if="!node.isRoot">
                <el-popover
                  :ref="`popover-${node.id}`"
                  placement="bottom"
                  trigger="click"
                  :append-to-body="false"
                  :popper-options="{ boundariesElement: 'body', gpuAcceleration: false,}"
                  popper-class="customCont"
                >
                  <div class="customCont-main">
                    <div
                      class="customCont-main-close"
                      @click="closePopover(`popover-${node.id}`)"
                    >
                      <i class="el-icon-close" />
                    div>
                    <div
                      v-if="node.children && node.children.length"
                      class="organization"
                    >
                      <div
                        v-for="(item,index) in node.children"
                        :key="index"
                        class="organization-list"
                      >
                        <div class="organization-list-top">{{ item.department }}div>
                        <div class="organization-list-content">{{ item.name }}div>
                      div>
                    div>
                    <div
                      v-else
                      class="organization organization-empty"
                    >
                      <div class="organization-empty-info">
                        暂无消息
                      div>
                    div>
                  div>
                  <div slot="reference">
                    <i class="iconfont icon-zuzhijiagou" />
                  div>
                el-popover>
                <div @click="onNodeHandleBtn('edit',node)">
                  <i class="iconfont icon-bianjishuru-xianxing" />
                div>
                <div @click="onNodeDeleteBtn(node)">
                  <i class="iconfont icon-shanchu" />
                div>
              template>
            div>
          div>
        template>
      zm-tree-org>
    div>
    <el-dialog
      :title="dialogType==='add' ? '新增部门' :'编辑部门'"
      :visible.sync="dialogVisible"
      custom-class="custom-dialog"
      center
    >
      <el-input
        v-model="departmentName"
        autocomplete="off"
        placeholder="部门名称(50字内)"
      />
      <div
        slot="footer"
        class="dialog-footer"
      >
        <el-button
          type="primary"
          @click="handelDepartment"
        >保 存el-button>
      div>
    el-dialog>
  div>
template>
<script>
import {
  getParentNode,
  getBeforeBrotherNode,
  getAfterBrotherNode,
  changeBeforeNode,
  changeAfterNode,
  handleOnNodeDelete,
} from "./common";
export default {
  data() {
    return {
      toolBar: {
        scale: false,
      },
      data: {
        id: 1,
        department: "某某某死扣的公司",
        operation: "管理员",
        name: "哈哈哈哈",
        isRoot: true,
        children: [
          {
            id: 2,
            pid: 1,
            department: "产品研发部",
            operation: "研发主管",
            name: "张三",
            children: [
              {
                id: 3,
                pid: 2,
                department: "科技创新中心",
                operation: "研发-前端",
                name: "前端哈",
              },
            ],
          },
          {
            id: 4,
            pid: 1,
            department: "销售部",
            operation: "销售主管",
            name: "李四",
            children: [
              {
                id: 5,
                pid: 4,
                department: "销售一部",
                operation: "销售1",
                name: "李四1",
              },
              {
                id: 6,
                pid: 4,
                department: "销售二部",
                operation: "销售2",
                name: "李四2",
              },
            ],
          },
          {
            id: 7,
            pid: 1,
            department: "财务部",
            operation: "财务总监",
            name: "王二",
            children: [
              {
                id: 8,
                pid: 7,
                department: "销售一部",
                operation: "销售1",
                name: "李四1",
              },
              {
                id: 9,
                pid: 7,
                department: "销售二部",
                operation: "销售2",
                name: "李四2",
                children: [
                  {
                    id: 10,
                    pid: 9,
                    department: "销售一部",
                    operation: "销售1",
                    name: "李四1",
                  },
                  {
                    id: 11,
                    pid: 9,
                    department: "销售二部",
                    operation: "销售2",
                    name: "李四2",
                    children: [
                      {
                        id: 12,
                        pid: 11,
                        department: "销售一部",
                        operation: "销售1",
                        name: "李四1",
                      },
                      {
                        id: 13,
                        pid: 11,
                        department: "销售二部",
                        operation: "销售2",
                        name: "李四2",
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ],
      },
      horizontal: false, // 是否横向
      collapsable: true, // 是否可展开收起
      onlyOneNode: false, // 是否仅拖动当前节点
      cloneNodeDrag: false, // 是否拷贝节点拖拽
      expandAll: true, //
      dialogVisible: false, // 弹框显隐
      dialogType: "", // 弹框类型
      departmentName: "", // 部门名称
      nodeTree: "", // 当前点击的nodeTree
    };
  },
  methods: {
    handleOnNodeDelete,
    // 隐藏左键菜单
    defineMenus() {
      return [];
    },

    // 截取名字
    getNameCicle(name, node) {
      const len = name && name.length;
      return name && len && name.slice(len - 2, len);
    },

    // 关闭组织架构
    closePopover(refs) {
      document.body.click();
    },

    // 增加/编辑部门节点
    onNodeHandleBtn(type, node) {
      this.dialogType = type;
      this.dialogVisible = true;
      this.nodeTree = node;
      if (type === "edit") {
        this.departmentName = node.department;
      }
    },

    // 弹框保存按钮
    handelDepartment() {
      // 添加
      if (this.dialogType === "add") {
        const params = {
          id: Math.ceil(Math.random() * 1000 + 100),
          pid: this.nodeTree.id,
          level: this.nodeTree.level || -1,
          operation: "职位",
          name: "",
          department: this.departmentName,
        };
        if (Array.isArray(this.nodeTree["children"])) {
          this.nodeTree["children"].push(params);
        } else {
          this.$set(this.nodeTree, "children", [].concat(params));
        }
        // 编辑
      } else {
        this.$set(this.nodeTree, "department", this.departmentName);
      }
      this.dialogVisible = false;
      this.departmentName = "";
    },

    // 删除部门节点
    onNodeDeleteBtn(node) {
      const _this = this;
      if (node.root) {
        // 根节点不允许删除
        this.$Message.warning("根节点不允许删除!");
        return false;
      }
      // 部门无职位信息
      const tips =
        node.children && node.children.length
          ? `
系统检测到该部门下仍有相关职位信息,请转移/删除对应职位后再试!
`
: `
您确定要删除部门:${node.department} 吗?
`
; _this .$alert(tips, "提示", { dangerouslyUseHTMLString: true, customClass: "deleteDailog", showCancelButton: !(node.children && node.children.length), showConfirmButton: !(node.children && node.children.length), confirmButtonText: "确定", cancelButtonText: "取消", }) .then(async () => { try { const parentNode = getParentNode(this.data, "id", node.pid); handleOnNodeDelete(this, node, parentNode); } catch (error) { _this.$message({ type: "error", message: "操作失败,请重试!", }); } }) .catch(() => {}); }, // 同级部门交换节点 handleChangeNode(e, node, type) { e.stopPropagation(); const resultData = [].concat(this.data); if (type === "before") { // 判断是否有前面兄弟节点 const isHasBeforNode = getBeforeBrotherNode(resultData, node, type); if (!isHasBeforNode?.id) { // 前面无兄弟节点 return false; } else { // 有兄弟节点 进行交换处理 const _data = Object.assign( {}, { ...changeBeforeNode(resultData, isHasBeforNode, node) } )[0]; this.$nextTick(() => { this.$set( this.data, "children", JSON.parse(JSON.stringify(_data.children)) ); }); this.$message({ message: "操作成功!", type: "success", }); } } else { // 判断后面是否有兄弟节点 const isHasAfterNode = getAfterBrotherNode(resultData, node, type); if (!isHasAfterNode?.id) { return false; } else { const _data = Object.assign( {}, changeAfterNode(resultData, isHasAfterNode, node) )[0]; this.$nextTick(() => { this.$set( this.data, "children", JSON.parse(JSON.stringify(_data.children)) ); }); this.$message({ message: "操作成功!", type: "success", }); } } }, }, };
script> <style lang="scss" scoped> /* 每个节点样式 */ .card-main { min-width: 15vw; font-size: 12px; .top-position { background-color: #899cc1; color: #ffffff; padding: 10px; display: flex; justify-content: space-between; align-items: center; } .top-position-root { justify-content: center; } .p-title { color: #868686; padding: 15px 0; } .p-people { display: flex; justify-content: space-evenly; .p-peopleName { display: flex; flex-direction: column; align-items: center; } .p-cicle { background: rgba(4, 45, 124, 0.5); color: #ffffff; border-radius: 42px 42px 42px 42px; width: 48px; height: 48px; line-height: 48px; font-size: 14px; margin-bottom: 5px; } .p-cicle-empty { display: flex; flex-direction: column; justify-content: center; line-height: 20px; } .p-name { color: #000000; min-height: 13.8px; } } .operation-btn { display: flex; justify-content: space-between; background: #f9f9f9; height: 40px; padding: 0 10px; align-items: center; margin-top: 15px; position: relative; } .operation-root-btn { justify-content: flex-end; } } /* 节点操作按钮 */ .iconfont { font-size: 20px; color: #878787; } /* popover样式 */ .operation-btn ::v-deep.customCont { background: #e2f4e9; margin-top: 15px; .popper__arrow::after { border-bottom-color: #e2f4e9; } } .customCont-main { position: relative; &-close { position: absolute; z-index: 10; top: -5px; right: 0; .el-icon-close:before { font-size: 16px; font-weight: 600; } } } .organization { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; padding: 0 18px 18px; min-width: 30vw; } .organization-list { font-size: 12px; min-height: 80px; border: 1px solid rgba(0, 0, 0, 0.06); margin-top: 18px; width: 45%; &-top { background: rgba(42, 178, 98, 0.88); color: #ffffff; padding: 10px; text-align: center; } &-content { padding: 20px; color: #000000; background: #ffffff; } } .organization-empty { min-width: 20vw; min-height: 50px; padding: 0; &-info { width: 100%; text-align: center; font-size: 12px; color: #000000; } } style> <style lang="scss"> /* 新增部门弹框 */ .custom-dialog { width: 18%; .el-dialog__header { border-bottom: 1px solid #e8e8e8; .el-dialog__title { font-size: 14px; } .el-dialog__headerbtn .el-dialog__close { font-weight: 600; } } .el-dialog__body { .el-input.is-active .el-input__inner, .el-input__inner:focus { border-color: #042d7c; outline: 0; } } .dialog-footer { text-align: center; .el-button { background: #042d7c; color: #ffffff; } } .error-tips { font-size: 12px; color: #f56c6c; line-height: 1; padding-top: 4px; position: absolute; } } .tree-org-node__inner:hover { box-shadow: 2px 2px 5px rgba(4, 45, 124, 0.55); } /* 删除部门弹框 */ .deleteDailog { min-width: 30vw; .el-message-box__header { border-bottom: 1px solid #e8e8e8; } .el-message-box__message { text-align: center; } /* .el-message-box__btns { .el-button:focus, .el-button:hover { color: #606266; border-color: #dcdfe6; background-color: #ffffff; } .el-button--primary { background-color: #042d7c; border-color: #042d7c; } .el-button--primary:focus, .el-button--primary:hover { background-color: #042d7c; color: #ffffff; border-color: #042d7c; } } */ } style>

common.js中的方法

// 递归查找父节点
/**
 *
 * @param {*} treeData 整个组织架构数据
 * @param {*} key id键值名
 * @param {*} pid 父节点id
 * @returns
 */
export const getParentNode = (treeData, key, pid) => {
  if (treeData[key] === pid) {
    return treeData;
  } else if (Array.isArray(treeData.children)) {
    const list = treeData.children;
    for (let i = 0, len = list.length; i < len; i++) {
      const row = list[i];
      const pNode = getParentNode(row, key, pid);
      if (pNode) {
        return pNode;
      }
    }
  }
}

// 查找前面兄弟节点
/**
 *
 * @param {*} treeData 整个组织架构数据
 * @param {*} nowNode 当前节点
 * @returns
 */
export const getBeforeBrotherNode = (treeData, nowNode) => {
  for (let i = 0, len = treeData.length; i < len; i++) {
    if (treeData[i].id === nowNode.id) {
      if (i > 0) {
        return treeData[i - 1];
      } else {
        // 没有前面兄弟节点
        return false;
      }
    } else if (treeData[i].children) {
      const isHasBeforNode = getBeforeBrotherNode(treeData[i].children, nowNode);
      if (isHasBeforNode) return isHasBeforNode;
    }
  }
}

// 和前面兄弟节点进行交换
/**
 *
 * @param {*} treeData 整个组织架构数据
 * @param {*} beforeNode 前兄弟节点
 * @param {*} nowNode 当前节点
 * @returns
 */
export const changeBeforeNode = (treeData, beforeNode, nowNode) => {
  for (let i = 0, len = treeData.length; i < len; i++) {
    if (treeData[i].id === nowNode.id) {
      let obj = {};
      obj = treeData[i];
      treeData[i] = beforeNode;
      treeData[i - 1] = nowNode;
      break;
    } else if (treeData[i].children) {
      changeBeforeNode(treeData[i].children, beforeNode, nowNode);
    }
  }
  return treeData;
}

// 查找后面的兄弟节点
/**
 *
 */
export const getAfterBrotherNode = (treeData, nowNode) => {
  for (let i = 0, len = treeData.length; i < len; i++) {
    if (treeData[i].id === nowNode.id) {
      if (i < treeData.length - 1) {
        return treeData[i + 1];
      } else {
        // 没有后面兄弟节点
        return false;
      }
    } else if (treeData[i].children) {
      const isHasAfterNode = getAfterBrotherNode(
        treeData[i].children,
        nowNode
      );
      if (isHasAfterNode) return isHasAfterNode;
    }
  }
}

// 和后面兄弟节点进行交换
/**
 *
 * @param {*} treeData 整个组织架构数据
 * @param {*} beforeNode 后兄弟节点
 * @param {*} nowNode 当前节点
 * @returns
 */
export const changeAfterNode = (treeData, afterNode, nowNode) => {
  for (let i = 0, len = treeData.length; i < len; i++) {
    if (treeData[i].id === nowNode.id) {
      let obj = {};
      obj = treeData[i];
      treeData[i] = afterNode;
      treeData[i + 1] = nowNode;
      break;
    } else if (treeData[i].children) {
      changeAfterNode(treeData[i].children, afterNode, nowNode);
    }
  }
  return treeData;
}

// 处理删除节点(调用原组件事件)
/**
 *
 * @param {*} nowNode  当前节点
 * @param {*} parentNode 父节点
 */
export const handleOnNodeDelete = (_, nowNode, parentNode) => {
  const list = parentNode["children"];
  for (let i = 0, len = list.length; i < len; i++) {
    if (list[i]["id"] === nowNode["id"]) {
      list.splice(i, 1);
      _.$emit("on-node-delete", nowNode, parentNode);
      break;
    }
  }
}

你可能感兴趣的:(vue,vue.js,前端,javascript)