良哥汇总:WEB基于SVG图形绘制——手把手教你完成矢量示意图绘制

一、简介

SVG 意为可缩放矢量图形(Scalable Vector Graphics)。
SVG 是一种用于描述二维图形的 XML 标记语言,与位图图像不同,SVG图像以文本形式存储,并且可以缩放到任意大小而不会失真,因为它们基于数学描述而不是像素。
SVG 图形是可伸缩的,无需分辨率依赖,这意味着它们可以在不失真的情况下被放大或缩小。
SVG 广泛应用于网页设计、图标制作、数据可视化和其他图形相关的领域。

SVG 的主要特点
矢量图形:SVG 使用基于路径的矢量图形,这意味着图形可以无限放大而不失真。
可伸缩性:SVG 文件可以在不同的分辨率下保持清晰,适合用于响应式设计。
互动性:SVG 可以与 JavaScript 结合,实现动画和交云效果。
集成性:SVG 可以直接嵌入 HTML5 中,无需使用外部文件。
兼容性:大多数现代浏览器都支持 SVG。

SVG 的应用场景
网页图标:由于 SVG 的可伸缩性,它非常适合用来制作网页图标。
数据可视化:SVG 常用于图表和图形的创建,如条形图、饼图等。
动画:SVG 可以与 CSS 和 JavaScript 结合,创建复杂的动画效果。
游戏开发:在某些情况下,SVG 也被用于创建简单的游戏图形。
设计原型:设计师可以使用 SVG 来创建可交互的设计原型。

二、在html中如何使用

使用 < embed>标签
允许使用脚本
不推荐在html和html4,html5可以

<embed src="circle1.svg" type="image/svg+xml" />

使用 object 标签
支持html4和xhtml 和html5
不允许使用脚本

<object data="circle1.svg" type="image/svg+xml"></object>

使用iframe标签
允许使用脚本
不推荐在html和html4,html5可以

<iframe src="circle1.svg"></iframe>

也可以直接内嵌在html中

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
   <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
</svg>

也可以使用超文本链接到文件

<a href="circle1.svg">查看 SVG 文件</a>

三、形状和属性

1)矩形

基本语法:

<rect
  x="x-coordinate"        <!-- 矩形左上角的 x 坐标 -->
  y="y-coordinate"        <!-- 矩形左上角的 y 坐标 -->
  width="width-value"     <!-- 矩形的宽度 -->
  height="height-value"   <!-- 矩形的高度 -->
  rx="rx-value"           <!-- 矩形的圆角半径(水平方向) -->
  ry="ry-value"           <!-- 矩形的圆角半径(垂直方向) -->
  fill="fill-color"       <!-- 矩形的填充颜色 -->
  stroke="stroke-color"   <!-- 矩形的描边颜色 -->
  stroke-width="width-value" <!-- 矩形的描边宽度 -->
/>

属性解析:

x 和 y 属性指定了矩形左上角的坐标,即矩形的起始点。

width 和 height 属性定义了矩形的宽度和高度。

rx 和 ry 属性用于指定矩形的圆角半径。如果只设置 rx,则所有角的圆角半径都相同;如果同时设置 rx 和 ry,则可以分别指定水平和垂直方向的圆角半径。

fill 属性定义了矩形的填充颜色。

stroke 属性定义了矩形的描边颜色。

stroke-width 属性定义了矩形的描边宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
	<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)"/>
</svg>

2)圆形

基本语法:

<circle
  cx="x-coordinate"      <!-- 圆心的 x 坐标 -->
  cy="y-coordinate"      <!-- 圆心的 y 坐标 -->
  r="radius"             <!-- 圆的半径 -->
  fill="fill-color"      <!-- 圆的填充颜色 -->
  stroke="stroke-color"  <!-- 圆的描边颜色 -->
  stroke-width="width"   <!-- 圆的描边宽度 -->
/>

属性解析:

cx 和 cy 属性定义了圆心的坐标,即圆的中心点的位置。

r 属性定义了圆的半径,以确定圆的大小。

fill 属性定义了圆的填充颜色。

stroke 属性定义了圆的描边颜色。

stroke-width 属性定义了圆的描边宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
	<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red"/>
</svg>

3)椭圆

基本语法

<ellipse
  cx="x-coordinate"      <!-- 椭圆中心点的 x 坐标 -->
  cy="y-coordinate"      <!-- 椭圆中心点的 y 坐标 -->
  rx="x-radius"          <!-- 椭圆水平轴的半径 -->
  ry="y-radius"          <!-- 椭圆垂直轴的半径 -->
  fill="fill-color"      <!-- 椭圆的填充颜色 -->
  stroke="stroke-color"  <!-- 椭圆的描边颜色 -->
  stroke-width="width"   <!-- 椭圆的描边宽度 -->
/>

属性解析:

cx 和 cy 属性定义了椭圆的中心点坐标,即椭圆的中心位置。

rx 属性定义了椭圆水平轴(x轴)的半径。

ry 属性定义了椭圆垂直轴(y轴)的半径。

fill 属性定义了椭圆的填充颜色。

stroke 属性定义了椭圆的描边颜色。

stroke-width 属性定义了椭圆的描边宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <ellipse cx="300" cy="80" rx="100" ry="50" style="fill:yellow;stroke:purple;stroke-width:2"/>
</svg>

4)直线

基本语法

<line
  x1="x1-coordinate"     <!-- 起点的 x 坐标 -->
  y1="y1-coordinate"     <!-- 起点的 y 坐标 -->
  x2="x2-coordinate"     <!-- 终点的 x 坐标 -->
  y2="y2-coordinate"     <!-- 终点的 y 坐标 -->
  stroke="stroke-color"  <!-- 直线的颜色 -->
  stroke-width="width"   <!-- 直线的宽度 -->
/>

属性解析:

x1 和 y1 属性定义了直线的起点坐标。

x2 和 y2 属性定义了直线的终点坐标。

stroke 属性定义了直线的颜色。

stroke-width 属性定义了直线的宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
	<line x1="0" y1="0" x2="200" y2="200"
style="stroke:rgb(255,0,0);stroke-width:2"/>
</svg>

5)多边形

基本语法

<polygon
  points="x1,y1 x2,y2 x3,y3 ..."   <!-- 多边形各个顶点的坐标 -->
  fill="fill-color"                <!-- 多边形的填充颜色 -->
  stroke="stroke-color"            <!-- 多边形的边框颜色 -->
  stroke-width="width"             <!-- 多边形的边框宽度 -->
/>

属性解析:

points 属性定义了多边形各个顶点的坐标,多个顶点的坐标以空格或逗号分隔,并且每对坐标使用逗号分隔。

fill 属性定义了多边形的填充颜色。

stroke 属性定义了多边形的边框颜色。

stroke-width 属性定义了多边形的边框宽度。

示例:

<svg  height="210" width="500">
  <polygon points="200,10 250,190 160,210" style="fill:lime;stroke:purple;stroke-width:1"/>
</svg>

6)多线段

基本语法

<polyline
  points="x1,y1 x2,y2 x3,y3 ..."   <!-- 多段线各个顶点的坐标 -->
  fill="none"                      <!-- 多段线的填充颜色,使用 "none" 表示不填充 -->
  stroke="stroke-color"            <!-- 多段线的边框颜色 -->
  stroke-width="width"             <!-- 多段线的边框宽度 -->
/>

属性解析:

points 属性定义了多段线各个顶点的坐标,多个顶点的坐标以空格或逗号分隔,并且每对坐标使用逗号分隔。

fill 属性用于定义多段线的填充颜色,通常设置为 "none" 表示不填充。

stroke 属性定义了多段线的边框颜色。

stroke-width 属性定义了多段线的边框宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" style="fill:none;stroke:black;stroke-width:3" />
</svg>

7)路径

基本语法

<path
  d="path-data"            <!-- 定义路径的路径数据 -->
  fill="fill-color"        <!-- 路径的填充颜色 -->
  stroke="stroke-color"    <!-- 路径的描边颜色 -->
  stroke-width="width"     <!-- 路径的描边宽度 -->
/>

属性解析:

d 属性定义了路径的路径数据,即路径命令序列。路径数据由一系列的路径命令组成,每个路径命令以字母开头,后面跟随一组数字参数。常用的路径命令包括:M(移动到)、L(直线到)、H(水平线到)、V(垂直线到)、C(三次贝塞尔曲线)、S(光滑曲线)、Q(二次贝塞尔曲线)、T(光滑二次贝塞尔曲线)、A(圆弧)、Z(闭合路径)等。

fill 属性定义了路径的填充颜色。

stroke 属性定义了路径的描边颜色。

stroke-width 属性定义了路径的描边宽度。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
    <path d="M150 0 L75 200 L225 200 Z" />
</svg>

8)文本

基本语法

<text
  x="x-coordinate"          <!-- 文本左上角的 x 坐标 -->
  y="y-coordinate"          <!-- 文本左上角的 y 坐标 -->
  font-family="font"        <!-- 字体名称 -->
  font-size="size"          <!-- 字体大小 -->
  fill="fill-color"         <!-- 文本颜色 -->
  text-anchor="anchor"      <!-- 文本锚点 -->
>
  Text content              <!-- 文本内容 -->
</text>

属性解析:

x 和 y 属性定义了文本左上角的坐标,即文本的起始点位置。

font-family 属性定义了文本的字体名称,可以是系统字体或自定义字体。

font-size 属性定义了文本的字体大小,以像素为单位。

fill 属性定义了文本的颜色。

text-anchor 属性定义了文本锚点,即文本相对于指定坐标的对齐方式,常用取值有 "start"(默认,左对齐)、"middle"(居中对齐)和 "end"(右对齐)。

8)Stroke 属性

SVG 中的 stroke 属性用于定义图形元素的描边(边框)颜色,它可以应用于任何具有轮廓的图形元素,如 <rect>、<circle>、<path> 等。

基本语法

<element stroke="color" />

属性解析:

stroke 属性定义了图形元素的描边颜色,可以使用颜色名称、十六进制颜色值、RGB值、RGBA值等来表示颜色。

如果不想显示描边,可以将 stroke 属性设置为 "none"。

SVG 提供了一个范围广泛 stroke 属性。

stroke
stroke-width
stroke-linecap
stroke-dasharray
所有 stroke 属性,可应用于任何种类的线条,文字和元素就像一个圆的轮廓。

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <g fill="none">
    <path stroke="red" d="M5 20 l215 0" />
    <path stroke="blue" d="M5 40 l215 0" />
    <path stroke="black" d="M5 60 l215 0" />
  </g>
</svg>

9)SVG 滤镜

SVG 滤镜是一种强大的图形效果技术,可以用来实现各种视觉效果,例如模糊、阴影、光照等。

滤镜可以应用于 SVG 图形元素,例如矩形、圆形、路径等,以及 SVG 文本元素,使它们呈现出不同的外观和效果。

基本语法

SVG 滤镜通常使用 <filter> 元素定义,	并通过 filter 属性将其应用于目标元素。
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义滤镜 -->
  <filter id="filter_id">
    <!-- 滤镜效果 -->
  </filter>
  
  <!-- 应用滤镜的目标元素 -->
  <rect x="50" y="50" width="100" height="80" filter="url(#filter_id)" />
</svg>

滤镜效果
SVG 滤镜可以实现多种效果,常见的滤镜效果包括:

模糊(Blur):使图像产生模糊效果,通过 <feGaussianBlur> 元素实现。

阴影(Shadow):为图像添加阴影效果,通过 <feDropShadow> 元素实现。

亮度、对比度调整(Brightness, Contrast):调整图像的亮度和对比度,通过 <feComponentTransfer> 元素实现。

颜色矩阵(Color Matrix):通过颜色矩阵操作修改图像的颜色,通过 <feColorMatrix> 元素实现。

混合模式(Blend Mode):将两个图像混合在一起,通过 <feBlend> 元素实现。

以下代码定义了一个模糊滤镜,然后将其应用于一个红色填充的矩形,使矩形呈现出模糊的效果。

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义模糊滤镜 -->
  <filter id="blur_filter">
    <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
  </filter>
  
  <!-- 应用模糊滤镜的矩形 -->
  <rect x="50" y="50" width="100" height="80" fill="red" filter="url(#blur_filter)" />
</svg>

SVG 可用的滤镜是:

feBlend - 与图像相结合的滤镜
feColorMatrix - 用于彩色滤光片转换
feComponentTransfer
feComposite
feConvolveMatrix
feDiffuseLighting
feDisplacementMap
feFlood
feGaussianBlur
feImage
feMerge
feMorphology
feOffset - 过滤阴影
feSpecularLighting
feTile
feTurbulence
feDistantLight - 用于照明过滤
fePointLight - 用于照明过滤
feSpotLight - 用于照明过滤

除此之外,您可以在每个 SVG 元素上使用多个滤镜!

注意事项

SVG 滤镜可以组合使用,可以在一个 <filter> 元素中定义多个滤镜效果。

每种滤镜效果都有不同的参数可以调整,例如模糊滤镜的标准差参数、阴影滤镜的偏移量和模糊半径等。

SVG 滤镜可以与 CSS 样式表一起使用,也可以直接在SVG元素上使用 style 属性进行定义。

通过使用 SVG 滤镜,你可以为 SVG 图形元素添加各种视觉效果,使其呈现出更加生动、多样化的外观。

10)SVG 模糊效果

SVG 中的模糊效果可以通过 <feGaussianBlur> 元素实现,该元素使用高斯模糊算法来模糊图像。

模糊效果可以用于创建柔和的阴影、景深效果、模糊背景等各种视觉效果。

<feGaussianBlur> 元素:
<feGaussianBlur> 元素用于对图像进行高斯模糊处理,它有两个主要参数:

stdDeviation:指定高斯模糊的标准差。标准差越大,模糊程度越高。可以使用一个或两个数字,分别表示水平和垂直方向的标准差。如果只提供一个数字,则水平和垂直方向的标准差相同。

in:指定输入图像,通常为 SourceGraphic,表示应用滤镜效果的目标元素本身。

以下代码定义了一个模糊滤镜,然后将其应用于一个红色填充的矩形,使矩形呈现出模糊的效果,stdDeviation=“5” 表示水平和垂直方向的标准差均为 5,即模糊程度为 5 个像素。

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义模糊滤镜 -->
  <filter id="blur_filter">
    <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
  </filter>
  
  <!-- 应用模糊滤镜的矩形 -->
  <rect x="50" y="50" width="100" height="80" fill="red" filter="url(#blur_filter)" />
</svg>

示例:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <defs>
    <filter id="f1" x="0" y="0">
      <feGaussianBlur in="SourceGraphic" stdDeviation="15" 
/>
    </filter>
  </defs>
  <rect width="90" height="90" stroke="green" stroke-width="3"
  fill="yellow" filter="url(#f1)" />
</svg>

代码解析:

<filter>元素id属性定义一个滤镜的唯一名称
<feGaussianBlur>元素定义模糊效果
in="SourceGraphic"这个部分定义了由整个图像创建效果
stdDeviation属性定义模糊量
<rect>元素的滤镜属性用来把元素链接到"f1"滤镜

注意事项

可以将模糊效果应用于任何SVG图形元素,包括矩形、圆形、路径等。

模糊效果的参数 stdDeviation 可以根据需要进行调整,以获得适合的模糊程度。

模糊效果可以与其他滤镜效果组合使用,例如阴影、混合模式等。

通过使用 <feGaussianBlur> 元素,你可以为SVG图形元素添加模糊效果,使其呈现出柔和、模糊的外观,从而实现各种视觉效果。

11)SVG 阴影

<feOffset>
<feOffset> 是 SVG 滤镜中的一个元素,用于在图像上创建一个偏移效果,可以用来创建阴影、轮廓和其他视觉效果。它的作用是将输入图像的每个像素沿着指定的水平和垂直方向移动一定的距离,然后将结果图像作为滤镜效果的输出。

基本语法

<feOffset dx="x-offset" dy="y-offset" />
dx 属性定义了阴影在水平方向上的偏移量。

dy 属性定义了阴影在垂直方向上的偏移量。

示例:

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义偏移滤镜 -->
  <filter id="offset_filter">
    <feOffset dx="5" dy="5" />
  </filter>
  
  <!-- 应用偏移滤镜的矩形 -->
  <rect x="50" y="50" width="100" height="80" fill="red" filter="url(#offset_filter)" />
</svg>
<feDropShadow>
SVG 阴影效果可以通过 <feDropShadow> 元素实现,它可以为 SVG 图形元素添加阴影效果,使其看起来更加立体和真实。

基本语法

<feDropShadow dx="offset-x" dy="offset-y" stdDeviation="blur-radius" flood-color="color" flood-opacity="opacity" />
dx 属性定义了阴影在水平方向上的偏移量。

dy 属性定义了阴影在垂直方向上的偏移量。

stdDeviation 属性定义了阴影的模糊半径,通常用于控制阴影的模糊程度。

flood-color 属性定义了阴影的颜色,默认为黑色。

flood-opacity 属性定义了阴影的不透明度,默认为1(完全不透明)。

以下代码定义了一个阴影滤镜,然后将其应用于一个红色填充的矩形,使矩形呈现出带有灰色阴影的效果,阴影在水平方向和垂直方向上分别偏移了 5 个像素,模糊半径为 5 个像素,阴影的颜色为灰色,不透明度为 0.5。

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <!-- 定义阴影滤镜 -->
  <filter id="shadow_filter">
    <feDropShadow dx="5" dy="5" stdDeviation="5" flood-color="gray" flood-opacity="0.5" />
  </filter>
  
  <!-- 应用阴影滤镜的矩形 -->
  <rect x="50" y="50" width="100" height="80" fill="red" filter="url(#shadow_filter)" />
</svg>

12)SVG 渐变 - 线性

渐变是一种从一种颜色到另一种颜色的平滑过渡。另外,可以把多个颜色的过渡应用到同一个元素上。

SVG 渐变主要有两种类型:

线性渐变 - <linearGradient>
放射性渐变 - <radialGradient>

SVG 线性渐变 -
SVG 中的 元素用于创建线性渐变,它可以沿着一条直线从一个颜色过渡到另一个颜色,从而创建平滑的渐变效果。线性渐变可用于填充或描边 SVG 图形元素,使其呈现出丰富的颜色变化。

基本语法

<linearGradient id="gradient_id" x1="x1" y1="y1" x2="x2" y2="y2">
  <stop offset="offset1" stop-color="color1" />
  <stop offset="offset2" stop-color="color2" />
  <!-- 更多的 <stop> 元素 -->
</linearGradient>

参数说明:

id 属性定义了线性渐变的唯一标识符,用于在SVG图像中引用该渐变。

x1 和 y1 属性定义了渐变的起始点坐标。

x2 和 y2 属性定义了渐变的结束点坐标。

<stop> 元素用于指定渐变中的颜色和颜色的位置。

线性渐变可以定义为水平,垂直或角渐变:

当 y1 和 y2 相等,而 x1 和 x2 不同时,可创建水平渐变
当 x1 和 x2 相等,而 y1 和 y2 不同时,可创建垂直渐变
当 x1 和 x2 不同,且 y1 和 y2 不同时,可创建角形渐变

实例 1
定义水平线性渐变从黄色到红色的椭圆形:

下面是 SVG 代码:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <defs>
    <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:rgb(255,255,0);stop-opacity:1" />
      <stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1" />
    </linearGradient>
  </defs>
  <ellipse cx="200" cy="70" rx="85" ry="55" fill="url(#grad1)" />
</svg>

13)SVG 渐变- 放射性

SVG 中的 <radialGradient> 元素用于创建径向渐变,它可以从一个中心点向外扩散形成渐变效果,使图形呈现出环形、放射状等丰富的颜色变化。

径向渐变可以应用于填充或描边 SVG 图形元素,为其添加立体感和视觉效果。

基本语法

<radialGradient id="gradient_id" cx="cx" cy="cy" r="r" fx="fx" fy="fy">
  <stop offset="offset1" stop-color="color1" />
  <stop offset="offset2" stop-color="color2" />
  <!-- 更多的 <stop> 元素 -->
</radialGradient>

参数说明:

id 属性定义了径向渐变的唯一标识符,用于在SVG图像中引用该渐变。

cx 和 cy 属性定义了渐变的中心点坐标。

r 属性定义了渐变的半径。

fx 和 fy 属性定义了渐变焦点的坐标(可选),用于控制渐变的形状和方向。

<stop> 元素用于指定渐变中的颜色和颜色的位置。

示例
定义一个放射性渐变从白色到蓝色椭圆:
下面是SVG代码:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
 
<defs>
   
<radialGradient id="grad1" cx="50%" cy="50%" r="50%"
fx="50%" fy="50%">
     
<stop offset="0%" style="stop-color:rgb(255,255,255);
      stop-opacity:0" />
     
<stop offset="100%" style="stop-color:rgb(0,0,255);stop-opacity:1" />
   
</radialGradient>
 
</defs>
 
<ellipse cx="200" cy="70" rx="85" ry="55"
fill="url(#grad1)" />
</svg>

四、SVG图形在线编辑器

https://www.jyshare.com/more/svgeditor/

五、vue页面实例(拿去不谢!)

<template>
  <div >
    <!-- 功能按钮区 -->
    <div style="display: flex; border: lightgrey solid 5px; border-radius: 5px; background-color: lightgrey">
      <el-select placeholder="请选择所属区域"size="small" style="width: 120px;"
        v-model="queryParams.subarea" @change="getMapElementList" >
        <el-option
          v-for="dict in dict.type.heatnet_map_element_subarea"
          :key="dict.value"
          :value="parseInt(dict.value)"
          :label="dict.label">
        </el-option>
      </el-select>

      <el-radio-group v-model="operate" @change="changeOperate" size="small" >
        <el-radio-button label="power" >电厂</el-radio-button>
        <el-radio-button label="consumer" >用户</el-radio-button>
        <el-radio-button label="pipe">管道</el-radio-button>
        <el-radio-button label="valve">阀门</el-radio-button>
        <el-radio-button label="meter">计量表</el-radio-button>
        <el-radio-button label="tag">标签</el-radio-button>
      </el-radio-group>

      <div style="margin-left: 10px;">
        <el-button type="warning" size="small" :disabled="currentElement==null" @click="handleEditElement">编辑图元</el-button>
        <el-button type="danger" size="small" :disabled="currentElement==null" @click="handleDeleteElement" >删除图元</el-button>
        <el-button type="success" size="small" :disabled="!saveFlag" @click="handleSaveLayer">保存布局</el-button>
      </div>
    </div>

    <!-- 地图容器 -->
    <div ref="map-container" id="map-container"
         @mousedown="containerMousedown" @mouseup="containerMouseup" @mousemove="containerMousemove" @dblclick="containerDbclick"
         style="width: 100%; height: 680px; margin: 1px 1px 0px 2px; border: red 0px solid; ">
      <!-- 底图 -->
<!--      <img style="width: 100%; height: 100%; border: lightpink solid 0px;" src="@/assets/heatnet/diagram-all-02.jpg">-->

      <!-- 绘图区域.start -->
      <!-- position: relative; top: -685px; -->
      <svg ref="svg-canvas" id="svg-canvas" style="position: relative; top: -0px; left: 0px; width: 100%; height: 680px; border: #00afff solid 1px;" xmlns="http://www.w3.org/2000/svg">
        <!-- 绘制电厂和用户 -->
        <rect
          v-for="(item, index) in mapElementList.filter((value)=>{ return [0,1,99].includes(value.elementType) })"
          :ref="item.elementId+'_rect'" :id="item.elementId"
          :x="item.coord[0][0]-((item.iconWidth<=0?100:item.iconWidth)/2)"
          :y="item.coord[0][1]-((item.iconHeight<=0?40:item.iconHeight)/2)" rx="10" ry="10"
          :width="item.iconWidth<=0?100:item.iconWidth" :height="item.iconHeight<=0?40:item.iconHeight"
          :stroke="item.lineColor" :stroke-width="item.lineSize" :fill="item.fillColor"
          :transform="'rotate('+item.rotate+' '+item.coord[0][0]+','+item.coord[0][1]+')'"
          style="opacity:0.9; cursor: pointer; "
          @mousedown="elementMousedown" @dblclick="elementDbclick"
          @mouseover="elementMouseover" @mouseout="elementMouseout"
        />
        <text
          v-for="(item, index) in mapElementList.filter((value)=>{ return [0,1,99].includes(value.elementType) })"
          :ref="item.elementId+'_text'" :id="item.elementId"
          :x="item.coord[0][0]-((item.iconWidth<=0?100:item.iconWidth)/2)+10"
          :y="item.coord[0][1]-((item.iconHeight<=0?40:item.iconHeight)/2)+(item.fontSize*1.5)"
          :font-size="item.fontSize" :fill="item.fontColor"
          :transform="'rotate('+item.rotate+' '+item.coord[0][0]+','+item.coord[0][1]+')'"
          style="font-weight: normal; cursor: pointer; user-select: none; "
          @mousedown="elementMousedown" @dblclick="elementDbclick"
          @mouseover="elementMouseover" @mouseout="elementMouseout" >
          {{ item.elementName }}
        </text>

        <!-- 绘制管道 -->
        <path
          v-for="(item, index) in mapElementList.filter((value)=>{ return value.elementType==2 })"
          :ref="item.elementId+'_path'" :id="item.elementId"
          :d="item.path" :stroke="item.lineColor" :stroke-width="item.lineSize" fill="none"
          :transform="'rotate('+item.rotate+' '+item.coord[0][0]+','+item.coord[0][1]+')'"
          style="opacity: 0.9; cursor: pointer; "
          @mousedown="elementMousedown" @dblclick="elementDbclick"
          @mouseover="elementMouseover" @mouseout="elementMouseout"
        />

        <!-- 绘制阀门 -->
        <path
          v-for="(item, index) in mapElementList.filter((value)=>{ return [3,4].includes(value.elementType) })"
          :ref="item.elementId+'_path'" :id="item.elementId"
          :d="item.path" :stroke="item.lineColor" :stroke-width="item.lineSize" :fill="item.fillColor"
          :transform="'rotate('+item.rotate+' '+item.coord[0][0]+','+item.coord[0][1]+')'"
          style="opacity: 0.8; cursor: pointer; "
          @mousedown="elementMousedown" @dblclick="handleEditElement"
          @mouseover="elementMouseover" @mouseout="elementMouseout"
        />

        <!-- 鼠标绘图过程中用于显示轨迹的控件 -->
        <polyline ref="line-pen" points="100,100 200,200 200,300" style="display: none; fill:none; stroke: black; stroke-width:4; opacity: 0.6"/>
        <polyline ref="refer-lineX" points="100,0 100,10000" style="display: none; stroke: black; stroke-width: 1; stroke-dasharray: 5,5;" />
        <polyline ref="refer-lineY" points="0,100 10000,100" style="display: none; stroke: black; stroke-width: 1; stroke-dasharray: 5,5;" />
        <!--        <path ref="path-pen" d="M100 100 l0 40 l60 -40 l0 40 l-30 -20 l0 -20 l0 40 l0 -20 z " transform="rotate(30 100,100)" style="fill: none; stroke: red; stroke-width: 5; " />-->

      </svg>
      <!-- 绘图区域.end -->
    </div>

    <div ref="mytip" style="display: none; position: absolute; top: 100px; left: 200px; padding: 10px;
      border: purple solid 1px; background-color: yellow; opacity: 0.7; border-radius: 3px; font-size: 12px; ">
      用于跟随鼠标显示图元属性的小组件<br><br>dddd
    </div>

    <!-- 图元属性编辑窗口 -->
    <el-dialog :title=dialogTitle :visible.sync="dialogVisible" width="600px" append-to-body>
      <el-form ref="form"  :model="form" :rules="rules" label-width="120px" >
        <el-form-item label="名  称" prop="elementName">
          <el-input v-model="form.elementName" placeholder="请输入图元名称" />
        </el-form-item>
        <el-form-item label="图元宽度" prop="iconWidth" v-show="[0,1,99].includes(form.elementType)">
          <el-input v-model="form.iconWidth" placeholder="请输入图元宽度" />
        </el-form-item>
        <el-form-item label="图元高度" prop="iconHeight" v-show="[0,1,99].includes(form.elementType)">
          <el-input v-model="form.iconHeight" placeholder="请输入图元高度" />
        </el-form-item>
        <el-form-item label="阀门类型" prop="elementType" v-show="[3,4].includes(form.elementType)">
          <el-radio-group v-model="form.elementType">
            <el-radio :label="3">阀门</el-radio>
            <el-radio :label="4">阀门组</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="旋转角度" prop="rotate">
          <el-input-number v-model="form.rotate" :min="-360" :max="360" controls-position="right" />
        </el-form-item>
        <el-form-item label="字体大小" prop="fontSize" v-show="[0,1,99].includes(form.elementType)">
          <el-input v-model="form.fontSize" label="请输入字体大小"></el-input>
        </el-form-item>
        <el-form-item label="字体颜色" prop="fontColor" v-show="[0,1,99].includes(form.elementType)">
          <div style="display: flex; flex-direction: row">
            <el-input v-model="form.fontColor" disabled placeholder="请选择字体颜色" />
            <el-color-picker v-model="form.fontColor"></el-color-picker>
          </div>
        </el-form-item>
        <el-form-item label="框线粗细" prop="lineSize">
          <el-input v-model="form.lineSize" label="请输入框线粗细"></el-input>
        </el-form-item>
        <el-form-item label="框线颜色" prop="lineColor">
          <div style="display: flex; flex-direction: row">
            <el-input v-model="form.lineColor" disabled placeholder="请选择框线颜色" />
            <el-color-picker v-model="form.lineColor"></el-color-picker>
          </div>
        </el-form-item>
        <el-form-item label="填充颜色" prop="fillColor">
          <div style="display: flex; flex-direction: row">
            <el-input v-model="form.fillColor" disabled placeholder="请选择填充颜色" />
            <el-color-picker v-model="form.fillColor"></el-color-picker>
          </div>
        </el-form-item>
        <el-form-item label="告警颜色" prop="alarmColor">
          <div style="display: flex; flex-direction: row">
            <el-input v-model="form.alarmColor" disabled placeholder="请选择告警颜色" />
            <el-color-picker v-model="form.alarmColor"></el-color-picker>
          </div>
        </el-form-item>
        <el-form-item label="坐标" prop="coordinate">
          <el-input v-model="form.coordinate" placeholder="请输入图元坐标" />
        </el-form-item>

        <el-form-item label="前置节点" prop="previousNode">
          <el-select v-model="form.previousNode" placeholder="请选择前置节点" >
            <el-option key="-1" value="-1" label=""></el-option>
            <el-option
              v-for="element in mapElementList"
              :key="element.elementId"
              :value="element.elementId"
              :label="element.elementName">
            </el-option>
          </el-select>
        </el-form-item>

        <el-form-item label="绑定测点" prop="pid">
          <el-select v-model="form.pid" placeholder="请选择需要绑定的测点" >
            <el-option key="-1" value="-1" label=""></el-option>
            <el-option
              v-for="point in measurePointList"
              :key="point.pid"
              :value="point.pid"
              :label="point.pname">
            </el-option>
          </el-select>
        </el-form-item>
      </el-form>

      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button type="default" @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>

</template>

<script>
import { listMapElement, getMapElement, delMapElement, addMapElement, updateMapElement } from "@/api/heatnet/mapElement";
import { listMeasurePoint } from "@/api/heatnet/measurePoint";
import item from "@/layout/components/Sidebar/Item";

export default {
  name: "Draw",
  dicts: ['heatnet_map_element_classify', 'heatnet_map_element_type', 'heatnet_map_element_subarea'],
  data() {
    return {
      // 操作类型(画直线、椭圆等)
      operate: "",
      // 图标拖拽标志
      dragFlag: false,
      // 保存标记
      saveFlag: false,
      // 当前鼠标点选中的图元
      currentElement: null,
      // 图元列表
      mapElementList: [],
      // 测点列表
      measurePointList: [],

      // 图元属性窗口相关变量
      dialogTitle: "属性设置",
      dialogVisible: false,
      form: {
        elementId: null,
        elementName: '',
        elementClassify: 2,
        elementType: null,
        iconWidth: null,
        iconHeight: null,
        iconRx: null,
        iconRy: null,
        rotate: 0,
        fontSize: 12,
        fontColor: '#000000',
        lineSize: 2,
        lineColor: '#FF00EA',
        fillColor: '#1e90ff',
        alarmColor: '#ff0000',
        coordinate: '',
        previousNode: null,
        subarea: null,
        pid: null,
        pname: null,
        remark: null,
      },
      rules: {
        elementName: [
          { required: true, message: "请输入图元名称", trigger: "blur" }
        ],
        coordinate: [
          { required: true, message: "请输入图元坐标", trigger: "blur"}
        ]
      },

      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 9999999,
        elementId: null,
        elementName: null,
        elementClassify: 2,
        elementType: null,
        rotate: null,
        fontSize: null,
        fontColor: null,
        lineSize: null,
        lineColor: null,
        fillColor: null,
        alarmColor: null,
        coordinate: null,
        previousNode: null,
        subarea: 0,
        pid: null,
        pname: null,
        remark: null
      },

    };
  },

  created() {
    this.getMapElementList();
    this.getMeasurePointList();
  },
  beforeDestroy() {
  },
  methods: {
    /* 保存布局 */
    handleSaveLayer() {
      let olist = []
      let flist = []
      for (const element of this.mapElementList) {
        updateMapElement(element).then(response => {
          olist.push(item)
          // 循环结束则弹出提示框并刷新页面
          if (olist.length + flist.length >= this.mapElementList.length) {
            if (flist.length == 0) {
              this.$modal.msgSuccess("保存成功!");
              this.getMapElementList();
            } else {
              this.$modal.msgError("下列图元保存失败!请检查:" + JSON.stringify(flist));
            }
          }
        }).catch(error => {
          flist.push(item)
          // 循环结束则弹出提示框并刷新页面
          if (olist.length + flist.length >= this.mapElementList.length) {
            this.$modal.msgError("下列图元保存失败!请检查:" + JSON.stringify(flist));
          }
        })
      }
    },

    /* 编辑图元属性 */
    handleEditElement() {
      this.dialogTitle = "修改图元属性";
      this.form = this.currentElement;
      this.dialogVisible = true;

      // getMapElement(elementId).then(response => {
      //   this.form = response.data;
      //   this.dialogVisible = true;
      //   this.dialogTitle = "修改图元属性";
      // });
    },

    /** 删除当前选中的图元 **/
    handleDeleteElement() {
      let elementId = this.currentElement.elementId
      if (elementId) {
        this.$modal.confirm('是否确认删除编号为"' + elementId + '"的图元?').then(function() {
          return delMapElement(elementId);
        }).then(() => {
          this.getMapElementList();
          this.$modal.msgSuccess("删除成功");
          this.currentElement = null;
        }).catch(() => {});
      }
    },

    /** 鼠标选中当前图元 **/
    selectElement(e) {
      let elementId = e.currentTarget.id;
      if (!elementId || elementId==='') return;

      for (const item of this.mapElementList) {
        // 找到对应的页面标签
        let rtag = this.$refs[item.elementId+'_rect'] || this.$refs[item.elementId+'_path'];
        let rtext = this.$refs[item.elementId+'_text'];
        if (!rtag || rtag.length<=0)
          continue;

        // 根据是否选中设置边框颜色
        if (item.elementId == elementId) {
          this.currentElement = item;
          rtag[0].style.stroke = "black";
          if (rtext && rtext.length>0) rtext[0].style.fontWeight = "bold";
        } else {
          rtag[0].style.stroke = "";
          if (rtext && rtext.length>0) rtext[0].style.fontWeight = "normal";
        }
      }
    },

    /** 生成界面图元所需要的路径数据 **/
    genTagPath(elem) {
      let coord = elem.coord;
      let path = '';
      switch (elem.elementType) {
        // 电厂、用户
        case 0:
        case 1:
          break;
        // 管道绘制路径
        case 2:
          coord.forEach((value, index)=>{
            path += index==0?'M ':'L ';
            path += `${coord[index][0]} ${coord[index][1]} `
          })
          break;
        // 阀门绘制路径
        case 3:
          path = `M ${coord[0][0]} ${coord[0][1]} l 15 10 l 0 -20 l -30 20 l 0 -20 l 15 10 l 0 -10 l 0 20 `
          break;
        // 阀门组绘制路径
        case 4:
          path = `M ${coord[0][0]} ${coord[0][1]} l 10 15 l -20 0 l 20 -30 l -20 0 l 10 15 l -10 0 l 20 0 `;
          path += `l 5 -5 l 5 10 l 5 -10 l 5 10 l 5 -10 l 5 5 l 20 0 l -10 0 `;
          path += `l 10 15 l -20 0 l 20 -30 l -20 0 l 10 15 l -10 0 `;
          break;
      }
      elem.path = path;
    },

    /** 查询热网地图元素列表 */
    getMapElementList() {
      this.loading = true;
      this.queryParams.elementClassify = 2;
      listMapElement(this.queryParams).then(response => {
        this.mapElementList = []
        response.rows.forEach((item)=>{
          // 整理坐标和旋转角度适配页面显示
          item.coord = JSON.parse(`[${item.coordinate}]`);
          this.genTagPath(item);
          // 将数据存入图元列表
          this.mapElementList.push(item);
        })
        // this.mapElementList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },

    /** 查询测点列表 */
    getMeasurePointList() {
      this.loading = true;
      let param = { pageNum: 1, pageSize: 9999999 }
      listMeasurePoint(param).then(response => {
        this.measurePointList = response.rows;
        this.loading = false;
      });
    },


    /** 鼠标按下事件 **/
    containerMousedown(event) {
      if (this.operate == '' || this.operate == null) return;

      if (event.button == 0) {
        // 按下鼠标左键
        switch (this.operate) {
          // 电厂、用户
          case "power":
          case "consumer":
            this.reset();
            // 设置默认属性和坐标
            this.form.elementType = this.operate=='power'?0:1;
            this.form.iconWidth = 70;
            this.form.iconHeight = 28;
            this.form.rotate = 0;
            this.form.fontSize = 12;
            this.form.fontColor = '#ffffff';
            this.form.lineSize = 2;
            this.form.lineColor = '#1cc61f';
            this.form.fillColor = this.operate=='power'?'#ff0000':'#1e90ff';
            this.form.alarmColor = '#ff0000';
            this.form.coordinate = JSON.stringify([event.layerX, event.layerY]);
            this.dialogTitle = this.operate=='power'?'设置电厂属性':'设置用户属性';
            this.dialogVisible = true;
            break;
          // 管道
          case "pipe":
            // 拾取坐标
            this.form.coordinate = (this.form.coordinate && this.form.coordinate!='')?this.form.coordinate + "," + JSON.stringify([event.layerX, event.layerY]):JSON.stringify([event.layerX, event.layerY]);
            break;
          // 阀门
          case "valve":
            this.reset();
            // 设置默认属性和坐标
            this.form.elementType = 3;
            this.form.lineSize = 4;
            this.form.lineColor = '#68ffde';
            this.form.fillColor = '#ffffff';
            this.form.rotate = 0;
            this.form.coordinate = JSON.stringify([event.layerX, event.layerY]);
            this.dialogTitle = '设置阀门属性';
            this.dialogVisible = true;
            break;
          // 标签
          case "tag":
            this.reset();
            // 设置默认属性和坐标
            this.form.elementType = 99;
            this.form.iconWidth = 100;
            this.form.iconHeight = 28;
            this.form.rotate = 0;
            this.form.fontSize = 12;
            this.form.fontColor = '#000000';
            this.form.lineSize = 1;
            this.form.lineColor = '#000000';
            this.form.fillColor = '#ffffff';
            this.form.alarmColor = '#ff0000';
            this.form.coordinate = JSON.stringify([event.layerX, event.layerY]);
            this.dialogTitle = '设置标签属性';
            this.dialogVisible = true;
            break;
          default: ;
        }
      } else if (event.button == 1) {
        // 按下鼠标中间键
      } else if (event.button == 2) {
        // 按下鼠标右键
      }
    },

    /** 鼠标释放 **/
    containerMouseup(event) {
      this.dragFlag = false;
    },

    /** 鼠标移动 **/
    containerMousemove(event) {
      // 执行拖拽图标功能
      this.dragElement(this.currentElement);
      // 显示绘制视图效果
      this.drawMapView(event);
    },

    /* 鼠标双击事件 */
    containerDbclick(event) {
      if (this.operate == 'pipe') {
        this.form.elementId = null;   // 新增线段
        // 设置默认属性和坐标
        this.form.elementName = '';
        this.form.elementType = 2;
        this.form.lineSize = 5;
        this.form.lineColor = '#009520';
        this.dialogTitle = '设置管道属性';
        this.dialogVisible = true;
      }
    },

    /** 图元鼠标按下事件 **/
    elementMousedown(e) {
      if (this.operate != null && this.operate != '') return;

      this.selectElement(e)
      this.dragFlag = true;
    },

    /** 图元鼠标双击事件 **/
    elementDbclick() {
      if (this.operate != null && this.operate != '') return;

      this.handleEditElement()
    },

    /** 图元鼠标经过事件 **/
    elementMouseover(e) {
      if (this.operate != null && this.operate != '') return;
      const elementId = e.currentTarget.id;
      if (!elementId || elementId=='') {
        // 隐藏提示框
        this.$refs['mytip'].style.display = 'none';
        return;
      }
      const element = this.mapElementList.find((item)=>{ return item.elementId==elementId } )
      if (!element || element=='') return;

      const tips = `名称:${ element.elementName } <br>
                    类型:${ element.elementType==0?"电厂":(element.elementType==1?"用户":(element.elementType==2?"管道":(element.elementType==3?"阀门":(element.elementType==4?"阀门组":(element.elementType==5?"计量表":"标签"))))) } <br>
                    流速:468m³/s <br>
                    温度:89℃ <br>`;

      const mytip = this.$refs['mytip'];
      // console.log(event.clientX, window.innerWidth)
      // console.log(event.clientY, window.innerHeight)
      if (event.clientY > window.innerHeight -200) {
        mytip.style.top = event.layerY-50 + "px";
      } else {
        mytip.style.top = event.layerY+60 + "px";
      }
      if (event.clientX > window.innerWidth - 200) {
        mytip.style.left = event.layerX-120 + "px";
      } else {
        mytip.style.left = event.layerX+10 + "px";
      }
      mytip.innerHTML = tips;
      mytip.style.display = 'block';
    },

    /** 鼠标离开图元 **/
    elementMouseout() {
      this.$refs['mytip'].style.display = 'none';
    },


    /** 显示绘制视图效果 **/
    drawMapView(event) {
      // 绘制元素时,显示光标跟随的参照线
      let referX = this.$refs['refer-lineX']
      let referY = this.$refs['refer-lineY']
      let points = null
      if (this.operate && this.operate != '') {
        points = `0,${event.layerY} 10000,${event.layerY}`;
        referX.setAttribute("points", points);
        referX.style.display = 'block';

        points = `${event.layerX},0 ${event.layerX}, 10000`;
        referY.setAttribute("points", points);
        referY.style.display = 'block';
      } else {
        this.$refs['refer-lineX'].style.display = 'none';
        this.$refs['refer-lineY'].style.display = 'none';
      }

      // 绘制线段时,显示线段跟随鼠标移动的效果
      let line = this.$refs['line-pen']
      if (this.operate !== 'pipe') {
        line.style.display = "none";
      } else {
        if (!this.form.coordinate || this.form.coordinate.trim()=='')
          return;
        points = this.form.coordinate.replaceAll('],[', ' ').replaceAll('[','').replaceAll(']','');
        points += ' ' + event.layerX + ',' + event.layerY;
        line.setAttribute("points", points);
        line.style.display = "block";
      }
    },

    /* 拖拽图标功能 */
    dragElement(elem) {
      // 鼠标松开或未选中图元
      if (!this.dragFlag || !elem || !elem.elementId || elem.elementId=='')
        return;
      // 未移动位置
      if (event.layerX-elem.coord[0][0]==0 && event.layerY-elem.coord[0][1])
        return;
      this.saveFlag = true;

      // 根据鼠标移动调整图元坐标
      switch (elem.elementType) {
        case 0: ;
        case 1: ;
        case 3: ;
        case 4: ;
        case 99:
          elem.coord = [[event.layerX, event.layerY]];
          elem.coordinate = JSON.stringify(elem.coord[0]);
          break;
        case 2:
          let offset = [event.layerX - elem.coord[0][0], event.layerY - elem.coord[0][1]];
          elem.coord.forEach((value)=>{
            value[0] = value[0] + offset[0]
            value[1] = value[1] + offset[1]
          })
          elem.coordinate = JSON.stringify(elem.coord).replace('[[','[').replace(']]',']');
          break;
      }
      // 生成图元path数据
      this.genTagPath(elem);
    },

    /* 图元属性窗口确定按钮 */
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          if (this.form.elementId != null) {
            updateMapElement(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.getMapElementList();
            });
          } else {
            this.form.subarea = this.queryParams.subarea;
            addMapElement(this.form).then(response => {
              this.$modal.msgSuccess("添加成功");
              this.getMapElementList();
            });
          }

          this.operate = '';
          this.drawMapView();
          this.dialogVisible = false;
        }
      });
    },

    /* 图元属性窗口取消按钮 */
    cancel() {
      this.operate = ''
      this.drawMapView();
      this.dialogVisible = false;
      this.reset();
    },

    /* 切换操作按钮时触发的事件 */
    changeOperate() {
      // this.form.elementName = '';
      this.form.coordinate = '';
    },

    // 表单重置
    reset() {
      this.form = {
        elementId: null,
        elementName: '',
        elementClassify: 2,
        elementType: null,
        iconWidth: null,
        iconHeight: null,
        iconRx: null,
        iconRy: null,
        rotate: 0,
        fontSize: 12,
        fontColor: '#000000',
        lineSize: 2,
        lineColor: '#008000',
        fillColor: '#1e90ff',
        alarmColor: '#ff0000',
        coordinate: '',
        previousNode: null,
        pid: null,
        pname: null,
        remark: null,
      };
      this.resetForm("form");
    },

  },
};
</script>

<style scoped lang="scss">

</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值