1163 lines
33 KiB
Vue
1163 lines
33 KiB
Vue
<template>
|
|
<div class="index-page" @wheel="handleWheel">
|
|
<div class="index-page-container" ref="indexPageContainerRef">
|
|
<div
|
|
class="map-container"
|
|
:style="{
|
|
cursor: isDrag ? 'pointer' : 'default',
|
|
transformOrigin: '0 0',
|
|
transform: `scale(${isSizeRadio})`,
|
|
height: imgBgObj.height + 'px',
|
|
width: imgBgObj.width + 'px'
|
|
}"
|
|
>
|
|
<div
|
|
:class="isDrag ? 'indexpage-container-active' : 'indexpage-container'"
|
|
v-if="imgBgObj.imgUrl"
|
|
v-drag="isDrag"
|
|
:style="{ scale: 1, transformOrigin: '0 0' }"
|
|
ref="draggableElement"
|
|
>
|
|
<div class="indexpage-container-box">
|
|
<img :src="imgBgObj.imgUrl" class="map-bg" />
|
|
<div class="point-information">
|
|
<div class="line-list" v-if="legendObj.driveLineShow">
|
|
<svg id="svg" :width="imgBgObj.width" :height="imgBgObj.height">
|
|
<template v-for="(item, index) in state.mapRouteList" :key="index">
|
|
<template v-if="item.method == 0">
|
|
<!-- 直线 -->
|
|
<line
|
|
:x1="Number(item.startPointX) * radio"
|
|
:y1="Number(item.startPointY) * radio"
|
|
:x2="Number(item.endPointX) * radio"
|
|
:y2="Number(item.endPointY) * radio"
|
|
:stroke="item.isSelect ? '#f48924' : '#2d72d9'"
|
|
:stroke-width="4 * radio"
|
|
/>
|
|
</template>
|
|
<!-- 曲线 -->
|
|
<template v-else>
|
|
<path
|
|
:d="getCurvePath(item)"
|
|
:stroke="item.isSelect ? '#f48924' : '#2d72d9'"
|
|
:stroke-width="4 * radio"
|
|
fill="none"
|
|
/>
|
|
</template>
|
|
</template>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- 小车 -->
|
|
<div v-for="(item, index) in testCarList" :key="item.macAddress">
|
|
<el-popover placement="bottom">
|
|
<template #reference>
|
|
<div
|
|
class="car-item"
|
|
v-if="legendObj.carShow"
|
|
:style="item.style"
|
|
@dblclick="carDbClick(item, index)"
|
|
>
|
|
<div style="font-size: 15px; color: #fff; z-index: 9999; background: #cb00c7">
|
|
{{ item.robotNo || '' }}
|
|
</div>
|
|
<img
|
|
src="@/assets/imgs/indexPage/car2.png"
|
|
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<div class="popover-robot-no">{{ item.robotNo }}</div>
|
|
</el-popover>
|
|
</div>
|
|
<div
|
|
class="map-point-list"
|
|
v-for="(item, index) in state.allMapPointInfo"
|
|
:key="index"
|
|
:style="{
|
|
left:
|
|
(Number(item.locationX) - Number(item.locationWidePx) / 2) * Number(radio) +
|
|
'px',
|
|
top:
|
|
(Number(item.locationY) - Number(item.locationDeepPx) / 2) * Number(radio) +
|
|
'px',
|
|
width: Number(item.locationWidePx) * Number(radio) + 'px',
|
|
height: Number(item.locationDeepPx) * Number(radio) + 'px'
|
|
}"
|
|
>
|
|
<!-- 1 路径点 -->
|
|
<el-tooltip effect="light" placement="top">
|
|
<template #content>
|
|
<div v-if="item.type === 2">
|
|
<div
|
|
class="indexpage-popover-item"
|
|
v-if="item.dataList && item.dataList[0]?.laneName"
|
|
>
|
|
<div> 所属线库: </div>
|
|
<div>
|
|
{{ item.dataList[0]?.laneName || '' }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="indexpage-popover-item"
|
|
v-if="item.dataList && item.dataList[0]?.areaName"
|
|
>
|
|
<div> 所属区域: </div>
|
|
<div>
|
|
{{ item.dataList[0]?.areaName || '' }}
|
|
</div>
|
|
</div>
|
|
<div class="indexpage-popover-item">
|
|
<div> 库位号: </div>
|
|
<div>
|
|
<div
|
|
v-for="(location, locationIndex) in item.dataList"
|
|
:key="locationIndex"
|
|
>
|
|
{{ location.locationNo || '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="item.type === 3">
|
|
<div class="indexpage-popover-item">
|
|
<div> 设备编号: </div>
|
|
<div>
|
|
{{ item.dataObj?.deviceNo || '' }}
|
|
</div>
|
|
</div>
|
|
<div class="indexpage-popover-item">
|
|
<div> 设备类型: </div>
|
|
<div>
|
|
{{ filterTypeFun(item.dataObj.deviceType) || '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div class="indexpage-popover-item">
|
|
<div> 节点类型: </div>
|
|
<div>{{
|
|
item.type == 1
|
|
? '路径点'
|
|
: item.type == 4
|
|
? '停车点'
|
|
: item.type == 5
|
|
? '区域变更点'
|
|
: item.type == 6
|
|
? '等待点'
|
|
: ''
|
|
}}</div>
|
|
<div>
|
|
{{ item.sortNum || '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div>
|
|
<div
|
|
v-if="
|
|
item.type === 1 &&
|
|
legendObj.sortNumberShow &&
|
|
item.sortNum &&
|
|
sortNumberShow
|
|
"
|
|
class="sort-num"
|
|
:style="getSortNumStyle(item, index)"
|
|
>
|
|
{{ item.sortNum }}
|
|
</div>
|
|
<div
|
|
v-if="
|
|
item.type !== 1 &&
|
|
legendObj.sortNumberShow &&
|
|
item.sortNum &&
|
|
sortNumberShow
|
|
"
|
|
class="sort-num-location"
|
|
:style="getSortNumLocationStyle(item, index)"
|
|
>
|
|
{{ item.sortNum }}
|
|
</div>
|
|
<div
|
|
v-if="item.type === 1"
|
|
:style="{
|
|
width: Number(item.locationWidePx) * Number(radio) + 'px',
|
|
height: Number(item.locationDeepPx) * Number(radio) + 'px',
|
|
backgroundColor: '#000',
|
|
borderRadius: '50%'
|
|
}"
|
|
>
|
|
</div>
|
|
<!-- 库位点 -->
|
|
<img
|
|
v-else-if="item.type === 2"
|
|
src="@/assets/imgs/indexPage/bin-location.png"
|
|
:style="nodeStyle(item, index)"
|
|
@dblclick="storeClick(item)"
|
|
/>
|
|
<!-- 设备点 -->
|
|
<img
|
|
v-else-if="item.type === 3"
|
|
:src="item.dataObj.mapImageUrl || '@/assets/imgs/indexPage/equipment.png'"
|
|
style="background: #fff"
|
|
:style="nodeStyle(item, index)"
|
|
/>
|
|
<!-- 停车点 -->
|
|
<img
|
|
v-else-if="item.type === 4"
|
|
src="@/assets/imgs/indexPage/stop-car.png"
|
|
style="background: #fff"
|
|
:style="nodeStyle(item, index)"
|
|
/>
|
|
<!-- 区域变更点 -->
|
|
<img
|
|
v-else-if="item.type === 5"
|
|
src="@/assets/imgs/indexPage/change-point.png"
|
|
style="background: #fff"
|
|
:style="nodeStyle(item, index)"
|
|
/>
|
|
<!-- 等待点 -->
|
|
<img
|
|
v-else-if="item.type === 6"
|
|
src="@/assets/imgs/indexPage/wait-point.png"
|
|
style="background: #fff"
|
|
:style="nodeStyle(item, index)"
|
|
/>
|
|
</div>
|
|
</el-tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 左下角图层 -->
|
|
<div class="affix-container-left" v-if="!isAllBoard">
|
|
<div class="affix-container-left-box">
|
|
<div
|
|
class="legend-list"
|
|
:style="{
|
|
height: legendObj.legendShow ? '126px' : '0',
|
|
overflow: 'hidden',
|
|
transition: 'all 0.3s ease-in-out'
|
|
}"
|
|
>
|
|
<div class="legend-item" v-for="item in layerList" :key="item.type">
|
|
<div class="legend-item-left"> {{ item.name }} </div>
|
|
<el-icon
|
|
class="legend-item-img"
|
|
size="20"
|
|
v-if="item.isShow"
|
|
@click="changDriveLineShow(item)"
|
|
color="#1677FF"
|
|
><View
|
|
/></el-icon>
|
|
<el-icon
|
|
class="legend-item-img"
|
|
size="20"
|
|
v-if="!item.isShow"
|
|
@click="changDriveLineShow(item)"
|
|
color="#444444"
|
|
><Hide
|
|
/></el-icon>
|
|
</div>
|
|
</div>
|
|
<div class="legend-item-bottom" @click="legendObj.legendShow = !legendObj.legendShow">
|
|
<div class="legend-item-bottom-left"> 图例 </div>
|
|
<el-icon size="22" v-if="legendObj.legendShow" color="#98A4BF"><CaretTop /></el-icon>
|
|
<el-icon size="22" v-else color="#98A4BF"><CaretBottom /></el-icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右下角的功能 -->
|
|
<div class="affix-container-right" v-if="!isAllBoard">
|
|
<div class="item" @click="changeIsDrag">
|
|
<img src="@/assets/imgs/indexPage/hand.png" />
|
|
</div>
|
|
<div class="item">
|
|
<img src="@/assets/imgs/indexPage/zoom.png" @click="changeSize('add')" />
|
|
</div>
|
|
<div class="item">
|
|
<img src="@/assets/imgs/indexPage/search.png" @click="changeSize('sub')" />
|
|
</div>
|
|
<div class="item">
|
|
<img src="@/assets/imgs/indexPage/full-screen.png" @click="toggleFullScreen" />
|
|
</div>
|
|
</div>
|
|
<storeDialog ref="storeDialogRef" @success="emitParent" />
|
|
<carDialog ref="carDialogRef" />
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
ref,
|
|
defineComponent,
|
|
reactive,
|
|
nextTick,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
onUnmounted
|
|
} from 'vue'
|
|
import * as MapApi from '@/api/map/map'
|
|
import WebSocketClient from '../webSocket.js'
|
|
import storeDialog from './storeDialog.vue'
|
|
import { color } from 'echarts'
|
|
import { resetDragPosition } from '@/utils/drag'
|
|
import carDialog from './carDialog.vue'
|
|
import { is } from 'bpmn-js/lib/util/ModelUtil'
|
|
import JSONBigInt from 'json-bigint'
|
|
import { propTypes } from '@/utils/propTypes'
|
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
|
import dayjs from 'dayjs'
|
|
import { throttle, debounce } from 'lodash-es'
|
|
|
|
const message = useMessage() // 消息弹窗
|
|
|
|
const carDialogRef = ref(null)
|
|
const socketClient = ref(null)
|
|
const router = useRouter() // 路由对象
|
|
const emit = defineEmits(['transmitMapId'])
|
|
const storeDialogRef = ref(null) // 仓库信息弹窗
|
|
const list = ref([])
|
|
const testCarList = ref([]) //小车数组
|
|
const carWidth = ref(226)
|
|
const carHeight = ref(124)
|
|
|
|
// 定义属性
|
|
const props = defineProps({
|
|
isAllBoard: propTypes.bool.def(false), // 当前选中的链接
|
|
isFullScreen: {
|
|
type: Boolean,
|
|
default: () => false
|
|
},
|
|
sortNumberShow: {
|
|
type: Boolean,
|
|
default: () => true
|
|
}
|
|
})
|
|
|
|
//节点样式
|
|
const nodeStyle = (item, index) => {
|
|
return {
|
|
verticalAlign: 'top',
|
|
objectFit: 'cover',
|
|
width: Number(item.locationWidePx) * Number(radio.value) + 'px',
|
|
height: Number(item.locationDeepPx) * Number(radio.value) + 'px',
|
|
borderRadius: '3px'
|
|
}
|
|
}
|
|
//sortNum路径点的样式
|
|
const getSortNumStyle = (item, index) => {
|
|
let leftNum = 0
|
|
if (item.sortNum.toString().length === 1) {
|
|
leftNum = 3
|
|
} else if (item.sortNum.toString().length === 2) {
|
|
leftNum = 7
|
|
} else if (item.sortNum.toString().length === 3) {
|
|
leftNum = 10
|
|
} else if (item.sortNum.toString().length === 4) {
|
|
leftNum = 14
|
|
} else if (item.sortNum.toString().length === 5) {
|
|
leftNum = 18
|
|
} else if (item.sortNum.toString().length === 6) {
|
|
leftNum = 21
|
|
} else if (item.sortNum.toString().length === 7) {
|
|
leftNum = 25
|
|
} else if (item.sortNum.toString().length === 8) {
|
|
leftNum = 28
|
|
} else if (item.sortNum.toString().length === 9) {
|
|
leftNum = 31
|
|
}
|
|
return {
|
|
left: Number(item.locationWidePx) / 2 - leftNum + 'px',
|
|
top: 6 + 'px'
|
|
}
|
|
}
|
|
//sortNum非路径点的样式
|
|
const getSortNumLocationStyle = (item, index) => {
|
|
let leftNum = 0
|
|
if (item.sortNum.toString().length === 1) {
|
|
leftNum = 3
|
|
} else if (item.sortNum.toString().length === 2) {
|
|
leftNum = 7
|
|
} else if (item.sortNum.toString().length === 3) {
|
|
leftNum = 10
|
|
} else if (item.sortNum.toString().length === 4) {
|
|
leftNum = 14
|
|
} else if (item.sortNum.toString().length === 5) {
|
|
leftNum = 18
|
|
} else if (item.sortNum.toString().length === 6) {
|
|
leftNum = 21
|
|
} else if (item.sortNum.toString().length === 7) {
|
|
leftNum = 25
|
|
} else if (item.sortNum.toString().length === 8) {
|
|
leftNum = 28
|
|
} else if (item.sortNum.toString().length === 9) {
|
|
leftNum = 31
|
|
}
|
|
return {
|
|
left: Number(item.locationWidePx) / 2 - leftNum + 'px',
|
|
top: Number(item.locationDeepPx) / 2 - 2 + 'px'
|
|
}
|
|
}
|
|
|
|
//返回设备类型
|
|
const filterTypeFun = (deviceType) => {
|
|
let list = getIntDictOptions(DICT_TYPE.DEVICE_TYPE)
|
|
let deviceItem = list.find((item) => {
|
|
return item.value == deviceType
|
|
})
|
|
return deviceItem.label
|
|
}
|
|
|
|
const convertActualToBrowser = (pointX, pointY) => {
|
|
let resolution = Number(imgBgObj.resolution)
|
|
let origin = imgBgObj.origin
|
|
|
|
const y1 = Number(origin[1]) + Number(imgBgObj.height) * resolution
|
|
let x = Math.max(Number(pointX) - Number(origin[0]), 0)
|
|
let y = Math.max(y1 - Number(pointY), 0)
|
|
|
|
return {
|
|
x: x / resolution - carWidth.value / resolution / 100 / 2,
|
|
y: y / resolution - carHeight.value / resolution / 100 / 2
|
|
}
|
|
}
|
|
|
|
//是否可以拖拽
|
|
const isDrag = ref(false)
|
|
const changeIsDrag = () => {
|
|
nextTick(() => {
|
|
isDrag.value = !isDrag.value
|
|
if (!isDrag.value) {
|
|
//还原位置
|
|
resetPosition()
|
|
}
|
|
})
|
|
}
|
|
|
|
// 获取曲线的路径
|
|
const getCurvePath = (curve) => {
|
|
let startPointX = Number(curve.startPointX) * radio.value
|
|
let startPointY = Number(curve.startPointY) * radio.value
|
|
let endPointX = Number(curve.endPointX) * radio.value
|
|
let endPointY = Number(curve.endPointY) * radio.value
|
|
return `M ${startPointX} ${startPointY} C ${curve.beginControlX * radio.value} ${curve.beginControlY * radio.value}, ${curve.endControlX * radio.value} ${curve.endControlY * radio.value}, ${endPointX} ${endPointY}`
|
|
}
|
|
|
|
//放大缩小
|
|
const isSizeRadio = ref(1)
|
|
const changeSize = (type) => {
|
|
if (type == 'add') {
|
|
if (isSizeRadio.value < 4) {
|
|
isSizeRadio.value += 0.1
|
|
} else {
|
|
isSizeRadio.value = 3.9
|
|
message.warning('不能在放大了')
|
|
}
|
|
} else {
|
|
if (isSizeRadio.value > 0.2) {
|
|
isSizeRadio.value -= 0.1
|
|
} else {
|
|
isSizeRadio.value = 0.1
|
|
message.warning('不能在缩小了')
|
|
}
|
|
}
|
|
}
|
|
|
|
// 图层显示切换相关
|
|
const layerList = ref([
|
|
{
|
|
type: 1,
|
|
name: '行驶路线',
|
|
isShow: true
|
|
},
|
|
{
|
|
type: 2,
|
|
name: '车辆',
|
|
isShow: true
|
|
},
|
|
{
|
|
type: 3,
|
|
name: '节点ID',
|
|
isShow: true
|
|
}
|
|
])
|
|
const legendObj = reactive({
|
|
driveLineShow: true,
|
|
carShow: true,
|
|
sortNumberShow: true,
|
|
legendShow: true
|
|
})
|
|
const changDriveLineShow = (item) => {
|
|
item.isShow = !item.isShow
|
|
if (item.type == 1) {
|
|
legendObj.driveLineShow = !legendObj.driveLineShow
|
|
} else if (item.type == 2) {
|
|
legendObj.carShow = !legendObj.carShow
|
|
} else if (item.type == 3) {
|
|
legendObj.sortNumberShow = !legendObj.sortNumberShow
|
|
}
|
|
}
|
|
|
|
//全屏相关
|
|
const indexPageContainerRef = ref()
|
|
const isFullScreenDisplay = ref(false)
|
|
const toggleFullScreen = () => {
|
|
if (!isFullScreenDisplay.value) {
|
|
indexPageContainerRef.value.requestFullscreen().catch((err) => {
|
|
console.error(`全屏错误: ${err.message}`)
|
|
})
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen()
|
|
}
|
|
}
|
|
}
|
|
//全屏切换
|
|
const handleFullscreenChange = () => {
|
|
isFullScreenDisplay.value = !!document.indexPageContainerRef
|
|
}
|
|
|
|
//库位双击
|
|
const storeClick = async (item) => {
|
|
let storeData = await MapApi.houseLocationGetByMapItemId({
|
|
mapId: item.positionMapId,
|
|
mapItemId: item.id
|
|
})
|
|
storeDialogRef.value.open(JSON.parse(JSON.stringify(storeData)))
|
|
}
|
|
|
|
//将节点实际宽高cm转换成px
|
|
const cmConversionPx = (cWidth, cHeight) => {
|
|
let pWidth = Number(cWidth) / Number(imgBgObj.resolution) / 100
|
|
let pHeight = Number(cHeight) / Number(imgBgObj.resolution) / 100
|
|
return {
|
|
pWidth,
|
|
pHeight
|
|
}
|
|
}
|
|
|
|
const draggableElement = ref(null)
|
|
const resetPosition = () => {
|
|
if (draggableElement.value) {
|
|
resetDragPosition(draggableElement.value)
|
|
}
|
|
}
|
|
|
|
//websocket相关
|
|
const replaceHttpWithWs = (str) => {
|
|
return str.replace(/^http/, 'ws')
|
|
}
|
|
|
|
// 优化轮询机制
|
|
const robotListTimerRef = ref(null)
|
|
const robotByFloorAndAreaList = ref([])
|
|
|
|
const getRobotByFloorAndAreaList = async () => {
|
|
try {
|
|
const res = await MapApi.getRobotByFloorAndArea({
|
|
floor: imgBgObj.floor,
|
|
area: imgBgObj.area
|
|
})
|
|
robotByFloorAndAreaList.value = res
|
|
if (testCarList.value.length) {
|
|
// 使用 Set 优化查找性能
|
|
const onlineRobots = new Set(robotByFloorAndAreaList.value)
|
|
testCarList.value = testCarList.value.filter((item) =>
|
|
onlineRobots.has(item.data.pose2d.robotNo)
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取机器人列表失败:', error)
|
|
}
|
|
}
|
|
|
|
// 使用 ref 管理 WebSocket 连接
|
|
const wsConnection = ref(null)
|
|
|
|
// WebSocket 连接管理
|
|
const initWebsocket = () => {
|
|
if (wsConnection.value) {
|
|
wsConnection.value.disconnect()
|
|
}
|
|
|
|
const websocketUrl = `${replaceHttpWithWs(import.meta.env.VITE_BASE_URL)}/infra/ws?type=map&floor=${imgBgObj.floor}&area=${imgBgObj.area}`
|
|
wsConnection.value = new WebSocketClient(websocketUrl)
|
|
|
|
if (wsConnection.value) {
|
|
socketClient.value = wsConnection.value
|
|
// 监听消息
|
|
socketClient.value.onMessage((message) => {
|
|
if (message == 'ping' || message == 'pong') return
|
|
let jsonMsg = JSON.parse(message)
|
|
if (jsonMsg.type == 'map_push') {
|
|
requestAnimationFrame(() => {
|
|
let data = JSON.parse(jsonMsg.content)
|
|
// console.log(data)
|
|
|
|
let dataList = Object.entries(data).map(([key, value]) => ({
|
|
macAddress: key,
|
|
data: JSON.parse(value)
|
|
}))
|
|
testCarList.value = throttledUpdateCarList(testCarList.value, dataList, imgBgObj)
|
|
})
|
|
}
|
|
//告警信息
|
|
if (jsonMsg.type == 'agv_warn') {
|
|
ElMessage({
|
|
message: () =>
|
|
h(
|
|
'div',
|
|
{
|
|
style: 'background-color: #C60606;'
|
|
},
|
|
[
|
|
h(
|
|
'span',
|
|
{ style: 'color: rgba(255,255,255,0.88);font-size: 14px;' },
|
|
`${JSON.parse(jsonMsg.content)}`
|
|
),
|
|
h(
|
|
'span',
|
|
{
|
|
onClick: () => lookError(),
|
|
style:
|
|
'color: rgba(255,255,255,0.88);font-size: 14px;cursor: pointer;text-decoration-line: underline;margin-left: 32px;'
|
|
},
|
|
'详情'
|
|
)
|
|
]
|
|
),
|
|
showClose: false,
|
|
duration: 3000, // 让消息持续显示,直到用户关闭
|
|
type: 'info',
|
|
customClass: 'indexpage-custom-message-style'
|
|
})
|
|
}
|
|
// 规划路线信息
|
|
if (jsonMsg.type == 'planning_move_pose') {
|
|
let data = JSON.parse(jsonMsg.content)
|
|
let dataList = JSON.parse(data).data
|
|
if (state.mapRouteList.length > 0) {
|
|
state.mapRouteList = setIsSelect(state.mapRouteList, dataList)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const throttledUpdateCarList = throttle((currentList, newDataList, mapInfo) => {
|
|
return updateCarList(currentList, newDataList, mapInfo)
|
|
}, 100) // 100ms 节流
|
|
|
|
// 优化车辆数据合并逻辑
|
|
const mergeArraysWithoutDelete = (arr1, arr2) => {
|
|
const macAddressMap = new Map(arr1.map((item) => [item.macAddress, item]))
|
|
|
|
arr2.forEach((item2) => {
|
|
const existingItem = macAddressMap.get(item2.macAddress)
|
|
if (existingItem) {
|
|
existingItem.data.pose2d = item2.data.pose2d
|
|
} else {
|
|
macAddressMap.set(item2.macAddress, item2)
|
|
}
|
|
})
|
|
|
|
return Array.from(macAddressMap.values())
|
|
}
|
|
|
|
// 优化车辆位置计算
|
|
const processCarData = (car, mapInfo) => {
|
|
const browserPos = convertActualToBrowser(car.data.pose2d.x, car.data.pose2d.y)
|
|
return {
|
|
...car,
|
|
originWidth: mapInfo.width,
|
|
originHeight: mapInfo.height,
|
|
origin: mapInfo.origin,
|
|
realX: browserPos.x,
|
|
realY: browserPos.y,
|
|
robotNo: car.data.pose2d.robotNo,
|
|
style: {
|
|
left: browserPos.x * radio.value + 'px',
|
|
top: browserPos.y * radio.value + 'px',
|
|
width: (carWidth.value / imgBgObj.resolution / 100) * radio.value + 'px',
|
|
height: (carHeight.value / imgBgObj.resolution / 100) * radio.value + 'px',
|
|
zIndex: 9999,
|
|
transform: `rotate(${-car.data.pose2d.yaw * (180 / Math.PI)}deg)`
|
|
}
|
|
}
|
|
}
|
|
|
|
// 更新车辆列表
|
|
const updateCarList = (currentList, newDataList, mapInfo) => {
|
|
const updatedList = mergeArraysWithoutDelete(currentList, newDataList)
|
|
return updatedList.map((car) => processCarData(car, mapInfo))
|
|
}
|
|
|
|
const setIsSelect = (arr1, arr2) => {
|
|
for (let i = 0; i < arr1.length; i++) {
|
|
const element = arr1[i]
|
|
const isExist = arr2.includes(element.id)
|
|
element.isSelect = isExist
|
|
}
|
|
return arr1
|
|
}
|
|
|
|
// 查看更多异常列表
|
|
const lookError = () => {
|
|
router.push({
|
|
path: '/carError'
|
|
})
|
|
}
|
|
|
|
const emitParent = () => {
|
|
getMapData(imgBgObj)
|
|
}
|
|
|
|
const computedRatio = () => {
|
|
nextTick(() => {
|
|
if (props.isFullScreen) {
|
|
let width = getElementWidthByClass('index-page-container')
|
|
|
|
testCarList.value.forEach((item) => {
|
|
item.originWidth = imgBgObj.width
|
|
item.originHeight = imgBgObj.height
|
|
item.origin = imgBgObj.origin
|
|
item.realX = convertActualToBrowser(item.data.pose2d.x, item.data.pose2d.y).x
|
|
item.realY = convertActualToBrowser(item.data.pose2d.x, item.data.pose2d.y).y
|
|
})
|
|
|
|
radio.value = width / imgBgObj.width
|
|
imgBgObj.width = imgBgObj.width * radio.value
|
|
imgBgObj.height = imgBgObj.height * radio.value
|
|
}
|
|
})
|
|
}
|
|
|
|
//偏航率斜率算旋转
|
|
const radianToDegree = (radian) => {
|
|
const degree = radian * (180 / Math.PI)
|
|
return `${-degree}`
|
|
}
|
|
|
|
const radio = ref(1) //放大缩小的倍数
|
|
const state = reactive({
|
|
mapRouteList: [],
|
|
allMapPointInfo: []
|
|
})
|
|
|
|
//获取扫描图 地图背景相关的信息
|
|
const imgBgObj = reactive({
|
|
imgUrl: '',
|
|
positionMapId: '',
|
|
width: '',
|
|
height: '',
|
|
floor: '',
|
|
area: '',
|
|
resolution: 0,
|
|
origin: null
|
|
})
|
|
const getMapData = async (item) => {
|
|
let yamlJson = JSON.parse(item.yamlJson)
|
|
imgBgObj.positionMapId = item.id
|
|
imgBgObj.floor = item.floor
|
|
imgBgObj.area = item.area
|
|
imgBgObj.width = yamlJson.width
|
|
imgBgObj.height = yamlJson.height
|
|
imgBgObj.origin = yamlJson.origin
|
|
imgBgObj.resolution = yamlJson.resolution
|
|
//获取地图
|
|
getMapDownloadPng(imgBgObj)
|
|
//初始化Websocket
|
|
initWebsocket()
|
|
}
|
|
const getMapDownloadPng = async (mapInfo) => {
|
|
let data = await MapApi.getPositionMapdDwnloadPngBase64({
|
|
floor: mapInfo.floor,
|
|
area: mapInfo.area
|
|
})
|
|
imgBgObj.imgUrl = data
|
|
|
|
//获取节点 路径等信息
|
|
await getAllNodeList()
|
|
await getAllMapRoute()
|
|
await computedRatio()
|
|
await getRobotByFloorAndAreaList()
|
|
}
|
|
// 获取地图连线
|
|
const getAllMapRoute = async () => {
|
|
state.mapRouteList = await MapApi.getPositionMapLineByPositionMapId(imgBgObj.positionMapId)
|
|
}
|
|
//获取节点
|
|
const getAllNodeList = async (positionMapId) => {
|
|
state.allMapPointInfo = await MapApi.getPositionMapItemList({
|
|
positionMapId: imgBgObj.positionMapId
|
|
})
|
|
state.allMapPointInfo?.forEach((item) => {
|
|
if (item.type === 1) {
|
|
item.locationDeep = 40
|
|
item.locationWide = 40
|
|
} else if (item.type === 5 || item.type === 6) {
|
|
item.locationDeep = 150
|
|
item.locationWide = 150
|
|
} else if (item.type === 2) {
|
|
//库位点
|
|
item.dataList = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
|
|
item.dataList = item.dataList.reverse()
|
|
item.locationDeep = item.dataList[0].locationDeep
|
|
item.locationWide = item.dataList[0].locationWide
|
|
} else if (item.type === 3) {
|
|
item.dataObj = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
|
|
item.locationDeep = item.dataObj.locationDeep
|
|
item.locationWide = item.dataObj.locationWide
|
|
} else if (item.type === 4) {
|
|
item.dataObj = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
|
|
item.locationDeep = item.dataObj.locationDeep
|
|
item.locationWide = item.dataObj.locationWide
|
|
}
|
|
//要将实际的cm改成px
|
|
if (item.locationWide && item.locationDeep) {
|
|
let pxObj = cmConversionPx(item.locationWide, item.locationDeep)
|
|
item.locationWidePx = pxObj.pWidth
|
|
item.locationDeepPx = pxObj.pHeight
|
|
}
|
|
})
|
|
}
|
|
|
|
//鼠标滚轮
|
|
const handleWheel = (event) => {
|
|
// 判断 Ctrl 键是否被按下
|
|
if (event.ctrlKey && !props.isFullScreen) {
|
|
// 阻止默认的滚动行为
|
|
event.preventDefault()
|
|
|
|
// 根据滚轮滚动方向调整缩放比例
|
|
if (event.deltaY < 0) {
|
|
// 向上滚动,放大
|
|
//放大
|
|
if (isSizeRadio.value < 4) {
|
|
isSizeRadio.value += 0.1
|
|
} else {
|
|
isSizeRadio.value = 3.9
|
|
message.warning('不能在放大了')
|
|
}
|
|
} else {
|
|
//缩小
|
|
if (isSizeRadio.value > 0.2) {
|
|
isSizeRadio.value -= 0.1
|
|
} else {
|
|
isSizeRadio.value = 0.1
|
|
message.warning('不能在缩小了')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const getElementWidthByClass = (className) => {
|
|
const element = document.querySelector(`.${className}`)
|
|
if (element) {
|
|
const widthWithUnit = window.getComputedStyle(element).width
|
|
return widthWithUnit.slice(0, -2)
|
|
}
|
|
return null
|
|
}
|
|
//小车双击
|
|
const carDbClick = (item, index) => {
|
|
carDialogRef.value.open(JSON.parse(JSON.stringify(item)))
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange)
|
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
|
|
// 设置轮询间隔
|
|
robotListTimerRef.value = setInterval(() => {
|
|
if (document.hidden) return // 页面不可见时暂停轮询
|
|
getRobotByFloorAndAreaList()
|
|
}, 10000)
|
|
|
|
// 监听页面可见性变化
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden && robotListTimerRef.value === null) {
|
|
robotListTimerRef.value = setInterval(getRobotByFloorAndAreaList, 10000)
|
|
}
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
|
|
document.removeEventListener('mozfullscreenchange', handleFullscreenChange)
|
|
document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
|
|
|
|
clearTheTimer()
|
|
})
|
|
onBeforeUnmount(() => {
|
|
clearTheTimer()
|
|
})
|
|
|
|
const clearTheTimer = () => {
|
|
// 清除轮询定时器
|
|
if (robotListTimerRef.value) {
|
|
clearInterval(robotListTimerRef.value)
|
|
robotListTimerRef.value = null
|
|
}
|
|
|
|
// 清理WebSocket连接
|
|
if (wsConnection.value) {
|
|
wsConnection.value.disconnect()
|
|
wsConnection.value = null
|
|
}
|
|
|
|
// 清理socketClient引用
|
|
if (socketClient.value) {
|
|
socketClient.value = null
|
|
}
|
|
|
|
// 清理其他相关数据
|
|
testCarList.value = []
|
|
robotByFloorAndAreaList.value = []
|
|
}
|
|
|
|
defineExpose({ getMapData, computedRatio, clearTheTimer }) // 提供 open 方法,用于打开弹窗
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.index-page {
|
|
width: 100%;
|
|
height: 100%;
|
|
box-sizing: border-box;
|
|
|
|
.index-page-container {
|
|
position: relative;
|
|
width: 100%;
|
|
|
|
.map-container {
|
|
width: 100%;
|
|
position: relative;
|
|
transform-style: preserve-3d; /* 3D 变换优化 */
|
|
backface-visibility: hidden;
|
|
perspective: 1000;
|
|
|
|
.indexpage-container {
|
|
width: 100%;
|
|
position: relative;
|
|
transform: translateZ(0);
|
|
will-change: transform;
|
|
}
|
|
|
|
.indexpage-container-active {
|
|
width: 100%;
|
|
position: relative;
|
|
cursor: grab;
|
|
transform: translateZ(0);
|
|
will-change: transform;
|
|
}
|
|
|
|
.indexpage-container-active:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.indexpage-container-box {
|
|
width: 100%;
|
|
position: relative;
|
|
|
|
.map-bg {
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
}
|
|
|
|
.point-information {
|
|
--radio: 1;
|
|
width: 100%;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
transform: translateZ(0);
|
|
|
|
.line-list {
|
|
position: absolute;
|
|
transform: translateZ(0);
|
|
will-change: transform;
|
|
}
|
|
}
|
|
|
|
.car-item {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
will-change: transform; /* 提示浏览器该元素会频繁变化 */
|
|
transform: translateZ(0); /* 强制GPU加速 */
|
|
}
|
|
|
|
.map-point-list {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
transform: translateZ(0);
|
|
will-change: transform;
|
|
|
|
.sort-num {
|
|
position: absolute;
|
|
font-size: 13px;
|
|
user-select: none;
|
|
color: #000;
|
|
}
|
|
.sort-num-location {
|
|
position: absolute;
|
|
font-size: 13px;
|
|
user-select: none;
|
|
color: #000;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.indexpage-popover-item {
|
|
display: flex;
|
|
font-family:
|
|
PingFangSC,
|
|
PingFang SC;
|
|
font-weight: 400;
|
|
font-size: 14px;
|
|
color: #0d162a;
|
|
padding: 3px;
|
|
}
|
|
|
|
.affix-container-left {
|
|
position: fixed;
|
|
right: 30px;
|
|
bottom: 70px;
|
|
z-index: 999;
|
|
|
|
.affix-container-left-box {
|
|
width: 144px;
|
|
}
|
|
|
|
.legend-list {
|
|
width: 100%;
|
|
border-bottom: 1px solid #eeeeee;
|
|
background: #ffffff;
|
|
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
.legend-item {
|
|
width: 100%;
|
|
padding: 0 18px;
|
|
height: 42px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
|
|
.legend-item-left {
|
|
font-family:
|
|
PingFangSC,
|
|
PingFang SC;
|
|
font-weight: 400;
|
|
font-size: 14px;
|
|
color: rgba(0, 0, 0, 0.88);
|
|
}
|
|
|
|
.legend-item-img {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.legend-item-bottom {
|
|
width: 100%;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.legend-item-bottom-left {
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
font-family:
|
|
PingFangSC,
|
|
PingFang SC;
|
|
font-weight: 400;
|
|
font-size: 16px;
|
|
color: #98a4bf;
|
|
margin-right: 2px;
|
|
}
|
|
}
|
|
|
|
.affix-container-right {
|
|
position: fixed;
|
|
z-index: 999;
|
|
right: 30px;
|
|
bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
|
|
.item {
|
|
width: 30px;
|
|
height: 30px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
margin-left: 8px;
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.popover-robot-no {
|
|
font-size: 15px;
|
|
text-align: center;
|
|
color: #000;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* 全屏模式下的样式 */
|
|
.index-page-container:-webkit-full-screen {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: white;
|
|
box-sizing: border-box;
|
|
overflow: auto; /* 允许滚动 */
|
|
}
|
|
|
|
.index-page-container:-moz-full-screen {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: white;
|
|
box-sizing: border-box;
|
|
overflow: auto;
|
|
}
|
|
|
|
.index-page-container:-ms-fullscreen {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: white;
|
|
box-sizing: border-box;
|
|
overflow: auto;
|
|
}
|
|
|
|
.index-page-container:fullscreen {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: white;
|
|
box-sizing: border-box;
|
|
overflow: auto;
|
|
}
|
|
</style>
|