需求:
1、 根据后端返回的数据,动态绘制出信令图
2、根据 dataStatus 返回值: 0 和 1, 判断 文字内容的颜色,0:#000,1:red
3.、根据 lineType 返回值: 0 和 1, 判断 箭头线的显示 是实线、虚线
4、根据返回的文字内容的换行符:“\r\n” 自动换行 (这步比较难,得计算高度)
最后的效果图大概是这样的:
一、标题的动态获取
1-1、如果后端给你返回的标题是随机顺序的,这里需要根据全部标题数组做一下排序。
// 全部标题数组:
titletypeArr: ['ATP', 'MT', 'BTS', 'BSC', 'MSC', 'RBC'],// 后端返回的随机标题数组:
resultTitle: ['MT', 'ATP' ]// 处理方法
this.typeArr = this.titletypeArr.filter(item => this.resultTitle.indexOf(item) !== -1)
二、canvas绘制初始化
// 初始化加载initData() {var mycanvas = document.getElementById("myCanvas");this.canvas = mycanvas;var context = mycanvas.getContext("2d");// 动态设置宽高一定要在 myCanvas 节点添加之后document.getElementById("myCanvas").width = this.typeArr.length * 320 - 120;document.getElementById("myCanvas").style.background = "#fff";const minHeight = window.innerHeight - 180;if (this.xlArr.length > 0) {document.getElementById("myCanvas").height =30 * this.xlArr.length + 80 < minHeight? minHeight: 30 * this.xlArr.length + 80;} else {document.getElementById("myCanvas").height = minHeight;}var height = this.paddingTop + 62; // 初始值this.xlArr.map((v, i) => {const k = this.typeArr.indexOf(v.startDataDir);const j = this.typeArr.indexOf(v.endDataDir);context.font = '13px "微软雅黑"'; // 设置字体// 时间文字context.fillStyle = '#000' // 时间颜色context.fillText(v.recTime.split(' ')[1], 40, height);// 箭头this.paintArr(v,[this.gapX * k + this.paddingLeft, height],[this.gapX * j + this.paddingLeft, height],k < j ? "right" : "left",context);var maxWidth = 260; // 最大宽度,超过这个宽度会自动换行var words = v.showInfo.split("\r\n");// 文字自动换行this.wrapText(v,context,words,this.gapX * (k < j ? k : j) + this.paddingLeft,height - 10,maxWidth,this.lineHeight);if (i < this.xlArr.length - 1) {let nextWords = this.xlArr[i + 1].showInfo.split("\r\n");height += (this.lineHeight * (words.length + nextWords.length)) / 2 + 30;} else {height += this.lineHeight * words.length + 30;}// console.log(height, "height")})// 画虚线以及标题this.typeArr.map((v, i) => {this.paintText(context, v, i);setTimeout(() => {this.drawDashed(context, i);}, 300)})// document.getElementById('container').onscroll = (e) => {// // console.log('e:', e.target)// this.left = e.target.scrollLeft// }// 屏蔽所有页面 右键菜单// document.oncontextmenu = (event) => {// event.returnValue = false// }// 屏蔽当前页面 右键菜单// document.getElementById('container').oncontextmenu = (event) => {// event.returnValue = false// }}
三、绘制箭头
// 箭头paintArr(item, s, t, direction, ctx) {ctx.beginPath()ctx.lineWidth = 1if (item.dataStatus == 1) {ctx.strokeStyle = 'red'} else {ctx.strokeStyle = '#000' // 箭头线的颜色}if (item.lineType === 1) {ctx.setLineDash([5, 2]); // 虚线}ctx.moveTo(s[0], s[1])ctx.lineTo(t[0], t[1])ctx.stroke()ctx.closePath()ctx.beginPath()if (direction === 'right') {ctx.moveTo(t[0] - 10, t[1] + 3)ctx.lineTo(t[0], t[1])ctx.lineTo(t[0] - 10, t[1] - 3)} else {ctx.moveTo(t[0] + 10, t[1] - 3)ctx.lineTo(t[0], t[1])ctx.lineTo(t[0] + 10, t[1] + 3)}// ctx.closePath()ctx.stroke()// ctx.fill()},
四、绘制 标题列的虚线
// 标题列的虚线drawDashed(ctx, i) {ctx.beginPath()ctx.lineWidth = 1ctx.strokeStyle = '#696969' // '#FF8080'//虚线的颜色ctx.setLineDash([5, 2])ctx.moveTo(320 * i + this.paddingLeft, this.paddingTop + 40);ctx.lineTo(320 * i + this.paddingLeft, 400 * this.typeArr.length);ctx.fill()ctx.stroke()ctx.closePath()},
五、文字自动换行 遇到换行符换行
// 文字自动换行 遇到换行符换行,并且超出最大宽度换行,只计算了最多显示7行的情况,超出7行得再计算wrapText(item, context, words, x, y, maxWidth, lineHeight) {// console.log(words, "words")let originY = y;let len = words.length;let rectWidth = 0;for (var n = 0; n < len; n++) {// 不超出一行var testWidth = context.measureText(words[n]).width;if (testWidth < maxWidth) {if (rectWidth < testWidth) {rectWidth = testWidth;}}}// 在上面循环计算出文字实际最宽的位置,画出背景色遮挡箭头// 画背景色遮盖箭头, 背景色自己调,跟画布统一就行context.fillStyle = "#fff"; // 背景颜色context.fillRect(x + this.gapX / 2 - rectWidth / 2 - 4,originY,rectWidth + 6,lineHeight); // 填充黄色背景for (var n = 0; n < len; n++) {// 不超出一行var testWidth = context.measureText(words[n]).width;if (testWidth < maxWidth) {// console.log(words[n], 1);let currentY = y;if (len === 1) {currentY = y + 14;} else if (len === 2) {currentY = y + 2;} else if (len === 3) {currentY = y - 6;} else if (len === 4) {currentY = y - 18;} else if (len === 5) {currentY = y - 28;} else if (len === 6) {currentY = y - 38;} else if (len === 7) {currentY = y - 48;}if (item.dataStatus == 1) {context.fillStyle = 'red'} else {context.fillStyle = '#000' // 字体颜色}// context.fillStyle = "#000"; // 字体颜色context.fillText(words[n], x + this.gapX / 2 - testWidth / 2, currentY);if (len > 1) {y += lineHeight;}} else {console.log(words[n], 2);// 文字超出一行,需要换行展示// 实际大于页面width font-size: 12, 计算出显示多少行let singleWordwith = 13;// 计算一行显示的最大字数,以及显示多少行let len = Math.floor(maxWidth / singleWordwith);let lineCount = Math.ceil(words[n].length / len);for (let j = 0; j <= lineCount; j++) {// 截取出每行显示的字let word = words[n].substr(j * len, len);let wordWidth = context.measureText(word).width;// 写入画布// 画背景色遮盖箭头, 背景色自己调,跟画布统一就行context.fillStyle = "#fff";context.fillRect(x + this.gapX / 2 - wordWidth / 2,y - 4,wordWidth,lineHeight); // 填充黄色背景let currentY = y;if (lineCount === 2) {currentY = y + 2;} else if (lineCount === 3) {currentY = y - 6;} else if (lineCount === 4) {currentY = y - 18;} else if (lineCount === 5) {currentY = y - 28;} else if (lineCount === 6) {currentY = y - 38;} else if (lineCount === 7) {currentY = y - 48;}if (item.dataStatus == 1) {context.fillStyle = 'red'} else {context.fillStyle = '#000' // 字体颜色}// context.fillStyle = "#000";context.fillText(word, x + this.gapX / 2 - wordWidth / 2, currentY);y += lineHeight; // 换行}}}},
六、模拟后端返回的数据
// signalTimeData: [// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:12:48',// showInfo: "M136\r\nT_Train=9340940ms\r\nDT:16",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:12:49',// showInfo: "M24\r\nT_Train=9341070ms",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:13:06',// showInfo: "M136\r\nT_Train=9358940ms\r\nDT:19\r\n此时,M24之后ATP发送3条APDU",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:13:07',// showInfo: "AK:20\r\n此时,M24之后RBC发送3条AK,无APDU",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:13:08',// showInfo: "TPDU_DR/SaPDU_D",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:13:09',// showInfo: "TPDU_DC",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: "2023-09-10 09:13:09",// showInfo: "DISC",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: "2023-09-10 09:13:09",// showInfo: "NO CARRIER",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// }// ],
全部代码如下:
<template><!-- :width="dialogWidth" --><vxe-modal v-model="sigModal" :title="titles" width="1200" min-width="550" :height="dialogHeight" :position="{ top: '3vh' }" @close="closeEvent" resize destroy-on-close><div class="con"><el-row style="margin-bottom:10px"><el-button type="primary" icon="el-icon-upload" @click="screenShot()">上传</el-button></el-row><div ref="screen" :style="{width: canvasWidth, height: canvasHeight}"><canvas id="myCanvas" :width="canvasWidth" :height="canvasHeight">你的浏览器还不支持canvas</canvas></div></div></vxe-modal>
</template><script>
import html2canvas from 'html2canvas'
import { DownLoadFromTime } from '@/utils/times.js'
import { get_signallInfo } from '@/api/c3/offlineImportant.js'
import axios from 'axios'
export default {data() {return {uploadId: '', titles: '信令图',dialogWidth: '90%',dialogHeight: '92%',sigModal: false,// 'ATP'-----'MT'------------ 'BTS'-------'BSC'----'MSC'------ 'RBC'// Igsmr-R Um_AMS/Um_BMS Abis A PRIresultTitle: ['ATP', 'MT'], // 后台返回的随机顺序titletypeArr: ['ATP', 'MT', 'BTS', 'BSC', 'MSC', 'RBC'],typeArr: [],canvasWidth: '1080px',canvasHeight: (window.innerHeight) - 170 + 'px',minHeight: (window.innerHeight) - 170,// signalTimeData: [// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:12:48',// showInfo: "M136\r\nT_Train=9340940ms\r\nDT:16",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:12:49',// showInfo: "M24\r\nT_Train=9341070ms",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:13:06',// showInfo: "M136\r\nT_Train=9358940ms\r\nDT:19\r\n此时,M24之后ATP发送3条APDU",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:13:07',// showInfo: "AK:20\r\n此时,M24之后RBC发送3条AK,无APDU",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: '2023-09-10 09:13:08',// showInfo: "TPDU_DR/SaPDU_D",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 1// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: '2023-09-10 09:13:09',// showInfo: "TPDU_DC",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "ATP",// endDataDir: "MT",// recTime: "2023-09-10 09:13:09",// showInfo: "DISC",// dataDir: 0,// dataDirStr: "上行",// lineType: 0,// dataStatus: 0// },// {// startDataDir: "MT",// endDataDir: "ATP",// recTime: "2023-09-10 09:13:09",// showInfo: "NO CARRIER",// dataDir: 1,// dataDirStr: "下行",// lineType: 0,// dataStatus: 0// }// ],xlArr: [],canvas: null,left: 0,paddingLeft: 120,paddingTop: 20,gapX: 320, // x轴间隔gapY: 90, // y轴间隔lineHeight: 20 // 行高}},created() {// this.typeArr = this.titletypeArr.filter(item => this.resultTitle.indexOf(item) !== -1)// this.xlArr = this.signalTimeData// this.canvasWidth = (this.typeArr.length * 320) - 120 + 'px'this.fnHight()window.addEventListener('resize', this.fnHight, true)},mounted() {},destroyed() {this.$destroy()window.removeEventListener('resize', this.fnHight, true)},methods: {// 获取信令图数据getSignallList(accidentId) {let parmsData = {accidentId: accidentId}get_signallInfo(parmsData).then(res => {if (res.status === 200) {if (res.data.code === 0) {if (res.data.data) {this.resultTitle = res.data.data.interfaceTypesthis.typeArr = this.titletypeArr.filter(item => this.resultTitle.indexOf(item) !== -1)this.xlArr = res.data.data.mtDiagramInfoVoListthis.canvasWidth = (this.typeArr.length * 320) - 120 + 'px'} else {this.$XModal.message({ content: '未查询到信令图数据.', status: 'warning' })}} else {this.$XModal.message({ content: res.message, status: 'error' })}}})},// 截屏screenShot() {html2canvas(this.$refs.screen, {backgroundColor: '#FFFFFF',useCORS: true}).then((canvas) => {// 获取到canvascanvas.toBlob(blob => {// 将二进制对象的内容 转成fileconst file = new File([blob], Date.now() + '.png', { type: 'image/png' })const formData = new FormData()formData.append('file', file)formData.append('uploadId', this.uploadId)// 发起请求axios({headers: {'Authorization': this.$store.state.user.token,'showInfo-Type': 'multipart/form-data'},method: 'post',url: '/api/offline/analysisContent/uploadReportPdf',data: formData,}).then((res) => {// 上传成功if (res.status === 200) {this.$XModal.message({ content: '上传成功', status: 'success'})this.$emit('sumitSuccess', 'ok')}}).catch((error) => {// 上传失败 执行对应操作this.$XModal.message({ content: '上传失败', status: 'error' })})}, 'image/png')if (navigator.msSaveBlob) { // IE10+ let blob = canvas.msToBlob(); return navigator.msSaveBlob(blob, name);} else {let imageurl = canvas.toDataURL('image/png')const newTime = DownLoadFromTime(new Date())//这里需要自己选择命名规则let imagename = '信令图_' + 'G1884' + '_' + newTimethis.fileDownload(imageurl, imagename)} })},// 下载截屏图片fileDownload(downloadUrl, downloadName) {let aLink = document.createElement("a")aLink.style.display = "none"aLink.href = downloadUrlaLink.download = `${downloadName}.png`// 触发点击-然后移除document.body.appendChild(aLink)aLink.click()document.body.removeChild(aLink)},closeEvent() {this.sigModal = false},fnHight() {setTimeout(() => {this.minHeight = (window.innerHeight) - 170}, 300)},// 标题的边框paintText(ctx, text, i) {// ctx.fillStyle = '#FEFECE'ctx.fillStyle = '#FFF'ctx.fillRect(320 * i + this.paddingLeft - 30, this.paddingTop, 60, 30); // 填充黄色背景// ctx.strokeStyle = 'red'ctx.strokeStyle = '#000' // '#A80036' // 标题的边框颜色ctx.strokeRect(320 * i + this.paddingLeft - 30, this.paddingTop, 60, 30); // 设置边框ctx.font = '12px "微软雅黑"' // 设置字体ctx.textBaseline = 'bottom' // 设置字体底线对齐绘制基线ctx.textAlign = 'left' // 设置字体对齐的方式ctx.fillStyle = 'Black'ctx.fillText(text, 320 * i + this.paddingLeft - 10, this.paddingTop + 22); // 填充文字标题// ctx.closePath()},// 箭头paintArr(item, s, t, direction, ctx) {ctx.beginPath()ctx.lineWidth = 1if (item.dataStatus == 1) {ctx.strokeStyle = 'red'} else {ctx.strokeStyle = '#000' // 箭头线的颜色}if (item.lineType === 1) {ctx.setLineDash([5, 2]); // 虚线}ctx.moveTo(s[0], s[1])ctx.lineTo(t[0], t[1])ctx.stroke()ctx.closePath()ctx.beginPath()if (direction === 'right') {ctx.moveTo(t[0] - 10, t[1] + 3)ctx.lineTo(t[0], t[1])ctx.lineTo(t[0] - 10, t[1] - 3)} else {ctx.moveTo(t[0] + 10, t[1] - 3)ctx.lineTo(t[0], t[1])ctx.lineTo(t[0] + 10, t[1] + 3)}// ctx.closePath()ctx.stroke()// ctx.fill()},// 标题列的虚线drawDashed(ctx, i) {ctx.beginPath()ctx.lineWidth = 1ctx.strokeStyle = '#696969' // '#FF8080'//虚线的颜色ctx.setLineDash([5, 2])ctx.moveTo(320 * i + this.paddingLeft, this.paddingTop + 40);ctx.lineTo(320 * i + this.paddingLeft, 400 * this.typeArr.length);ctx.fill()ctx.stroke()ctx.closePath()},// 文字自动换行 遇到换行符换行,并且超出最大宽度换行,只计算了最多显示7行的情况,超出7行得再计算wrapText(item, context, words, x, y, maxWidth, lineHeight) {// console.log(words, "words")let originY = y;let len = words.length;let rectWidth = 0;for (var n = 0; n < len; n++) {// 不超出一行var testWidth = context.measureText(words[n]).width;if (testWidth < maxWidth) {if (rectWidth < testWidth) {rectWidth = testWidth;}}}// 在上面循环计算出文字实际最宽的位置,画出背景色遮挡箭头// 画背景色遮盖箭头, 背景色自己调,跟画布统一就行context.fillStyle = "#fff"; // 背景颜色context.fillRect(x + this.gapX / 2 - rectWidth / 2 - 4,originY,rectWidth + 6,lineHeight); // 填充黄色背景for (var n = 0; n < len; n++) {// 不超出一行var testWidth = context.measureText(words[n]).width;if (testWidth < maxWidth) {// console.log(words[n], 1);let currentY = y;if (len === 1) {currentY = y + 14;} else if (len === 2) {currentY = y + 2;} else if (len === 3) {currentY = y - 6;} else if (len === 4) {currentY = y - 18;} else if (len === 5) {currentY = y - 28;} else if (len === 6) {currentY = y - 38;} else if (len === 7) {currentY = y - 48;}if (item.dataStatus == 1) {context.fillStyle = 'red'} else {context.fillStyle = '#000' // 字体颜色}// context.fillStyle = "#000"; // 字体颜色context.fillText(words[n], x + this.gapX / 2 - testWidth / 2, currentY);if (len > 1) {y += lineHeight;}} else {console.log(words[n], 2);// 文字超出一行,需要换行展示// 实际大于页面width font-size: 12, 计算出显示多少行let singleWordwith = 13;// 计算一行显示的最大字数,以及显示多少行let len = Math.floor(maxWidth / singleWordwith);let lineCount = Math.ceil(words[n].length / len);for (let j = 0; j <= lineCount; j++) {// 截取出每行显示的字let word = words[n].substr(j * len, len);let wordWidth = context.measureText(word).width;// 写入画布// 画背景色遮盖箭头, 背景色自己调,跟画布统一就行context.fillStyle = "#fff";context.fillRect(x + this.gapX / 2 - wordWidth / 2,y - 4,wordWidth,lineHeight); // 填充黄色背景let currentY = y;if (lineCount === 2) {currentY = y + 2;} else if (lineCount === 3) {currentY = y - 6;} else if (lineCount === 4) {currentY = y - 18;} else if (lineCount === 5) {currentY = y - 28;} else if (lineCount === 6) {currentY = y - 38;} else if (lineCount === 7) {currentY = y - 48;}if (item.dataStatus == 1) {context.fillStyle = 'red'} else {context.fillStyle = '#000' // 字体颜色}// context.fillStyle = "#000";context.fillText(word, x + this.gapX / 2 - wordWidth / 2, currentY);y += lineHeight; // 换行}}}},// 初始化加载initData() {var mycanvas = document.getElementById("myCanvas");this.canvas = mycanvas;var context = mycanvas.getContext("2d");// 动态设置宽高一定要在 myCanvas 节点添加之后document.getElementById("myCanvas").width = this.typeArr.length * 320 - 120;document.getElementById("myCanvas").style.background = "#fff";const minHeight = window.innerHeight - 180;if (this.xlArr.length > 0) {document.getElementById("myCanvas").height =30 * this.xlArr.length + 80 < minHeight? minHeight: 30 * this.xlArr.length + 80;} else {document.getElementById("myCanvas").height = minHeight;}var height = this.paddingTop + 62; // 初始值this.xlArr.map((v, i) => {const k = this.typeArr.indexOf(v.startDataDir);const j = this.typeArr.indexOf(v.endDataDir);context.font = '13px "微软雅黑"'; // 设置字体// 时间文字context.fillStyle = '#000' // 时间颜色context.fillText(v.recTime.split(' ')[1], 40, height);// 箭头this.paintArr(v,[this.gapX * k + this.paddingLeft, height],[this.gapX * j + this.paddingLeft, height],k < j ? "right" : "left",context);var maxWidth = 260; // 最大宽度,超过这个宽度会自动换行var words = v.showInfo.split("\r\n");// 文字自动换行this.wrapText(v,context,words,this.gapX * (k < j ? k : j) + this.paddingLeft,height - 10,maxWidth,this.lineHeight);if (i < this.xlArr.length - 1) {let nextWords = this.xlArr[i + 1].showInfo.split("\r\n");height += (this.lineHeight * (words.length + nextWords.length)) / 2 + 30;} else {height += this.lineHeight * words.length + 30;}// console.log(height, "height")})// 画虚线以及标题this.typeArr.map((v, i) => {this.paintText(context, v, i);setTimeout(() => {this.drawDashed(context, i);}, 300)})// document.getElementById('container').onscroll = (e) => {// // console.log('e:', e.target)// this.left = e.target.scrollLeft// }// 屏蔽所有页面 右键菜单// document.oncontextmenu = (event) => {// event.returnValue = false// }// 屏蔽当前页面 右键菜单// document.getElementById('container').oncontextmenu = (event) => {// event.returnValue = false// }}}
}
</script><style lang="scss" scoped>.con {position: relative;}.topTitle{position: absolute;background: #fff;top: 38px;padding-left: 15px;// width: 2580px;display: flex;li {display: inline-block;width: 320px;margin: 3px 0;}li:nth-last-child(1) {width: 100px;}span {// border: 1px solid #A80036;border: 1px solid #000;padding: 5px 10px;// background-color: #FEFECE;background-color: #fff;display: inline-block;}}#container {// overflow-x: scroll;// overflow-y: scroll;overflow: hidden;}#canvas {display: block;background-color: #fff;}
</style>