feat: 加载地图

This commit is contained in:
zhanghao 2026-08-05 11:08:44 +08:00
parent d2130ffaf5
commit 3104b34cb0
5 changed files with 108 additions and 55 deletions

1
auto-imports.d.ts vendored
View File

@ -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

View File

@ -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)
try {
const res = await fetch(url, { const res = await fetch(url, {
method: method, method,
headers: headers, 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()
if (!res.ok) {
throw new Error(data.message || data.error || `请求失败(${res.status}`)
}
return data 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

View File

@ -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,9 +105,10 @@ const testConnect = async () => {
robotStore.setCameraDeviceId('real_cam1') robotStore.setCameraDeviceId('real_cam1')
} }
try {
const result = await getRuntimeState({ const result = await getRuntimeState({
ip: robotStore.ip, ip: robotStore.ip,
deviceId: ruleForm.value.agvDeviceId deviceId: robotStore.agvDeviceId
}) })
loading.value = false loading.value = false
if (result.code === 200) { if (result.code === 200) {
@ -117,11 +119,13 @@ const testConnect = async () => {
path: '/home' path: '/home'
}) })
} else { } else {
ElMessage.error(result?.message || '连接服务错误') ElMessage.error(result?.message || result?.error || '连接服务错误')
}
}
catch (error) {
loading.value = false
ElMessage.error(error?.message || '连接服务错误')
} }
router.push({
path: '/home'
})
} }
} }

View File

@ -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;

View File

@ -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;