feat: 加载地图
This commit is contained in:
parent
d2130ffaf5
commit
3104b34cb0
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@ -8,6 +8,7 @@ export {}
|
|||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue').EffectScope
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
const ElMess: typeof import('element-plus/es').ElMess
|
const ElMess: typeof import('element-plus/es').ElMess
|
||||||
|
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||||
const computed: typeof import('vue').computed
|
const computed: typeof import('vue').computed
|
||||||
const createApp: typeof import('vue').createApp
|
const createApp: typeof import('vue').createApp
|
||||||
|
|||||||
@ -1,25 +1,35 @@
|
|||||||
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
|
import { ElMessage } from 'element-plus'
|
||||||
// 1. 创建控制器
|
|
||||||
const controller = new AbortController();
|
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
|
||||||
const { signal } = controller;
|
const controller = new AbortController()
|
||||||
|
const { signal } = controller
|
||||||
|
|
||||||
// 2. 设置定时器,超时后调用 abort()
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
controller.abort(); // 触发中止信号
|
controller.abort()
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
const res = await fetch(url, {
|
try {
|
||||||
method: method,
|
const res = await fetch(url, {
|
||||||
headers: headers,
|
method,
|
||||||
|
headers,
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
signal
|
signal
|
||||||
}).finally(() => {
|
})
|
||||||
// 4. 无论请求成功还是失败,都要清除定时器,防止内存泄漏
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data
|
if (!res.ok) {
|
||||||
|
throw new Error(data.message || data.error || `请求失败(${res.status})`)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
const message = error.name === 'AbortError'
|
||||||
|
? '请求超时,请稍后重试'
|
||||||
|
: (error.message || '请求失败,请稍后重试')
|
||||||
|
ElMessage.error(message)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default request
|
export default request
|
||||||
@ -43,6 +43,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { getRuntimeState } from '@/api/agv.js'
|
import { getRuntimeState } from '@/api/agv.js'
|
||||||
import { useRobotStore } from '@/stores/robot'
|
import { useRobotStore } from '@/stores/robot'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ca } from 'element-plus/es/locales.mjs'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -104,24 +105,27 @@ const testConnect = async () => {
|
|||||||
robotStore.setCameraDeviceId('real_cam1')
|
robotStore.setCameraDeviceId('real_cam1')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getRuntimeState({
|
try {
|
||||||
ip: robotStore.ip,
|
const result = await getRuntimeState({
|
||||||
deviceId: ruleForm.value.agvDeviceId
|
ip: robotStore.ip,
|
||||||
})
|
deviceId: robotStore.agvDeviceId
|
||||||
loading.value = false
|
|
||||||
if (result.code === 200) {
|
|
||||||
robotStore.setPosition(result.data.pose)
|
|
||||||
robotStore.setBattery(result.data.battery)
|
|
||||||
robotStore.setCurrentMap(result.data.current_map)
|
|
||||||
router.push({
|
|
||||||
path: '/home'
|
|
||||||
})
|
})
|
||||||
} else {
|
loading.value = false
|
||||||
ElMessage.error(result?.message || '连接服务错误')
|
if (result.code === 200) {
|
||||||
|
robotStore.setPosition(result.data.pose)
|
||||||
|
robotStore.setBattery(result.data.battery)
|
||||||
|
robotStore.setCurrentMap(result.data.current_map)
|
||||||
|
router.push({
|
||||||
|
path: '/home'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || result?.error || '连接服务错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(error?.message || '连接服务错误')
|
||||||
}
|
}
|
||||||
router.push({
|
|
||||||
path: '/home'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -348,6 +348,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.map-body {
|
.map-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: $color-background-base;
|
background: $color-background-base;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div ref="containerRef" class="container">
|
||||||
<canvas id="map-canvas"></canvas>
|
<canvas ref="mapCanvasRef" id="map-canvas"></canvas>
|
||||||
<div id="tooltip">
|
<div id="tooltip">
|
||||||
<div class="tooltip-title"></div>
|
<div class="tooltip-title"></div>
|
||||||
<div class="tooltip-detail"></div>
|
<div class="tooltip-detail"></div>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
worldToScreen,
|
worldToScreen,
|
||||||
@ -90,8 +90,12 @@ const applicationState = {
|
|||||||
let mapCanvas; // 主渲染画布 DOM 元素
|
let mapCanvas; // 主渲染画布 DOM 元素
|
||||||
let canvasCtx; // 2D 渲染上下文
|
let canvasCtx; // 2D 渲染上下文
|
||||||
|
|
||||||
|
const containerRef = ref(null)
|
||||||
|
const mapCanvasRef = ref(null)
|
||||||
|
let resizeObserver
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
mapCanvas = document.getElementById('map-canvas');
|
mapCanvas = mapCanvasRef.value;
|
||||||
canvasCtx = mapCanvas.getContext('2d');
|
canvasCtx = mapCanvas.getContext('2d');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,14 +104,34 @@ const init = () => {
|
|||||||
* 在窗口 resize 时自动调用
|
* 在窗口 resize 时自动调用
|
||||||
*/
|
*/
|
||||||
function resizeCanvasToContainer() {
|
function resizeCanvasToContainer() {
|
||||||
const containerRect = mapCanvas.parentElement.getBoundingClientRect(); // 获取容器实际尺寸
|
if (!mapCanvas || !containerRef.value) return;
|
||||||
devicePixelRatio = window.devicePixelRatio || 1; // 获取设备像素比
|
const containerRect = containerRef.value.getBoundingClientRect(); // 获取容器实际尺寸
|
||||||
|
if (!containerRect.width || !containerRect.height) return;
|
||||||
|
applicationState.devicePixelRatio = window.devicePixelRatio || 1; // 获取设备像素比
|
||||||
applicationState.canvasWidthPx = containerRect.width; // 更新逻辑宽度
|
applicationState.canvasWidthPx = containerRect.width; // 更新逻辑宽度
|
||||||
applicationState.canvasHeightPx = containerRect.height; // 逻辑高度 = 容器高度 - 状态栏高度
|
applicationState.canvasHeightPx = containerRect.height; // 逻辑高度 = 容器高度 - 状态栏高度
|
||||||
mapCanvas.width = applicationState.canvasWidthPx * devicePixelRatio; // Canvas 物理像素宽
|
mapCanvas.width = applicationState.canvasWidthPx * applicationState.devicePixelRatio; // Canvas 物理像素宽
|
||||||
mapCanvas.height = applicationState.canvasHeightPx * devicePixelRatio; // Canvas 物理像素高
|
mapCanvas.height = applicationState.canvasHeightPx * applicationState.devicePixelRatio; // Canvas 物理像素高
|
||||||
mapCanvas.style.width = applicationState.canvasWidthPx + 'px'; // CSS 显示宽度
|
mapCanvas.style.width = applicationState.canvasWidthPx + 'px'; // CSS 显示宽度
|
||||||
mapCanvas.style.height = applicationState.canvasHeightPx + 'px'; // CSS 显示高度
|
mapCanvas.style.height = applicationState.canvasHeightPx + 'px'; // CSS 显示高度
|
||||||
|
|
||||||
|
if (applicationState.parsedMap) {
|
||||||
|
fitMapToCanvas(applicationState.parsedMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitMapToCanvas(parsedMap) {
|
||||||
|
const worldWidthMeters = parsedMap.maxX - parsedMap.originX;
|
||||||
|
const worldHeightMeters = parsedMap.maxY - parsedMap.originY;
|
||||||
|
if (!worldWidthMeters || !worldHeightMeters || !applicationState.canvasWidthPx || !applicationState.canvasHeightPx) return;
|
||||||
|
|
||||||
|
applicationState.camera.centerX = (parsedMap.originX + parsedMap.maxX) / 2;
|
||||||
|
applicationState.camera.centerY = (parsedMap.originY + parsedMap.maxY) / 2;
|
||||||
|
|
||||||
|
const fitScaleX = applicationState.canvasWidthPx / worldWidthMeters;
|
||||||
|
const fitScaleY = applicationState.canvasHeightPx / worldHeightMeters;
|
||||||
|
// 等比例覆盖整个 Canvas;比例不一致时,超出 Canvas 的方向由容器裁切。
|
||||||
|
applicationState.camera.pixelsPerMeter = Math.max(1, Math.max(fitScaleX, fitScaleY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -172,14 +196,7 @@ async function loadMapIntoApplication(parsedMap) {
|
|||||||
applicationState.gridOffscreen = buildOccupancyGridImage(parsedMap, applicationState.layerVisibility.edges, applicationState);
|
applicationState.gridOffscreen = buildOccupancyGridImage(parsedMap, applicationState.layerVisibility.edges, applicationState);
|
||||||
|
|
||||||
// ── 设置相机初始位置(地图中心)──
|
// ── 设置相机初始位置(地图中心)──
|
||||||
applicationState.camera.centerX = (parsedMap.originX + parsedMap.maxX) / 2; // 水平居中
|
fitMapToCanvas(parsedMap);
|
||||||
applicationState.camera.centerY = (parsedMap.originY + parsedMap.maxY) / 2; // 垂直居中
|
|
||||||
// 计算合适的缩放级别,使地图完整显示在画布内
|
|
||||||
const worldWidthMeters = parsedMap.maxX - parsedMap.originX; // 地图世界宽度
|
|
||||||
const worldHeightMeters = parsedMap.maxY - parsedMap.originY; // 地图世界高度
|
|
||||||
const fitScaleX = applicationState.canvasWidthPx / worldWidthMeters; // 水平方向恰好填满的缩放
|
|
||||||
const fitScaleY = applicationState.canvasHeightPx / worldHeightMeters; // 垂直方向恰好填满的缩放
|
|
||||||
applicationState.camera.pixelsPerMeter = Math.max(1, Math.max(fitScaleX, fitScaleY) * 1); // 取较小值
|
|
||||||
|
|
||||||
// 标记加载完成并更新UI
|
// 标记加载完成并更新UI
|
||||||
applicationState.isMapLoaded = true;
|
applicationState.isMapLoaded = true;
|
||||||
@ -379,9 +396,12 @@ const centerCanvasView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadSourceMap = async (mapName) => {
|
const loadSourceMap = async (mapName) => {
|
||||||
|
await nextTick()
|
||||||
|
if (!mapCanvas) init()
|
||||||
|
resizeCanvasToContainer()
|
||||||
|
|
||||||
const res = await getCurrentMap({
|
const res = await getCurrentMap({
|
||||||
ip: robotStore.ip,
|
ip: robotStore.ip,
|
||||||
// deviceId: VITE_AGV_DEVICE_ID,
|
|
||||||
deviceId: robotStore.agvDeviceId,
|
deviceId: robotStore.agvDeviceId,
|
||||||
mapName: mapName
|
mapName: mapName
|
||||||
})
|
})
|
||||||
@ -419,6 +439,10 @@ const moveRobot = (robot, dx, dy, dtheta = 0) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init()
|
init()
|
||||||
|
resizeCanvasToContainer()
|
||||||
|
resizeObserver = new ResizeObserver(resizeCanvasToContainer)
|
||||||
|
resizeObserver.observe(containerRef.value)
|
||||||
|
window.addEventListener('resize', resizeCanvasToContainer)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.mapName,
|
watch(() => props.mapName,
|
||||||
@ -427,8 +451,6 @@ watch(() => props.mapName,
|
|||||||
await loadSourceMap(newVal)
|
await loadSourceMap(newVal)
|
||||||
addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot'))
|
addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot'))
|
||||||
requestAnimationFrame(renderFrame);
|
requestAnimationFrame(renderFrame);
|
||||||
window.addEventListener('resize', resizeCanvasToContainer); // 监听窗口大小变化
|
|
||||||
resizeCanvasToContainer(); // 初始调用一次
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
immediate: true
|
immediate: true
|
||||||
@ -442,15 +464,29 @@ defineExpose({
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', resizeCanvasToContainer)
|
window.removeEventListener('resize', resizeCanvasToContainer)
|
||||||
|
resizeObserver?.disconnect()
|
||||||
canvasCtx = null
|
canvasCtx = null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
inset: 0;
|
||||||
position: relative;
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
#map-canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
#tooltip {
|
#tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user