注:参考其他博主的案例http://t.csdnimg.cn/vZeEp+自己整合
效果图
功能
根据数据自动渲染生成树状图
点击+可以展开分支,点击 - 折叠分支,没有分支则不展示
可放大缩小并拖拽svg
实现
父组件:app.vue
<div class="imgItem">
<div style="font-size:14px;color: black;width: 50%;height:calc(100vh - 200px);user-select: none;">
<svg :width="svgWidth" :height="svgHeight" v-if="loaded" @wheel="handleWheel" @mousedown="dragStart" @mouseup="dragEnd" @mousemove="drag">
<g :style="{ transform: `scale(${zoomFactor})` }">
<g :transform="`translate(${x}, ${y})`">
<line :x1="org.x + 100" :y1="org.y + 60" :x2="org.x + 100" :y2="org.y + 80" stroke="rgb(48, 170, 199)" stroke-width="6" fill="none" />
<foreignObject width="200" height="100" :x="org.x" :y="org.y">
<div style="background-color:rgb(48, 170, 199);color:#fff;text-align:center;height:60px;width:200px;border-radius: 15px;cursor:pointer;">
<p>{{ org.name }}</p>
<p>{{ org.value }}{{ org.unit }}</p>
</div>
</foreignObject>
<BLine v-for='(item, index) in org.children' :key='"line0_" + index' :x='org.x + 100' :y='org.y + 60' :x1='org.x + 100' :y1='org.y + 100' :x2='item.x + 100' :y2='org.y + 100' :x3='item.x + 100' :y3='item.y' v-if='org.expand'></BLine>
<Children v-for='(item, index) in org.children' :key='index' :org='item' v-if='org.expand' @toggle='toggle'></Children>
</g>
</g>
</svg>
</div>
</div>
import Children from './components/Children.vue'
import BLine from './components/BLine'
export default {
components: { Children, BLine },
data() {
return {
dragging: false,
startX: 0,
startY: 0,
x: 0,
y: 0,
zoomFactor: 1, // 放大或缩小的比例
loaded: false,
org: {
name: '总',
value: 0,
unit: 'kg',
x: 0,
y: 0,
children: [
{
name: '一级1', id: 11,
x: 200, y: 50,
value: 0,
unit: 'kg',
children: [
{
name: '二级2', value: 0,
unit: 'kg',
},
{
name: '二级2', value: 0,
unit: 'kg',
},
]
},
{
name: '一级2', id: 12,
x: 250, y: 50,
value: 0,
unit: 'kg',
children: [
{
name: '二级3',
children: [
{ name: '三级1' },
{ name: '三级2' },
{ name: '三级3' }
]
},
{ name: '二级4' },
]
},
{
name: '一级3', id: 13,
x: 300, y: 50,
value: 0,
unit: 'kg',
},
{
name: '一级4', id: 13,
x: 300, y: 50,
value: 0,
unit: 'kg',
}
]
},
svgWidth: 1600, // SVG画布的宽度
svgHeight: 700, // SVG画布的高度
horItemNum: 0, // 横向最大节点数
verItemNum: 0, // 显示的最大层数
xTemp: {}, // 临时记录每层的最大x坐标,上层定位要用
firstHeight: 150, // 第一层的高度(比较特殊)
itemWidth: 200, // 元素的宽度
itemHeight: 200, // 元素的高度
gap: 50, //兄弟之间的间隔
defaultExpandLevel: 0, // 从0开始
broGap: 10, //表兄弟之间的间隔
}
},
mounted() {
this.setExpand(this.org, 0)
this.genLocation()
this.loaded = true
},
methods: {
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
dragStart(event) {
this.dragging = true;
this.startX = event.clientX - this.x;
this.startY = event.clientY - this.y;
},
dragEnd() {
this.dragging = false;
},
drag(event) {
if (this.dragging) {
this.x = event.clientX - this.startX;
this.y = event.clientY - this.startY;
}
},
handleWheel(event) {
// 阻止默认的滚轮行为
event.preventDefault();
// 检查滚轮的方向
const delta = event.wheelDelta ? event.wheelDelta : -event.deltaY;
if (delta > 0) {
// 向上滚动,放大
this.zoomFactor = this.zoomFactor + 0.1
} else {
// 向下滚动,缩小
this.zoomFactor = this.zoomFactor - 0.1
}
},
setExpand(item, level) {
if (level <= this.defaultExpandLevel || item.expand) {
item.expand = true
} else {
item.expand = false
}
if (item.children) {
item.showDetail = true
} else {
item.showDetail = false
}
if (item.children) {
item.children.forEach(element => {
this.setExpand(element, level + 1)
});
}
},
genLocation() {
this.getChildrenLocation(this.org, 0)
this.org.minx = 0
this.org.x = (this.org.minx + parseInt(this.org.maxx) + this.itemWidth + this.gap) / 2 - 100
console.log(this.org.minx, parseInt(this.org.maxx), this.itemWidth, this.gap);
this.org.y = 0
this.width = this.xTemp[this.verItemNum] + this.itemWidth + this.gap + 10
this.height = this.itemHeight * (this.verItemNum) + 100
console.log(this.org)
},
getChildrenLocation(item, level, index) {
item.maxx = 0
if (item.children && item.children.length > 0 && item.expand) {
for (let i = 0; i < item.children.length; i++) {
item.children[i].minx = this.xTemp[level + 1] ? (this.xTemp[level + 1] + this.itemWidth + this.gap + this.broGap) : 0
this.getChildrenLocation(item.children[i], level + 1, i)
item.maxx = item.children[i].maxx
this.xTemp[level] = item.children[i].maxx
}
this.xTemp[level + 1] += this.broGap
item.x = (item.minx + item.maxx) / 2
if (level == 0) {
item.y = 0
} else {
item.y = (level - 1) * this.itemHeight + this.firstHeight
}
} else {
this.verItemNum = level > this.verItemNum ? level : this.verItemNum
this.horItemNum++
if (!this.xTemp[level]) {
this.xTemp[level] = 0.001
item.minx = 0
} else {
this.xTemp[level] += this.itemWidth + this.gap
item.minx = this.xTemp[level]
}
if (!item.maxx || this.xTemp[level] > item.maxx) {
item.maxx = this.xTemp[level]
}
if (!this.xTemp[level + 1]) {
this.xTemp[level + 1] = 0.001
} else {
this.xTemp[level + 1] += this.itemWidth + this.gap
}
}
item.x = (item.minx + item.maxx) / 2
item.y = (level - 1) * this.itemHeight + this.firstHeight
},
toggle(item) {
this.loaded = false
item.expand = !item.expand
item.showDetail = !item.showDetail
this.$nextTick(() => {
this.horItemNum = 0
this.verItemNum = 0
this.xTemp = {}
this.org.x = 0
this.genLocation()
this.loaded = true
})
},
},
}
</script>
- user-select: none; 禁止选中其中的文字
- @wheel="handleWheel" 滚轮放大缩小svg的方法
- :style="{ transform: `scale(${zoomFactor})`}" 滚轮放大缩小svg的样式
- @mousedown="dragStart" @mouseup="dragEnd" @mousemove="drag" 鼠标拖拽svg的方法
- :transform="`translate(${x}, ${y})`" 鼠标拖拽svg的样式
子组件:BLine.vue
<template>
<svg width="5000" height="5000">
<line :x1="x" :y1="y + 20" :x2="x1" :y2="y1" stroke="rgb(48, 170, 199)" stroke-width="6" fill="none" />
<line :x1="x1" :y1="y1" :x2="x2" :y2="y2" stroke="rgb(48, 170, 199)" stroke-width="6" fill="none" />
<line :x1="x2" :y1="y2" :x2="x3" :y2="y3" stroke="rgb(48, 170, 199)" stroke-width="6" fill="none" />
</svg>
</template>
<script>
export default {
name: 'BLine',
props: {
x: { type: Number, default: 0 },
y: { type: Number, default: 0 },
x1: { type: Number, default: 0 },
y1: { type: Number, default: 0 },
x2: { type: Number, default: 0 },
y2: { type: Number, default: 0 },
x3: { type: Number, default: 0 },
y3: { type: Number, default: 0 },
},
data() {
return {}
},
}
</script>
子组件:Children.vue
<template>
<svg width="5000" height="5000" style="font-size:14px;">
<foreignObject :width="realWidth" :height="height" :x="org.x" :y="org.y">
<div style="display:flex;flex-direction: column;">
<div style="text-align:center;height:60px;width:200px;border:solid 3px rgb(48, 170, 199);color:#000;border-radius: 15px;">
<p>{{ org.name }}</p>
<p style="color: rgb(48, 170, 199);">{{ org.value }}{{ org.unit }}</p>
</div>
<div v-if='org.children'>
<div @click='toggle(org)' style="cursor: pointer;margin-left: 90px;line-height: 18px;text-align: center;width:20px;height:20px;background-color: rgb(23, 53, 53);border-radius: 50%;border: rgb(48, 170, 199) 1px solid;">
<span v-show="org.showDetail" style="color:rgb(48, 170, 199);font-weight:700;">{{ '➕' }} </span>
<span v-show="!org.showDetail" style="color:rgb(48, 170, 199);font-weight:700;">{{'➖'}} </span>
</div>
</div>
</div>
</foreignObject>
<BLine v-for='(item, index) in org.children' :key='"line_" + index' :x='org.x + 100' :y='org.y + 60' :x1='org.x + 100' :y1='org.y + 100' :x2='x2(item)' :y2='org.y + 100' :x3='x2(item)' :y3='item.y' v-if='org.expand'></BLine>
<Children v-for='(item, index) in org.children' :key='index' :org='item' v-if='org.expand' @toggle='toggle'>
</Children>
</svg>
</template>
<script>
import BLine from './BLine'
export default {
name: 'Children',
components: { BLine },
props: {
org: { type: Object, required: true },
},
data() {
return {
width: 200,
height: 100,
realWidth: 200
}
},
computed: {
x2: function () {
return (item) => {
return item.x + this.width / 2
}
}
},
created() {
this.realWidth = this.width
},
methods: {
toggle(item) {
this.$emit('toggle', item)
}
}
}
</script>