1.先看效果
a.这个是一级效果
b.二级效果
2.代码实现
a.完整代码示例
<template>
<div>
<div class="route-preview" :style="{ ...styleExternalIcon }">
<!-- <div style="margin-top: -20px;float: left;white-space: nowrap;margin-right:4px;">{{ tunnelEnterance }}
</div> -->
<div style="margin-top: -20px;float: left;margin-right:4px; min-width: 80px; max-width: 80px; word-break: break-word; white-space: normal;max-height: 60px;">{{ tunnelEnterance }}</div>
<!-- <div class="road-name">康护理大道</div> -->
<div class="legend" ref="legendContainer">
<svg ref="svgElement" :width="tunnelWidth" :key="this.key" :height="60" style="margin-bottom: -8px;">
<defs>
<pattern id="image" x="0" y="0" width="100%" height="100%">
<image
x="0"
y="0"
:width="localLineLength"
:height="lineHeight"
href="../../assets/crop/tunnelWire.png"
preserveAspectRatio="none"
/>
</pattern>
</defs>
<!-- 使用图案填充矩形 -->
<rect x="0" y="20" :width="localLineLength" :height="lineHeight" fill="url(#image)" />
<!-- 绘制蓝色覆盖区域 -->
<template v-for="(item, index) in this.tempList">
<rect v-if="item.endStation >= item.startStation" :x="scaleValue3(item.startStation,item.startCode)" :y="20"
:width="scaleValue3(item.endStation,item.endCode) - scaleValue3(item.startStation,item.startCode)" :height="lineHeight" fill="#9b99f9"
:key="'rect1-' + index" />
<rect v-else :x="scaleValue3(item.endStation,item.endCode)" :y="20"
:width="scaleValue3(item.startStation,item.startCode) - scaleValue3(item.endStation,item.endCode)" :height="lineHeight" fill="#000080"
:key="'rect-' + index" />
</template>
<!-- 绘制圆形节点 -->
<image v-for="(node, index) in nodes" :key="'image-no' + index" :x="scaleValue(node.cx)"
:y="lineHeight / 1 - nodeRadius + 16" :width="nodeRadius * 2"
xlink:href="../../assets/crop/pointWhite.png" />
<image v-for="(node, index) in nodes" :key="'image-' + index" v-if="isNodeInCoverage(node)"
:x="scaleValue(node.cx)" :y="lineHeight / 1 - nodeRadius + 16" :width="nodeRadius * 2"
xlink:href="../../assets/crop/pointPurple.png" />
<!-- 告警节点 -->
<!-- <image style="cursor: pointer;" v-for="(node, index) in alarmNodes" :key="'image1-' + index"
v-if="node.type == 0" @click="openAlarmInfo(node.id)" :x="scaleValue3(node.cx,node.code) - nodeRadius"
:y="lineHeight / 1 - nodeRadius + 16" :width="nodeRadius * 2" :height="nodeRadius * 2"
xlink:href="../../assets/crop/pointYellow.png" />
<image style="cursor: pointer;" v-for="(node, index) in alarmNodes" :key="'image2-' + index"
@click="openAlarmInfo(node.id)" v-if="node.type == 1" :x="scaleValue3(node.cx,node.code) - nodeRadius"
:y="lineHeight / 1 - nodeRadius + 16" :width="nodeRadius * 2" :height="nodeRadius * 2"
xlink:href="../../assets/crop/pointRed.png" /> -->
<!-- 机器人位置 -->
<image v-if="!this.robotStationCode" style="cursor: pointer;height: 20px;width: 40px;" :x="20"
:y="lineHeight / 1 - nodeRadius +10" :width="nodeRadius * 2" :height="nodeRadius * 2"
xlink:href="../../assets/crop/robot.png" :transform="getTransform()" />
<image v-if="this.robotStationCode" style="cursor: pointer;height: 20px;width: 40px;" :x="scaleValue3(this.robotCode,this.robotStationCode)"
:y="lineHeight / 1 - nodeRadius +10" :width="nodeRadius * 2" :height="nodeRadius * 2"
xlink:href="../../assets/crop/robot.png" :transform="getTransform()" />
<!-- 在告警节点上方放置另一张位置标点图片 -->
<image style="cursor: pointer;" v-for="(node, index) in alarmNodes" @click="openAlarmInfo(node.id)"
v-if="node.type == 0" :key="'image-marker1-' + index" :x="scaleValue3(node.cx,node.code) - nodeRadius" :y="0"
:width="nodeRadius * 2" :height="nodeRadius * 2" xlink:href="../../assets/crop/yellowDot.png" />
<image style="cursor: pointer;" v-for="(node, index) in alarmNodes" @click="openAlarmInfo(node.id)"
v-if="node.type == 1" :key="'image-marker1-' + index" :x="scaleValue3(node.cx,node.code) - nodeRadius" :y="0"
:width="nodeRadius * 2" :height="nodeRadius * 2" xlink:href="../../assets/crop/redDot.png" />
<text v-for="(node, index) in nodes" :key="'text-' + index" :x="scaleValue(node.cx)"
:y="lineHeight + 25 + 4 + 12" font-size="13px" text-anchor="middle" fill="#000">
{{ node.nodeName }}
</text>
<!-- 绘制三角形箭头 -->
<template v-for="(item, index) in this.tempList">
<polygon v-if="item.endStation >= item.startStation"
:points="getArrowPoints(scaleValue3(item.endStation,item.endCode), lineHeight + 26,item.endCode)" fill="#FF0000"
:key="'arrow-' + index" />
<polygon v-else :points="getArrowPointsReverse(scaleValue3(item.endStation,item.endCode), lineHeight + 26,item.endCode)"
fill="#FF0000" :key="'arrow-' + index" />
</template>
</svg>
</div>
<div style="margin-top: -19px; float: right; margin-right: -198px; margin-left: 6px;min-width: 80px; max-width: 80px; word-break: break-word; white-space: normal;max-height: 60px;">{{ tunnelEixt }}</div>
<!-- <div style="margin-top: -19px; float: right; margin-right: -198px; margin-left: 6px; max-width: 190px; word-break: break-word; white-space: normal;">韩元里大道</div> -->
<!-- <div class="road-name">韩元里大道</div> -->
</div>
<div class="zoom-buttons-container" :style="{ position: 'absolute', top: bmt + 'px', right: bml + 'px', zIndex: 1 }" >
<div style="display: flex; flex-direction: column; align-items: flex-end;">
<el-button @click.stop="zoomIn" size="mini" icon="el-icon-plus" style="margin-bottom: 4px;margin-top: -19px;"></el-button>
<el-button @click="zoomOut" size="mini" icon="el-icon-minus"></el-button>
</div>
</div>
</div>
</template>
<script>
import { sendGet, sendPostByKeyValue } from "@/utils/httpUtils";
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export default {
props: {
//隧道入口名称
tunnelEnterance: {
type: String
},
//隧道出口
tunnelEixt: {
type: String
},
//工位地图Id
addrId: {
type: String
},
//标签数
tagsNum: {
type: Number,
default: 0
},
//上边距
mt: {
type: Number,
},
//左边距
ml: {
type: Number,
},
//节点集合
tempNodes: {
type: Array,
default: function () {
return []; // 对象或 [] 空数组
}
},
//起始和终止工位集合
tempList: {
type: Array,
default: function () {
return []; // 对象或 [] 空数组
}
},
//报警信息组成节点
addrAlarmInfo: {
type: Array,
default: function () {
return []; // 对象或 [] 空数组
}
},
lineLength: {
type: Number,
default: 1600
},
lineHeight: {
type: Number,
default: 8
},
//机器人当前桩号位置
robotStationCode: {
type: String
},
//机器人站位号
robotCode: {
type: Number
},
//加减按钮边距
bmt:{
type: Number,
default: 90
},
//加减按钮边距
bml:{
type:Number,
default: 40
},
},
data() {
return {
localLineLength: this.lineLength,//svg长度
scale: 1,//放大缩小倍数
nodeCount: 11,//节点数
index: 1,//节点倍数
level: 1,//节点倍数
key:0,
tunnelWidth:1600,
tempNode:[]//节点名称数组
};
},
created() {
this.tempNode=this.tempNodes
// console.log(this.addrAlarmInfo,this.tempNode,this.tempList)
},
watch: {
localLineLength(newLength) {
this.$refs.svgElement.setAttribute('width', this.calculateSvgWidth(newLength));
},
// 监听机器人位置变化
robotStationCode(newVal, oldVal) {
// 当机器人位置发生变化时,更新滚动条位置
if (newVal !== oldVal) {
this.updateScrollbarPosition();
}
}
},
methods: {
// 更新滚动条以确保机器人图标可见
updateScrollbarPosition() {
const robotPositionX = this.calculateRobotPositionX();
const container = this.$refs.legendContainer; // 假设你的 SVG 容器有一个 ref="legendContainer"
if (container && robotPositionX != null) {
container.scrollLeft = robotPositionX - (container.offsetWidth / 2);
}
},
// 计算机器人图标的 X 坐标
calculateRobotPositionX() {
// 使用你的方法计算机器人的 X 坐标
return this.scaleValue3(this.robotCode,this.robotStationCode);
},
getTransform() {
// 在这里根据你的需求计算缩放值
const scaleFactor = 1.3; // 例如,放大 1.5 倍
return `scale(${scaleFactor})`;
},
updateImage() {
this.key += 1;
this.$forceUpdate(); // Force a re-render
},
calculateSvgWidth(lineLength) {
return lineLength > 1600 ? lineLength : 1600;
},
zoomIn() {
if (this.index >=5) {
this.$message({
type: "warning",
message: "已是最高等级"
});
return false
}
this.index += 1
switch (this.index) {
case 1:
this.getAddrNodes(1);
break;
case 2:
//查询
this.getAddrNodes(2);
break;
case 3:
this.getAddrNodes(3);
break;
case 4:
this.getAddrNodes(4);
break;
case 5:
this.getAddrNodes(5);
break;
default:
break;
}
// this.nodeCount = 11 * this.index
this.nodeCount = (10 * this.index)+1
this.tunnelWidth = this.localLineLength;
},
zoomOut() {
if (this.index<2) {
this.$message({
type: "warning",
message: "已是最低等级"
});
return false
}
if(this.index>5){
this.index=5
}
this.index -= 1
switch (this.index) {
case 1:
this.getAddrNodes(1);
break;
case 2:
this.getAddrNodes(2);
break;
case 3:
this.getAddrNodes(3);
break;
case 4:
this.getAddrNodes(4);
break;
case 5:
this.getAddrNodes(5);
break;
default:
break;
}
this.nodeCount = (10 * this.index)+1
this.tunnelWidth = this.localLineLength;
this.level=this.index
},
async getAddrNodes(level){
if(this.addrId){
const url='/Addr/getAddrNodesByLevel'
const res=await sendPostByKeyValue(url,{addrId:this.addrId,level:level})
if(res&&res.code==200){
this.tempNode=res.data
}
}
},
//点击图片,查看警告详情
openAlarmInfo(data) {
this.$emit('getAlarmDetailInfo', data);
},
getAlarmNodeImage(type) {
// 根据告警类型返回不同的图片路径
return type === 0 ? "../.../assets/crop/pointYellow.png" : "../../assets/crop/pointRed.png";
},
// 判断节点是否在覆盖区域内的方法
isNodeInCoverage(node) {
for (let i = 0; i < this.tempList.length; i++) {
const startStation = this.scaleValue3(this.tempList[i].startStation, this.tempList[i].startCode);
const endStation = this.scaleValue3(this.tempList[i].endStation, this.tempList[i].endCode);
if (node.cx >= startStation && node.cx <= endStation) {
return true;
}
}
return false;
},
getArrowPoints(x, y, endCode) {
const triangleSize = 20; // 三角形的大小
const halfTriangleSize = triangleSize / 2;
const upperCaseCode = endCode.toUpperCase();
// 使用正则表达式进行模糊匹配
// 如果箭头在节点上,添加一个偏移量
const yOffset = this.nodes.find(node => node.nodeName && new RegExp(escapeRegExp(upperCaseCode)).test(node.nodeName.toUpperCase())) ? 18 : 0;
return `${x+ yOffset},${y } ${x+ yOffset},${y - triangleSize } ${x + halfTriangleSize+ yOffset},${y - halfTriangleSize }`;
},
getArrowPointsReverse(x, y,endCode) {
//反方向箭头三角
const triangleSize = 20; // 三角形的大小
const halfTriangleSize = triangleSize / 2;
const upperCaseCode = endCode.toUpperCase();
// 使用正则表达式进行模糊匹配
// 如果箭头在节点上,添加一个偏移量
const yOffset = this.nodes.find(node => node.nodeName && new RegExp(escapeRegExp(upperCaseCode)).test(node.nodeName.toUpperCase())) ? 18 : 0;
return `${x+yOffset},${y} ${x+yOffset},${y - triangleSize} ${x - halfTriangleSize+yOffset},${y - halfTriangleSize}`;
},
//没对应的话,找最接近的取其近似值,比如nodes有K500+700,code为k500+720,把node中K500+700到它的上一个节点均分,再赋值给value
findNearestNode(value, code) {
// 如果 code 不包含加号,则在末尾添加三个零
// console.log(code,'nearcd');
if (!code.includes('+')) {
code += '000';
}
// 提取数字部分,并转换为浮点数
const upperCaseCode = parseFloat(code.replace(/[^0-9.]/g, ''));
let nearestNode1, nearestNode2;
this.nodes.some((currentNode, index) => {
const nextNode = this.nodes[index + 1];
// 判断 code 在当前节点和下一个节点之间
const isInRange = (nodeValue) => {
const nodeCode = parseFloat((nodeValue.includes('+') ? nodeValue : nodeValue + '000').replace(/[^0-9.]/g, ''));
return upperCaseCode >= nodeCode;
};
const isLeRange = (nodeValue) => {
const nodeCode = parseFloat((nodeValue.includes('+') ? nodeValue : nodeValue + '000').replace(/[^0-9.]/g, ''));
return upperCaseCode <= nodeCode;
};
if (currentNode&&nextNode&¤tNode.nodeName && nextNode.nodeName && isInRange(currentNode.nodeName) && isLeRange(nextNode.nodeName)) {
nearestNode1 = currentNode;
nearestNode2 = nextNode;
return true; // 停止循环
}
return false;
});
// 如果找到最接近的两个节点,计算均分值并返回
if (nearestNode1 && nearestNode2) {
//转换两个接近点,按比例取两个点之间的位置
//开始节点
const n1 =parseFloat((nearestNode1.nodeName.includes('+') ? nearestNode1.nodeName : nearestNode1.nodeName + '000').replace(/[^0-9.]/g, ''));
//终止节点
const n2 =parseFloat((nearestNode2.nodeName.includes('+') ? nearestNode2.nodeName : nearestNode2.nodeName + '000').replace(/[^0-9.]/g, ''));
// 根据 upperCaseCode 在 n1 和 n2 之间的位置,计算 middleValue
const range = n2 - n1;
const positionWithinRange = upperCaseCode - n1;
const relativePosition = positionWithinRange / range;
const middleValue = nearestNode1.cx + (relativePosition * (nearestNode2.cx - nearestNode1.cx));
return { cx: middleValue, nodeName: 'Interpolated Node' };
}
return null;
}
},
computed: {
scaleValue() {
return (value) => {
if (this.localLineLength !== 0 && value) {
return value;
}
return 0; // 处理 lineLength 为 0 的情况,避免零除错误
};
},
scaleValue3() {
return (value, code) => {
if (this.localLineLength !== 0 && value && this.tagsNum != 0) {
// 查询node里面name与code相同,把cx的值赋给value
// 转换 code 为大写
const upperCaseCode = code.toUpperCase();
// 使用正则表达式进行模糊匹配
const matchingNode = this.nodes.find(node => node.nodeName && new RegExp(escapeRegExp(upperCaseCode)).test(node.nodeName.toUpperCase()));
// console.log(matchingNode,'matchingNode');
if (matchingNode) {
value = matchingNode.cx;
return value
} else {
//如果没有相同的,则找到最接近的节点的cx赋值给value
const nearestNode = this.findNearestNode(value,upperCaseCode);
if (nearestNode) {
value = nearestNode.cx;
}
}
return value;
}
return value;
};
},
styleExternalIcon() {
return {
marginLeft: `${this.ml}px`,
marginTop: `${this.mt}px`,
}
},
nodeRadius() {
return 10;
},
nodes() {
const nodes = [];
const totalWidth = (this.nodeCount) * this.nodeRadius * 2;
let spaceBetweenNodes = ((this.lineLength - totalWidth) / (this.nodeCount)) + 2;
spaceBetweenNodes=spaceBetweenNodes<=60?60:spaceBetweenNodes
// 增加一个偏移量,用于调整第一个节点的位置
if(spaceBetweenNodes==60){
this.localLineLength=(60*(this.nodeCount+this.index))+((this.nodeCount+this.index)*(this.nodeRadius))+(40*this.index)
this.tunnelWidth=this.localLineLength
this.updateImage()
}
for (let i = 0; i < this.nodeCount; i++) {
let cx = i * (this.nodeRadius * 2 + spaceBetweenNodes) +60;
let nodeName = this.tempNode[i];
nodes.push({ cx, nodeName });
}
return nodes;
},
alarmNodes() {
const alarmNodeList = [];
// 遍历 addrAlarmInfo 并转换为节点数据格式
if (this.addrAlarmInfo && this.addrAlarmInfo.length != 0) {
this.addrAlarmInfo.forEach((alarm, index) => {
// 使用适当的方式获取告警点的 X 坐标和节点名称
const cx = Number(alarm.stationCode)==0?50:Number(alarm.stationCode); // 获取告警点的 X 坐标
const code=alarm.value;
const type = alarm.eventLevel
const id = alarm.alarmRecordId
if(code){
alarmNodeList.push({ cx,code, type, id });
}
// 创建告警节点对象并添加到数组中
});
}
console.log('告警节点信息',alarmNodeList)
return alarmNodeList;
},
}
};
</script>
<style scoped>
.legend::-webkit-scrollbar {
cursor: pointer;
width: 0px;
height:8px;
}
.legend::-webkit-scrollbar-thumb {
background-color: rgb(7, 29, 230);
}
.legend {
overflow-x: scroll;
width: 1660px;
}
.route-preview {
transform-origin: top left;
width: 80%;
height: 100%;
display: flex;
/* 使用 Flex 布局 */
align-items: center;
/* 垂直居中对齐元素 */
}
.flex-container {
display: flex;
/* 创建水平布局 */
align-items: center;
/* 垂直居中对齐 */
}
.container {
display: flex;
}
.zoom-buttons {
margin-left: 10px;
}
.route-preview {
transform-origin: top left;
width: 80%;
height: 100%;
display: flex;
align-items: center;
position: relative;
/* 让容器内的元素可以使用绝对定位 */
}
.zoom-buttons-container {
position: absolute;
top: 90px;
/* 调整按钮的垂直位置 */
right: 40px;
/* 调整按钮的水平位置 */
display: flex;
justify-content: space-around;
}
.el-button {
font-size: 16px;
/* 调整按钮文字大小 */
text-align: center;
}
</style>
3.总结
a.文章代码主要是提供一个实现思路,可以根据自己的需要去调整代码。
b.props是外部父组件传值。
c.如果有疑问可私信,乐意解答。