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实现组织架构图
整体如图所示:
在这里插入图片描述
因为需求没有用插件里面的自定义右键菜单(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="top-position">{
    
    { node.department }}</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>
              <!-- root节点只支持添加 -->
              <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
          ? `<div>系统检测到该部门下仍有相关职位信息,请转移/删除对应职位后再试!</div>`
          : `<div>您确定要删除部门:<span style="color:#0469c0">${ 
        node.department}</span> 吗?</div>`;
      _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;
    }
  }
}

猜你喜欢

转载自blog.csdn.net/weixin_45324044/article/details/129239043