vue 组织树学习笔记

1、v-org-tree

1-1、安装

cnpm install v-org-tree --save
cnpm install v-click-outside-x --save

1-2、注册

//main.js
import OrgTree from 'v-org-tree'
import 'v-org-tree/dist/v-org-tree.css'
Vue.use(OrgTree)

import { directive as clickOutside } from 'v-click-outside-x'
Vue.directive('clickOutside', clickOutside)

//下拉框用iview
import ViewUI from 'view-design';
import 'view-design/dist/styles/iview.css';
Vue.use(ViewUI);

1-3、使用

HTML

<template>
  <div class="view-box">
    <div
      ref="dragWrapper"
      class="org-tree-drag-wrapper"
      @mousedown="mousedownView"
      @contextmenu="handleDocumentContextmenu"
    >
      <div class="org-tree-wrapper" :style="orgTreeStyle">
        <v-org-tree
          v-if="data"
          :data="data"
          :node-render="nodeRender"
          :expand-all="true"
          @on-node-click="handleNodeClick"
          collapsable
        ></v-org-tree>
      </div>
    </div>
  </div>
</template>

JS

<script>
import { on, off } from "@/utils/tools";
const menuList = [
  {
    key: "edit",
    label: "编辑部门",
  },
  {
    key: "detail",
    label: "查看部门",
  },
  {
    key: "new",
    label: "新增子部门",
  },
  {
    key: "delete",
    label: "删除部门",
  },
];
export default {
  name: "OrgView",
  data() {
    return {
      data: {
        id: 0,
        label: "XXX科技有限公司",
        children: [
          {
            id: 2,
            label: "产品研发部",
            children: [
              {
                id: 5,
                label: "研发-前端",
              },
              {
                id: 6,
                label: "研发-后端",
              },
              {
                id: 9,
                label: "UI设计",
              },
              {
                id: 10,
                label: "产品经理",
              },
            ],
          },
          {
            id: 3,
            label: "销售部",
            children: [
              {
                id: 7,
                label: "销售一部",
              },
              {
                id: 8,
                label: "销售二部",
              },
            ],
          },
          {
            id: 4,
            label: "财务部",
          },
          {
            id: 11,
            label: "HR人事",
          },
        ],
      },
      currentContextMenuId: "",
      orgTreeOffsetLeft: 0,
      orgTreeOffsetTop: 0,
      initPageX: 0,
      initPageY: 0,
      oldMarginLeft: 0,
      oldMarginTop: 0,
      canMove: false,
    };
  },
  computed: {
    orgTreeStyle() {
      return {
        transform: `translate(-50%, -50%) scale(${this.zoomHandled}, ${this.zoomHandled})`,
        marginLeft: `${this.orgTreeOffsetLeft}px`,
        marginTop: `${this.orgTreeOffsetTop}px`,
      };
    },
  },
  methods: {
    handleNodeClick(e, data, expand) {
      expand();
    },
    closeMenu() {
      this.currentContextMenuId = "";
    },
    getBgColor(data) {
      return this.currentContextMenuId === data.id
        ? data.isRoot
          ? "#0d7fe8"
          : "#5d6c7b"
        : "";
    },
    nodeRender(h, data) {
      return (
        <div
          class={[
            "custom-org-node",
            data.children && data.children.length ? "has-children-label" : "",
          ]}
          on-mousedown={(event) => event.stopPropagation()}
          on-contextmenu={this.contextmenu.bind(this, data)}
        >
          {data.label}
          <dropdown
            trigger="custom"
            class="context-menu"
            visible={this.currentContextMenuId === data.id}
            nativeOn-click={this.handleDropdownClick}
            on-on-click={this.handleContextMenuClick.bind(this, data)}
            style={{
              transform: `scale(${1 / this.zoomHandled}, ${
                1 / this.zoomHandled
              })`,
            }}
            v-click-outside={this.closeMenu}
          >
            <dropdown-menu slot="list">
              {menuList.map((item) => {
                return (
                  <dropdown-item name={item.key}>{item.label}</dropdown-item>
                );
              })}
            </dropdown-menu>
          </dropdown>
        </div>
      );
    },
    contextmenu(data, $event) {
      let event = $event || window.event;
      event.preventDefault
        ? event.preventDefault()
        : (event.returnValue = false);
      this.currentContextMenuId = data.id;
    },
    setDepartmentData(data) {
      data.isRoot = true;
      this.departmentData = data;
    },
    mousedownView(event) {
      this.canMove = true;
      this.initPageX = event.pageX;
      this.initPageY = event.pageY;
      this.oldMarginLeft = this.orgTreeOffsetLeft;
      this.oldMarginTop = this.orgTreeOffsetTop;
      on(document, "mousemove", this.mousemoveView);
      on(document, "mouseup", this.mouseupView);
    },
    mousemoveView(event) {
      if (!this.canMove) return;
      const { pageX, pageY } = event;
      this.orgTreeOffsetLeft = this.oldMarginLeft + pageX - this.initPageX;
      this.orgTreeOffsetTop = this.oldMarginTop + pageY - this.initPageY;
    },
    mouseupView() {
      this.canMove = false;
      off(document, "mousemove", this.mousemoveView);
      off(document, "mouseup", this.mouseupView);
    },
    handleDropdownClick(event) {
      event.stopPropagation();
    },
    handleDocumentContextmenu() {
      this.canMove = false;
    },
    handleContextMenuClick(data, key) {
      console.log(data,key,'点击节点')
    },
  },
  mounted() {
    on(document, "contextmenu", this.handleDocumentContextmenu);
  },
  beforeDestroy() {
    off(document, "contextmenu", this.handleDocumentContextmenu);
  },
};
</script>

Css

<style scoped lang='scss'>
.view-box {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1;
  cursor: move;
  .org-tree-drag-wrapper {
    width: 100%;
    height: 100%;
  }
  .org-tree-wrapper {
    display: inline-block;
    position: absolute;
    left: 0;
    top: 20%;
    transition: transform 0.2s ease-out;
    .org-tree-node-label {
      box-shadow: 0px 2px 12px 0px rgba(143, 154, 165, 0.4);
      border-radius: 4px;
      .org-tree-node-label-inner {
        padding: 0;
        .custom-org-node {
          padding: 14px 41px;
          background: #738699;
          user-select: none;
          word-wrap: none;
          white-space: nowrap;
          border-radius: 4px;
          color: #ffffff;
          font-size: 14px;
          font-weight: 500;
          line-height: 20px;
          transition: background 0.1s ease-in;
          cursor: default;
          &:hover {
            background: #5d6c7b;
            transition: background 0.1s ease-in;
          }
          &.has-children-label {
            cursor: pointer;
          }
          .context-menu {
            position: absolute;
            right: -10px;
            bottom: 20px;
            z-index: 10;
          }
        }
      }
    }
  }
}
</style>

tools.js

/**
 * @description 绑定事件 on(element, event, handler)
 */
export const on = (function () {
    if (document.addEventListener) {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.addEventListener(event, handler, false)
        }
      }
    } else {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.attachEvent('on' + event, handler)
        }
      }
    }
  })()
  
  /**
   * @description 解绑事件 off(element, event, handler)
   */
  export const off = (function () {
    if (document.removeEventListener) {
      return function (element, event, handler) {
        if (element && event) {
          element.removeEventListener(event, handler, false)
        }
      }
    } else {
      return function (element, event, handler) {
        if (element && event) {
          element.detachEvent('on' + event, handler)
        }
      }
    }
  })()

2、vue-tree-chart

2-1、安装

cnpm install vue-tree-chart --save

2-2、全局过滤器

//main.js
Vue.directive('drag',el=>{
  let l=0;
  let t=0;
  el.onmousedown=function(e){
      console.log(e);
      //计算出元素距离上边和左边的距离(鼠标点击的位置-元素的位置)
      //这个应该能理解吧
      var disX=e.clientX-el.offsetLeft;
      var disY=e.clientY-el.offsetTop;
      document.onmousemove = function(e){
          //鼠标要按住不松开移动才行,松开就会触发onmouseup的事件
          //计算出元素移动后的位置(鼠标位置-元素初始的disX/disY)
           l=e.clientX-disX;
           t=e.clientY-disY;
          el.style.left=l+'px';
          el.style.top=t+'px';
      }
      document.onmouseup=function(e){
          document.onmousemove=null;
          document.onmouseup=null;
          el.style.left=l+'px';
          el.style.top=t+'px';
      }
  }
})

 

2-3、使用

2-3-1、在components中新建TreeChart.vue组件

<template>
    <div class="tree_box" >
      <table v-if="treeData.name" >
      <tr>
        <td :colspan="Array.isArray(treeData.children) ? treeData.children.length * 2 : 1" 
          :class="{parentLevel: Array.isArray(treeData.children) && treeData.children.length, extend: Array.isArray(treeData.children) && treeData.children.length && treeData.extend}"
        >
          <div :class="{node: true, hasMate: treeData.mate}">
            <div class="person" 
              :class="Array.isArray(treeData.class) ? treeData.class : []"
             
            >
              <div class="avat">
                <img :src="treeData.image_url" @contextmenu="$emit('click-node', treeData)"/>
              </div>
              <!-- <div class="name">{{treeData.name}}</div> -->
            </div>
             <div class="paeson_name">{{treeData.name}}</div>
            <template v-if="Array.isArray(treeData.mate) && treeData.mate.length">
              <div class="person" v-for="(mate, mateIndex) in treeData.mate" :key="treeData.name+mateIndex"
                :class="Array.isArray(mate.class) ? mate.class : []"
                @click="$emit('click-node', mate)"
              >
                <div class="avat">
                  <img :src="mate.image_url" />
                </div>
                <!-- <div class="name">{{mate.name}}</div> -->
              </div>
              <div class="paeson_name">{{treeData.name}}</div>
            </template>
          </div>
          <div class="extend_handle" v-if="Array.isArray(treeData.children) && treeData.children.length" @click="toggleExtend(treeData)">
            <Icon   v-if="open" class="open" type="ios-add-circle-outline" />
            <Icon   v-else class="close" type="ios-remove-circle-outline" />
          </div>
        </td>
      </tr>
      <tr v-if="Array.isArray(treeData.children) && treeData.children.length && treeData.extend">
        <td v-for="(children, index) in treeData.children" :key="index" colspan="2" class="childLevel">
          <TreeChart :node-render="nodeRender" :json="children" @click-node="$emit('click-node', $event)"/>
        </td>
      </tr>
    </table>
    </div>
    
</template>

<script>
import TreeChart from "vue-tree-chart";
// import draggable from 'vuedraggable'
export default {
  name: "TreeChart",
  props: ["json"],
  components: {
    	TreeChart
	},
  data() {
    return {
      open:false,
      treeData: {}
    }
  },
  watch: {
    json: {
      handler: function(Props){
        let extendKey = function(jsonData){
          jsonData.extend = (jsonData.extend===void 0 ? true: !!jsonData.extend);
          if(Array.isArray(jsonData.children)){
            jsonData.children.forEach(c => {
              extendKey(c)
            })
          }
          return jsonData;
        }
        if(Props){
          this.treeData = extendKey(Props);
        }
      },
      immediate: true
    }
  },
  methods: {
    toggleExtend: function(treeData){
      this.open=!this.open;
      treeData.extend = !treeData.extend;
      this.$forceUpdate();
    },
    nodeRender(h, data) {
      return (
        <div
          class={[
            "custom-org-node",
            data.children && data.children.length ? "has-children-label" : "",
          ]}
          on-mousedown={(event) => event.stopPropagation()}
          on-contextmenu={this.contextmenu.bind(this, data)}
        >
          {data.label}
          <dropdown
            trigger="custom"
            class="context-menu"
            visible={this.currentContextMenuId === data.id}
            nativeOn-click={this.handleDropdownClick}
            on-on-click={this.handleContextMenuClick.bind(this, data)}
            style={{
              transform: `scale(${1 / this.zoomHandled}, ${
                1 / this.zoomHandled
              })`,
            }}
            v-click-outside={this.closeMenu}
          >
            <dropdown-menu slot="list">
              {menuList.map((item) => {
                return (
                  <dropdown-item name={item.key}>{item.label}</dropdown-item>
                );
              })}
            </dropdown-menu>
          </dropdown>
        </div>
      );
    },
  }
}
</script>

<style scoped>
.tree_box{
  width: 100%;
  cursor: move;
}
table{border-collapse: separate!important;border-spacing: 0!important;width: 100%;}
td{position: relative; vertical-align: top;padding:0 0 60px 0;text-align: center; }
.extend_handle{position: absolute;left:50%;bottom:30px;transform: translate3d(-10px,0,0);cursor: pointer;}
.extend_handle .open,.extend_handle .close{width: 20px;height: 20px;border-radius: 50%;text-align: center;line-height: 20px;font-size: 20px;}
/* .extend_handle:before{content:""; display: block; width:100%;height: 100%;box-sizing: border-box; border:2px solid;border-color:#ccc #ccc transparent transparent; 
transform: rotateZ(135deg);transform-origin: 50% 50% 0;transition: transform ease 300ms;}
/* .extend_handle:hover:before{border-color:#333 #333 transparent transparent;} */
/* .extend .extend_handle:before{transform: rotateZ(-45deg);} */
.extend::after{content: "";position: absolute;left:50%;bottom:15px;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
.childLevel::before{content: "";position: absolute;left:50%;bottom:100%;height:15px;border-left:2px solid #ccc;transform: translate3d(-1px,0,0)}
.childLevel::after{content: "";position: absolute;left:0;right:0;top:-15px;border-top:2px solid #ccc;}
.childLevel:first-child:before, .childLevel:last-child:before{display: none;}
.childLevel:first-child:after{left:50%;height:15px; border:2px solid;border-color:#ccc transparent transparent #ccc;border-radius: 6px 0 0 0;transform: translate3d(1px,0,0)}
.childLevel:last-child:after{right:50%;height:15px; border:2px solid;border-color:#ccc #ccc transparent transparent;border-radius: 0 6px 0 0;transform: translate3d(-1px,0,0)}
.childLevel:first-child.childLevel:last-child::after{left:auto;border-radius: 0;border-color:transparent #ccc transparent transparent;transform: translate3d(1px,0,0)}
.node{position: relative; display: inline-block;margin: 0 1em;box-sizing: border-box; text-align: center;}
.node:hover{color: #2d8cf0;cursor: pointer;}
.node .person{position: relative; display: inline-block;z-index: 2;width:6em; }
.node .person .avat{display: block;width:4em;height: 4em;margin:auto;overflow:hidden; background:#fff;box-sizing: border-box;}
.node .person .avat img{width:100%;height: 100%;}
.node .person .name{height:2em;line-height: 2em;overflow: hidden;width:100%;}
.node.hasMate::after{content: "";position: absolute;left:2em;right:2em;top:2em;border-top:2px solid #ccc;z-index: 1;}
.node .paeson_name{position: absolute; top: 55px;right: 0;width: 88px;text-align: center;text-overflow: ellipsis; overflow: hidden; white-space: nowrap;}
.landscape{transform:translate(-100%,0) rotate(-90deg);transform-origin: 100% 0;}
.landscape .node{text-align: left;height: 8em;width:8em;right: 18px;}
.landscape .person{position: absolute; height: 4em;top:4em;left: 2.5em;}
.landscape .person .avat{position: absolute;left: 0;border-radius: 2em;border-width:2px;}
.landscape .person .name{height: 4em; line-height: 4em;}
.landscape .hasMate{position: relative;}
.landscape .hasMate .person{position: absolute; }
.landscape .hasMate .person:first-child{left:auto; right:-4em;}
.landscape .hasMate .person:last-child{left: -4em;margin-left:0;}
</style>

2-3-2、组件使用

<template>
  <div class="about" v-drag>
    <TreeChart :json="jsonData" @click-node="clickNode"  />
    <div class="gl_prs_ctn" :style='[contextstyle]'>
          <ul class='gl_prs_li'>
              <li @click="handleClick(1)">添加</li>
              <li @click="handleClick(2)">详情</li>
              <li @click="handleClick(3)">编辑</li>
              <li @click="handleClick(4)">删除</li>
          </ul>
    </div>  
  </div>
</template>

<script>
import TreeChart from "@/components/TreeChart";
export default {
  components: {
    TreeChart
  },
  data() {
    return {
      selectId:0,
      jsonData: {
        id:0,
        name: 'root',
        image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
        class: ["rootNode"],
        children: [
          {
            id:1,
            name: 'children1',
            image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
             children: [
              {
                id:3,
                name: 'grandchild',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              },
              {
                id:4,
                name: 'grandchild2',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              },
              {
                id:5,
                name: 'grandchild3',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              }
            ]
          },
          {
            id:2,
            name: 'children2',
            image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
            children: [
              {
                id:6,
                name: 'grandchild',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              },
              {
                id:7,
                name: 'grandchild2',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              },
              {
                id:8,
                name: 'grandchild3',
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
              }
            ]
          }
        ]
      },
      contextstyle: {
          display: 'none',
          right: '0px',
          top: '0px',
          left: '0px',
          bottom: '0px',
      }, 
    }
  },
  
  created(){
      document.oncontextmenu = ()=>{return false}
      document.addEventListener("click", () => {
            if(this.contextstyle.display == 'block'){
                this.contextstyle.display = 'none'
            }
      })
  },
  methods: {
      handleClick(type){
        console.log(type,this.selectId)
      },
      clickNode(e){
        this.selectId=e.id;
        if(window.event.x + 188 > document.documentElement.clientWidth){
            this.contextstyle.left = 'unset';
            this.contextstyle.right = document.documentElement.clientWidth - window.event.x + 'px';
        }else{
            this.contextstyle.left = window.event.x + 'px';
        }
        if(window.event.y + 166 > document.documentElement.clientHeight){
            this.contextstyle.top = 'unset';
            this.contextstyle.bottom = document.documentElement.clientHeight - window.event.y + 'px';
        }else{
            this.contextstyle.top = window.event.y + 'px';
        }                       
        this.contextstyle.display = 'block';
      },
  }
}
</script>

<style>
.about {
  width: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.gl_prs_ctn{
            width: 188px;
            background: rgb(255, 255, 255);
            box-shadow: rgba(0, 0, 0, 0.075) 0px 1px 1px inset, rgba(102, 175, 233, 0.6) 0px 0px 8px;
            z-index: 99999;
            position: fixed;
            padding: 10px;
            box-sizing: content-box;
            height: 142px;
            border-radius: 10px;
}
.gl_prs_li{padding: unset;margin: unset;}
.gl_prs_li>li{
    cursor: pointer;   
    list-style: none;
    border-bottom: 1px solid #efefef;
    padding: 7px 10px;
}
li:last-child { border: unset }
li:hover{
      background: #ccc;
      color: #fff;
}
</style>

3、需求实现

<template>
  <div class="box" v-drag>
    <TreeChart  :json="treeData" @click-node='clickNode' />
  </div>
</template>

<script>
import TreeChart from "vue-tree-chart";
export default {
  data() {
    return {
      treeData: {
        name: "父亲",
            image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
            mate: {
              name: "母亲",
              image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
            },
            children: [
              {
                name: "儿子",
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
                 mate: {
                    name: "儿媳",
                    image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
                    },
                    children:[
                        {
                            name: "孙子",
                            image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
                        },
                        {
                            name: "孙子",
                            image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
                        },
                    ]
              },
              {
                name: "兄弟",
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
              },
              {
                name: "兄弟",
                image_url: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
              },
        ],
      },
    };
  },
  components: {
    TreeChart,
  },
  methods: {
      clickNode(node) {
        console.log(node,'2222')
      }
  },
};
</script>

<style lang="scss" scoped>
.box{
    position: relative;
    cursor: move;
    /deep/ .extend_handle{
        display: none;
    }
    /deep/ .node .person .avat{
        border: none;
    }
    /deep/ .extend:after{
        height: 35px;
    }
    /deep/ .node .person{
        cursor: pointer;
    }
}
</style>

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值