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 {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const ElMess: typeof import('element-plus/es').ElMess
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
|
||||
@ -1,25 +1,35 @@
|
||||
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
|
||||
// 1. 创建控制器
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
|
||||
const controller = new AbortController()
|
||||
const { signal } = controller
|
||||
|
||||
// 2. 设置定时器,超时后调用 abort()
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort(); // 触发中止信号
|
||||
controller.abort()
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(params),
|
||||
signal
|
||||
}).finally(() => {
|
||||
// 4. 无论请求成功还是失败,都要清除定时器,防止内存泄漏
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
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
|
||||
@ -43,6 +43,7 @@ import { useRouter } from 'vue-router'
|
||||
import { getRuntimeState } from '@/api/agv.js'
|
||||
import { useRobotStore } from '@/stores/robot'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ca } from 'element-plus/es/locales.mjs'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@ -104,9 +105,10 @@ const testConnect = async () => {
|
||||
robotStore.setCameraDeviceId('real_cam1')
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getRuntimeState({
|
||||
ip: robotStore.ip,
|
||||
deviceId: ruleForm.value.agvDeviceId
|
||||
deviceId: robotStore.agvDeviceId
|
||||
})
|
||||
loading.value = false
|
||||
if (result.code === 200) {
|
||||
@ -117,11 +119,13 @@ const testConnect = async () => {
|
||||
path: '/home'
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(result?.message || '连接服务错误')
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: $color-background-base;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<canvas id="map-canvas"></canvas>
|
||||
<div ref="containerRef" class="container">
|
||||
<canvas ref="mapCanvasRef" id="map-canvas"></canvas>
|
||||
<div id="tooltip">
|
||||
<div class="tooltip-title"></div>
|
||||
<div class="tooltip-detail"></div>
|
||||
@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
worldToScreen,
|
||||
@ -90,8 +90,12 @@ const applicationState = {
|
||||
let mapCanvas; // 主渲染画布 DOM 元素
|
||||
let canvasCtx; // 2D 渲染上下文
|
||||
|
||||
const containerRef = ref(null)
|
||||
const mapCanvasRef = ref(null)
|
||||
let resizeObserver
|
||||
|
||||
const init = () => {
|
||||
mapCanvas = document.getElementById('map-canvas');
|
||||
mapCanvas = mapCanvasRef.value;
|
||||
canvasCtx = mapCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
@ -100,14 +104,34 @@ const init = () => {
|
||||
* 在窗口 resize 时自动调用
|
||||
*/
|
||||
function resizeCanvasToContainer() {
|
||||
const containerRect = mapCanvas.parentElement.getBoundingClientRect(); // 获取容器实际尺寸
|
||||
devicePixelRatio = window.devicePixelRatio || 1; // 获取设备像素比
|
||||
if (!mapCanvas || !containerRef.value) return;
|
||||
const containerRect = containerRef.value.getBoundingClientRect(); // 获取容器实际尺寸
|
||||
if (!containerRect.width || !containerRect.height) return;
|
||||
applicationState.devicePixelRatio = window.devicePixelRatio || 1; // 获取设备像素比
|
||||
applicationState.canvasWidthPx = containerRect.width; // 更新逻辑宽度
|
||||
applicationState.canvasHeightPx = containerRect.height; // 逻辑高度 = 容器高度 - 状态栏高度
|
||||
mapCanvas.width = applicationState.canvasWidthPx * devicePixelRatio; // Canvas 物理像素宽
|
||||
mapCanvas.height = applicationState.canvasHeightPx * devicePixelRatio; // Canvas 物理像素高
|
||||
mapCanvas.width = applicationState.canvasWidthPx * applicationState.devicePixelRatio; // Canvas 物理像素宽
|
||||
mapCanvas.height = applicationState.canvasHeightPx * applicationState.devicePixelRatio; // Canvas 物理像素高
|
||||
mapCanvas.style.width = applicationState.canvasWidthPx + '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.camera.centerX = (parsedMap.originX + parsedMap.maxX) / 2; // 水平居中
|
||||
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); // 取较小值
|
||||
fitMapToCanvas(parsedMap);
|
||||
|
||||
// 标记加载完成并更新UI
|
||||
applicationState.isMapLoaded = true;
|
||||
@ -379,9 +396,12 @@ const centerCanvasView = () => {
|
||||
}
|
||||
|
||||
const loadSourceMap = async (mapName) => {
|
||||
await nextTick()
|
||||
if (!mapCanvas) init()
|
||||
resizeCanvasToContainer()
|
||||
|
||||
const res = await getCurrentMap({
|
||||
ip: robotStore.ip,
|
||||
// deviceId: VITE_AGV_DEVICE_ID,
|
||||
deviceId: robotStore.agvDeviceId,
|
||||
mapName: mapName
|
||||
})
|
||||
@ -419,6 +439,10 @@ const moveRobot = (robot, dx, dy, dtheta = 0) => {
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
resizeCanvasToContainer()
|
||||
resizeObserver = new ResizeObserver(resizeCanvasToContainer)
|
||||
resizeObserver.observe(containerRef.value)
|
||||
window.addEventListener('resize', resizeCanvasToContainer)
|
||||
})
|
||||
|
||||
watch(() => props.mapName,
|
||||
@ -427,8 +451,6 @@ watch(() => props.mapName,
|
||||
await loadSourceMap(newVal)
|
||||
addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot'))
|
||||
requestAnimationFrame(renderFrame);
|
||||
window.addEventListener('resize', resizeCanvasToContainer); // 监听窗口大小变化
|
||||
resizeCanvasToContainer(); // 初始调用一次
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
@ -442,15 +464,29 @@ defineExpose({
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeCanvasToContainer)
|
||||
resizeObserver?.disconnect()
|
||||
canvasCtx = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
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 {
|
||||
position: absolute;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user