基于vue和svg的树形UI

275 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

vue-svg-tree

基于vue和svg的动态树形UI

vue2
license

截图

截图

应用

  npm install vue-svg-tree

示例

<template>
  <div>
      <vue-svg-tree
        :treeData="treeData"
        svgId='svg'
        ref="svgTree"
      ></vue-svg-tree>
  </div>
</template>

<script>
import VueSvgTree from "vue-svg-tree"
export default {
  components:{
    VueSvgTree
  },
  data(){
    return {
        treeData:[
            {id: 100, name: 'Calamus',  des:'www.calamus.xyz',color:'#E1244E',content:'你可以选择爱我或者不爱我,而我只能选择爱你或者更爱你', value: 123, delay: 120, fatherId: 0,tlevel:1},
            {id: 101, name: 'Calamus1', des:'www.calamus.xyz',color:'#E1244E',content:'你可以选择爱我或者不爱我,而我只能选择爱你或者更爱你',value: 0, fatherId: 100,tlevel:1},
            {id: 102, name: 'Calamus2', des:'www.calamus.xyz',color:'#aaa',content:'你可以选择爱我或者不爱我,而我只能选择爱你或者更爱你',value: 100, fatherId: 100,tlevel:0},
            {id: 103, name: 'Calamus3', des:'www.calamus.xyz',color:'#aaa',content:'你可以选择爱我或者不爱我,而我只能选择爱你或者更爱你',value: 123, fatherId: 100,tlevel:0},
            {id: 104, name: 'Calamus4', des:'www.calamus.xyz',color:'#E1244E',content:'你可以选择爱我或者不爱我,而我只能选择爱你或者更爱你',value: 200, fatherId: 100,tlevel:0},
          ]
    }
  }
}
</script>

参数

参数描述类型默认/是否必须
treeData树形结构数据Array必须
direction树形方向String‘row’/‘col’(纵/横)
svgIdsvgIdString‘svgId’(一个页面多个图时svgId不能相同)
curveness连接线是直线还是弧线Booleanfalse(false:弧线;true:直线)

ToDo

  • [x]横向显示还有点小问题没有修复
  • [x]弧度不可调整
  • [x]框框样式暂时不可自定义,暂时建议复制源码修改,后期会修改为可配置,欢迎pr

部分源码

<template>
  <div id="app">
        <div  class="draw-area" id="treeContent" ref="treeContent">
              <div   v-for="(arr, index) in levels" :key="index">
                  <div v-for="(v,index) in arr" v-if="!v.parent || v.parent.open" class="vnode" v-bind:class="{pnode: v.children && v.children.length > 0}" :key="index" :style="'left:' + (v.left) + 'px; top:' + (v.top) + 'px'" @click="toggle(v)">
                      <div class="text">
                        <div class="node_title">
                          <span :class="v.tlevel == '0' ? 'pink':'blue'" class="OKR">
                            {{v.tlevel == '0' ? 'C' : 'L'}}
                          </span>
                          <span class="label">
                            {{v.name}}
                          </span>
                        </div>
                        <div class="node_des">
                          <div>{{v.content}}</div>
                        </div>
                        <div class="node_progress">
                          {{v.des}}
                        </div>
                        <div class="showTips">
                            <a target="_blank" href="https://www.cnblogs.com/calamus"   class="tips_icon icon_edit ">
                              B
                            </a>
                            <a target="_blank" href="https://www.calamus.xyz"  class="tips_icon icon_edit ">
                              C
                            </a>
                            <a target="_blank" href="https://github.com/calamus0427"  class="tips_icon icon_edit ">
                              G
                            </a>
                        </div>
                      </div>

                  </div>
            </div>
            <svg :id="svgId" v-if="curveness">
                <!-- 直线 -->
                <line  class="link" v-for="(link, index) in list" v-if="link.deep > 0 && link.parent.open" :x1="link.left + 90" :y1="link.top" :x2="link.parent.left + 105" :y2="link.parent.top + 150" :stroke="link.color ? link.color : '#aaa'" :stroke-width="link.strokeWidth ? link.strokeWidth : '1px'"></line>
            </svg>

            <svg :id="svgId" v-if="!curveness">
                <path  class="link" v-for="(link, index) in list" v-if="link.deep > 0 && link.parent.open"  :d="link.path" :stroke="link.color ? link.color : '#aaa'"  :stroke-width="link.strokeWidth ? link.strokeWidth : '1px'"></path>
            </svg>
        </div>
    </div>
</template>



<script>

var width = 800;
var height = 600;
var blockHeight = 50;
var blockWidth = 300;

export default {
    name:"VueSvgTree",
    data(){
      return {
          rules: {
              min: 200,
              max: 350
          },
          delayRules: {
              min: 10,
              max: 300
          },
          root: null, // 顶层根节点s
          list: null, // 列表
          levels: null, // 层次存储
      }
    },
    props:{
      treeData:{
        type:Array
      },
      direction:{
        type:String,
        default:'row'  //col:横向 row:纵向
      },
      svgId:{
        type:String,
        default:'svg'
      },
      curveness:{
        type:Boolean,
        default:false
      }
    },
    mounted(){
      if(this.treeData && this.treeData.length > 0){
          this.initData(JSON.parse(JSON.stringify(this.treeData)))
      }
    },
    watch:{
      treeData(val){
        if(val && val.length > 0){
          this.initData(JSON.parse(JSON.stringify(val)))
        }
      }
    },
    methods: {
        compare: function (v1, v2) {
            if (v1.deep !== v2.deep) {
                return v1.deep - v2.deep;
            }

            if (v1.parent === v2.parent) {
                return v1.id - v2.id;
            }

            return this.compare(v1.parent, v2.parent);
        },
        // 初始化数据: 计算deep等
        initData (data) {
          console.log("data",data)
            var keys = {};
            var root = null;
            var levels = [];

            if (!data && !(data.length > 0)) {
                return;
            }

            data.forEach( (v) =>{
                keys[v.id] = v;
                v.deep = 0;
                v.top = 0;
                v.height = 0;
                v.width = 0 ;
                v.path = '';
                v.left = 0;
                v.prev = null; // 前一个节点
            });
            data.forEach( (v)=> {
                if (v.fatherId || v.fatherId > 0) {
                    var p = keys[v.fatherId];
                    p.children = p.children || [];
                    p.children.push(v);
                    v.parent = p;
                    v.deep = p.deep + 1;
                    // v.left = v.deep * 150 + 10;
                    v.left = this.direction == 'col' ? v.deep * 300 + 10 : 0;
                    v.top = this.direction == 'row' ? v.deep * 250 + 5 : 0;
                    v.open = v.deep < 1;
                    v.show = v.deep < 2;
                }
                else {
                    root = v ;
                    v.open = true;
                    v.show = true
                }
            });
            data.sort(this.compare);
            data.forEach( (v) =>{
                levels[v.deep] = levels[v.deep] || [];
                levels[v.deep].push(v);
                v.prev = levels[v.deep][levels[v.deep].length - 2];
            });

            this.root = root;
            this.list = data;
            console.log("daya",data)
            this.levels = levels;
            if(this.direction == 'col'){
              this.calcHeight(root);
              this.calcTop();
              this.calSvg();
            }else{
              //default
              this.calWidth(root);
              this.calcLeft();
              this.calSvgVer();
            }
        },
        // 计算所有节点占用的高度和宽度是否展示
        calcHeight (vnode) {
            var me = this;
            var height = 0;
            if (vnode.parent && !vnode.parent.open) {
                // 存在父节点并且父节点不展开
                vnode.height = 0;
                vnode.open = false;
            }
            else if (!vnode.open) {
                vnode.height = blockHeight;
            }

            if (vnode.children && vnode.children.length > 0) {
                vnode.children.forEach( (v) => {
                    me.calcHeight(v);
                    height += v.height;
                });
            }

            if (vnode.open) {
                vnode.height = height || blockHeight;
            }

        },
        calWidth(vnode){
            var me = this;
            var width = 0;
            if (vnode.parent && !vnode.parent.open) {
                // 存在父节点并且父节点不展开
                vnode.height = 0;
                vnode.width = 0 ;
                vnode.open = false;
            }
            else if (!vnode.open) {
                vnode.width = blockWidth;
            }
            if (vnode.children && vnode.children.length > 0) {
                vnode.children.forEach( (v)=> {
                    me.calWidth(v);
                    width += v.width
                });
            }
            if (vnode.open) {
                vnode.width = width  || blockWidth;
            }
        },
        //计算svg的大小
        calSvg(){
          this.$nextTick( () =>{
            let maxHeight = this.levels.flat(Infinity).filter((item)=>{
              return item.show
            }).sort((a,b) =>{
                return b.top - a.top
              })[0].top ;
            let svg = document.getElementById(this.svgId)
            console.log("svg",svg)
            svg.setAttribute('height', 500)
            svg.setAttribute('width', 700 )
            this.$emit('toggle', this.$refs.treeContent.scrollWidth ,this.root.height)
          })

        },
        calSvgVer(){
          this.$nextTick( () =>{
            let maxHeight = this.levels.flat(Infinity).filter((item)=>{
              return item.show
            }).sort((a,b) =>{
                return b.top - a.top
            })[0].top ;
            // let svg = document.getElementById('svg')
            let svg = document.getElementById(this.svgId)
            svg.setAttribute('height', this.$refs.treeContent.scrollHeight)
            svg.setAttribute('width', this.root.width )
            this.$emit('toggle',{'width':this.root.width,'height':this.$refs.treeContent.scrollHeight})
          })
        },
        // 计算节点top的位置
        calcTop (vnode, prevHeight) {
            if (!vnode) {
                vnode = this.root;
            }
            prevHeight = prevHeight || 0;
            vnode.top = prevHeight + vnode.height / 2;
            if (vnode.children && vnode.children.length > 0) {
                for (var i = 0; i < vnode.children.length; i++) {
                    var height = vnode.children[i].height;
                    this.calcTop(vnode.children[i], prevHeight);
                    prevHeight += height;
                }
            }
            if (vnode.parent) {
                var pLeft = vnode.parent.left + blockWidth - 40;
                var pTop = vnode.parent.top;
                var mLeft = (vnode.left + pLeft) / 2;
                var mTop = (vnode.top + pTop) / 2;
                vnode.path = 'M' + vnode.left + ',' + vnode.top
                    + ' C ' + mLeft + ' ' + vnode.top + ',' + mLeft + ' ' + pTop
                    + ',' + pLeft + ' ' + pTop + 'L ' + (vnode.parent.left + 10) + ',' + pTop;
            }
        },
        // 节点左边位置
        calcLeft(vnode, prevWidth){
            if (!vnode) {
                vnode = this.root;
            }
            prevWidth = prevWidth || 0;
            vnode.left = prevWidth + vnode.width / 2;
            if (vnode.children && vnode.children.length > 0) {
                for (var i = 0; i < vnode.children.length; i++) {
                    var width = vnode.children[i].width;
                    this.calcLeft(vnode.children[i], prevWidth);
                    prevWidth += width;
                }
            }
            if (vnode.parent) {
                var pLeft = vnode.parent.left + 115;
                var pTop =  vnode.parent.top + 150 ;
                var vLeft = vnode.left + 115 ;
                var vTop = vnode.top  ;
                var mLeft = (pLeft + vLeft) /2
                var mTop = (pTop + vTop) /2
                var x1 = vLeft > pLeft ? vLeft +  5 : vLeft -  5
                if(vLeft == pLeft){
                  vnode.path = 'M' + vLeft + ',' + vTop + ' ' + ' L ' + pLeft  + ',' + pTop
                }else{
                  vnode.path = 'M' + vLeft + ',' + vTop +
                      ' Q ' + x1 + ',' + (vTop - 30) + ' ' +
                      mLeft+ ',' + (vTop - 30) +
                      ' T '  + pLeft  + ',' + pTop
                }
            }
        },
        // 收缩和展开
        toggle (vnode) {
            vnode.open = !vnode.open;
            console.log("vnode",vnode)
            if(vnode.children){
              vnode.children.map( (child) =>{
                child.show = !child.show
              })
            }
            if(this.direction == "col"){
                this.calcHeight(this.root);
                this.calcTop()
                this.calSvg() ;
            }else{
              this.calWidth(this.root);
              this.calcLeft();
              this.calSvgVer() ;
            }
            console.log('toggle:', vnode, vnode.open);
        },
    }
}
</script>


链接

github
官网