Merge branch 'dev' of ssh://192.168.28.10:10022/SmartBench/CMVR-IOT-UI into dev
1
public/3.smap
Normal file
54
public/bot.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
>
|
||||
<rect width="22" height="22" x="0" y="0" />
|
||||
<path
|
||||
d="M11 7.33335L11 3.66669L7.33331 3.66669"
|
||||
fill-rule="nonzero"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<rect
|
||||
width="14.666667"
|
||||
height="11.000000"
|
||||
x="3.666687"
|
||||
y="7.333344"
|
||||
rx="1.833333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M1.83331 12.8333L3.66665 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M18.3333 12.8333L20.1666 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M13.75 11.9167L13.75 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M8.25 11.9167L8.25 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
54
src/assets/icons/svg/bot.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
>
|
||||
<rect width="22" height="22" x="0" y="0" />
|
||||
<path
|
||||
d="M11 7.33335L11 3.66669L7.33331 3.66669"
|
||||
fill-rule="nonzero"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<rect
|
||||
width="14.666667"
|
||||
height="11.000000"
|
||||
x="3.666687"
|
||||
y="7.333344"
|
||||
rx="1.833333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M1.83331 12.8333L3.66665 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M18.3333 12.8333L20.1666 12.8333"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M13.75 11.9167L13.75 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
<path
|
||||
d="M8.25 11.9167L8.25 13.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.833333"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
src/assets/icons/svg/chevron-down.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24.000000" height="24.000000" fill="none" customFrame="#000000">
|
||||
<rect id="chevron-down" width="24.000000" height="24.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 82" d="M6 9L12 15L18 9" fill-rule="nonzero" stroke="rgb(0,212,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
4
src/assets/icons/svg/chevron-left.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24.000000" height="24.000000" fill="none" customFrame="#000000">
|
||||
<rect id="chevron-left" width="24.000000" height="24.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 80" d="M15 18L9 12L15 6" fill-rule="nonzero" stroke="rgb(0,212,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 434 B |
4
src/assets/icons/svg/chevron-right.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24.000000" height="24.000000" fill="none" customFrame="#000000">
|
||||
<rect id="chevron-right" width="24.000000" height="24.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 81" d="M9 18L15 12L9 6" fill-rule="nonzero" stroke="rgb(0,212,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 434 B |
16
src/assets/icons/svg/chevron-stop.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52.000000" height="52.000000" fill="none" customFrame="url(#clipPath_0)">
|
||||
<defs>
|
||||
<clipPath id="clipPath_0">
|
||||
<rect width="52.000000" height="52.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,255,255)" />
|
||||
</clipPath>
|
||||
<clipPath id="clipPath_1">
|
||||
<rect width="22.000000" height="22.000000" x="15.000000" y="15.000000" fill="rgb(255,255,255)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="容器 86" width="52.000000" height="52.000000" x="0.000000" y="0.000000" rx="8.000000" fill="rgb(255,43,43)" fill-opacity="0.200000003" />
|
||||
<g id="square" customFrame="url(#clipPath_1)">
|
||||
<rect id="square" width="22.000000" height="22.000000" x="15.000000" y="15.000000" />
|
||||
<rect id="矩形 13" width="16.500000" height="16.500000" x="17.750000" y="17.750000" rx="1.833333" stroke="rgb(255,43,43)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.833333" />
|
||||
</g>
|
||||
<rect id="容器 86" width="51.000000" height="51.000000" x="0.500000" y="0.500000" rx="7.500000" stroke="rgb(255,43,43)" stroke-width="1.000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
src/assets/icons/svg/chevron-up.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24.000000" height="24.000000" fill="none" customFrame="#000000">
|
||||
<rect id="chevron-up" width="24.000000" height="24.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 79" d="M18 15L12 9L6 15" fill-rule="nonzero" stroke="rgb(0,212,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
17
src/assets/icons/svg/cpu.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="cpu" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 61" d="M8 13.3333L8 14.6666" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 62" d="M8 1.33331L8 2.66665" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 63" d="M11.3334 13.3333L11.3334 14.6666" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 64" d="M11.3334 1.33331L11.3334 2.66665" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 65" d="M1.33337 8L2.66671 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 66" d="M1.33337 11.3333L2.66671 11.3333" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 67" d="M1.33337 4.66669L2.66671 4.66669" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 68" d="M13.3334 8L14.6667 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 69" d="M13.3334 11.3333L14.6667 11.3333" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 70" d="M13.3334 4.66669L14.6667 4.66669" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 71" d="M4.66663 13.3333L4.66663 14.6666" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 72" d="M4.66663 1.33331L4.66663 2.66665" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<rect id="矩形 7" width="10.666667" height="10.666667" x="2.666626" y="2.666687" rx="1.333333" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<rect id="矩形 8" width="5.333333" height="5.333333" x="5.333374" y="5.333313" rx="0.666667" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
8
src/assets/icons/svg/gamepad.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="gamepad-2" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<line id="直线 10" x1="4" x2="6.66666698" y1="7.33334351" y2="7.33334351" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 11" x1="0" x2="2.66666675" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(0,1,-1,0,5.33337,6)" />
|
||||
<line id="直线 12" x1="10" x2="10.0066671" y1="8" y2="8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 13" x1="12" x2="12.0066671" y1="6.66665649" y2="6.66665649" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<path id="矢量 73" d="M4.45337 3.33334C4.01626 3.33345 3.58588 3.441 3.20011 3.64653C2.81433 3.85206 2.485 4.14928 2.24109 4.51201C1.99718 4.87474 1.84618 5.29187 1.80137 5.72668C1.79737 5.76134 1.79471 5.79401 1.79004 5.82801C1.73604 6.27734 1.33337 9.63734 1.33337 10.6667C1.33337 11.0177 1.42579 11.3626 1.60132 11.6667C1.77686 11.9707 2.02934 12.2232 2.33337 12.3987C2.63741 12.5743 2.9823 12.6667 3.33337 12.6667C4.00004 12.6667 4.33337 12.3333 4.66671 12L5.60937 11.0573C5.73316 10.9335 5.88011 10.8353 6.04186 10.7683C6.2036 10.7012 6.37696 10.6667 6.55204 10.6667L9.44804 10.6667C9.62312 10.6667 9.79648 10.7012 9.95823 10.7683C10.12 10.8353 10.2669 10.9335 10.3907 11.0573L11.3334 12C11.6667 12.3333 12 12.6667 12.6667 12.6667C13.0178 12.6667 13.3627 12.5743 13.6667 12.3987C13.9707 12.2232 14.2232 11.9707 14.3988 11.6667C14.5743 11.3626 14.6667 11.0177 14.6667 10.6667C14.6667 9.63668 14.264 6.27734 14.21 5.82801C14.2054 5.79468 14.2027 5.76134 14.1987 5.72734C14.154 5.29246 14.0031 4.87523 13.7592 4.5124C13.5153 4.14957 13.186 3.85226 12.8001 3.64666C12.4143 3.44105 11.9839 3.33346 11.5467 3.33334L4.45337 3.33334Z" fill-rule="nonzero" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
9
src/assets/icons/svg/locate-fixed.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="locate-fixed" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<line id="直线 6" x1="1.33337402" x2="3.33337402" y1="8" y2="8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 7" x1="12.666626" x2="14.666626" y1="8" y2="8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 8" x1="0" x2="2" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(0,1,-1,0,8,1.33331)" />
|
||||
<line id="直线 9" x1="0" x2="2" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(0,1,-1,0,8,12.6667)" />
|
||||
<circle id="椭圆 4" cx="8.00004101" cy="8.00001049" r="4.66666698" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<circle id="椭圆 5" cx="8" cy="8" r="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/assets/icons/svg/map.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18.000000" height="18.000000" fill="none" customFrame="#000000">
|
||||
<rect id="map" width="18.000000" height="18.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 20" d="M11.25 4.32296C11.4827 4.32296 11.7123 4.2688 11.9205 4.16476L14.6648 2.79226C14.7792 2.73508 14.9063 2.70812 15.0341 2.71394C15.1619 2.71976 15.2861 2.75817 15.3948 2.82551C15.5036 2.89285 15.5933 2.9869 15.6555 3.0987C15.7177 3.2105 15.7502 3.33634 15.75 3.46426L15.75 13.0373C15.7499 13.1765 15.7111 13.313 15.6378 13.4314C15.5646 13.5498 15.4598 13.6455 15.3353 13.7078L11.9205 15.4155C11.7123 15.5195 11.4827 15.5737 11.25 15.5737C11.0173 15.5737 10.7877 15.5195 10.5795 15.4155L7.4205 13.836C7.2123 13.732 6.98275 13.6778 6.75 13.6778C6.51726 13.6778 6.2877 13.732 6.0795 13.836L3.33525 15.2085C3.22077 15.2657 3.09355 15.2927 2.9657 15.2868C2.83785 15.281 2.71362 15.2425 2.60484 15.1751C2.49606 15.1076 2.40635 15.0135 2.34424 14.9016C2.28213 14.7897 2.24969 14.6637 2.25 14.5358L2.25 4.96351C2.25008 4.82427 2.28891 4.68779 2.36217 4.56937C2.43542 4.45095 2.54019 4.35526 2.66475 4.29301L6.0795 2.58526C6.2877 2.48122 6.51726 2.42706 6.75 2.42706C6.98275 2.42706 7.2123 2.48122 7.4205 2.58526L10.5795 4.16476C10.7877 4.2688 11.0173 4.32296 11.25 4.32296Z" fill-rule="nonzero" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.500000" />
|
||||
<path id="矢量 21" d="M11.25 4.323L11.25 15.573" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.500000" />
|
||||
<path id="矢量 22" d="M6.75 2.427L6.75 13.677" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.500000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
4
src/assets/icons/svg/navigation.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="navigation" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<path id="矢量 78" d="M14.6667 1.33334L8.66667 14L7.33333 8.66668L2 7.33334L14.6667 1.33334Z" fill-rule="nonzero" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
7
src/assets/icons/svg/zoom-in.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="zoom-in" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<circle id="椭圆 2" cx="7.33333349" cy="7.33333349" r="5.33333349" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 1" x1="0" x2="4.10121965" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(-0.707107,-0.707107,0.707107,-0.707107,14,14)" />
|
||||
<line id="直线 2" x1="0" x2="4" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(0,1,-1,0,7.33337,5.33334)" />
|
||||
<line id="直线 3" x1="5.33337402" x2="9.33337402" y1="7.33334351" y2="7.33334351" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
6
src/assets/icons/svg/zoom-out.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none" customFrame="#000000">
|
||||
<rect id="zoom-out" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
|
||||
<circle id="椭圆 3" cx="7.33333349" cy="7.33333349" r="5.33333349" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
<line id="直线 4" x1="0" x2="4.10121965" y1="0" y2="0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" transform="matrix(-0.707107,-0.707107,0.707107,-0.707107,14,14)" />
|
||||
<line id="直线 5" x1="5.33337402" x2="9.33337402" y1="7.33334351" y2="7.33334351" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333333" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 835 B |
@ -1,53 +1,42 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true">
|
||||
<use :xlink:href="iconName" :fill="color" />
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
:style="svgStyle"
|
||||
class="svg-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default defineComponent({
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
iconName: computed(() => `#icon-${props.iconClass}`),
|
||||
svgClass: computed(() => {
|
||||
if (props.className) {
|
||||
return `svg-icon ${props.className}`
|
||||
}
|
||||
return 'svg-icon'
|
||||
})
|
||||
}
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 24
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor'
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const symbolId = computed(() => `#icon-${props.name}`)
|
||||
|
||||
const svgStyle = computed(() => ({
|
||||
verticalAlign: 'middle',
|
||||
color: props.color, // 用 color 替代 fill
|
||||
...props.customStyle
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.sub-el-icon,
|
||||
.nav-icon {
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
margin-right: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
fill: currentColor;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -14,6 +14,16 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
// 判断是否需要新窗口打开(内部页面)
|
||||
const needNewWindow = computed(() => {
|
||||
|
||||
// 获取路径字符串
|
||||
const path = typeof props.to === 'string' ? props.to : props.to.path
|
||||
// 规定:路径以 /new-window/ 开头的就在新窗口打开
|
||||
const list = ['/inspection/cockpit']
|
||||
return list.includes(path)
|
||||
})
|
||||
|
||||
const isExt = computed(() => {
|
||||
return isExternal(props.to)
|
||||
})
|
||||
@ -33,6 +43,19 @@ function linkProps() {
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
|
||||
if (needNewWindow.value) {
|
||||
// 如果是对象,需要拼接路径
|
||||
const fullPath = typeof props.to === 'string'
|
||||
? props.to
|
||||
: props.to.path + (props.to.query ? '?' + new URLSearchParams(props.to.query).toString() : '')
|
||||
return {
|
||||
href: fullPath,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
to: props.to
|
||||
}
|
||||
|
||||
@ -127,6 +127,14 @@ export const constantRoutes = [
|
||||
meta: { title: "触摸交互", noLogin: true },
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
//cockpit
|
||||
path: "/inspection/cockpit",
|
||||
component: () =>
|
||||
import("@/views/inspection/cockpit/index.vue"),
|
||||
meta: { title: "logicFlow", noLogin: true },
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
//logicFlow
|
||||
path: "/logic_flow",
|
||||
|
||||
37
src/store/modules/inspection.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { update } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useInspectionStore = defineStore('inspection', {
|
||||
state: () => ({
|
||||
gridOffscreen: null, // 栅格图像(离屏Canvas)
|
||||
hoveredElement: null, // 当前悬停的元素 {kind, index}
|
||||
animationTimeSeconds: 0, // 全局动画时间戳(秒),用于脉冲动画效果
|
||||
camera: { // 相机参数
|
||||
centerX: 0, // 相机中心的世界坐标 X
|
||||
centerY: 0, // 相机中心的世界坐标 Y
|
||||
pixelsPerMeter: 40, // 缩放:每米对应的屏幕像素数
|
||||
},
|
||||
canvasWidthPx: 0, // Canvas CSS 逻辑宽度(像素)
|
||||
canvasHeightPx: 0, // Canvas CSS 逻辑高度(像素)
|
||||
devicePixelRatio: 1, // 设备像素比(高DPI屏幕 > 1)
|
||||
}),
|
||||
actions: {
|
||||
updateCamera(value) {
|
||||
this.camera = { ...this.camera, ...value }
|
||||
},
|
||||
updateCanvasSize(width, height) {
|
||||
this.canvasWidthPx = width
|
||||
this.canvasHeightPx = height
|
||||
this.devicePixelRatio = window.devicePixelRatio || 1
|
||||
},
|
||||
async getCanvasWH() {
|
||||
const data = {
|
||||
canvasWidthPx: this.canvasWidthPx,
|
||||
canvasHeightPx: this.canvasHeightPx,
|
||||
devicePixelRatio: this.devicePixelRatio,
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
3
src/views/inspection/alarm/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>告警记录</div>
|
||||
</template>
|
||||
599
src/views/inspection/cockpit/canvasUtils.js
Normal file
@ -0,0 +1,599 @@
|
||||
import { useInspectionStore } from "@/store/modules/inspection";
|
||||
|
||||
const inspectionStore = useInspectionStore();
|
||||
|
||||
|
||||
/**
|
||||
* 6位十六进制颜色字符串转为 CSS rgba 字符串
|
||||
* @param {string} hex - 如 "#ff6e40"
|
||||
* @param {number} alpha - 透明度 0~1
|
||||
* @returns {string} CSS rgba 字符串
|
||||
*/
|
||||
function hexToCssRgba(hexString, alpha) {
|
||||
const r = parseInt(hexString.slice(1, 3), 16); // 提取 R 分量
|
||||
const g = parseInt(hexString.slice(3, 5), 16); // 提取 G 分量
|
||||
const b = parseInt(hexString.slice(5, 7), 16); // 提取 B 分量
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将世界坐标转换为 Canvas 屏幕像素坐标
|
||||
* 注意:世界坐标 Y 轴向上,Canvas Y 轴向下,因此需要翻转
|
||||
* @param {number} worldX - 世界坐标 X(米)
|
||||
* @param {number} worldY - 世界坐标 Y(米)
|
||||
* @returns {{x: number, y: number}} Canvas 上的屏幕像素坐标
|
||||
*/
|
||||
export const worldToScreen = (worldX, worldY) => {
|
||||
const camera = inspectionStore.camera;
|
||||
const screenX = (worldX - camera.centerX) * camera.pixelsPerMeter + inspectionStore.canvasWidthPx / 2; // 水平偏移 + 居中
|
||||
const screenY = -(worldY - camera.centerY) * camera.pixelsPerMeter + inspectionStore.canvasHeightPx / 2; // 垂直翻转 + 居中
|
||||
return { x: screenX, y: screenY };
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 Canvas 屏幕像素坐标反算为世界坐标
|
||||
* 与 worldToScreen 互为逆运算
|
||||
* @param {number} screenX - Canvas 像素 X
|
||||
* @param {number} screenY - Canvas 像素 Y
|
||||
* @returns {{x: number, y: number}} 世界坐标(米)
|
||||
*/
|
||||
export const screenToWorld = (screenX, screenY) => {
|
||||
const camera = inspectionStore.camera;
|
||||
const worldX = (screenX - inspectionStore.canvasWidthPx / 2) / camera.pixelsPerMeter + camera.centerX; // 水平反算
|
||||
const worldY = -(screenY - inspectionStore.canvasHeightPx / 2) / camera.pixelsPerMeter + camera.centerY; // 垂直反算(带翻转)
|
||||
return { x: worldX, y: worldY };
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 0xAARRGGBB 整数颜色解码为 {r, g, b, a} 对象
|
||||
* @param {number} argbInt - 32位 ARGB 整数,如 0xCCFF0040
|
||||
* @returns {{r:number, g:number, b:number, a:number}|null} RGBA 分量 (0~255),无效输入返回 null
|
||||
*/
|
||||
export const decodeArgbInteger = (argbInt) => {
|
||||
if (typeof argbInt !== 'number') return null; // 非数字类型直接返回
|
||||
return {
|
||||
a: (argbInt >>> 24) & 0xFF, // 高8位:Alpha 透明度
|
||||
r: (argbInt >>> 16) & 0xFF, // 次高8位:Red
|
||||
g: (argbInt >>> 8) & 0xFF, // 次低8位:Green
|
||||
b: argbInt & 0xFF, // 低8位:Blue
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 decodeArgbInteger 的结果转为 CSS rgba() 字符串
|
||||
* @param {{r,g,b,a}} colorObj - RGBA 分量对象
|
||||
* @param {number} [alphaOverride] - 可选,覆盖 alpha 值(0~1)
|
||||
* @returns {string|null} CSS rgba 字符串,如 "rgba(255,0,64,0.3)"
|
||||
*/
|
||||
export const toCssRgba = (colorObj, alphaOverride) => {
|
||||
if (!colorObj) return null; // 空对象返回 null
|
||||
const alpha = alphaOverride !== undefined // 如果提供了覆盖值
|
||||
? alphaOverride // 使用覆盖值
|
||||
: colorObj.a / 255; // 否则将 0~255 映射到 0~1
|
||||
return `rgba(${colorObj.r},${colorObj.g},${colorObj.b},${alpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格底图绘制函数
|
||||
* 将预渲染的离屏 Canvas 图像绘制到主 Canvas 上,并根据当前地图范围进行缩放和平移
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawGrid = (canvasCtx, map) => {
|
||||
const topLeftScreen = worldToScreen(map.originX, map.maxY); // 图像左上角(世界坐标→屏幕)
|
||||
const bottomRightScreen = worldToScreen(map.maxX, map.originY); // 图像右下角(世界坐标→屏幕)
|
||||
canvasCtx.imageSmoothingEnabled = false; // 关闭抗锯齿(保持像素清晰)
|
||||
canvasCtx.drawImage(
|
||||
inspectionStore.gridOffscreen, // 离屏 Canvas 图像源
|
||||
topLeftScreen.x, topLeftScreen.y, // 目标左上角
|
||||
bottomRightScreen.x - topLeftScreen.x, // 目标宽度
|
||||
bottomRightScreen.y - topLeftScreen.y // 目标高度
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格辅助线(每1米一条)
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} canvasWidthPx
|
||||
* @param {*} canvasHeightPx
|
||||
*/
|
||||
export const drawGridlines = (canvasCtx) => {
|
||||
// 计算当前可视范围的世界坐标
|
||||
const viewTopLeft = screenToWorld(0, 0); // 视口左上角世界坐标
|
||||
const viewBottomRight = screenToWorld(inspectionStore.canvasWidthPx, inspectionStore.canvasHeightPx); // 视口右下角世界坐标
|
||||
const viewMinX = Math.floor(Math.min(viewTopLeft.x, viewBottomRight.x)); // 可视X最小整数
|
||||
const viewMaxX = Math.ceil(Math.max(viewTopLeft.x, viewBottomRight.x)); // 可视X最大整数
|
||||
const viewMinY = Math.floor(Math.min(viewTopLeft.y, viewBottomRight.y)); // 可视Y最小整数
|
||||
const viewMaxY = Math.ceil(Math.max(viewTopLeft.y, viewBottomRight.y)); // 可视Y最大整数
|
||||
|
||||
// 绘制1米间隔的浅色网格线
|
||||
canvasCtx.strokeStyle = 'rgba(30,50,80,0.25)'; // 淡蓝灰色
|
||||
canvasCtx.lineWidth = 0.5; // 细线
|
||||
canvasCtx.beginPath();
|
||||
for (let worldX = viewMinX; worldX <= viewMaxX; worldX++) { // 逐列绘制垂直线
|
||||
const screenPos = worldToScreen(worldX, 0);
|
||||
canvasCtx.moveTo(screenPos.x, 0); // 从画布顶部
|
||||
canvasCtx.lineTo(screenPos.x, inspectionStore.canvasHeightPx); // 到画布底部
|
||||
}
|
||||
for (let worldY = viewMinY; worldY <= viewMaxY; worldY++) { // 逐行绘制水平线
|
||||
const screenPos = worldToScreen(0, worldY);
|
||||
canvasCtx.moveTo(0, screenPos.y); // 从画布左边
|
||||
canvasCtx.lineTo(inspectionStore.canvasWidthPx, screenPos.y); // 到画布右边
|
||||
}
|
||||
canvasCtx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制高级区域列表
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawAdvancedAreaList = (canvasCtx, map) => {
|
||||
for (let areaIndex = 0; areaIndex < map.advancedAreas.length; areaIndex++) {
|
||||
const area = map.advancedAreas[areaIndex]; // 当前区域
|
||||
const vertices = area.posGroup || []; // 多边形顶点数组
|
||||
if (vertices.length < 3) continue; // 少于3个顶点无法构成多边形,跳过
|
||||
|
||||
// ── 颜色:优先使用 attribute 中的 ARGB 整数,否则用默认品红色 ──
|
||||
let fillColorCss = 'rgba(224,64,251,0.12)'; // 默认填充(品红半透明)
|
||||
let strokeColorCss = 'rgba(224,64,251,0.5)'; // 默认描边(品红较不透明)
|
||||
if (area.attribute) {
|
||||
const brushColor = decodeArgbInteger(area.attribute.colorBrush); // 解码填充色
|
||||
const penColor = decodeArgbInteger(area.attribute.colorPen); // 解码描边色
|
||||
if (brushColor) {
|
||||
// 使用原始 alpha 值的一半作为填充透明度,最小 0.06
|
||||
fillColorCss = toCssRgba(brushColor, Math.max(0.06, (brushColor.a / 255) * 0.5));
|
||||
}
|
||||
if (penColor) {
|
||||
strokeColorCss = toCssRgba(penColor, 0.6); // 描边60%不透明
|
||||
}
|
||||
}
|
||||
|
||||
// ── 绘制多边形 ──
|
||||
canvasCtx.fillStyle = fillColorCss; // 设置填充色
|
||||
canvasCtx.strokeStyle = strokeColorCss; // 设置描边色
|
||||
canvasCtx.lineWidth = 1.2; // 描边宽度
|
||||
canvasCtx.beginPath();
|
||||
const firstVertexScreen = worldToScreen(vertices[0].x, vertices[0].y); // 第一个顶点的屏幕位置
|
||||
canvasCtx.moveTo(firstVertexScreen.x, firstVertexScreen.y); // 移动到起点
|
||||
for (let i = 1; i < vertices.length; i++) { // 依次连接后续顶点
|
||||
const vertexScreen = worldToScreen(vertices[i].x, vertices[i].y);
|
||||
canvasCtx.lineTo(vertexScreen.x, vertexScreen.y);
|
||||
}
|
||||
canvasCtx.closePath(); // 闭合路径
|
||||
canvasCtx.fill(); // 填充
|
||||
canvasCtx.stroke(); // 描边
|
||||
|
||||
// ── 在多边形质心处绘制区域名称标签 ──
|
||||
let centroidWorldX = 0, centroidWorldY = 0; // 质心世界坐标
|
||||
for (const v of vertices) { centroidWorldX += v.x; centroidWorldY += v.y; } // 累加顶点坐标
|
||||
centroidWorldX /= vertices.length; // 求平均得到质心 X
|
||||
centroidWorldY /= vertices.length; // 求平均得到质心 Y
|
||||
const centroidScreen = worldToScreen(centroidWorldX, centroidWorldY);
|
||||
canvasCtx.font = '500 8px "JetBrains Mono"'; // 标签字体
|
||||
canvasCtx.fillStyle = strokeColorCss; // 与描边同色
|
||||
canvasCtx.textAlign = 'center'; // 居中对齐
|
||||
canvasCtx.fillText(area.instanceName || '', centroidScreen.x, centroidScreen.y + 3); // 绘制文字
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 绘制高级线列表
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawAdvancedLineList = (canvasCtx, map) => {
|
||||
for (let advLineIndex = 0; advLineIndex < map.advancedLines.length; advLineIndex++) {
|
||||
const advLine = map.advancedLines[advLineIndex]; // 当前高级线
|
||||
const lineData = advLine.line; // 内部的 line 对象
|
||||
if (!lineData || !lineData.startPos || !lineData.endPos) continue; // 缺少端点则跳过
|
||||
|
||||
const startScreen = worldToScreen(lineData.startPos.x, lineData.startPos.y);
|
||||
const endScreen = worldToScreen(lineData.endPos.x, lineData.endPos.y);
|
||||
|
||||
// ── 根据 className 决定颜色和线型 ──
|
||||
let lineColorHex = '#ff6e40'; // 默认橙色
|
||||
let lineDashPattern = []; // 默认实线(无虚线)
|
||||
const classNameLower = (advLine.className || '').toLowerCase();
|
||||
if (classNameLower.includes('forbidden')) { // 禁行线 → 红色虚线
|
||||
lineColorHex = '#ff1744';
|
||||
lineDashPattern = [8, 4]; // 虚线模式:8px实 4px空
|
||||
} else if (classNameLower.includes('virtual')) { // 虚拟线 → 橙色虚线
|
||||
lineColorHex = '#ffab40';
|
||||
lineDashPattern = [4, 4];
|
||||
}
|
||||
|
||||
// 绘制高级线
|
||||
canvasCtx.save();
|
||||
canvasCtx.shadowColor = lineColorHex; // 辉光色与线条同色
|
||||
canvasCtx.shadowBlur = 4; // 辉光半径
|
||||
canvasCtx.strokeStyle = lineColorHex; // 线条颜色
|
||||
canvasCtx.lineWidth = 2.5; // 线宽(比基础线粗)
|
||||
canvasCtx.setLineDash(lineDashPattern); // 设置虚线模式
|
||||
canvasCtx.globalAlpha = 0.8; // 80%不透明
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(startScreen.x, startScreen.y);
|
||||
canvasCtx.lineTo(endScreen.x, endScreen.y);
|
||||
canvasCtx.stroke();
|
||||
canvasCtx.setLineDash([]); // 恢复实线
|
||||
canvasCtx.restore();
|
||||
|
||||
// 在中点绘制名称标签
|
||||
// const midpointScreenX = (startScreen.x + endScreen.x) / 2;
|
||||
// const midpointScreenY = (startScreen.y + endScreen.y) / 2;
|
||||
// canvasCtx.font = '500 8px "JetBrains Mono"';
|
||||
// canvasCtx.fillStyle = hexToCssRgba(lineColorHex, 0.55); // 55%不透明的文字
|
||||
// canvasCtx.textAlign = 'center';
|
||||
// canvasCtx.fillText(advLine.instanceName || advLine.className || '', midpointScreenX, midpointScreenY - 6);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 绘制基础直线列表
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawNormalLines = (canvasCtx, map) => {
|
||||
for (let lineIndex = 0; lineIndex < map.normalLines.length; lineIndex++) {
|
||||
const line = map.normalLines[lineIndex]; // 当前直线
|
||||
const startScreen = worldToScreen(line.startPos.x, line.startPos.y); // 起点屏幕位置
|
||||
const endScreen = worldToScreen(line.endPos.x, line.endPos.y); // 终点屏幕位置
|
||||
|
||||
// 绘制发光直线
|
||||
canvasCtx.save();
|
||||
canvasCtx.shadowColor = '#00e5ff'; // 阴影颜色(产生辉光)
|
||||
canvasCtx.shadowBlur = 5; // 辉光扩散半径
|
||||
canvasCtx.strokeStyle = '#00e5ff'; // 线条颜色:青色
|
||||
canvasCtx.lineWidth = 2; // 线宽
|
||||
canvasCtx.globalAlpha = 0.8; // 80%不透明
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(startScreen.x, startScreen.y); // 从起点
|
||||
canvasCtx.lineTo(endScreen.x, endScreen.y); // 到终点
|
||||
canvasCtx.stroke();
|
||||
canvasCtx.restore();
|
||||
|
||||
// 在中点绘制方向箭头
|
||||
const midpointX = (startScreen.x + endScreen.x) / 2; // 中点X
|
||||
const midpointY = (startScreen.y + endScreen.y) / 2; // 中点Y
|
||||
const arrowAngle = Math.atan2(endScreen.y - startScreen.y, endScreen.x - startScreen.x); // 线段方向角
|
||||
const arrowLength = 5; // 箭头长度
|
||||
canvasCtx.fillStyle = 'rgba(0,229,255,0.6)'; // 箭头填充色
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(midpointX + Math.cos(arrowAngle) * arrowLength, midpointY + Math.sin(arrowAngle) * arrowLength); // 箭头尖端
|
||||
canvasCtx.lineTo(midpointX + Math.cos(arrowAngle + 2.5) * 4, midpointY + Math.sin(arrowAngle + 2.5) * 4); // 左翼
|
||||
canvasCtx.lineTo(midpointX + Math.cos(arrowAngle - 2.5) * 4, midpointY + Math.sin(arrowAngle - 2.5) * 4); // 右翼
|
||||
canvasCtx.closePath();
|
||||
canvasCtx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 绘制高级曲线列表
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawAdvancedCurveList = (canvasCtx, map) => {
|
||||
for (let curveIndex = 0; curveIndex < map.advancedCurves.length; curveIndex++) {
|
||||
const curve = map.advancedCurves[curveIndex]; // 当前曲线
|
||||
|
||||
// 提取起点和终点的 pos(注意:startPos/endPos 是 {instanceName, pos:{x,y}} 结构)
|
||||
const startPoint = curve.startPos?.pos; // 起点世界坐标
|
||||
const endPoint = curve.endPos?.pos; // 终点世界坐标
|
||||
const controlPoint1 = curve.controlPos1; // 第一控制点(直接 {x,y})
|
||||
const controlPoint2 = curve.controlPos2; // 第二控制点(直接 {x,y})
|
||||
if (!startPoint || !endPoint) continue; // 缺少端点则跳过
|
||||
|
||||
// 转换为屏幕坐标
|
||||
const startScreen = worldToScreen(startPoint.x, startPoint.y);
|
||||
const endScreen = worldToScreen(endPoint.x, endPoint.y);
|
||||
const cp1Screen = controlPoint1 ? worldToScreen(controlPoint1.x, controlPoint1.y) : null; // 可能不存在
|
||||
const cp2Screen = controlPoint2 ? worldToScreen(controlPoint2.x, controlPoint2.y) : null;
|
||||
|
||||
// ── 绘制贝塞尔曲线(虚线)──
|
||||
canvasCtx.save();
|
||||
canvasCtx.shadowColor = '#76ff03'; // 酸绿色辉光
|
||||
canvasCtx.shadowBlur = 5;
|
||||
canvasCtx.strokeStyle = '#76ff03'; // 酸绿色线条
|
||||
canvasCtx.lineWidth = 2;
|
||||
canvasCtx.setLineDash([8, 5]); // 虚线模式
|
||||
canvasCtx.globalAlpha = 0.85;
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(startScreen.x, startScreen.y); // 起点
|
||||
if (cp1Screen && cp2Screen) {
|
||||
// 有2个控制点 → 三次贝塞尔 (cubic bezier)
|
||||
canvasCtx.bezierCurveTo(cp1Screen.x, cp1Screen.y, cp2Screen.x, cp2Screen.y, endScreen.x, endScreen.y);
|
||||
} else if (cp1Screen) {
|
||||
// 仅1个控制点 → 二次贝塞尔 (quadratic bezier)
|
||||
canvasCtx.quadraticCurveTo(cp1Screen.x, cp1Screen.y, endScreen.x, endScreen.y);
|
||||
} else {
|
||||
// 无控制点 → 直线
|
||||
canvasCtx.lineTo(endScreen.x, endScreen.y);
|
||||
}
|
||||
canvasCtx.stroke();
|
||||
canvasCtx.setLineDash([]); // 恢复实线
|
||||
canvasCtx.restore();
|
||||
|
||||
// ── 在 t=0.5 处绘制方向箭头 ──
|
||||
if (cp1Screen && cp2Screen) {
|
||||
const t = 0.5; // 参数 t = 0.5(曲线中点)
|
||||
const oneMinusT = 0.5; // 1-t = 0.5
|
||||
// 三次贝塞尔公式计算中点坐标
|
||||
const arrowX = oneMinusT ** 3 * startScreen.x + 3 * oneMinusT ** 2 * t * cp1Screen.x + 3 * oneMinusT * t ** 2 * cp2Screen.x + t ** 3 * endScreen.x;
|
||||
const arrowY = oneMinusT ** 3 * startScreen.y + 3 * oneMinusT ** 2 * t * cp1Screen.y + 3 * oneMinusT * t ** 2 * cp2Screen.y + t ** 3 * endScreen.y;
|
||||
// 三次贝塞尔的一阶导数(切线方向)
|
||||
const tangentX = 3 * oneMinusT ** 2 * (cp1Screen.x - startScreen.x) + 6 * oneMinusT * t * (cp2Screen.x - cp1Screen.x) + 3 * t ** 2 * (endScreen.x - cp2Screen.x);
|
||||
const tangentY = 3 * oneMinusT ** 2 * (cp1Screen.y - startScreen.y) + 6 * oneMinusT * t * (cp2Screen.y - cp1Screen.y) + 3 * t ** 2 * (endScreen.y - cp2Screen.y);
|
||||
const tangentAngle = Math.atan2(tangentY, tangentX); // 切线角度
|
||||
|
||||
// 绘制箭头
|
||||
canvasCtx.fillStyle = 'rgba(118,255,3,0.7)';
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(arrowX + Math.cos(tangentAngle) * 5, arrowY + Math.sin(tangentAngle) * 5);
|
||||
canvasCtx.lineTo(arrowX + Math.cos(tangentAngle + 2.5) * 4, arrowY + Math.sin(tangentAngle + 2.5) * 4);
|
||||
canvasCtx.lineTo(arrowX + Math.cos(tangentAngle - 2.5) * 4, arrowY + Math.sin(tangentAngle - 2.5) * 4);
|
||||
canvasCtx.closePath();
|
||||
canvasCtx.fill();
|
||||
|
||||
// 绘制控制点小圆(辅助调试用)
|
||||
canvasCtx.fillStyle = 'rgba(118,255,3,0.25)'; // 浅绿色半透明
|
||||
for (const controlScreen of [cp1Screen, cp2Screen]) {
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.arc(controlScreen.x, controlScreen.y, 2.5, 0, Math.PI * 2); // 半径2.5px的小圆
|
||||
canvasCtx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 曲线名称标签 ──
|
||||
// canvasCtx.font = '500 7px "JetBrains Mono"';
|
||||
// canvasCtx.fillStyle = 'rgba(118,255,3,0.5)';
|
||||
// canvasCtx.textAlign = 'center';
|
||||
// const labelScreenY = Math.min(startScreen.y, endScreen.y) - 8; // 标签放在最高点上方
|
||||
// canvasCtx.fillText(curve.instanceName || '', (startScreen.x + endScreen.x) / 2, labelScreenY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 绘制高级点列表
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawAdvancedPointList = (canvasCtx, map) => {
|
||||
for (let pointIndex = 0; pointIndex < map.advancedPoints.length; pointIndex++) {
|
||||
const advPoint = map.advancedPoints[pointIndex]; // 当前高级点
|
||||
const pointScreen = worldToScreen(advPoint.pos.x, advPoint.pos.y); // 屏幕位置
|
||||
|
||||
// 判断是否被鼠标悬停
|
||||
const isHovered = inspectionStore.hoveredElement &&
|
||||
inspectionStore.hoveredElement.kind === 'point' &&
|
||||
inspectionStore.hoveredElement.index === pointIndex;
|
||||
|
||||
// ── 根据 className 决定颜色 ──
|
||||
let pointColorHex = '#ffd600'; // 默认琥珀色
|
||||
const classNameLower = (advPoint.className || '').toLowerCase();
|
||||
if (classNameLower.includes('landmark') || classNameLower.includes('land')) {
|
||||
pointColorHex = '#ffd600'; // LandMark → 琥珀色
|
||||
} else if (classNameLower.includes('charge')) {
|
||||
pointColorHex = '#22c55e'; // 充电点 → 绿色
|
||||
} else if (classNameLower.includes('load')) {
|
||||
pointColorHex = '#3b82f6'; // 装载点 → 蓝色
|
||||
} else if (classNameLower.includes('work') || classNameLower.includes('process')) {
|
||||
pointColorHex = '#a855f7'; // 工位 → 紫色
|
||||
} else if (classNameLower.includes('station')) {
|
||||
pointColorHex = '#00e5ff'; // 站点 → 青色
|
||||
}
|
||||
|
||||
// ── 绘制菱形标记 ──
|
||||
const diamondRadius = isHovered ? 8 : 5; // 悬停时放大
|
||||
const pulseAlpha = isHovered ? Math.sin(inspectionStore.animationTimeSeconds * 5) * 0.2 + 0.8 : 1; // 悬停时脉冲闪烁
|
||||
|
||||
canvasCtx.save();
|
||||
canvasCtx.globalAlpha = pulseAlpha; // 应用透明度
|
||||
canvasCtx.shadowColor = pointColorHex; // 辉光颜色
|
||||
canvasCtx.shadowBlur = isHovered ? 18 : 8; // 悬停时增强辉光
|
||||
|
||||
// 绘制旋转45度的正方形(菱形)
|
||||
canvasCtx.fillStyle = pointColorHex;
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(pointScreen.x, pointScreen.y - diamondRadius); // 上顶点
|
||||
canvasCtx.lineTo(pointScreen.x + diamondRadius * 0.7, pointScreen.y); // 右顶点
|
||||
canvasCtx.lineTo(pointScreen.x, pointScreen.y + diamondRadius); // 下顶点
|
||||
canvasCtx.lineTo(pointScreen.x - diamondRadius * 0.7, pointScreen.y); // 左顶点
|
||||
canvasCtx.closePath();
|
||||
canvasCtx.fill();
|
||||
|
||||
// 中心白色小圆点(指示精确位置)
|
||||
canvasCtx.shadowBlur = 0;
|
||||
canvasCtx.fillStyle = '#fff';
|
||||
canvasCtx.globalAlpha = 0.9;
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.arc(pointScreen.x, pointScreen.y, 1.8, 0, Math.PI * 2); // 半径1.8px
|
||||
canvasCtx.fill();
|
||||
canvasCtx.restore();
|
||||
|
||||
// ── 方向箭头(基于 dir 字段,弧度)──
|
||||
if (advPoint.dir !== undefined && advPoint.dir !== null) {
|
||||
// dir 可能是字符串(smap 样例中为 "-1.5759999999999958")或数字
|
||||
const dirRadians = typeof advPoint.dir === 'string' ? parseFloat(advPoint.dir) : advPoint.dir;
|
||||
if (!isNaN(dirRadians)) { // 有效方向角
|
||||
const arrowLengthPx = 12; // 箭头长度(屏幕像素)
|
||||
canvasCtx.strokeStyle = hexToCssRgba(pointColorHex, 0.5); // 半透明的线条
|
||||
canvasCtx.lineWidth = 1;
|
||||
canvasCtx.beginPath();
|
||||
canvasCtx.moveTo(pointScreen.x, pointScreen.y); // 从点中心出发
|
||||
// 注意:世界坐标 Y 向上,但 atan2 使用的 dir 是标准数学角,屏幕 Y 翻转后取负
|
||||
canvasCtx.lineTo(
|
||||
pointScreen.x + Math.cos(dirRadians) * arrowLengthPx,
|
||||
pointScreen.y - Math.sin(dirRadians) * arrowLengthPx // Y 翻转
|
||||
);
|
||||
canvasCtx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 名称标签 ──
|
||||
canvasCtx.font = '500 7px "JetBrains Mono"';
|
||||
canvasCtx.fillStyle = hexToCssRgba(pointColorHex, 0.7);
|
||||
canvasCtx.textAlign = 'center';
|
||||
canvasCtx.fillText(advPoint.instanceName || '', pointScreen.x, pointScreen.y - diamondRadius - 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制地图边界
|
||||
* @param {*} canvasCtx
|
||||
* @param {*} map
|
||||
*/
|
||||
export const drawMapBoundary = (canvasCtx, map) => {
|
||||
const boundaryTopLeft = worldToScreen(map.originX, map.maxY); // 边界左上角屏幕位置
|
||||
const boundaryBottomRight = worldToScreen(map.maxX, map.originY); // 边界右下角屏幕位置
|
||||
canvasCtx.strokeStyle = 'rgba(0,229,160,0.1)'; // 极淡的青绿色
|
||||
canvasCtx.lineWidth = 1;
|
||||
canvasCtx.setLineDash([5, 4]); // 虚线
|
||||
canvasCtx.strokeRect(
|
||||
boundaryTopLeft.x, boundaryTopLeft.y,
|
||||
boundaryBottomRight.x - boundaryTopLeft.x,
|
||||
boundaryBottomRight.y - boundaryTopLeft.y
|
||||
);
|
||||
canvasCtx.setLineDash([]); // 恢复实线
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 normalPosList 构建占据栅格图像
|
||||
*
|
||||
* 原理:normalPosList 中每个点标记一个自由通行格子。
|
||||
* 未出现在列表中的格子 = 障碍物(墙壁)
|
||||
* 墙壁格子如果紧邻自由格子,则标记为"墙壁边缘",渲染高亮
|
||||
*
|
||||
* @param {Object} parsedMap - parseSmapJson 的输出
|
||||
* @param {boolean} showEdgeGlow - 是否计算墙壁边缘高亮效果
|
||||
* @returns {HTMLCanvasElement} 离屏 Canvas 元素,包含渲染好的栅格图像
|
||||
*/
|
||||
export const buildOccupancyGridImage = (parsedMap, showEdgeGlow) => {
|
||||
const gridW = parsedMap.gridWidth; // 栅格宽度(像素)
|
||||
const gridH = parsedMap.gridHeight; // 栅格高度(像素)
|
||||
const originX = parsedMap.originX; // 世界坐标原点 X
|
||||
const originY = parsedMap.originY; // 世界坐标原点 Y
|
||||
const resolution = parsedMap.resolution; // 分辨率
|
||||
|
||||
// ── 第一步:遍历 normalPosList,标记自由空间 ──
|
||||
const occupancyGrid = new Uint8Array(gridW * gridH); // 0=墙壁, 1=自由空间
|
||||
for (const freeSpacePoint of parsedMap.normalPositions) {
|
||||
// 将世界坐标转换为栅格像素坐标
|
||||
const gridColX = Math.floor((freeSpacePoint.x - originX) / resolution); // 列号(从左到右)
|
||||
const pixelRowFromBottom = Math.floor((freeSpacePoint.y - originY) / resolution); // 行号(从下到上,世界坐标系)
|
||||
const imageRowY = gridH - 1 - pixelRowFromBottom; // 翻转为图像行号(从上到下)
|
||||
// 安全边界检查后标记为自由空间
|
||||
if (gridColX >= 0 && gridColX < gridW && imageRowY >= 0 && imageRowY < gridH) {
|
||||
occupancyGrid[imageRowY * gridW + gridColX] = 1; // 标记该格为自由
|
||||
}
|
||||
}
|
||||
|
||||
// ── 第二步:遍历每个格子,按类型着色 ──
|
||||
const offscreenCanvas = document.createElement('canvas'); // 创建离屏 Canvas
|
||||
offscreenCanvas.width = gridW; // 设置尺寸
|
||||
offscreenCanvas.height = gridH;
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d'); // 获取上下文
|
||||
const imageData = offscreenCtx.createImageData(gridW, gridH);// 创建 ImageData
|
||||
const pixelData = imageData.data; // 像素数据数组 (RGBA 扁平排列)
|
||||
|
||||
// 定义三种颜色
|
||||
const FREE_SPACE_R = 10, FREE_SPACE_G = 16, FREE_SPACE_B = 30; // 自由空间:深蓝黑
|
||||
const WALL_INTERIOR_R = 22, WALL_INTERIOR_G = 36, WALL_INTERIOR_B = 64; // 墙壁内部:深蓝
|
||||
const WALL_EDGE_R = 45, WALL_EDGE_G = 130, WALL_EDGE_B = 255; // 墙壁边缘:亮蓝
|
||||
|
||||
for (let rowY = 0; rowY < gridH; rowY++) { // 逐行扫描
|
||||
for (let colX = 0; colX < gridW; colX++) { // 逐列扫描
|
||||
const gridIndex = rowY * gridW + colX; // 一维数组索引
|
||||
const pixelOffset = gridIndex << 2; // RGBA 偏移(×4)
|
||||
|
||||
if (occupancyGrid[gridIndex] === 1) {
|
||||
// ── 自由空间 ──
|
||||
pixelData[pixelOffset] = FREE_SPACE_R; // R
|
||||
pixelData[pixelOffset + 1] = FREE_SPACE_G; // G
|
||||
pixelData[pixelOffset + 2] = FREE_SPACE_B; // B
|
||||
pixelData[pixelOffset + 3] = 255; // A(不透明)
|
||||
} else {
|
||||
// ── 障物(墙壁)── 判断是否为边缘
|
||||
let isAdjacentToFreeSpace = false; // 是否紧邻自由空间
|
||||
if (showEdgeGlow) { // 仅在启用边缘效果时检测
|
||||
if (rowY > 0 && occupancyGrid[(rowY - 1) * gridW + colX] === 1) isAdjacentToFreeSpace = true; // 上方
|
||||
else if (rowY < gridH - 1 && occupancyGrid[(rowY + 1) * gridW + colX] === 1) isAdjacentToFreeSpace = true; // 下方
|
||||
else if (colX > 0 && occupancyGrid[rowY * gridW + colX - 1] === 1) isAdjacentToFreeSpace = true; // 左方
|
||||
else if (colX < gridW - 1 && occupancyGrid[rowY * gridW + colX + 1] === 1) isAdjacentToFreeSpace = true; // 右方
|
||||
}
|
||||
if (isAdjacentToFreeSpace) {
|
||||
// 墙壁边缘:亮蓝色
|
||||
pixelData[pixelOffset] = WALL_EDGE_R;
|
||||
pixelData[pixelOffset + 1] = WALL_EDGE_G;
|
||||
pixelData[pixelOffset + 2] = WALL_EDGE_B;
|
||||
pixelData[pixelOffset + 3] = 255;
|
||||
} else {
|
||||
// 墙壁内部:深蓝色
|
||||
pixelData[pixelOffset] = WALL_INTERIOR_R;
|
||||
pixelData[pixelOffset + 1] = WALL_INTERIOR_G;
|
||||
pixelData[pixelOffset + 2] = WALL_INTERIOR_B;
|
||||
pixelData[pixelOffset + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将 ImageData 绘制到离屏 Canvas 上
|
||||
offscreenCtx.putImageData(imageData, 0, 0);
|
||||
return offscreenCanvas;
|
||||
}
|
||||
|
||||
// import bot from '@/assets/icons/svg/bot.svg'
|
||||
export class MobileRobot {
|
||||
constructor(id, name, x, y, angleRad = 0, color= "#00D4FF", imageUrl = null) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.x = x; // 世界坐标X (米)
|
||||
this.y = y; // 世界坐标Y (米)
|
||||
this.angle = angleRad; // 朝向弧度
|
||||
this.imageUrl = imageUrl || '/bot.svg';
|
||||
// 预加载图片 (可选)
|
||||
this.imgElement = null;
|
||||
if (this.imageUrl) {
|
||||
this.init(color, this.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
async init(color, imageUrl) {
|
||||
console.log('imageUrl', imageUrl)
|
||||
const response = await fetch(imageUrl);
|
||||
let svgText = await response.text();
|
||||
console.log('svgText', svgText)
|
||||
// 替换颜色
|
||||
svgText = svgText.replace(/currentColor/g, color);
|
||||
svgText = svgText.replace(/fill="[^"]*"/g, `fill="${color}"`);
|
||||
// 创建Blob URL
|
||||
const blob = new Blob([svgText], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
this.imgElement = new Image();
|
||||
this.imgElement.src = url;
|
||||
}
|
||||
|
||||
// 绘制到canvas上下文 (屏幕坐标)
|
||||
draw(ctx, screenX, screenY, scale = 1) {
|
||||
console.log('imgElement', this.imgElement)
|
||||
const size = 18; // 绘制大小px
|
||||
if (this.imgElement && this.imgElement.complete) {
|
||||
ctx.save();
|
||||
ctx.translate(screenX, screenY);
|
||||
ctx.rotate(-this.angle); // canvas Y向下, 角度翻转适配
|
||||
ctx.drawImage(this.imgElement, -size / 2, -size / 2, size, size);
|
||||
ctx.restore();
|
||||
}
|
||||
// 名字标签
|
||||
ctx.font = '8px "JetBrains Mono"';
|
||||
ctx.fillStyle = '#a0c0f0';
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillText(this.name, screenX - 10, screenY - 10);
|
||||
}
|
||||
|
||||
// 更新位置
|
||||
setPosition(x, y) { this.x = x; this.y = y; }
|
||||
setAngle(rad) { this.angle = rad; }
|
||||
}
|
||||
|
||||
161
src/views/inspection/cockpit/components/Header.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="left">
|
||||
<div class="date">
|
||||
<div>{{ currentTime }}</div>
|
||||
<div class="week">{{ weekText }}</div>
|
||||
</div>
|
||||
<div class="binary-strip">{{ binaryText }}</div>
|
||||
</div>
|
||||
<div class="application-title">两江鱼复智能网联新能源汽车产业基地数字化平台</div>
|
||||
<div class="right">
|
||||
<div class="binary-strip">{{ binaryText }}</div>
|
||||
<div class="btn-container">
|
||||
<button class="nav-btn" :class="{ active: activePage === 'home' }" @click="handleBtn('home')">地图总览</button>
|
||||
<button class="nav-btn" :class="{ active: activePage === 'monitoring' }" @click="handleBtn('monitoring')">智能监控</button>
|
||||
<button class="nav-btn" :class="{ active: activePage === 'manualTakeover' }" @click="handleBtn('manualTakeover')">人工接管</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const emits = defineEmits(['activePage'])
|
||||
|
||||
const currentTime = ref('')
|
||||
const weekText = ref('')
|
||||
|
||||
const binaryText = ref('13513034532438213')
|
||||
|
||||
const weekNames = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
|
||||
const updateClock = () => {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const mo = String(now.getMonth()+1).padStart(2,'0');
|
||||
const d = String(now.getDate()).padStart(2,'0');
|
||||
const h = String(now.getHours()).padStart(2,'0');
|
||||
const mi = String(now.getMinutes()).padStart(2,'0');
|
||||
const s = String(now.getSeconds()).padStart(2,'0');
|
||||
currentTime.value = `${y}-${mo}-${d} ${h}:${mi}:${s}`
|
||||
weekText.value = weekNames[now.getDay()]
|
||||
}
|
||||
|
||||
const genBinary = (len) => {
|
||||
let str = '';
|
||||
for (let i = 0; i < len; i++) str += Math.round(Math.random()).toString();
|
||||
return str;
|
||||
}
|
||||
|
||||
const animateBinary = () => {
|
||||
binaryText.value = genBinary(30)
|
||||
}
|
||||
|
||||
const activePage = ref('home')
|
||||
const handleBtn = (page) => {
|
||||
activePage.value = page
|
||||
emits("activePage", page);
|
||||
}
|
||||
|
||||
let clockTimer, binaryTimer;
|
||||
onMounted(() => {
|
||||
updateClock();
|
||||
clockTimer = setInterval(updateClock, 1000);
|
||||
animateBinary()
|
||||
binaryTimer = setInterval(animateBinary, 400);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(clockTimer)
|
||||
clearInterval(binaryTimer)
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.header-container {
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
color: #00c2ff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
|
||||
.week {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #00c2ff;
|
||||
letter-spacing: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px ;
|
||||
|
||||
.nav-btn {
|
||||
background: rgba(0, 60, 140, 0.5);
|
||||
border: 1px solid rgba(0, 194, 255, 0.35);
|
||||
color: #a0d8f0;
|
||||
padding: 6px 20px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: rgba(0, 120, 220, 0.7);
|
||||
border-color: #00c2ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 194, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.binary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 10px;
|
||||
color: rgba(0, 194, 255, 0.3);
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
628
src/views/inspection/cockpit/components/ManualTakeover.vue
Normal file
@ -0,0 +1,628 @@
|
||||
<template>
|
||||
<div class="monitor-container">
|
||||
<div class="left-container">
|
||||
<div class="robot-container">
|
||||
<div class="title">选择机器人</div>
|
||||
<div class="robot-box">
|
||||
<div v-for="item, index in 4" class="robot" :class="{ active: activeBot === index }"
|
||||
@click="changeBot(index)">
|
||||
<div>
|
||||
<SvgIcon name="bot" :color="colorList[index]" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="bot-name">机器人#01</div>
|
||||
<div class="bot-local">厂区A-在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split-line"></div>
|
||||
<div class="mode-container">
|
||||
<div class="title">控制模式</div>
|
||||
<div class="mode-box">
|
||||
<div class="mode-item" :class="{ activeMode: activeMode === 'auto' }" @click="changeMode('auto')">
|
||||
<SvgIcon name="cpu" :color="activeMode === 'auto' ? '#00FF88' : '#fff'" :size="16" />
|
||||
<span class="label">自动巡检</span>
|
||||
</div>
|
||||
<div class="mode-item" :class="{ activeMode: activeMode === 'artificial' }"
|
||||
@click="changeMode('artificial')">
|
||||
<SvgIcon name="gamepad" :color="activeMode === 'artificial' ? '#00FF88' : '#fff'" :size="16" />
|
||||
<span class="label">人工接管</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeMode === 'artificial'" class="artificial-panel">
|
||||
<el-radio-group v-model="activeController" size="small" @change="handlerController">
|
||||
<el-radio :value="1">底盘</el-radio>
|
||||
<el-radio :value="2">机械臂</el-radio>
|
||||
<el-radio :value="3">相机</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="controller-panel">人工控制面板</div>
|
||||
<div v-if="activeController === 1" class="direction-controller-container">
|
||||
<div class="title">
|
||||
<SvgIcon name="navigation" color="00D4FF" :size="16" />
|
||||
<span class="label">方向控制器</span>
|
||||
</div>
|
||||
<div class="direction-box">
|
||||
<div class="direction-container">
|
||||
<div class="item"></div>
|
||||
<div class="item">
|
||||
<SvgIcon name="chevron-up" :size="24" />
|
||||
</div>
|
||||
<div class="item"></div>
|
||||
<div class="item">
|
||||
<SvgIcon name="chevron-left" :size="24" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<SvgIcon name="chevron-stop" :size="52" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<SvgIcon name="chevron-right" :size="24" />
|
||||
</div>
|
||||
<div class="item"></div>
|
||||
<div class="item">
|
||||
<SvgIcon name="chevron-down" :size="24" />
|
||||
</div>
|
||||
<div class="item"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="speed-box">
|
||||
<div class="label">移动速度</div>
|
||||
<div class="speed">{{ speed }}m/s</div>
|
||||
</div>
|
||||
<el-slider v-model="speed" :max="2" :step="0.1" size="small" />
|
||||
</div>
|
||||
<div v-if="activeController === 2" class="robotic-arm-controller-container">
|
||||
<el-radio-group v-model="activeRoboticArm" @change="handlerArm" size="small">
|
||||
<el-radio :value="1">末端控制</el-radio>
|
||||
<el-radio :value="2">关节控制</el-radio>
|
||||
<el-radio :value="3">姿态控制</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="mode-tabs">
|
||||
<div class="mode-tab active">左手</div>
|
||||
<div class="mode-tab">右手</div>
|
||||
</div>
|
||||
|
||||
<div class="robotic-arm-box">
|
||||
<div class="robotic-arm-status">
|
||||
<div class="coord-display">
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-x">X</div>
|
||||
<div class="coord-val" id="coordX">+124.50</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-y">Y</div>
|
||||
<div class="coord-val" id="coordY">−38.20</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-z">Z</div>
|
||||
<div class="coord-val" id="coordZ">+502.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="axis-group">
|
||||
<!-- X Axis -->
|
||||
<div class="axis-row x">
|
||||
<div class="axis-label x">X</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-slider-top">
|
||||
<span class="axis-name">前后平移</span>
|
||||
<span class="axis-value" id="valX">0.00</span>
|
||||
</div>
|
||||
<el-slider v-model="speed" :max="2" :step="0.1" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y Axis -->
|
||||
<div class="axis-row y">
|
||||
<div class="axis-label y">Y</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-slider-top">
|
||||
<span class="axis-name">左右平移</span>
|
||||
<span class="axis-value" id="valY">0.00</span>
|
||||
</div>
|
||||
<el-slider v-model="speed" :max="2" :step="0.1" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Z Axis -->
|
||||
<div class="axis-row z">
|
||||
<div class="axis-label z">Z</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-slider-top">
|
||||
<span class="axis-name">上下平移</span>
|
||||
<span class="axis-value" id="valZ">0.00</span>
|
||||
</div>
|
||||
<el-slider v-model="speed" :max="2" :step="0.1" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="video-container">
|
||||
<div v-if="activeMode === 'auto' || activeController === 1" class="map-container">地图</div>
|
||||
<div v-if="activeMode === 'artificial' && activeController === 2" class="model-container">机械臂模型</div>
|
||||
<div v-if="activeMode === 'artificial' && activeController === 3" class="monitor-container">监控</div>
|
||||
</div>
|
||||
<div class="right-container">
|
||||
<div class="container-title">车载监控</div>
|
||||
<div class="monitor-container"></div>
|
||||
<div class="split-line"></div>
|
||||
<div class="container-title">语音对话</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import SvgIcon from "@/components/SvgIcon";
|
||||
|
||||
const activeBot = ref(0)
|
||||
const changeBot = (index) => {
|
||||
activeBot.value = index
|
||||
}
|
||||
|
||||
const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#fff']
|
||||
|
||||
const activeMode = ref('auto')
|
||||
const changeMode = (mode) => {
|
||||
activeMode.value = mode
|
||||
}
|
||||
|
||||
const activeController = ref(1)
|
||||
|
||||
const speed = ref(0.8)
|
||||
|
||||
const activeRoboticArm = ref(1)
|
||||
|
||||
|
||||
const handlerController = (value) => {
|
||||
if (value === 2 && [1, 3].includes(activeRoboticArm.value)) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handlerArm = (value) => {
|
||||
if ([1, 3].includes(value)) {
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
.left-container {
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.robot-container {
|
||||
|
||||
.title {
|
||||
color: #8899BB;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.robot-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.robot {
|
||||
flex: 0 0 calc(50% - 10px);
|
||||
background: #0F1E38;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.bot-name {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bot-local {
|
||||
color: #00d4ff;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: #00c2ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 194, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: #1E3A5F;
|
||||
}
|
||||
|
||||
.mode-container {
|
||||
.title {
|
||||
color: #8899BB;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mode-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.mode-item {
|
||||
height: 36px;
|
||||
flex: 0 0 calc(50% - 10px);
|
||||
background: #0F1E38;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.label {
|
||||
color: #8899BB;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.activeMode {
|
||||
border-color: #00FF88;
|
||||
|
||||
.label {
|
||||
color: #00FF88;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.controller-panel {
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.direction-controller-container {
|
||||
padding: 16px;
|
||||
height: 272px;
|
||||
background: #0F1E38;
|
||||
border-radius: 10px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.label {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.direction-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.direction-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
width: 164px;
|
||||
height: 164px;
|
||||
|
||||
.item {
|
||||
// flex: 0 0 calc(33.333% - 4px);
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: #1E3A5F;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.speed-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0;
|
||||
|
||||
.label {
|
||||
color: #8899BB;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.speed {
|
||||
color: #00D4FF;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-slider__button) {
|
||||
scale: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.robotic-arm-controller-container {
|
||||
:deep(.el-radio-group) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
padding: 12px 20px 0;
|
||||
gap: 6px;
|
||||
|
||||
.mode-tab {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #4a5580;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #8b97bf;
|
||||
background: #3b82f60d;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #3b82f6;
|
||||
background: #111a36;
|
||||
border-color: #1c2a52;
|
||||
font-weight: 700;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 8px var(--accent-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.robotic-arm-box {
|
||||
.robotic-arm-status {
|
||||
.coord-display {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #070b1a;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.coord-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
.coord-key {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.key-x {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.key-y {
|
||||
color: #34d399;
|
||||
;
|
||||
}
|
||||
|
||||
.key-z {
|
||||
color: #38bdf8;
|
||||
;
|
||||
}
|
||||
|
||||
.coord-val {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #e8edf8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.axis-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.axis-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #0c1228;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #2a3f7a;
|
||||
background: #3b82f608;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.x {
|
||||
background: #f8717126;
|
||||
color: #f87171;
|
||||
border: 1px solid #f871714d;
|
||||
}
|
||||
|
||||
.y {
|
||||
background: #34d39926;
|
||||
color: #34d399;
|
||||
border: 1px solid #34d3994d;
|
||||
}
|
||||
|
||||
.z {
|
||||
background: #38bdf826;
|
||||
color: #38bdf8;
|
||||
border: 1px solid #38bdf84d;
|
||||
}
|
||||
|
||||
|
||||
.axis-slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.axis-slider-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.axis-name {
|
||||
font-size: 11px;
|
||||
color: #8b97bf;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.axis-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #e8edf8;
|
||||
min-width: 52px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-slider__button) {
|
||||
scale: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
flex: 1;
|
||||
border: 1px solid #1E3A5F;
|
||||
border-top: 0;
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #00c2ff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.model-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #00c2ff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.monitor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #00c2ff;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-container {
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.container-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: #1E3A5F;
|
||||
}
|
||||
|
||||
.monitor-container {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
350
src/views/inspection/cockpit/components/MapCanvas.vue
Normal file
@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<canvas id="map-canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
import {
|
||||
worldToScreen,
|
||||
screenToWorld,
|
||||
drawGrid,
|
||||
drawGridlines,
|
||||
drawAdvancedAreaList,
|
||||
drawAdvancedLineList,
|
||||
drawNormalLines,
|
||||
drawAdvancedCurveList,
|
||||
drawAdvancedPointList,
|
||||
drawMapBoundary,
|
||||
buildOccupancyGridImage,
|
||||
MobileRobot
|
||||
} from '../canvasUtils';
|
||||
|
||||
import { useInspectionStore } from "@/store/modules/inspection";
|
||||
const inspectionStore = useInspectionStore();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 应用全局状态对象,集中管理所有可变数据
|
||||
* - parsedMap: 解析后的地图数据结构
|
||||
* - layerVisibility: 各图层的可见性开关
|
||||
* - mouseState: 鼠标位置与拖拽状态
|
||||
* - isMapLoaded: 地图是否已加载完成
|
||||
*/
|
||||
const applicationState = {
|
||||
parsedMap: null, // 解析后的完整地图数据
|
||||
mouseState: { // 鼠标交互状态
|
||||
screenX: 0, // 鼠标在 Canvas 上的像素 X
|
||||
screenY: 0, // 鼠标在 Canvas 上的像素 Y
|
||||
worldX: 0, // 鼠标对应的世界坐标 X
|
||||
worldY: 0, // 鼠标对应的世界坐标 Y
|
||||
isDragging: false, // 是否正在拖拽平移
|
||||
},
|
||||
isMapLoaded: false, // 地图加载完成标志,
|
||||
layerVisibility: { // 各图层是否可见
|
||||
grid: true, // 占据栅格底图
|
||||
edges: true, // 墙壁边缘发光
|
||||
gridlines: true, // 1米网格辅助线
|
||||
normalLines: true, // normalLineList
|
||||
advLines: true, // advancedLineList
|
||||
curves: true, // advancedCurveList
|
||||
points: true, // advancedPointList
|
||||
areas: true, // advancedAreaList
|
||||
robots: true, // 机器人位置和朝向
|
||||
},
|
||||
robots: [] // 存储机器人实例
|
||||
};
|
||||
|
||||
let mapCanvas; // 主渲染画布 DOM 元素
|
||||
let canvasCtx; // 2D 渲染上下文
|
||||
|
||||
const init = () => {
|
||||
mapCanvas = document.getElementById('map-canvas');
|
||||
canvasCtx = mapCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据容器尺寸和设备像素比调整 Canvas 大小
|
||||
* 在窗口 resize 时自动调用
|
||||
*/
|
||||
function resizeCanvasToContainer() {
|
||||
const containerRect = mapCanvas.parentElement.getBoundingClientRect(); // 获取容器实际尺寸
|
||||
devicePixelRatio = window.devicePixelRatio || 1; // 获取设备像素比
|
||||
inspectionStore.canvasWidthPx = containerRect.width; // 更新逻辑宽度
|
||||
inspectionStore.canvasHeightPx = containerRect.height; // 逻辑高度 = 容器高度 - 状态栏高度
|
||||
mapCanvas.width = inspectionStore.canvasWidthPx * devicePixelRatio; // Canvas 物理像素宽
|
||||
mapCanvas.height = inspectionStore.canvasHeightPx * devicePixelRatio; // Canvas 物理像素高
|
||||
mapCanvas.style.width = inspectionStore.canvasWidthPx + 'px'; // CSS 显示宽度
|
||||
mapCanvas.style.height = inspectionStore.canvasHeightPx + 'px'; // CSS 显示高度
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析 SEER smap JSON 文件为内部地图数据结构
|
||||
*
|
||||
* @param {Object} rawJson - 原始 JSON 解析后的对象
|
||||
* @returns {Object} 解析后的统一地图数据结构
|
||||
*/
|
||||
function parseSmapJson(rawJson) {
|
||||
// ── 解析 header ──
|
||||
const header = rawJson.header || rawJson; // 兼容 header 嵌套或平铺
|
||||
const minimumPos = header.minPos || { x: 0, y: 0 }; // 地图左下角(世界坐标)
|
||||
const maximumPos = header.maxPos || { x: 1, y: 1 }; // 地图右上角(世界坐标)
|
||||
const resolution = header.resolution || 0.05; // 每像素代表的世界长度(米)
|
||||
|
||||
// ── 计算栅格尺寸 ──
|
||||
const worldWidth = maximumPos.x - minimumPos.x; // 地图世界宽度(米)
|
||||
const worldHeight = maximumPos.y - minimumPos.y; // 地图世界高度(米)
|
||||
const gridWidthPx = Math.ceil(worldWidth / resolution) + 1; // 栅格像素宽度
|
||||
const gridHeightPx = Math.ceil(worldHeight / resolution) + 1;// 栅格像素高度
|
||||
|
||||
// ── 构建统一数据结构 ──
|
||||
const parsedMap = {
|
||||
name: header.mapName || 'Untitled', // 地图名称
|
||||
version: header.version || '', // 版本号
|
||||
mapType: header.mapType || '', // 地图类型(如 "2D-Map")
|
||||
resolution: resolution, // 分辨率(米/像素)
|
||||
originX: minimumPos.x, // 地图左下角 X
|
||||
originY: minimumPos.y, // 地图左下角 Y
|
||||
maxX: maximumPos.x, // 地图右上角 X
|
||||
maxY: maximumPos.y, // 地图右上角 Y
|
||||
gridWidth: gridWidthPx, // 栅格宽度(像素)
|
||||
gridHeight: gridHeightPx, // 栅格高度(像素)
|
||||
// normalPosList: 自由空间点,直接拷贝
|
||||
normalPositions: rawJson.normalPosList || [],
|
||||
// normalLineList: 直接拷贝,每项含 {startPos:{x,y}, endPos:{x,y}}
|
||||
normalLines: rawJson.normalLineList || [],
|
||||
// advancedPointList: 直接拷贝,每项含 {className, instanceName, pos:{x,y}, dir, desc, property:[]}
|
||||
advancedPoints: rawJson.advancedPointList || [],
|
||||
// advancedLineList: 直接拷贝,每项含 {className, instanceName, line:{startPos, endPos}}
|
||||
advancedLines: rawJson.advancedLineList || [],
|
||||
// advancedCurveList: 直接拷贝,每项含 {className, instanceName, controlPos1, controlPos2, startPos, endPos}
|
||||
advancedCurves: rawJson.advancedCurveList || [],
|
||||
// advancedAreaList: 直接拷贝,每项含 {className, instanceName, dir, posGroup:[], attribute:{}}
|
||||
advancedAreas: rawJson.advancedAreaList || [],
|
||||
};
|
||||
|
||||
return parsedMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载地图到应用:构建栅格图像、设置相机、更新UI
|
||||
* @param {Object} parsedMap - parseSmapJson 的输出
|
||||
*/
|
||||
async function loadMapIntoApplication(parsedMap) {
|
||||
await new Promise(resolve => setTimeout(resolve, 30)); // 短暂等待让 UI 有机会刷新
|
||||
|
||||
// 保存地图数据到全局状态
|
||||
applicationState.parsedMap = parsedMap;
|
||||
// 构建栅格图像(带边缘高亮)
|
||||
inspectionStore.gridOffscreen = buildOccupancyGridImage(parsedMap, applicationState.layerVisibility.edges);
|
||||
|
||||
// ── 设置相机初始位置(地图中心)──
|
||||
inspectionStore.camera.centerX = (parsedMap.originX + parsedMap.maxX) / 2; // 水平居中
|
||||
inspectionStore.camera.centerY = (parsedMap.originY + parsedMap.maxY) / 2; // 垂直居中
|
||||
// 计算合适的缩放级别,使地图完整显示在画布内
|
||||
const worldWidthMeters = parsedMap.maxX - parsedMap.originX; // 地图世界宽度
|
||||
const worldHeightMeters = parsedMap.maxY - parsedMap.originY; // 地图世界高度
|
||||
const fitScaleX = inspectionStore.canvasWidthPx / worldWidthMeters; // 水平方向恰好填满的缩放
|
||||
const fitScaleY = inspectionStore.canvasHeightPx / worldHeightMeters; // 垂直方向恰好填满的缩放
|
||||
inspectionStore.camera.pixelsPerMeter = Math.max(1, Math.min(fitScaleX, fitScaleY) * 0.88); // 取较小值并留12%边距
|
||||
|
||||
// 标记加载完成并更新UI
|
||||
applicationState.isMapLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 主渲染函数,由 requestAnimationFrame 循环调用
|
||||
* 按层顺序绘制:栅格 → 网格线 → 区域 → 基础线 → 高级线 → 曲线 → 点 → 边界
|
||||
*
|
||||
* @param {number} timestamp - requestAnimationFrame 提供的时间戳(毫秒)
|
||||
*/
|
||||
function renderFrame(timestamp) {
|
||||
// 更新动画时间
|
||||
inspectionStore.animationTimeSeconds = (timestamp || 0) / 1000;
|
||||
|
||||
// 设置 Canvas 变换矩阵(考虑设备像素比)
|
||||
canvasCtx.setTransform(inspectionStore.devicePixelRatio, 0, 0, inspectionStore.devicePixelRatio, 0, 0);
|
||||
canvasCtx.clearRect(0, 0, inspectionStore.canvasWidthPx, inspectionStore.canvasHeightPx); // 清空画布
|
||||
|
||||
// 如果地图未加载,跳过绘制
|
||||
if (!applicationState.isMapLoaded) {
|
||||
requestAnimationFrame(renderFrame); // 继续请求下一帧
|
||||
return;
|
||||
}
|
||||
|
||||
const map = applicationState.parsedMap; // 当前地图数据
|
||||
const layers = applicationState.layerVisibility; // 图层可见性
|
||||
|
||||
// 图层1: 占据栅格底图
|
||||
if (layers.grid && inspectionStore.gridOffscreen) {
|
||||
drawGrid(canvasCtx, map);
|
||||
}
|
||||
|
||||
// 图层2: 网格辅助线(每1米一条)
|
||||
if (layers.gridlines) {
|
||||
drawGridlines(canvasCtx)
|
||||
}
|
||||
|
||||
// 图层3: advancedAreaList(半透明多边形区域)
|
||||
if (layers.areas) {
|
||||
drawAdvancedAreaList(canvasCtx, map)
|
||||
}
|
||||
|
||||
// 图层4: normalLineList(基础直线,青色 #00e5ff)
|
||||
if (layers.normalLines) {
|
||||
drawNormalLines(canvasCtx, map);
|
||||
}
|
||||
|
||||
// 图层5: advancedLineList(高级线,如 ForbiddenLine)
|
||||
if (layers.advLines) {
|
||||
drawAdvancedLineList(canvasCtx, map);
|
||||
}
|
||||
|
||||
// 图层6: advancedCurveList(贝塞尔曲线路径,酸绿色 #76ff03)
|
||||
if (layers.curves) {
|
||||
drawAdvancedCurveList(canvasCtx, map);
|
||||
}
|
||||
|
||||
// 图层7: advancedPointList(高级点标记,菱形)
|
||||
if (layers.points) {
|
||||
drawAdvancedPointList(canvasCtx, map);
|
||||
}
|
||||
|
||||
if (layers.robots) {
|
||||
drawAllRobots(canvasCtx)
|
||||
}
|
||||
|
||||
// 地图边界虚线框
|
||||
drawMapBoundary(canvasCtx, map);
|
||||
|
||||
// 请求下一帧渲染
|
||||
requestAnimationFrame(renderFrame);
|
||||
}
|
||||
|
||||
const drawAllRobots = (ctx) => {
|
||||
if (!applicationState.layerVisibility.robots) return;
|
||||
for (const robot of applicationState.robots) {
|
||||
console.log(1222)
|
||||
const screenPos = worldToScreen(robot.x, robot.y);
|
||||
// 裁剪超出画布简易跳过
|
||||
if (screenPos.x + 20 < 0 || screenPos.x - 20 > inspectionStore.canvasWidthPx || screenPos.y + 20 < 0 || screenPos.y - 20 > inspectionStore.canvasHeightPx) {
|
||||
continue;
|
||||
}
|
||||
robot.draw(ctx, screenPos.x, screenPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
const bindEvent = () => {
|
||||
// ── 鼠标移动:更新坐标、拖拽平移、悬停检测 ──
|
||||
mapCanvas.addEventListener('mousemove', (mouseEvent) => {
|
||||
const canvasRect = mapCanvas.getBoundingClientRect(); // Canvas 在页面中的位置
|
||||
applicationState.mouseState.screenX = mouseEvent.clientX - canvasRect.left; // 鼠标在 Canvas 上的 X
|
||||
applicationState.mouseState.screenY = mouseEvent.clientY - canvasRect.top; // 鼠标在 Canvas 上的 Y
|
||||
// 反算世界坐标
|
||||
const worldPos = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY);
|
||||
applicationState.mouseState.worldX = worldPos.x;
|
||||
applicationState.mouseState.worldY = worldPos.y;
|
||||
|
||||
// 如果正在拖拽,平移相机
|
||||
if (applicationState.mouseState.isDragging) {
|
||||
inspectionStore.camera.centerX -= mouseEvent.movementX / inspectionStore.camera.pixelsPerMeter; // 水平平移
|
||||
inspectionStore.camera.centerY += mouseEvent.movementY / inspectionStore.camera.pixelsPerMeter; // 垂直平移(Y翻转)
|
||||
}
|
||||
});
|
||||
|
||||
// ── 鼠标按下:开始拖拽 ──
|
||||
mapCanvas.addEventListener('mousedown', () => {
|
||||
applicationState.mouseState.isDragging = true; // 标记拖拽开始
|
||||
});
|
||||
|
||||
// ── 鼠标抬起:结束拖拽 ──
|
||||
mapCanvas.addEventListener('mouseup', () => {
|
||||
applicationState.mouseState.isDragging = false; // 标记拖拽结束
|
||||
});
|
||||
|
||||
// ── 鼠标离开画布:结束拖拽并隐藏 Tooltip ──
|
||||
mapCanvas.addEventListener('mouseleave', () => {
|
||||
applicationState.mouseState.isDragging = false;
|
||||
});
|
||||
|
||||
// ── 滚轮:缩放(向鼠标位置缩放)──
|
||||
mapCanvas.addEventListener('wheel', (wheelEvent) => {
|
||||
wheelEvent.preventDefault(); // 阻止页面滚动
|
||||
const zoomFactor = wheelEvent.deltaY > 0 ? 0.88 : 1.12; // 向下滚动=缩小,向上滚动=放大
|
||||
const mouseWorldBefore = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY); // 缩放前鼠标世界坐标
|
||||
inspectionStore.camera.pixelsPerMeter = Math.max(0.5, Math.min(2000, inspectionStore.camera.pixelsPerMeter * zoomFactor)); // 应用缩放并钳制范围
|
||||
const mouseWorldAfter = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY); // 缩放后鼠标世界坐标
|
||||
// 调整相机中心,使鼠标指向的世界坐标保持不变
|
||||
inspectionStore.camera.centerX += mouseWorldBefore.x - mouseWorldAfter.x;
|
||||
inspectionStore.camera.centerY += mouseWorldBefore.y - mouseWorldAfter.y;
|
||||
}, { passive: false }); // passive:false 允许 preventDefault
|
||||
}
|
||||
|
||||
const loadSourceMap = async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`加载失败:${response.status}`);
|
||||
return response.text();
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载 smap JSON 文件
|
||||
*/
|
||||
async function loadUserFile() {
|
||||
try {
|
||||
const fileTextContent = await loadSourceMap('/3.smap');
|
||||
const rawJsonObject = JSON.parse(fileTextContent); // 解析 JSON
|
||||
const parsedMap = parseSmapJson(rawJsonObject); // 转换为内部结构
|
||||
await loadMapIntoApplication(parsedMap); // 加载到应用
|
||||
} catch (parseError) {
|
||||
alert('Parse error: ' + parseError.message); // 显示错误信息
|
||||
}
|
||||
}
|
||||
|
||||
const addRobot = () => {
|
||||
const map = applicationState.parsedMap;
|
||||
console.log('map', map)
|
||||
// 放置于地图中央附近
|
||||
const centerX = (map.originX + map.maxX) / 2;
|
||||
const centerY = (map.originY + map.maxY) / 2;
|
||||
const id = 0;
|
||||
const robot = new MobileRobot(id, `Robot${id}`, centerX + (Math.random() - 0.5) * 3, centerY + (Math.random() - 0.5) * 3, 0);
|
||||
applicationState.robots.push(robot);
|
||||
}
|
||||
|
||||
const moveRobot = (robot, dx, dy, dtheta = 0) => {
|
||||
robot.x += dx;
|
||||
robot.y += dy;
|
||||
robot.angle += dtheta;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
// 启动渲染循环
|
||||
loadUserFile()
|
||||
requestAnimationFrame(renderFrame);
|
||||
window.addEventListener('resize', resizeCanvasToContainer); // 监听窗口大小变化
|
||||
resizeCanvasToContainer(); // 初始调用一次
|
||||
setTimeout(() => {
|
||||
bindEvent()
|
||||
addRobot()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeCanvasToContainer)
|
||||
canvasCtx = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
328
src/views/inspection/cockpit/components/MapOverview.vue
Normal file
@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="left-container">
|
||||
<div class="robot-container">
|
||||
<div class="robot-state">机器人状态</div>
|
||||
<div class="robot-info" v-for="item, index in 2">
|
||||
<div class="title-box">
|
||||
<div><SvgIcon name="bot" :color="index === 0 ? '#00D4FF' : '#FFB300'" /></div>
|
||||
<div class="title">巡检机器人#{{ index }}</div>
|
||||
<div class="state" :class="robotState">在线</div>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<div class="item-box">
|
||||
<div class="item-title">编号</div>
|
||||
<div class="item-detail">RB-2024-001</div>
|
||||
</div>
|
||||
<div class="item-box">
|
||||
<div class="item-title">当前任务</div>
|
||||
<div class="item-detail">厂区A-定时巡检</div>
|
||||
</div>
|
||||
<div class="item-box">
|
||||
<div class="item-title">电量</div>
|
||||
<div class="item-detail">78%</div>
|
||||
</div>
|
||||
<div class="item-box">
|
||||
<div class="item-title">速度</div>
|
||||
<div class="item-detail">0.8m/s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="split-line"></div>
|
||||
<div class="task-container">
|
||||
<div class="container-title">当前任务</div>
|
||||
<div class="task-box" v-for="item, index in 2">
|
||||
<div class="title">巡检机器人#{{ index }}</div>
|
||||
<div class="task-info">
|
||||
<div>执行中</div>
|
||||
<div>厂区A定时巡检</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div class="title-container">
|
||||
<div class="title">
|
||||
<SvgIcon name="map" color='#00D4FF' />
|
||||
<span>实时地图 — 工厂园区</span>
|
||||
</div>
|
||||
<div class="map-controller">
|
||||
<div class="btn-container">
|
||||
<SvgIcon name="zoom-in" color='#00D4FF' :size="16" />
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<SvgIcon name="zoom-out" color='#00D4FF' :size="16" />
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<SvgIcon name="locate-fixed" color='#00D4FF' :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-box">
|
||||
<MapCanvas />
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-container">
|
||||
<div class="sensor-container">
|
||||
<div class="sensor-title">传感器数据</div>
|
||||
</div>
|
||||
<div class="split-line"></div>
|
||||
<div class="alarm-container">
|
||||
<div class="container-title">告警记录</div>
|
||||
<div class="alarm-box" v-for="item, index in 2">
|
||||
<div class="title">#01 检测到异常噪声</div>
|
||||
<div class="alarm-info">14:28:33 — 厂区A 东侧走廊</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SvgIcon from "@/components/SvgIcon";
|
||||
import MapCanvas from "./MapCanvas.vue";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
|
||||
const robotState = ref('online')
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.left-container {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.robot-container {
|
||||
max-height: 50%;
|
||||
overflow-y: auto;
|
||||
|
||||
.robot-state {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.robot-info {
|
||||
width: 268px;
|
||||
height: 148px;
|
||||
background: #0F1E38;
|
||||
border-radius: 10px;
|
||||
// border: 1px solid #00D4FF;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-family: Inter;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.state {
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.online {
|
||||
background: #0f83;
|
||||
color: #0f8;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
.item-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 6px 0;
|
||||
|
||||
.item-title {
|
||||
color: #89B;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
color: #e0e8f0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.robot-info.last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: #1E3A5F;
|
||||
}
|
||||
|
||||
.task-container {
|
||||
max-height: 50%;
|
||||
overflow-y: auto;
|
||||
|
||||
.container-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.task-box {
|
||||
height: 72px;
|
||||
background: #0F1E38;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-family: Inter;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
font-size: 12px;
|
||||
color: #E0E8F0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
border: 1px solid #1E3A5F;
|
||||
border-top: 0;
|
||||
|
||||
.title-container {
|
||||
height: 52px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.map-controller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.btn-container {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #1E3A5F;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: #00D4FF;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map-box {
|
||||
height: calc(100% - 52px);
|
||||
}
|
||||
}
|
||||
|
||||
.right-container {
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.sensor-container {
|
||||
max-height: 50%;
|
||||
height: 45%;
|
||||
overflow-y: auto;
|
||||
|
||||
.sensor-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: #1E3A5F;
|
||||
}
|
||||
|
||||
.alarm-container {
|
||||
max-height: 50%;
|
||||
overflow-y: auto;
|
||||
|
||||
.container-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.alarm-box {
|
||||
height: 72px;
|
||||
background: #0F1E38;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title {
|
||||
color: #FF6B6B;
|
||||
font-size: 14px;
|
||||
font-family: Inter;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alarm-info {
|
||||
margin: 10px 0;
|
||||
font-size: 12px;
|
||||
color: #8899BB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
120
src/views/inspection/cockpit/components/Monitor.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="left-container">
|
||||
<div class="robot-container">
|
||||
<div class="title">选择机器人</div>
|
||||
<div class="robot-box">
|
||||
<div class="robot" :class="{ active: activeBot === 0 }" @click="changeBot(0)">
|
||||
<div><SvgIcon name="bot" color="#fff" /></div>
|
||||
<div class="bot-name">全部</div>
|
||||
</div>
|
||||
<div v-for="item, index in 4" class="robot" :class="{ active: activeBot === index + 1 }" @click="changeBot(index + 1)">
|
||||
<div>
|
||||
<SvgIcon name="bot" :color="colorList[index]" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="bot-name">机器人#0{{ index + 1}}</div>
|
||||
<div class="bot-local">厂区A-在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center-container">监控</div>
|
||||
<div v-if="activeBot != 0" class="right-container">
|
||||
<div class="container-title">OCR识别</div>
|
||||
<div class="ocr-container"></div>
|
||||
<div class="split-line"></div>
|
||||
<div class="container-title">红外热成像</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import SvgIcon from "@/components/SvgIcon";
|
||||
|
||||
const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#fd00ff']
|
||||
|
||||
const activeBot = ref(0)
|
||||
const changeBot = (index) => {
|
||||
activeBot.value = index
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.left-container {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.robot-container {
|
||||
|
||||
.title {
|
||||
color: #8899BB;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.robot-box {
|
||||
|
||||
.robot {
|
||||
background: #0F1E38;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.bot-name {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bot-local {
|
||||
color: #00d4ff;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: #00c2ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 194, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-container {
|
||||
flex: 1;
|
||||
border: 1px solid #1E3A5F;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.right-container {
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.container-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ocr-container {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/views/inspection/cockpit/index.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<Header @active-page="activePage" />
|
||||
</div>
|
||||
<div class="body-box">
|
||||
<MapOverview v-if="currentPage === 'home'" />
|
||||
<ManualTakeover v-if="currentPage === 'manualTakeover'" />
|
||||
<Monitor v-if="currentPage === 'monitoring'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Header from './components/Header.vue';
|
||||
import MapOverview from './components/MapOverview.vue';
|
||||
import ManualTakeover from './components/ManualTakeover.vue';
|
||||
import Monitor from './components/Monitor.vue';
|
||||
|
||||
|
||||
const currentPage = ref('home')
|
||||
const activePage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: #000d2e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
height: 64px;
|
||||
background: linear-gradient(180deg, rgba(0, 20, 70, 0.98) 0%, rgba(0, 10, 50, 0.95) 100%);
|
||||
border-bottom: 1px solid rgba(0, 194, 255, 0.3);
|
||||
}
|
||||
|
||||
.body-box {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
src/views/inspection/map/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>地图管理</div>
|
||||
</template>
|
||||
3
src/views/inspection/point/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>点位管理</div>
|
||||
</template>
|
||||
3
src/views/inspection/task/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>任务管理</div>
|
||||
</template>
|
||||
@ -51,7 +51,7 @@ export default defineConfig(({mode, command}) => {
|
||||
//李小龙 http://192.168.0.5:13080
|
||||
// dev http://10.148.20.34:13080
|
||||
// target: VITE_API_URL,
|
||||
target: command === 'build' ? VITE_API_URL : 'http://192.168.0.100:13080',
|
||||
target: command === 'build' ? VITE_API_URL : 'http://10.148.121.100:13080',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
|
||||