Vue3使用canvas根据递归类型数据绘制继承树

一、声明变量

        //返回值: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=colorctx.fill();//绘制横线ctx.beginPath();ctx.strokeStyle=colorctx.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 = wordColorctx.lineWidth = 2;ctx.strokeStyle = borderColor;ctx.strokeRect(startX,startY-10,borderWidth,20);                 //绘制一个矩形的边框   strokeRect(x,y,width,height)ctx.font="14px";//文字左边的空隙是5pxctx.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=colorctx.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/2leftVerticalAddHeight=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/2leftVerticalAddHeight=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/2leftVerticalAddHeight=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坐标是50drawInheritedElements(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   canvasHeightif(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/2leftVerticalAddHeight=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   canvasHeightif (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)=>{//重置canvasDatacanvasData.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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25910.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何正确使用ChatGPT?英国大学对AI使用要求汇总

不同于ChatGPT刚变火爆起来的时候的警惕与排斥&#xff0c;许多大学似乎已经选择了与这些新兴的AI技术“和平共处”。 他们不仅对这些技术进行了相关研究&#xff0c;还将ChatGPT融入课程&#xff0c;甚至允许学生们在作业中使用它们。 但“和平共处”也并不意味学生可以抛弃…

考研英语阅读技巧总结(唐迟)

正文 做题顺序&#xff1a;看一段做一题 总结更多的是为了找共性&#xff1a; 1.文章疑问句后的回答是文章的中心。 1&#xff09;因为疑问句为了引起读者注意&#xff0c;而需要读者注意的往往是文章想要强调的观点。【2015 T2 第一句】 2&#xff09;疑问句同样可以表示…

四川大学图书情报档案专业考研初试介绍和经验(2023.1.02已更新)

文章目录 川大图情基本情况2023年招生情况近5年录取数据复试2021-2022年复试线学硕复试线图情专硕复试线 2021-2022年复试录取分数2022年学硕部分拟录取人员详细分数(不含调剂) 专业课备考专业课资料辅导资料&#xff08;可选&#xff09;667科目备考参考策略972科目备考方法参…

ChatGPT评未来考研最好就业的十大专业。你的上榜了吗?

各位考研人大家好&#xff0c;作为考研人&#xff0c;考研的目的无非就是想继续深造&#xff0c;然后有好的就业&#xff0c;而对于好的就业专业&#xff0c;ChatGPT评出未来考研最好就业的十大专业&#xff0c;给大家参考&#xff0c;看有你本专业吗&#xff1f; 01 人工智能与…

AI大语言模型创业路上的赢家与输家

这是一篇来自于Sam Hogan的博文译文&#xff1a; 随着像Jasper这样的公司开始放缓脚步&#xff0c;为经历数年低迷的风险投资创业生态带来一波复兴的预期似乎难以实现。目前明确的赢家不多&#xff0c;失败者也就那么几家&#xff0c;还有一小部分前景看好的公司充满不确定性。…

领略未来无需远方,华为全屋智能将在AWE描绘智慧生活新图景

作者 | 曾响铃 文 | 响铃说 4月27日-30日&#xff0c;AWE 2023中国家电及消费电子博览会将在上海新国际博览中心举行&#xff0c;这是AWE展沉淀两年后的再次回归。 作为家电及消费电子领域TOP3的国际盛会&#xff0c;本届AWE以“智科技&#xff0c;创未来”为主题&#xff0…

艾瑞报告:预计2023年家用智能照明市场规模过百亿,Yeelight易来引领行业发展

照明是家居的主要部分&#xff0c;以智能化控制技术光环境设计为核心的智能照明成为智能家居重要的子系统与子应用&#xff0c;智能照明通过精准的设计&#xff0c;将单品链接成系统&#xff0c;通过算法和云平台实现智能化&#xff0c;针对不同的空间适配不同的灯光&#xff0…

【周末闲谈】文心一言,模仿还是超越?

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️周末闲谈】 周末闲谈 ✨第一周 二进制VS三进制 文章目录 周末闲谈前言一、背景环境二、文心一言&#xff1f;(_)?三、文心一言的优势&#xff1f;&#x1f617;&#x1f617;&#x1f617;四、文心…

文心一言和讯飞星火全面对比测试:(五)编程能力

相关文章&#xff1a; 实战 | 用ChatGPT处理word表格数据&#xff1a;直接采用ChatGPt和利用ChatGPT编写python脚本两种方法 「文心一言」 vs ChatGPT&#xff0c;结果没有你想向中的那么不堪 文心一言和讯飞星火全面对比测试&#xff1a;&#xff08;一&#xff09;语言理解…

文心一言 vs GPT-4实测!百度背水一战交卷

GPT-4发布一天之后&#xff0c;压力全部给到百度这边。 就在刚刚&#xff0c;百度交卷。 文心一言&#xff0c;百度全新一代知识增强大语言模型&#xff0c;正式在百度总部“挥手点江山”会议室里发布。 在一片静寂的氛围里&#xff0c;李彦宏小步登场&#xff0c;语气里带着点…

快捷工具箱小程序-做你的小树洞

今天闲来无事&#xff0c;发现了一个有趣的小程序-做你的小树洞&#xff0c;包含ChatGpt小机器人 小程序总体界面是这样的 这个小程序里边有很多有趣的小功能&#xff0c;最让我喜欢的就是藏头诗的创作。仅仅需要输入关键词语&#xff0c;然后就能够进行创作诗句&#xff0c;他…

藏头诗生成器

一个藏头诗生成器的小程序&#xff0c;自定义文字即可生成一首诗词。 该小程序通过机器学习&#xff0c;预训练8万多首诗词&#xff0c;5千多个韵词&#xff0c;能通过关键词生成押韵的藏头诗&#xff0c;也可以生成藏字诗&#xff1b; 在生成结果页面&#xff0c;可选择复制…

ChatGPT + MindShow 三分钟搞定PPT制作

制作一份“通用性”的PPT需要几步&#xff1f; 三步 接下来&#xff0c;我们借助ChatGPT和MindShow&#xff0c;大概三分钟完成操作&#xff0c;就能制作出来完胜大部分人的PPT文件。具体可看文末效果导示。 解锁更多AIGC&#xff08;ChatGPT、AI绘画&#xff09;玩法&#…

狼人杀凉了,贴着AI标签的剧本杀如何构建自己的商业版图

文 | 魏启扬 来源 | 智能相对论&#xff08;ID&#xff1a;aixdlun&#xff09; “天黑请闭眼”。 这是“狼人杀”的开场台词&#xff0c;也可用来形容“狼人杀”的现状——前景黑暗&#xff0c;惨不忍睹。 2017年&#xff0c;“狼人杀”的热度达到顶点&#xff0c;无论是线下…

百变大侦探剧本杀开启新玩法!等你一本正经胡说八“倒”

“1234” “4321” 小时候的你有玩过这样的游戏吗&#xff1f; 当你一本正经的胡说八“倒”时候&#xff0c;童年的趣味就在游戏间。当然&#xff0c;正所谓“一千个读者就有一千个哈姆莱特”&#xff0c;游戏也是一样&#xff01;一千个用户就有一千种玩法&#xff0c;但游…

【洞见研报】剧本杀行业研究报告——告别野蛮生长,剧本杀如何“杀”出一条合规路?

剧本杀起源于西方宴会实况角色扮演推理游戏谋杀之谜&#xff08;Mistery of Murder)&#xff0c;是一种围绕剧情演绎进行的真人角色扮演推理游戏。游戏全程由 DM&#xff08;游戏主持人&#xff09;负责引导&#xff0c;通常有1-10位玩家参与&#xff0c;游戏时长1-5小时不等。…

景区剧本杀小程序解决方案

景区剧本杀小程序可以通过以下解决方案实现&#xff1a; 确定需求&#xff1a;定义剧本杀小程序需要实现的功能和特性&#xff0c;例如角色选择、游戏规则、游戏流程等。 设计UI和UX&#xff1a;设计剧本杀小程序的界面和用户交互流程&#xff0c;使其易于使用和操作。…

基于Spring+SpringMvc实现的足球队管理系统,java技术经理岗位职责

1.账号密码错误 2.账号密码正确,却没有登录权限 3.网络异常 4.正常登录 2.主界面 管理员主界面:教练组主界面 :球员组主界面:

基于Spring+SpringMvc实现的足球队管理系统

项目编号&#xff1a;BS-XX-018 本项目基于SpringSpringmvc实现了一个足球队管理系统&#xff0c;系统功能完整&#xff0c;页面简洁大方&#xff0c;适合于毕业设计使用。下面展示一下系统的设计结构以及系统功能。 系统功能结构图&#xff1a; 管理员&#xff08;球队经理&am…

厂长说关于嵌入式当前的门槛和分工的变化

厂长说关于嵌入式当前的门槛和分工的变化 ///插播一条&#xff1a;我自己在今年年初录制了一套还比较系统的入门单片机教程&#xff0c;想要的同学找我拿就行了免費的&#xff0c;私信我就可以哦~点我头像黑色字体加我地球呺也能领取哦。最近比较闲&#xff0c;带做毕设&#x…