zn-admin-vue3-wcs/src/views/mapPage/realTimeMap/components/indexPage.vue
2025-07-03 16:19:44 +08:00

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>