zn-admin-vue3-wcs/src/views/mapPage/realTimeMap/components/indexPage.vue
2025-04-22 09:20:31 +08:00

1314 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="index-page" @wheel="handleWheel">
<div class="index-page-container" :class="!isFullScreen ? 'is-full-screen ' : ''">
<div
class="affix-container"
id="indexpage-container"
:style="{
cursor: isDrag ? 'pointer' : 'default',
transformOrigin: '0 0',
transform: `scale(${isSizeRadio})`,
height: imgBgObj.height + 'px',
width: isFull ? '100%' : 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="indexpage-container-box-img" />
<div class="indexpage-container-box-point">
<!-- 连线 -->
<div v-if="legendObj.driveLineShow">
<div class="line-box">
<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="#2d72d9"
:stroke-width="4 * radio"
@click="handleChooseRoute(item, index)"
/>
</template>
<!-- 曲线 -->
<template v-else>
<path
:d="getCurvePath(item)"
stroke="#2d72d9"
:stroke-width="4 * radio"
fill="none"
@click="handleChooseRoute(item, index)"
/>
</template>
</template>
</svg>
</div>
</div>
<!-- 小车 -->
<div
v-for="(item, index) in testCarList"
:key="index"
@dblclick="carDbClick(item, index)"
>
<div
class="indexpage-car-item"
v-if="legendObj.carShow"
:style="{
left: item.realX * radio + 'px',
top: item.realY * radio + 'px',
width: (carWidth / imgBgObj.resolution / 100) * radio + 'px',
height: (carHeight / imgBgObj.resolution / 100) * radio + 'px',
transform: 'rotate(' + radianToDegree(item.data.pose2d.yaw) + 'deg)',
zIndex: 9999
}"
>
<div
style="
font-size: 0.6rem;
position: absolute;
top: 50%;
left: 40%;
transform: translateX(-50%) translateY(-50%);
color: greenyellow;
"
>
{{ item.robotNo || '' }}
</div>
<img
src="@/assets/imgs/indexPage/chache-4备份 7@2x.png"
style="width: 100%; height: 100%"
/>
</div>
</div>
<div
class="indexpage-container-box-point-item"
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 class="box-item" effect="light" placement="top">
<template #content>
<div v-if="item.type === 2">
<div class="indexpage-popover-item">
<div> 库位号: </div>
<div>
{{ item.showData?.locationNo || '' }}
</div>
</div>
<div class="indexpage-popover-item">
<div> 所属线库: </div>
<div>
{{ item.showData?.laneName || '' }}
</div>
</div>
<div class="indexpage-popover-item">
<div> 所属区域: </div>
<div>
{{ item.showData?.areaName || '' }}
</div>
</div>
</div>
<div v-else-if="item.type === 3">
<div class="indexpage-popover-item">
<div> 设备编号: </div>
<div>
{{ item.showData?.deviceNo || '' }}
</div>
</div>
<div class="indexpage-popover-item">
<div> 设备类型: </div>
<div>
{{ item.showData ? filterTypeFun(item.showData.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"
class="sort-num"
:style="getSortNumStyle(item, index)"
>
{{ item.sortNum }}
</div>
<div
v-if="item.type !== 1 && legendObj.sortNumberShow && item.sortNum"
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="https://api.znkjfw.com/admin-api/infra/file/4/get/库位库存_png_179_1744098544821.png"
:style="nodeStyle(item, index)"
@dblclick="storeClick(item)"
/>
<!-- 设备点 -->
<img
v-else-if="item.type === 3"
:src="
item.formattedData.mapImageUrl ||
'https://api.znkjfw.com/admin-api/infra/file/4/get/设备 (4)_png_179_1744102025024.png'
"
style="background: #fff"
:style="nodeStyle(item, index)"
/>
<!-- 停车点 -->
<img
v-else-if="item.type === 4"
src="https://api.znkjfw.com/admin-api/infra/file/4/get/停车位_png_179_1744098982069.png"
style="background: #fff"
:style="nodeStyle(item, index)"
/>
<!-- 区域变更点 -->
<img
v-else-if="item.type === 5"
src="https://api.znkjfw.com/admin-api/infra/file/4/get/区域变更 (1)_png_179_1744100605191.png"
style="background: #fff"
:style="nodeStyle(item, index)"
/>
<!-- 等待点 -->
<img
v-else-if="item.type === 6"
src="https://api.znkjfw.com/admin-api/infra/file/4/get/等待_png_179_1744102070670.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">
<div class="legend-item-left"> 行驶路线 </div>
<el-icon
class="legend-item-img"
size="20"
v-if="legendObj.driveLineShow"
@click="changDriveLineShow"
color="#1677FF"
><View
/></el-icon>
<el-icon
class="legend-item-img"
size="20"
v-if="!legendObj.driveLineShow"
@click="changDriveLineShow"
color="#444444"
><Hide
/></el-icon>
</div>
<div class="legend-item">
<div class="legend-item-left"> 车辆 </div>
<el-icon
class="legend-item-img"
size="20"
v-if="legendObj.carShow"
@click="changCarShow"
color="#1677FF"
><View
/></el-icon>
<el-icon
class="legend-item-img"
size="20"
v-if="!legendObj.carShow"
@click="changCarShow"
color="#444444"
><Hide
/></el-icon>
</div>
<div class="legend-item">
<div class="legend-item-left"> 节点ID </div>
<el-icon
class="legend-item-img"
size="20"
v-if="legendObj.sortNumberShow"
@click="changeSortNumber"
color="#1677FF"
><View
/></el-icon>
<el-icon
class="legend-item-img"
size="20"
v-if="!legendObj.sortNumberShow"
@click="changeSortNumber"
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/编组 12.png" style="width: 100%; height: 100%" />
</div>
<!-- 放大 -->
<div class="item">
<img
src="@/assets/imgs/indexPage/编组 14.png"
style="width: 100%; height: 100%"
@click="changeSizeRaio('add')"
/>
</div>
<!-- 缩小 -->
<div class="item">
<img
src="@/assets/imgs/indexPage/编组 15.png"
style="width: 100%; height: 100%"
@click="changeSizeRaio('sub')"
/>
</div>
<!-- 全屏 -->
<div class="item">
<img
src="@/assets/imgs/indexPage/编组 22.png"
style="width: 100%; height: 100%"
@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'
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(200)
const carHeight = ref(100)
// 定义属性
const props = defineProps({
isAllBoard: propTypes.bool.def(false), // 当前选中的链接
isFullScreen: {
type: Boolean,
default: () => false
}
})
//节点样式
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 isFull = ref(false)
//是否可以拖拽
const isDrag = ref(false)
const changeIsDrag = () => {
nextTick(() => {
isDrag.value = !isDrag.value
if (!isDrag.value) {
//还原位置
resetPosition()
}
})
}
//点击选中线 如果选中就变成非选中 否则相反
const handleChooseRoute = (val, i) => {
if (lineList.value.length) {
if (val.isSelect) {
lineList.value.forEach((item, index) => {
if (index == i) {
item.isSelect = false
} else {
item.isSelect = false
}
})
} else {
lineList.value.forEach((item, index) => {
if (index == i) {
item.isSelect = true
} else {
item.isSelect = false
}
})
}
}
}
// 获取曲线的路径
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 changeSizeRaio = (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 legendObj = reactive({
driveLineShow: true,
carShow: true,
sortNumberShow: true,
legendShow: true
})
// 车辆是否显示
const changCarShow = () => {
legendObj.carShow = !legendObj.carShow
}
// 车辆行驶路线是否展示
const changDriveLineShow = () => {
legendObj.driveLineShow = !legendObj.driveLineShow
}
//sortNumber 是否显示
const changeSortNumber = () => {
legendObj.sortNumberShow = !legendObj.sortNumberShow
}
const toggleFullScreen = () => {
var elem = document.getElementById('indexpage-container') // 获取元素
if (!document.fullscreenElement) {
// 检查是否已经是全屏模式
if (elem.requestFullscreen) {
// 支持requestFullscreen API的标准方式
elem.requestFullscreen().catch((err) => {
alert(`无法进入全屏模式: ${err.message}`) // 处理错误情况
})
} else if (elem.mozRequestFullScreen) {
// 旧版Firefox的API名称已废弃
elem.mozRequestFullScreen()
} else if (elem.webkitRequestFullscreen) {
// WebKit/Safari/Chrome的API名称已废弃
elem.webkitRequestFullscreen()
} else if (elem.msRequestFullscreen) {
// IE/Edge的API名称已废弃
elem.msRequestFullscreen()
}
isDrag.value = true
} else {
// 退出全屏模式
if (document.exitFullscreen) {
// 标准API退出全屏模式
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
// 旧版Firefox的API名称已废弃
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
// WebKit/Safari/Chrome的API名称已废弃
document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
// IE/Edge的API名称已废弃
document.msExitFullscreen()
}
console.log('退出全屏')
isDrag.value = false
resetPosition()
}
// 监听全屏状态变化事件
document.addEventListener('fullscreenchange', function () {
if (!document.fullscreenElement) {
// 在这里可以添加退出全屏后的逻辑
isDrag.value = false
isFull.value = false
radio.value = 1
resetPosition()
} else {
// 在这里可以添加进入全屏后的逻辑
isDrag.value = true
isFull.value = true
nextTick(() => {
let width = getElementWidthByClass('affix-container')
radio.value = width / imgBgObj.width
})
}
})
// 监听旧版浏览器的全屏状态变化事件
document.addEventListener('mozfullscreenchange', function () {
if (!document.mozFullScreenElement) {
console.log('已退出全屏模式 (Firefox旧版)')
isDrag.value = false
resetPosition()
} else {
console.log('已进入全屏模式 (Firefox旧版)')
isDrag.value = true
}
})
document.addEventListener('webkitfullscreenchange', function () {
if (!document.webkitFullscreenElement) {
console.log('已退出全屏模式 (WebKit旧版)')
isDrag.value = false
resetPosition()
} else {
console.log('已进入全屏模式 (WebKit旧版)')
isDrag.value = true
}
})
document.addEventListener('msfullscreenchange', function () {
if (!document.msFullscreenElement) {
console.log('已退出全屏模式 (IE/Edge旧版)')
isDrag.value = false
resetPosition()
} else {
console.log('已进入全屏模式 (IE/Edge旧版)')
isDrag.value = true
}
})
}
//库位双击
const storeClick = async (item) => {
let storeData = await MapApi.houseLocationGetByMapItemId({
mapId: item.positionMapId,
mapItemId: item.id
})
storeDialogRef.value.open(JSON.parse(JSON.stringify(storeData)))
}
const lineList = ref([])
const pointList = ref([])
//将节点实际宽高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)
}
}
// 计算直线中间箭头的路径
const getLineMidArrowPath = (item) => {
const midX = ((Number(item.startPointX) + Number(item.endPointX)) / 2) * radio.value
const midY = ((Number(item.startPointY) + Number(item.endPointY)) / 2) * radio.value
let dx = item.endPointX * radio.value - item.startPointX * radio.value
let dy = item.endPointY * radio.value - item.startPointY * radio.value
let length = Math.sqrt(dx * dx + dy * dy)
if (length === 0) {
return `M ${midX} ${midY} L ${midX} ${midY}`
}
const unitDx = dx / length
const unitDy = dy / length
const arrowLength = 1
const startXArrow = midX - unitDx * arrowLength
const startYArrow = midY - unitDy * arrowLength
const endXArrow = midX
const endYArrow = midY
return `M ${startXArrow} ${startYArrow} L ${endXArrow} ${endYArrow}`
}
const calculateDistanceAndAngle = (point1, point2) => {
// 计算水平和垂直方向的差值
const dx = point2.left - point1.left
const dy = point2.top - point1.top
// 计算两点之间的长度(使用勾股定理)
const distance = Math.sqrt(dx * dx + dy * dy)
// 计算两点连线与水平轴正方向的夹角(弧度)
const angleInRadians = Math.atan2(dy, dx)
// 将弧度转换为角度
const angleInDegrees = angleInRadians * (180 / Math.PI)
return {
distance: distance,
angle: angleInDegrees,
left: point1.left,
top: point1.top
}
}
const replaceHttpWithWs = (str) => {
return str.replace(/^http/, 'ws')
}
// 连接websocket
const linkWebSocket = (url) => {
socketClient.value = new WebSocketClient(url)
if (socketClient.value) {
// 监听消息
socketClient.value.onMessage((message) => {
//心跳信息不处理
if (message == 'ping' || message == 'pong') return
let jsonMsg = JSON.parse(message)
// 车辆信息
if (jsonMsg.type == 'map_push') {
let data = JSON.parse(jsonMsg.content)
let dataList = []
for (let key in data) {
dataList.push({
macAddress: key,
data: JSON.parse(data[key])
})
}
testCarList.value = mergeArraysWithoutDelete(testCarList.value, dataList)
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
item.robotNo = item.data.pose2d.robotNo
})
}
//告警信息
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)
// console.log('======规划路线======', JSON.parse(data).data)
let dataList = JSON.parse(data).data
if (lineList.value.length > 0) {
// console.log(lineList.value)
lineList.value = setIsSelect(lineList.value, dataList)
}
}
})
}
}
// 现在车辆的逻辑是这样 websoket map_push类型推送消息 每次推送新车 我就添加到车辆列表中 testCarList 不删除车辆
// 每几秒轮询调用查看当前楼层区域的在线车辆 如果不存在 则再把testCarList 中的车辆删除
const robotByFloorAndAreaList = ref([]) // 机器人列表
const robotListTimerRef = ref(null) //轮询定时器
const getRobotByFloorAndAreaList = () => {
MapApi.getRobotByFloorAndArea({ floor: imgBgObj.floor, area: imgBgObj.area }).then((res) => {
robotByFloorAndAreaList.value = res
if (testCarList.value.length) {
testCarList.value = filterArrayByRobotNo(testCarList.value, robotByFloorAndAreaList.value)
}
})
}
robotListTimerRef.value = setInterval(getRobotByFloorAndAreaList, 5000)
// 删掉不在线的车辆
const filterArrayByRobotNo = (arr1, arr2) => {
return arr1.filter((item) => {
const robotNo = item.data.pose2d.robotNo
return arr2.includes(robotNo)
})
}
const mergeCarArrays = (arr1, arr2) => {
const result = []
const macAddressSet = new Set()
// 先处理第一个数组中原本就有的元素,进行更新或保留
for (const item1 of arr1) {
const macAddress1 = item1.macAddress
macAddressSet.add(macAddress1)
let foundInSecond = false
for (const item2 of arr2) {
const macAddress2 = item2.macAddress
if (macAddress1 === macAddress2) {
foundInSecond = true
// 更新pose2d信息
item1.data.pose2d = item2.data.pose2d
result.push(item1)
break
}
}
if (!foundInSecond) {
// 如果没在第二个数组中找到,就不添加到结果中,相当于删除
continue
}
}
// 再处理第二个数组中存在但第一个数组中不存在的元素,添加到结果中
for (const item2 of arr2) {
const macAddress2 = item2.macAddress
if (!macAddressSet.has(macAddress2)) {
result.push(item2)
}
}
return result
}
const mergeArraysWithoutDelete = (arr1, arr2) => {
// 复制第一个数组,避免修改原始数组
const result = [...arr1]
// 遍历第二个数组
for (const item2 of arr2) {
const macAddress2 = item2.macAddress
let found = false
// 遍历结果数组,查找是否存在相同 macAddress 的元素
for (let i = 0; i < result.length; i++) {
const item1 = result[i]
const macAddress1 = item1.macAddress
if (macAddress1 === macAddress2) {
// 若存在相同 macAddress 的元素,更新其 pose2d 信息
result[i].data.pose2d = item2.data.pose2d
found = true
break
}
}
// 若未找到相同 macAddress 的元素,将该元素添加到结果数组中
if (!found) {
result.push(item2)
}
}
return result
}
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'
})
}
// 发送websocket消息
const sendMessage = () => {
socketClient.value.send('Hello, WebSocket!')
}
// 断开连接
const disconnect = () => {
socketClient.value.disconnect()
}
const emitParent = () => {
getMapData(imgBgObj)
}
//获取扫描图 地图背景相关的信息
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 initWebsocket = () => {
let websocketUrl = `${replaceHttpWithWs(import.meta.env.VITE_BASE_URL)}/infra/ws?type=map&floor=${imgBgObj.floor}&area=${imgBgObj.area}`
linkWebSocket(websocketUrl)
}
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
// console.log("====",testCarList.value)
}
})
}
//偏航率斜率算旋转
const radianToDegree = (radian) => {
const degree = radian * (180 / Math.PI)
return `${-degree}`
}
const state = reactive({
mapRouteList: [],
allMapPointInfo: []
})
const radio = ref(1) //放大缩小的倍数
// 获取地图连线
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) => {
item.formattedData = item.dataJson ? JSON.parse(item.dataJson) : ''
item.showData = item.dataJson
? item.type == 2
? JSON.parse(item.dataJson)[0]
: JSON.parse(item.dataJson)
: null
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.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 convertToFrontendCoordinates = (origin, width, height, target) => {
// 提取原点的 x 和 y 坐标
const [originX, originY] = origin
// 提取目标点的 x 和 y 坐标
const [targetX, targetY] = target
// 计算目标点相对于原点在 x 方向上的偏移量
const offsetX = targetX - originX
// 计算目标点相对于原点在 y 方向上的偏移量
const offsetY = targetY - originY
// 计算前端页面中的 left 坐标
const left = offsetX
// 计算前端页面中的 top 坐标,需要考虑坐标系的转换
const top = height - offsetY
return {
top,
left,
origin
}
}
const getImageWidth = (imageUrl, name) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
resolve(img[name])
}
img.onerror = function () {
reject(new Error('图片加载失败'))
}
img.src = imageUrl
})
}
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)))
}
defineExpose({ getMapData }) // 提供 open 方法,用于打开弹窗
onMounted(() => {})
onBeforeUnmount(() => {
clearInterval(robotListTimerRef.value)
robotListTimerRef.value = null
})
</script>
<style lang="scss" scoped>
.index-page {
width: 100%;
.index-page-container {
width: 100%;
position: relative;
}
.is-full-screen {
height: calc(100vh - 120px);
overflow: auto;
}
}
.affix-container {
width: 100%;
position: relative;
}
.indexpage-container {
width: 100%;
position: relative;
}
.indexpage-container-active {
width: 100%;
position: relative;
cursor: grab;
}
.indexpage-container-active:active {
cursor: grabbing;
}
.indexpage-container .affix-container-top {
width: 100%;
height: 80px;
background-color: #fff;
border-bottom: 1px solid #f5f5f5;
}
.indexpage-container-box-point {
width: 100%;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.indexpage-container-box-point-item {
box-sizing: border-box;
position: absolute;
cursor: pointer;
}
.scrollbar-flex-content {
padding: 2px 6px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.scrollbar-demo-item {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 90px;
height: 30px;
margin: 10px 6px;
text-align: center;
border-radius: 4px;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
position: relative;
}
.scrollbar-demo-item {
cursor: pointer;
border: none;
}
:focus-visible {
outline: none;
}
.indexpage-container-box {
width: 100%;
position: relative;
}
.indexpage-container-box-img {
width: 100%;
height: auto;
}
.indexpage-popover-item {
display: flex;
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 14px;
color: #0d162a;
padding: 3px;
}
.line-box {
position: absolute;
}
.indexpage-car-item {
position: absolute;
cursor: pointer;
}
.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;
.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;
}
}
.sort-num {
position: absolute;
font-size: 0.75rem;
user-select: none;
color: #000;
}
.sort-num-location {
position: absolute;
font-size: 0.75rem;
user-select: none;
color: #000;
}
</style>