Compare commits

..

1 Commits
v1.0.0 ... main

Author SHA1 Message Date
37a5269d57 feat: 长安巡检 2026-05-25 09:10:10 +08:00
24 changed files with 2198 additions and 46 deletions

1
public/2.smap Normal file

File diff suppressed because one or more lines are too long

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -1,53 +1,42 @@
<template> <template>
<svg :class="svgClass" aria-hidden="true"> <svg
<use :xlink:href="iconName" :fill="color" /> :width="size"
:height="size"
:style="svgStyle"
class="svg-icon"
aria-hidden="true"
>
<use :xlink:href="symbolId" />
</svg> </svg>
</template> </template>
<script> <script setup>
export default defineComponent({ import { computed } from 'vue'
props: {
iconClass: { const props = defineProps({
type: String, name: {
required: true type: String,
}, required: true
className: {
type: String,
default: ''
},
color: {
type: String,
default: ''
},
}, },
setup(props) { size: {
return { type: [String, Number],
iconName: computed(() => `#icon-${props.iconClass}`), default: 24
svgClass: computed(() => { },
if (props.className) { color: {
return `svg-icon ${props.className}` type: String,
} default: 'currentColor'
return 'svg-icon' },
}) customStyle: {
} type: Object,
default: () => ({})
} }
}) })
const symbolId = computed(() => `#icon-${props.name}`)
const svgStyle = computed(() => ({
verticalAlign: 'middle',
color: props.color, // color fill
...props.customStyle
}))
</script> </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>

View File

@ -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(() => { const isExt = computed(() => {
return isExternal(props.to) return isExternal(props.to)
}) })
@ -33,6 +43,19 @@ function linkProps() {
rel: 'noopener' 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 { return {
to: props.to to: props.to
} }

View File

@ -127,6 +127,14 @@ export const constantRoutes = [
meta: { title: "触摸交互", noLogin: true }, meta: { title: "触摸交互", noLogin: true },
hidden: true, hidden: true,
}, },
{
//cockpit
path: "/inspection/cockpit",
component: () =>
import("@/views/inspection/cockpit/index.vue"),
meta: { title: "logicFlow", noLogin: true },
hidden: true,
},
{ {
//logicFlow //logicFlow
path: "/logic_flow", path: "/logic_flow",

View 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>

View File

@ -0,0 +1,521 @@
<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="leftHand">左手</div>
<div v-if="[1,3].includes(activeRoboticArm)" class="coordinate-system" ref="coordinateSystem">
<div class="control-buttons">
<button v-for="(pos, index) in buttonPositions" :key="index" class="pos-btn"
:data-target="pos.target" :style="{ left: `${pos.left}px`, top: `${pos.top}px` }">
{{ pos.label }}
</button>
</div>
</div>
<div class="rightHand">右手</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 coordinateSystem = ref(null); //
const buttonPositions = ref([]); //
const buttonDirections = [
{ target: "z_plus", direction: "top", label: "Z+" }, // left
{ target: "x_plus", direction: "right", label: "X+" }, // bottom
{ target: "y_minus", direction: "bottom", label: "Y-" },
{ target: "z_minus", direction: "bottom", label: "Z-" },
{ target: "x_minus", direction: "left", label: "X-" },
{ target: "y_plus", direction: "left", label: "Y+" }, // right
];
//
const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817;
const handlerController = (value) => {
if (value === 2 && [1, 3].includes(activeRoboticArm.value)) {
updateButtonPositions()
}
}
const handlerArm = (value) => {
if ([1, 3].includes(value)) {
updateButtonPositions()
}
}
const updateButtonPositions = () => {
if (coordinateSystem.value) {
const containerRect = coordinateSystem.value.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
// background-size: contain
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT;
const containerAspectRatio = containerWidth / containerHeight;
let imageWidth, imageHeight, imageLeft, imageTop;
if (containerAspectRatio > imageAspectRatio) {
imageHeight = containerHeight;
imageWidth = imageHeight * imageAspectRatio;
imageLeft = (containerWidth - imageWidth) / 2;
imageTop = 0;
} else {
imageWidth = containerWidth;
imageHeight = imageWidth / imageAspectRatio;
imageLeft = 0;
imageTop = (containerHeight - imageHeight) / 2;
}
//
const centerX = imageLeft + imageWidth / 2;
const centerY = imageTop + imageHeight / 2;
//
const radiusScale = 0.85; // 0-1
const radius = Math.min(imageWidth, imageHeight) * radiusScale / 2;
// top0
const directionAngles = {
"top": 0,
"top-right": Math.PI / 6, // 30
"right": Math.PI / 3, // 60
"bottom-right": Math.PI / 2, // 90
"bottom": 2 * Math.PI / 3, // 120
"bottom-left": 5 * Math.PI / 6, // 150
"left": Math.PI, // 180
"top-left": 7 * Math.PI / 6, // 210
};
// 12360/12=30Math.PI/6
const totalButtons = buttonDirections.length;
const angleStep = 2 * Math.PI / totalButtons;
//
buttonPositions.value = buttonDirections.map((btn, index) => {
// top
const angle = index * angleStep;
//
const x = centerX + radius * Math.sin(angle);
const y = centerY - radius * Math.cos(angle);
return {
...btn,
left: x,
top: y,
};
});
console.log('buttonPositions', buttonPositions.value)
}
};
onMounted(() => {
//
window.addEventListener("resize", updateButtonPositions);
});
onUnmounted(() => {
window.removeEventListener("resize", updateButtonPositions);
});
</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;
}
.coordinate-system {
height: 200px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
background-image: url("@/assets/images/arm.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
.control-buttons {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
.pos-btn {
position: absolute;
width: 28px;
height: 28px;
background: #4076ff;
color: white;
border: none;
cursor: pointer;
font-size: 14px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
/* 按钮中心对齐坐标 */
}
}
}
}
}
.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>

View File

@ -0,0 +1,811 @@
<template>
<div class="container">
<canvas id="map-canvas"></canvas>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
//
// 1.
//
/**
* 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
*/
function decodeArgbInteger(argbInt) {
if (typeof argbInt !== 'number') return null; //
return {
a: (argbInt >>> 24) & 0xFF, // 8Alpha
r: (argbInt >>> 16) & 0xFF, // 8Red
g: (argbInt >>> 8) & 0xFF, // 8Green
b: argbInt & 0xFF, // 8Blue
};
}
/**
* 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)"
*/
function 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})`;
}
/**
* 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})`;
}
//
// 2.
//
/**
* 应用全局状态对象集中管理所有可变数据
* - parsedMap: 解析后的地图数据结构
* - gridOffscreen: 离屏 Canvas存储绘制好的占据栅格图像
* - camera: 相机状态中心世界坐标 + 缩放级别
* - layerVisibility: 各图层的可见性开关
* - mouseState: 鼠标位置与拖拽状态
* - hoveredElement: 当前鼠标悬停命中的元素
* - isMapLoaded: 地图是否已加载完成
*/
const applicationState = {
parsedMap: null, //
gridOffscreen: null, // Canvas
camera: { //
centerX: 0, // X
centerY: 0, // Y
pixelsPerMeter: 40, //
},
layerVisibility: { //
grid: true, //
edges: true, //
gridlines: true, // 1线
normalLines: true, // normalLineList
advLines: true, // advancedLineList
curves: true, // advancedCurveList
points: true, // advancedPointList
areas: true, // advancedAreaList
},
mouseState: { //
screenX: 0, // Canvas X
screenY: 0, // Canvas Y
worldX: 0, // X
worldY: 0, // Y
isDragging: false, //
},
hoveredElement: null, // {kind, index}
isMapLoaded: false, //
};
let mapCanvas; // DOM
let canvasCtx; // 2D
let canvasWidthPx = 0; // Canvas CSS
let canvasHeightPx = 0; // Canvas CSS
let devicePixelRatio = 1; // DPI > 1
const init = () => {
mapCanvas = document.getElementById('map-canvas');
canvasCtx = mapCanvas.getContext('2d');
}
/**
* 根据容器尺寸和设备像素比调整 Canvas 大小
* 在窗口 resize 时自动调用
*/
function resizeCanvasToContainer() {
const containerRect = mapCanvas.parentElement.getBoundingClientRect(); //
devicePixelRatio = window.devicePixelRatio || 1; //
canvasWidthPx = containerRect.width; //
canvasHeightPx = containerRect.height; // = -
mapCanvas.width = canvasWidthPx * devicePixelRatio; // Canvas
mapCanvas.height = canvasHeightPx * devicePixelRatio; // Canvas
mapCanvas.style.width = canvasWidthPx + 'px'; // CSS
mapCanvas.style.height = canvasHeightPx + 'px'; // CSS
}
/**
* 将世界坐标转换为 Canvas 屏幕像素坐标
* 注意世界坐标 Y 轴向上Canvas Y 轴向下因此需要翻转
* @param {number} worldX - 世界坐标 X
* @param {number} worldY - 世界坐标 Y
* @returns {{x: number, y: number}} Canvas 上的屏幕像素坐标
*/
function worldToScreen(worldX, worldY) {
const camera = applicationState.camera;
const screenX = (worldX - camera.centerX) * camera.pixelsPerMeter + canvasWidthPx / 2; // +
const screenY = -(worldY - camera.centerY) * camera.pixelsPerMeter + canvasHeightPx / 2; // +
return { x: screenX, y: screenY };
}
/**
* Canvas 屏幕像素坐标反算为世界坐标
* worldToScreen 互为逆运算
* @param {number} screenX - Canvas 像素 X
* @param {number} screenY - Canvas 像素 Y
* @returns {{x: number, y: number}} 世界坐标
*/
function screenToWorld(screenX, screenY) {
const camera = applicationState.camera;
const worldX = (screenX - canvasWidthPx / 2) / camera.pixelsPerMeter + camera.centerX; //
const worldY = -(screenY - canvasHeightPx / 2) / camera.pixelsPerMeter + camera.centerY; //
return { x: worldX, y: worldY };
}
/**
* 解析 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;
}
//
// 7.
// normalPosList Canvas
//
/**
* normalPosList 构建占据栅格图像
*
* 原理normalPosList 中每个点标记一个自由通行格子
* 未出现在列表中的格子 = 障碍物墙壁
* 墙壁格子如果紧邻自由格子则标记为"墙壁边缘"渲染高亮
*
* @param {Object} parsedMap - parseSmapJson 的输出
* @param {boolean} showEdgeGlow - 是否计算墙壁边缘高亮效果
* @returns {HTMLCanvasElement} 离屏 Canvas 元素包含渲染好的栅格图像
*/
function 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;
}
/**
* 加载地图到应用构建栅格图像设置相机更新UI
* @param {Object} parsedMap - parseSmapJson 的输出
*/
async function loadMapIntoApplication(parsedMap) {
await new Promise(resolve => setTimeout(resolve, 30)); // UI
//
applicationState.parsedMap = parsedMap;
//
applicationState.gridOffscreen = buildOccupancyGridImage(parsedMap, applicationState.layerVisibility.edges);
//
applicationState.camera.centerX = (parsedMap.originX + parsedMap.maxX) / 2; //
applicationState.camera.centerY = (parsedMap.originY + parsedMap.maxY) / 2; //
// 使
const worldWidthMeters = parsedMap.maxX - parsedMap.originX; //
const worldHeightMeters = parsedMap.maxY - parsedMap.originY; //
const fitScaleX = canvasWidthPx / worldWidthMeters; //
const fitScaleY = canvasHeightPx / worldHeightMeters; //
applicationState.camera.pixelsPerMeter = Math.max(1, Math.min(fitScaleX, fitScaleY) * 0.88); // 12%
// UI
applicationState.isMapLoaded = true;
}
//
// 9.
//
/** 全局动画时间戳(秒),用于脉冲动画效果 */
let animationTimeSeconds = 0;
/**
* 主渲染函数 requestAnimationFrame 循环调用
* 按层顺序绘制栅格 网格线 区域 基础线 高级线 曲线 边界
*
* @param {number} timestamp - requestAnimationFrame 提供的时间戳毫秒
*/
function renderFrame(timestamp) {
//
animationTimeSeconds = (timestamp || 0) / 1000;
// Canvas
canvasCtx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
canvasCtx.clearRect(0, 0, canvasWidthPx, canvasHeightPx); //
//
if (!applicationState.isMapLoaded) {
requestAnimationFrame(renderFrame); //
return;
}
const map = applicationState.parsedMap; //
const layers = applicationState.layerVisibility; //
//
// 1:
//
if (layers.grid && applicationState.gridOffscreen) {
const topLeftScreen = worldToScreen(map.originX, map.maxY); //
const bottomRightScreen = worldToScreen(map.maxX, map.originY); //
canvasCtx.imageSmoothingEnabled = false; // 齿
canvasCtx.drawImage(
applicationState.gridOffscreen, // Canvas
topLeftScreen.x, topLeftScreen.y, //
bottomRightScreen.x - topLeftScreen.x, //
bottomRightScreen.y - topLeftScreen.y //
);
}
//
// 2: 线1
//
if (layers.gridlines) {
//
const viewTopLeft = screenToWorld(0, 0); //
const viewBottomRight = screenToWorld(canvasWidthPx, 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, canvasHeightPx); //
}
for (let worldY = viewMinY; worldY <= viewMaxY; worldY++) { // 线
const screenPos = worldToScreen(0, worldY);
canvasCtx.moveTo(0, screenPos.y); //
canvasCtx.lineTo(canvasWidthPx, screenPos.y); //
}
canvasCtx.stroke();
}
//
// 3: advancedAreaList
//
if (layers.areas) {
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); //
}
}
//
// 4: normalLineList线 #00e5ff
//
if (layers.normalLines) {
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();
}
}
//
// 5: advancedLineList线 ForbiddenLine
//
if (layers.advLines) {
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);
}
}
//
// 6: advancedCurveList线绿 #76ff03
//
if (layers.curves) {
for (let curveIndex = 0; curveIndex < map.advancedCurves.length; curveIndex++) {
const curve = map.advancedCurves[curveIndex]; // 线
// posstartPos/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);
}
}
//
// 7: advancedPointList
//
if (layers.points) {
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 = applicationState.hoveredElement &&
applicationState.hoveredElement.kind === 'point' &&
applicationState.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(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);
}
}
//
// 线
//
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([]); // 线
//
requestAnimationFrame(renderFrame);
}
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) {
applicationState.camera.centerX -= mouseEvent.movementX / applicationState.camera.pixelsPerMeter; //
applicationState.camera.centerY += mouseEvent.movementY / applicationState.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); //
applicationState.camera.pixelsPerMeter = Math.max(0.5, Math.min(2000, applicationState.camera.pixelsPerMeter * zoomFactor)); //
const mouseWorldAfter = screenToWorld(applicationState.mouseState.screenX, applicationState.mouseState.screenY); //
// 使
applicationState.camera.centerX += mouseWorldBefore.x - mouseWorldAfter.x;
applicationState.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('/2.smap');
const rawJsonObject = JSON.parse(fileTextContent); // JSON
const parsedMap = parseSmapJson(rawJsonObject); //
await loadMapIntoApplication(parsedMap); //
} catch (parseError) {
alert('Parse error: ' + parseError.message); //
}
}
onMounted(() => {
init()
//
loadUserFile()
requestAnimationFrame(renderFrame);
window.addEventListener('resize', resizeCanvasToContainer); //
resizeCanvasToContainer(); //
setTimeout(() => {
bindEvent()
}, 100)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeCanvasToContainer)
canvasCtx = null
})
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
}
</style>

View 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>

View 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>

View 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>

View File

@ -51,7 +51,7 @@ export default defineConfig(({mode, command}) => {
//李小龙 http://192.168.0.5:13080 //李小龙 http://192.168.0.5:13080
// dev http://10.148.20.34:13080 // dev http://10.148.20.34:13080
// target: VITE_API_URL, // 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, changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '') rewrite: (p) => p.replace(/^\/dev-api/, '')
} }