一、声明变量
//返回值:1,绘图x的最小值和最大值,绘图y的最小值和最大值
//计算改变canvas 画布的宽高,重新改变起始X坐标,起始Y坐标
let flowData = [
{
"name": "女娲",
"projectTreeVOList": [
{
"name": "刘备",
"projectTreeVOList": [
{
"name": "张飞",
"projectTreeVOList": [
]
},
{
"name": "赵云",
"projectTreeVOList": [
]
},
{
"name": "黄忠",
"projectTreeVOList": [
]
},
]
},
{
"name": "百里玄策",
"projectTreeVOList": [
{
"name": "百里守约",
"projectTreeVOList": [
{
"name": "干将莫邪",
"projectTreeVOList": [
]
},
{
"name": "花木兰",
"projectTreeVOList": [
]
},
]
},
]
},
{
"name": "马超",
"projectTreeVOList": [
{
"name": "嬴政",
"projectTreeVOList": [
]
},
{
"name": "典韦",
"projectTreeVOList": [
]
},
{
"name": "关羽",
"projectTreeVOList": [
]
},
]
},
{
"name": "后裔",
"projectTreeVOList": [
{
"name": "嫦娥",
"projectTreeVOList": [
]
},
]
},
]
},
]
//用于保存每层数据元素绘制完成后的每个元素的右边框中心位置
let canvasData = []
let canvas = document.getElementById("tutorial");
var ctx = canvas.getContext('2d');
console.log("ctx",ctx)
let drawStartX = 50; //第一层元素起始中心X坐标
let drawStartY = 700 //第一层元素起始中心Y坐标
let arrowHeadSpacing = 10 //箭头前置间距
let arrowSpacingAfter = 10 //箭头后置间距
let arrowWidth = 15; //绘制箭头的宽度
let horizontalLineSpacing = 60 //分组横线间距
let verticalLineSpacing = 66 //分组纵线间距
// let borderWidth = 60 //元素边框宽度
let borderHeight = 20 //元素边框高度
二、封装功能函数
1,绘制箭头函数
//绘制箭头函数 参数1:箭头三角形的起始x坐标 参数2:箭头三角形起始y轴坐标,参数3:绘制箭头的颜色
function drawArrow(startX,startY,color){
ctx.beginPath();
ctx.moveTo(startX,startY);
ctx.lineTo(startX+5,startY+5);
ctx.lineTo(startX+5,startY-5);
ctx.fillStyle=color
ctx.fill();
//绘制横线
ctx.beginPath();
ctx.strokeStyle=color
ctx.moveTo(startX+5,startY);
ctx.lineTo(startX+15,startY);
ctx.stroke();
}
2,绘制继承元素
//绘制继承元素 参数1:矩形边框左侧边中心x坐标,参数2:矩形边框左侧边中心y坐标 参数3:填充的文字,参数4:矩形边框的颜色
//参数5 i,其父元素位于canvasData的横轴坐标,j:其父元素位于canvasData的纵轴坐标
function drawInheritedElements(startX,startY,name,wordColor,borderColor,arr){
//根据字符内容的多少动态改变元素边框的宽度
//60px宽最多容纳5个纯汉字
let borderWidth = getBorderWidth(60,name); //默认元素边框宽度,最多只能容纳5个字符,当字符数目超过5个字符的时候,每多一个字符增加15px宽度
ctx.fillStyle = wordColor
ctx.lineWidth = 2;
ctx.strokeStyle = borderColor;
ctx.strokeRect(startX,startY-10,borderWidth,20); //绘制一个矩形的边框 strokeRect(x,y,width,height)
ctx.font="14px";
//文字左边的空隙是5px
ctx.fillText(name,startX+5,startY+4)
if(arr){
arr.push({
endX:startX + borderWidth ,
endY:startY
})
}
}
3,绘制纵线
//绘制纵线
function drawVerticalLine(startX,startY,endX,endY,color){
ctx.beginPath();
ctx.strokeStyle=color
ctx.moveTo(startX,startY);
ctx.lineTo(endX,endY);
ctx.stroke();
}
4,绘制横线
//绘制横线
function drawHorizontalLine(startX,startY,endX,endY,color){
ctx.strokeStyle=color;
ctx.beginPath();
ctx.moveTo(startX,startY);
ctx.lineTo(endX,endY);
ctx.stroke();
}
5,获取元素边框宽度
//获取元素边框宽度,参数1是最小宽度,参数2是需要显示的字符串
function getBorderWidth(minWidth,strData){
//字符串共包括特殊字符6px、小写英文字符6、大写英文字符7px、汉字10px,数字6px五种
//默认前前后各留5px空隙
let wordWidth = 10;
for(let i=0; i<strData.length;i++){
console.log(strData.charCodeAt(i))
if(strData.charCodeAt(i)>255){
//当前字符是汉字
console.log("汉字",strData.charAt(i),strData.charCodeAt(i))
wordWidth+=10;
}else if(strData.charCodeAt(i)>=65&&strData.charCodeAt(i)<=90){
//当前是大写英文字符
wordWidth+=7;
}else if(!isNaN(Number(strData.charAt(i)))){
//当前是小写英文字符、数字、特殊字符
wordWidth+=6
}else{
wordWidth+=5
}
}
if(wordWidth<minWidth){
wordWidth = minWidth;
}
return wordWidth;
}
6,获取上半部分左线高
//获取上半部分左线高
function getLeftTopLineHeight(data){
if(!data||data.length===0||data===undefined){
return 15;
}
let lineHeight = 0;
if(data.length === 1){
lineHeight = getLeftTopLineHeight(data[0].projectTreeVOList)
}else{
//当子元素所在层存在多个数据的时候
if(data.length %2===0){
//当前层元素为偶数个
for(let j=0;j<data.length/2;j++){
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}
}else{
//当前层元素为奇数个
for(let j = 0;j<=(data.length-1)/2;j++){
if(j===(data.length-1)/2){
//当该元素存在于中间位置时只需要其上半部分左线即可
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
}else{
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}
}
}
}
return lineHeight;
}
7,绘制下半部分左线高
//获取下半部分左线高
function getLeftBottomLineHeight(data,floor){
if(!data||data.length===0){
return 15;
}
let lineHeight = 0;
if(data.length === 1){
lineHeight += getLeftBottomLineHeight(data[0].projectTreeVOList)
}else{
//当子元素所在层存在多个数据的时候
if(data.length %2===0){
//当前层元素为偶数个
for(let j=data.length/2;j<data.length;j++){
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
// lineHeight+=getAllLeftLineHeight(data[j].projectTreeVOList)
}
}else{
//当前层元素为奇数个
for(let j = (data.length-1)/2;j<data.length;j++){
if(j===(data.length-1)/2){
//当该元素存在于中间位置时只需要其下半部分左线即可
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}else{
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList,floor+1)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList,floor+1)
// lineHeight+=getAllLeftLineHeight(data[j])
}
}
}
}
return lineHeight;
}
8,主题递归绘制函数
//递归绘制流程图 data:当前层的数组,floow:父元素的层数,num,父元素位于其该层第几个,
function recursiveDraw(data,floor,num){
if(data&&data.length>0){
canvasData[floor+1] = []
//绘制左箭头
let arrStartX = canvasData[floor][num].endX+10;
let arrStartY = canvasData[floor][num].endY;
drawArrow(arrStartX,arrStartY,"rgba(37, 137, 255, 1)")
if(data.length===1){
//当前层元素只有一个的时候直接绘制
drawInheritedElements(arrStartX+arrowWidth+10,arrStartY,data[0].name,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[0].projectTreeVOList,floor+1,0)
}else {
let leftVerticalStartX = arrStartX+arrowWidth;
let leftVerticalStartY = arrStartY-getLeftTopLineHeight(data)+getLeftTopLineHeight(data[0].projectTreeVOList);
let leftVerticalEndY = arrStartY+getLeftBottomLineHeight(data)-getLeftBottomLineHeight(data[data.length-1].projectTreeVOList);
//绘制左纵线
drawVerticalLine(leftVerticalStartX,leftVerticalStartY,leftVerticalStartX,leftVerticalEndY,"rgba(37, 137, 255, 1)")
//循环分层绘制元素
//在最小高度大于标准高度时前半线的累计起始Y坐标
let leftVerticalAddHeight=0;
let frontLineStartY=0;
let frontLineEndX = leftVerticalStartX+horizontalLineSpacing;
for(let j = 0; j<data.length;j++){
//当标准左纵线高度小于最小左纵线高度的时候
if(j===0){
leftVerticalAddHeight = leftVerticalStartY;
}else if(j===data.length-1){
leftVerticalAddHeight = leftVerticalEndY
}else{
//前横线的起始Y坐标等于上一个节点子节点所需高度的一半加上该节点子节点所需高度的一半再加10px间隔
// leftVerticalAddHeight+=(data[j-1].length-1)*verticalLineSpacing/2
leftVerticalAddHeight=leftVerticalAddHeight + getLeftBottomLineHeight(data[j-1].projectTreeVOList)+getLeftTopLineHeight(data[j].projectTreeVOList);
}
frontLineStartY = leftVerticalAddHeight;
//绘制前横线
drawHorizontalLine(leftVerticalStartX,frontLineStartY,frontLineEndX,frontLineStartY,"rgba(37, 137, 255, 1)")
//绘制元素
drawInheritedElements(frontLineEndX+10,frontLineStartY,data[j].name,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[j].projectTreeVOList,floor+1,j)
}
}
}
}
9,调用绘制函数
function draw(){
//绘制第一层元素
canvasData[0]=[]
drawInheritedElements(drawStartX,drawStartY,flowData[0].name,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[0])
//递归绘制2-n层继承树
recursiveDraw(flowData[0].projectTreeVOList,0,0)
}
draw()
三、效果图
四,引用至Vue项目
下面只提供在Vue3项目引用的思路
1,封装第一个hooks函数将上面的recursiveDraw()主题绘制函数执行一遍,可以将绘制逻辑删除,用以求得所需的canvasHeight、canvasWidth、startY,也就是完整绘制该图的所需的canvas画布的宽、高、以及起始的Y坐标值
2,将返回的canvasHeight,canvasWidth赋值给canvas标签修正canvas画布的宽高,可将cavas父元素设置固定宽高,添加overflow:auto属性。
3,将起始Y坐标点作为参数传给另一个封装的带有绘制逻辑recursiveDraw()的hooks函数,则画出该继承树流程图。
封装成hook函数进行引用
//更改箭头方向之后的
import { ref , reactive} from 'vue'
//获取上半部分左线高
const getLeftTopLineHeight = (data:any)=>{
if(!data||data.length===0||data===undefined){
return 15;
}
let lineHeight = 0;
if(data.length === 1){
lineHeight = getLeftTopLineHeight(data[0].projectTreeVOList)
}else{
//当子元素所在层存在多个数据的时候
if(data.length %2===0){
//当前层元素为偶数个
for(let j=0;j<data.length/2;j++){
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}
}else{
//当前层元素为奇数个
for(let j = 0;j<=(data.length-1)/2;j++){
if(j===(data.length-1)/2){
//当该元素存在于中间位置时只需要其上半部分左线即可
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
}else{
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}
}
}
}
return lineHeight;
}
//获取下半部分左线高
const getLeftBottomLineHeight = (data:any)=>{
if(!data||data.length===0){
return 15;
}
let lineHeight = 0;
if(data.length === 1){
lineHeight += getLeftBottomLineHeight(data[0].projectTreeVOList)
}else{
//当子元素所在层存在多个数据的时候
if(data.length %2===0){
//当前层元素为偶数个
for(let j=data.length/2;j<data.length;j++){
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
// lineHeight+=getAllLeftLineHeight(data[j].projectTreeVOList)
}
}else{
//当前层元素为奇数个
for(let j = (data.length-1)/2;j<data.length;j++){
if(j===(data.length-1)/2){
//当该元素存在于中间位置时只需要其下半部分左线即可
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
}else{
lineHeight+=getLeftTopLineHeight(data[j].projectTreeVOList)
lineHeight+=getLeftBottomLineHeight(data[j].projectTreeVOList)
// lineHeight+=getAllLeftLineHeight(data[j])
}
}
}
}
return lineHeight;
}
//获取元素边框宽度,参数1是最小宽度,参数2是需要显示的字符串
const getBorderWidth = (minWidth:number,strData:string) => {
//字符串共包括特殊字符6px、小写英文字符6、大写英文字符7px、汉字10px,数字6px五种
//默认前前后各留5px空隙
let wordWidth = 10;
for(let i=0; i<strData.length;i++){
console.log(strData.charCodeAt(i))
if(strData.charCodeAt(i)>255){
//当前字符是汉字
wordWidth+=10;
}else if(strData.charCodeAt(i)>=65&&strData.charCodeAt(i)<=90){
//当前是大写英文字符
wordWidth+=7;
}else if(!isNaN(Number(strData.charAt(i)))){
//当前是小写英文字符、数字、特殊字符
wordWidth+=6
}else{
wordWidth+=5
}
}
if(wordWidth<minWidth){
wordWidth = minWidth;
}
return wordWidth;
}
export const useDrawInherTree = (data: any, canvas:HTMLCanvasElement, drawStartY:number) => {
//用于保存每层数据元素绘制完成后的每个元素的右边框中心位置
let canvasData:Array<any> = []
const ctx = (canvas as HTMLCanvasElement).getContext('2d');
// let drawStartX = 50; //第一层元素起始中心X坐标
// let drawStartY = 150 //第一层元素起始中心Y坐标
let arrowHeadSpacing = 10 //箭头前置间距
let arrowSpacingAfter = 10 //箭头后置间距
let arrowWidth = 15; //绘制箭头的宽度
let horizontalLineSpacing = 60 //分组横线间距
let verticalLineSpacing = 66 //分组纵线间距
// let borderWidth = 60 //元素边框宽度
let borderHeight = 20 //元素边框高度
//绘制箭头函数 参数1:箭头三角形的起始x坐标 参数2:箭头三角形起始y轴坐标,参数3:绘制箭头的颜色
const drawArrow = (startX:number,startY:number,color:string)=>{
if(ctx){
//绘制横线
ctx.beginPath();
ctx.strokeStyle=color;
ctx.moveTo(startX,startY);
ctx.lineTo(startX+10,startY);
ctx.stroke();
//绘制箭头
ctx.beginPath();
ctx.moveTo(startX+10,startY);
ctx.lineTo(startX+10,startY+5);
ctx.lineTo(startX+15,startY);
ctx.lineTo(startX+10,startY-5);
// ctx.lineTo(startX+5,startY+5);
// ctx.lineTo(startX+5,startY-5);
ctx.fillStyle=color;
ctx.fill();
}
}
//绘制继承元素 参数1:矩形边框左侧边中心x坐标,参数2:矩形边框左侧边中心y坐标 参数3:填充的文字,参数4:矩形边框的颜色
//参数5 i,其父元素位于canvasData的横轴坐标,j:其父元素位于canvasData的纵轴坐标
const drawInheritedElements = (startX:number,startY:number,name:string,wordColor:string,borderColor:string,arr:Array<any>) =>{
//根据字符内容的多少动态改变元素边框的宽度
let borderWidth = getBorderWidth(60,name); //默认元素边框宽度,最多只能容纳5个字符,当字符数目超过5个字符的时候,每多一个字符增加15px宽度
if(ctx){
ctx.fillStyle = wordColor;
ctx.lineWidth = 2;
ctx.strokeStyle = borderColor;
ctx.strokeRect(startX,startY-10,borderWidth,20); //绘制一个矩形的边框 strokeRect(x,y,width,height)
ctx.font="14px";
ctx.fillText(name,startX+5,startY+4)
}
if(arr){
arr.push({
endX:startX + borderWidth ,
endY:startY
})
}
}
//绘制纵线
const drawVerticalLine = (startX:number,startY:number,endX:number,endY:number,color:string) =>{
if(ctx){
ctx.beginPath();
ctx.strokeStyle=color;
ctx.moveTo(startX,startY);
ctx.lineTo(endX,endY);
ctx.stroke();
}
}
//绘制横线
const drawHorizontalLine = (startX:number,startY:number,endX:number,endY:number,color:string)=>{
if(ctx){
ctx.strokeStyle=color;
ctx.beginPath();
ctx.moveTo(startX,startY);
ctx.lineTo(endX,endY);
ctx.stroke();
}
}
//递归绘制流程图 data:当前层的数组,floow:父元素的层数,num,父元素位于其该层第几个,
const recursiveDraw = (data:any,floor:number,num:number) => {
if(data&&data.length>0){
canvasData[floor+1] = []
//绘制左箭头
let arrStartX = canvasData[floor][num].endX+10;
let arrStartY = canvasData[floor][num].endY;
// drawArrow(arrStartX,arrStartY,"rgba(37, 137, 255, 1)")
if(data.length===1){
drawArrow(arrStartX,arrStartY,"rgba(37, 137, 255, 1)")
//当前层元素只有一个的时候直接绘制
drawInheritedElements(arrStartX+arrowWidth+10,arrStartY,data[0].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[0].projectTreeVOList,floor+1,0)
}else {
drawHorizontalLine(arrStartX,arrStartY,arrStartX+arrowWidth,arrStartY,"rgba(37, 137, 255, 1)")
let leftVerticalStartX = arrStartX+arrowWidth;
let leftVerticalStartY = arrStartY-getLeftTopLineHeight(data)+getLeftTopLineHeight(data[0].projectTreeVOList);
let leftVerticalEndY = arrStartY+getLeftBottomLineHeight(data)-getLeftBottomLineHeight(data[data.length-1].projectTreeVOList);
//绘制左纵线
drawVerticalLine(leftVerticalStartX,leftVerticalStartY,leftVerticalStartX,leftVerticalEndY,"rgba(37, 137, 255, 1)")
//循环分层绘制元素
//在最小高度大于标准高度时前半线的累计起始Y坐标
let leftVerticalAddHeight=0;
let frontLineStartY=0;
let frontLineEndX = leftVerticalStartX+horizontalLineSpacing-arrowWidth;
for(let j = 0; j<data.length;j++){
//当标准左纵线高度小于最小左纵线高度的时候
if(j===0){
leftVerticalAddHeight = leftVerticalStartY;
}else if(j===data.length-1){
leftVerticalAddHeight = leftVerticalEndY
}else{
//前横线的起始Y坐标等于上一个节点子节点所需高度的一半加上该节点子节点所需高度的一半再加10px间隔
// leftVerticalAddHeight+=(data[j-1].length-1)*verticalLineSpacing/2
leftVerticalAddHeight=leftVerticalAddHeight + getLeftBottomLineHeight(data[j-1].projectTreeVOList)+getLeftTopLineHeight(data[j].projectTreeVOList);
}
frontLineStartY = leftVerticalAddHeight;
//绘制前横线
drawHorizontalLine(leftVerticalStartX,frontLineStartY,frontLineEndX,frontLineStartY,"rgba(37, 137, 255, 1)")
//绘制箭头
drawArrow(frontLineEndX,frontLineStartY,"rgba(37, 137, 255, 1)")
//绘制元素
drawInheritedElements(frontLineEndX+25,frontLineStartY,data[j].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[j].projectTreeVOList,floor+1,j)
}
}
}
}
//绘制第一层元素
canvasData[0]=[]
drawInheritedElements(50,drawStartY,data[0].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[0])
//递归绘制2-n层继承树
recursiveDraw(data[0].projectTreeVOList,0,0)
}
//获取通过计算获取canvas画布的宽高以及计算后的起始Y轴坐标
export const useGetTreeCoordinate = (data: any,normalWidth: number, normalHeight: number) =>{
//声明起始坐标
const drawStartY = ref<number>(normalHeight/2)
//声明边界坐标 当数据过多时调节canvas的宽高
const boundary = reactive<any>({
minX:50,
minY:0,
maxX:0,
maxY:0
})
//声明canvas 画布经过计算之后的宽高
const canvasWidth =ref<number>(normalWidth)
const canvasHeight = ref<number>(normalHeight)
//用于保存每层数据元素绘制完成后的每个元素的右边框中心位置
let canvasData:Array<any> = []
// let drawStartX = 50; //第一层元素起始中心X坐标
// let drawStartY = 150 //第一层元素起始中心Y坐标
let arrowHeadSpacing = 10 //箭头前置间距
let arrowSpacingAfter = 10 //箭头后置间距
let arrowWidth = 15; //绘制箭头的宽度
let horizontalLineSpacing = 60 //分组横线间距
let verticalLineSpacing = 66 //分组纵线间距
// let borderWidth = 60 //元素边框宽度
let borderHeight = 20 //元素边框高度
//绘制继承元素 参数1:矩形边框左侧边中心x坐标,参数2:矩形边框左侧边中心y坐标 参数3:填充的文字,参数4:矩形边框的颜色
//参数5 i,其父元素位于canvasData的横轴坐标,j:其父元素位于canvasData的纵轴坐标
const drawInheritedElements = (startX:number,startY:number,name:string,wordColor:string,borderColor:string,arr:Array<any>) =>{
//根据字符内容的多少动态改变元素边框的宽度
let borderWidth = getBorderWidth(60,name); //默认元素边框宽度,最多只能容纳5个字符,当字符数目超过5个字符的时候,每多一个字符增加15px宽度
if(arr){
arr.push({
endX:startX + borderWidth ,
endY:startY
})
}
//计算边界坐标 计算绘图的最大X坐标,最小Y坐标,最大Y坐标
//以便当数据过多的时候调节canvas的画布大小
//计算最大X坐标
if(startX+borderWidth>boundary.maxX){
boundary.maxX = startX+borderWidth;
}
//计算最小Y坐标
if(startY-10<boundary.minY){
boundary.minY = startY-10;
}
//计算最大Y坐标
if(startY+10 > boundary.maxY){
boundary.maxY = startY+10;
}
}
//递归绘制流程图 data:当前层的数组,floow:父元素的层数,num,父元素位于其该层第几个,
const recursiveDraw = (data:any,floor:number,num:number) => {
if(data&&data.length>0){
canvasData[floor+1] = []
//绘制左箭头
let arrStartX = canvasData[floor][num].endX+10;
let arrStartY = canvasData[floor][num].endY;
if(data.length===1){
//当前层元素只有一个的时候直接绘制
drawInheritedElements(arrStartX+arrowWidth+10,arrStartY,data[0].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[0].projectTreeVOList,floor+1,0)
}else {
let leftVerticalStartX = arrStartX+arrowWidth;
let leftVerticalStartY = arrStartY-getLeftTopLineHeight(data)+getLeftTopLineHeight(data[0].projectTreeVOList);
let leftVerticalEndY = arrStartY+getLeftBottomLineHeight(data)-getLeftBottomLineHeight(data[data.length-1].projectTreeVOList);
//循环分层绘制元素
//在最小高度大于标准高度时前半线的累计起始Y坐标
let leftVerticalAddHeight=0;
let frontLineStartY=0;
let frontLineEndX = leftVerticalStartX+horizontalLineSpacing;
for(let j = 0; j<data.length;j++){
//当标准左纵线高度小于最小左纵线高度的时候
if(j===0){
leftVerticalAddHeight = leftVerticalStartY;
}else if(j===data.length-1){
leftVerticalAddHeight = leftVerticalEndY
}else{
//前横线的起始Y坐标等于上一个节点子节点所需高度的一半加上该节点子节点所需高度的一半再加10px间隔
// leftVerticalAddHeight+=(data[j-1].length-1)*verticalLineSpacing/2
leftVerticalAddHeight=leftVerticalAddHeight + getLeftBottomLineHeight(data[j-1].projectTreeVOList)+getLeftTopLineHeight(data[j].projectTreeVOList);
}
frontLineStartY = leftVerticalAddHeight;
//绘制元素
drawInheritedElements(frontLineEndX+10,frontLineStartY,data[j].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[floor+1])
recursiveDraw(data[j].projectTreeVOList,floor+1,j)
}
}
}
}
//绘制第一层元素
canvasData[0]=[]
//起始x坐标是50
drawInheritedElements(50,drawStartY.value,data[0].projectName,"rgba(66, 76, 87, 1)","rgba(253, 141, 141, 1)",canvasData[0])
//递归绘制2-n层继承树
recursiveDraw(data[0].projectTreeVOList,0,0)
//根据boundry中的数据去调节canvasWidth canvasHeight
if(boundary.maxX-boundary.minX+100 > canvasWidth.value){
//100是为左右两侧预留的宽度
canvasWidth.value = boundary.maxX-boundary.minX+100;
}
if(boundary.maxY - boundary.minY+20 > canvasHeight.value){
canvasHeight.value = boundary.maxY - boundary.minY + 20;
}
//当canvas绘图超过画布上边界时将调节起始Y轴坐标
if(boundary.minY<0){
drawStartY.value = drawStartY.value - boundary.minY+10;
}
return {
canvasWidth,
canvasHeight,
drawStartY
}
}
封装成公用组件进行引用
//index.vue
<template>
<div class="myDiagram">
<InheritDiagram :data="data" :recursive="recursive" :draw-attr="showAttr">
</InheritDiagram>
</div>
</template>
<script setup>
import InheritDiagram from "./inheritDiagram.vue"
//一种递归类型的数据格式
let data = [{
name: "王者荣耀",
children: [
]
},
{
name: "刺客",
children: [
{
name: "夏洛特",
children: [
]
}
]
}
]
}]
//递归数组的属性名称
let recursive = "children"
//递归数组中对象要渲染的属性名称
let showAttr = "name"
</script>
<style lang="">
</style>
//inhertDiagram.vue
<template>
<div id="zyq-showInherit" ref="showInherit" class="zyq-diagram-inherit">
<canvas id="score-canvas" ref="canvasDom" :width="canvasSize.width" :height="canvasSize.height"></canvas>
</div>
</template>
<script setup>
import { ref, defineProps, reactive, toRefs, computed, onMounted, nextTick } from "vue"
const props = defineProps({
//1,数据源
data: {
type: Object,
default: () => ({})
},
//2,递归数组属性
recursive: {
type: String,
default: 'value'
},
//3,需要绘制的属性
drawAttr: {
type: String,
default: "name"
},
//4元素基础宽度
baseWidth: {
type: Number,
default: 60
},
//5,话框的基础高度
baseHeight: {
type: Number,
default: 20
},
//6,字体大小
fontSize: {
type: Number,
default: 14
},
//7,字体
fontFamily: {
type: String,
default: 'Microsoft YaHei'
},
//8,字体颜色
fontColor: {
type: String,
default: 'rgba(66, 76, 87, 1)'
},
//9,元素边框颜色
borderColor: {
type: String,
default: 'rgba(253, 141, 141, 1)'
},
//10,画布外边距
outerEdge: {
type: Number,
default: 20
},
//11,箭头颜色
arrowColor: {
type: String,
default: "rgba(37, 137, 255, 1)"
},
//12,关系线颜色
lineColor: {
type: String,
default: "rgba(37, 137, 255, 1)"
},
//13,箭头三角形的高
arrowHeight: {
type: Number,
default: 5
},
//14,箭头三角形后线长
arrowLineLength: {
type: Number,
default: 10
}
})
const canvasDom = ref()
const showInherit = ref()
const arrowWidth = computed(() => {
return props.arrowHeight + props.arrowLineLength
})
const canvasSize = reactive({
width: 0,
height: 0
})
//执行绘制逻辑计算的画布的边界值
const boundary = reactive({
maxX: 0,
minX: 0,
maxY: 0,
minY: 0
})
//用于保存每层数据元素绘制完成后的每个元素的右边框中心位置
const canvasData = ref([])
const {
data,
recursive,
drawAttr,
baseWidth,
baseHeight,
fontSize,
fontFamily,
fontColor,
borderColor,
outerEdge,
arrowColor,
arrowLineLength,
arrowHeight,
lineColor } = toRefs(props)
//经计算之后获得的起始绘制Y点坐标
const drawStartY = ref(outerEdge.value)
//经过计算之后获取的当前绘制元素的真实宽度
const realWidth = ref(baseWidth.value)
console.log("inherit", data.value, recursive.value, arrowWidth.value)
//箭头至前置元素边框的距离,或者后线末端到后置元素边框的距离
const elementInterval = ref(10)
//元素内置文字首尾间隔
const fontInterval = ref(5)
//分组横线间距
const horizonalLineSpacing = ref(60)
//获取上半部分左线高
const getLeftTopLineHeight = (data) => {
if (!data || data.length === 0 || data === undefined) {
return 15;
}
let lineHeight = 0;
if (data.length === 1) {
lineHeight = getLeftTopLineHeight(data[0][recursive.value])
} else {
//当子元素所在层存在多个数据的时候
if (data.length % 2 === 0) {
//当前层元素为偶数个
for (let j = 0; j < data.length / 2; j++) {
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight += getLeftTopLineHeight(data[j][recursive.value])
lineHeight += getLeftBottomLineHeight(data[j][recursive.value])
}
} else {
//当前层元素为奇数个
for (let j = 0; j <= (data.length - 1) / 2; j++) {
if (j === (data.length - 1) / 2) {
//当该元素存在于中间位置时只需要其上半部分左线即可
lineHeight += getLeftTopLineHeight(data[j][recursive.value])
} else {
lineHeight += getLeftTopLineHeight(data[j][recursive.value])
lineHeight += getLeftBottomLineHeight(data[j][recursive.value])
}
}
}
}
return lineHeight;
}
//获取下半部分左线高
const getLeftBottomLineHeight = (data) => {
if (!data || data.length === 0) {
return 15;
}
let lineHeight = 0;
if (data.length === 1) {
lineHeight += getLeftBottomLineHeight(data[0][recursive.value])
} else {
//当子元素所在层存在多个数据的时候
if (data.length % 2 === 0) {
//当前层元素为偶数个
for (let j = data.length / 2; j < data.length; j++) {
//获取该层子数据上半部分左线高等于上半部分子元素的上半部分左线+上半部分每层子元素下半部分左线高
lineHeight += getLeftTopLineHeight(data[j][recursive.value])
lineHeight += getLeftBottomLineHeight(data[j][recursive.value])
// lineHeight+=getAllLeftLineHeight(data[j].projectTreeVOList)
}
} else {
//当前层元素为奇数个
for (let j = (data.length - 1) / 2; j < data.length; j++) {
if (j === (data.length - 1) / 2) {
//当该元素存在于中间位置时只需要其下半部分左线即可
lineHeight += getLeftBottomLineHeight(data[j][recursive.value])
} else {
lineHeight += getLeftTopLineHeight(data[j][recursive.value])
lineHeight += getLeftBottomLineHeight(data[j][recursive.value])
// lineHeight+=getAllLeftLineHeight(data[j])
}
}
}
}
return lineHeight;
}
//计算字符串在canvas画布中绘制的长度
const getBorderWidth = (strData) => {
let wordWidth = 0;
for (let i = 0; i < strData.length; i++) {
if (strData.charCodeAt(i) > 255) {
//当前字符是汉字
wordWidth += fontSize.value;
} else if (strData.charCodeAt(i) > 47 && strData.charCodeAt(i) < 58) {
//当前字符是数字
wordWidth += fontSize.value - 6;
} else if (strData.charCodeAt(i) >= 65 && strData.charCodeAt(i) <= 90) {
//当前是大写英文字符
wordWidth += fontSize.value - 5;
} else if (strData.charCodeAt(i) > 96 && strData.charCodeAt(i) < 123) {
//当前是小写英文字符
wordWidth += fontSize.value - 6.5;
} else {
//特殊字符
wordWidth += fontSize.value - 6;
}
}
if (wordWidth < baseWidth.value) {
wordWidth = baseWidth.value;
}
return wordWidth;
};
//绘制直线
const drawLine = (startX, startY, endX, endY,ctx) => {
if (ctx) {
ctx.beginPath();
ctx.strokeStyle = lineColor.value;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
};
//绘制箭头
//绘制箭头函数 参数1:箭头三角形的起始x坐标 参数2:箭头三角形起始y轴坐标,参数3:绘制箭头的颜色
const drawArrow = (startX, startY, ctx) => {
if (ctx) {
//绘制横线
ctx.beginPath();
ctx.strokeStyle = arrowColor.value;
ctx.moveTo(startX, startY);
ctx.lineTo(startX + arrowLineLength.value, startY);
ctx.stroke();
//绘制箭头
ctx.beginPath();
ctx.moveTo(startX + arrowLineLength.value, startY);
ctx.lineTo(startX + arrowLineLength.value, startY + arrowHeight.value);
ctx.lineTo(startX + arrowLineLength.value+arrowHeight.value, startY);
ctx.lineTo(startX + arrowLineLength.value, startY - arrowHeight.value);
// ctx.lineTo(startX+5,startY+5);
// ctx.lineTo(startX+5,startY-5);
ctx.fillStyle = arrowColor.value;
ctx.fill();
}
}
//绘制继承元素,当传递ctx时进行真实绘制,不传ctx,只进行绘制元素坐标的计算
const drawInheritedElements = (startX, startY, name, floor, ctx) => {
//根据字符内容的多少动态改变元素边框的宽度
realWidth.value = getBorderWidth(name)+2*fontInterval.value; //默认元素边框宽度,最多只能容纳5个字符,当字符数目超过5个字符的时候,每多一个字符增加15px宽度
if (ctx) {
ctx.fillStyle = fontColor.value;
ctx.lineWidth = 2;
ctx.strokeStyle = borderColor.value;
ctx.strokeRect(startX, startY - baseHeight.value / 2, realWidth.value, baseHeight.value); //绘制一个矩形的边框 strokeRect(x,y,width,height)
ctx.font = "normal " + fontSize.value + "px " + fontFamily.value;
ctx.fillText(name, startX + fontInterval.value, startY + 4)
}
if (canvasData.value[floor]) {
canvasData.value[floor].push({
endX: startX + realWidth.value,
endY: startY
})
}
//计算边界坐标 计算绘图的最大X坐标,最小Y坐标,最大Y坐标
//以便当数据过多的时候调节canvas的画布大小
//计算最大X坐标
if (startX + realWidth.value > boundary.maxX) {
boundary.maxX = startX + realWidth.value;
}
//计算最小Y坐标
if (startY - baseHeight.value / 2 < boundary.minY) {
boundary.minY = startY - baseHeight.value / 2;
}
//计算最大Y坐标
if (startY + baseHeight.value / 2 > boundary.maxY) {
boundary.maxY = startY + baseHeight.value / 2;
}
}
//递归绘制流程图 data:当前层的数组,floow:父元素的层数,num,父元素位于其该层第几个,
const recursiveDraw = (data, floor, num,ctx) => {
if(data&&data.length>0){
canvasData.value[floor+1] = []
//绘制左箭头
let arrStartX = canvasData.value[floor][num].endX+elementInterval.value;
let arrStartY = canvasData.value[floor][num].endY;
// drawArrow(arrStartX,arrStartY,"rgba(37, 137, 255, 1)")
if(data.length===1){
drawArrow(arrStartX,arrStartY,ctx)
//当前层元素只有一个的时候直接绘制
drawInheritedElements(arrStartX+arrowWidth.value+elementInterval.value,arrStartY,data[0][drawAttr.value],floor+1,ctx)
recursiveDraw(data[0][recursive.value],floor+1,0,ctx)
}else {
drawLine(arrStartX,arrStartY,arrStartX+arrowWidth.value,arrStartY,ctx)
let leftVerticalStartX = arrStartX+arrowWidth.value;
let leftVerticalStartY = arrStartY-getLeftTopLineHeight(data)+getLeftTopLineHeight(data[0][recursive.value]);
let leftVerticalEndY = arrStartY+getLeftBottomLineHeight(data)-getLeftBottomLineHeight(data[data.length-1][recursive.value]);
//绘制左纵线
drawLine(leftVerticalStartX,leftVerticalStartY,leftVerticalStartX,leftVerticalEndY,ctx)
//循环分层绘制元素
//在最小高度大于标准高度时前半线的累计起始Y坐标
let leftVerticalAddHeight=0;
let frontLineStartY=0;
let frontLineEndX = leftVerticalStartX+horizonalLineSpacing.value-arrowWidth.value;
for(let j = 0; j<data.length;j++){
//当标准左纵线高度小于最小左纵线高度的时候
if(j===0){
leftVerticalAddHeight = leftVerticalStartY;
}else if(j===data.length-1){
leftVerticalAddHeight = leftVerticalEndY
}else{
//前横线的起始Y坐标等于上一个节点子节点所需高度的一半加上该节点子节点所需高度的一半再加10px间隔
// leftVerticalAddHeight+=(data[j-1].length-1)*verticalLineSpacing/2
leftVerticalAddHeight=leftVerticalAddHeight + getLeftBottomLineHeight(data[j-1][recursive.value])+getLeftTopLineHeight(data[j][recursive.value]);
}
frontLineStartY = leftVerticalAddHeight;
//绘制前横线
drawLine(leftVerticalStartX,frontLineStartY,frontLineEndX,frontLineStartY,ctx)
//绘制箭头
drawArrow(frontLineEndX,frontLineStartY,ctx)
//绘制元素
drawInheritedElements(frontLineEndX+25,frontLineStartY,data[j][drawAttr.value],floor+1,ctx)
recursiveDraw(data[j][recursive.value],floor+1,j,ctx)
}
}
}
}
//执行绘制逻辑获取最佳canvas尺寸以及canvas上的最佳绘制起始点Y坐标
const getTreeCoordinate = () => {
canvasData.value[0] = [];
drawInheritedElements(outerEdge.value, drawStartY.value, data.value[0][drawAttr.value], 0)
//递归绘制2-n层继承树
recursiveDraw(data.value[0][recursive.value], 0, 0)
//根据boundry中的数据去调节canvasWidth canvasHeight
if (boundary.maxX - boundary.minX + 2 * outerEdge.value > canvasSize.width) {
//100是为左右两侧预留的宽度
canvasSize.width = boundary.maxX - boundary.minX + 2 * outerEdge.value;
}
if (boundary.maxY - boundary.minY + 2 * outerEdge.value > canvasSize.height) {
canvasSize.height = boundary.maxY - boundary.minY + 2 * outerEdge.value;
}
// //当canvas绘图超过画布上边界时将调节起始Y轴坐标
if (boundary.minY < 0) {
drawStartY.value = drawStartY.value - boundary.minY + outerEdge.value;
}
}
//绘制继承树
const drawInheritTree = (ctx)=>{
//重置canvasData
canvasData.value = []
//绘制第一层元素
canvasData.value[0]=[]
drawInheritedElements(outerEdge.value,drawStartY.value,data.value[0][drawAttr.value],0,ctx)
//递归绘制2-n层继承树
recursiveDraw(data.value[0][recursive.value],0,0,ctx)
}
onMounted(() => {
const ctx = canvasDom.value.getContext('2d')
//执行绘制逻辑不传ctx进行绘制,用于计算canvas画布所需的尺寸以及起始绘制点y轴坐标
getTreeCoordinate()
nextTick(()=>{
//根据得到的精切canvas画布尺寸以及起始绘制点y轴坐标进行绘制
drawInheritTree(ctx)
})
})
</script>
<style lang="less" scoped>
.zyq-diagram-inherit {
width: 100%;
overflow: auto;
// height:100%;
display: flex;
justify-content: flex-start;
align-items: flex-start;
position: relative;
background-color: rgb(251, 252, 252);
}
</style>