vue+svg实现按等级缩放的路线组件

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&&currentNode.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.如果有疑问可私信,乐意解答。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue中,可以使用第三方库svg-pan-zoom来实现SVG图像的缩放和移动。svg-pan-zoom是一个独立的JavaScript库,可以轻松与Vue集成。以下是在Vue中使用svg-pan-zoom实现SVG缩放和移动的基本步骤: 1. 安装svg-pan-zoom库 可以使用npm或yarn来安装svg-pan-zoom库。 ``` npm install svg-pan-zoom --save ``` 2. 导入svg-pan-zoom库 在Vue组件中导入svg-pan-zoom库。 ```javascript import svgPanZoom from 'svg-pan-zoom'; ``` 3. 在Vue组件中创建SVG元素 使用SVG元素来创建SVG图像,并将其添加到Vue组件中。 ```html <template> <div> <svg ref="svg"> <circle cx="50" cy="50" r="40" fill="red"></circle> </svg> </div> </template> ``` 4. 初始化svg-pan-zoom 在Vue组件的mounted钩子函数中初始化svg-pan-zoom库。 ```javascript mounted() { const svg = this.$refs.svg; const panZoom = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true, minZoom: 0.1, maxZoom: 10, zoomScaleSensitivity: 0.2, panEnabled: true, contain: false, refreshRate: 'auto', beforePan: null, onPan: null, onZoom: null }); } ``` 在上述代码中,我们使用svg-pan-zoom的构造函数来初始化库,并将SVG元素传递给它。通过传递选项参数,我们可以自定义缩放和平移的行为。 5. 在Vue组件中使用svg-pan-zoom 可以使用svg-pan-zoom提供的API方法来实现SVG图像的缩放和平移。 ```javascript // 缩放到指定比例 panZoom.zoom(2); // 缩放到原始比例 panZoom.resetZoom(); // 平移到指定位置 panZoom.pan({x: 50, y: 50}); // 平移到原始位置 panZoom.resetPan(); ``` 以上就是在Vue中使用svg-pan-zoom实现SVG缩放和移动的基本步骤。注意,这只是一个简单的示例,实际应用中可能需要更复杂的代码来实现更高级的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值