1314 lines
39 KiB
Vue
1314 lines
39 KiB
Vue
<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>
|