本文已参与「新人创作礼」活动,一起开启掘金创作之路。
vue-svg-tree
基于vue和svg的动态树形UI
截图
应用
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’(纵/横) |
| svgId | svgId | String | ‘svgId’(一个页面多个图时svgId不能相同) |
| curveness | 连接线是直线还是弧线 | Boolean | false(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>