From 35e98b9ab122d1dffa1270825eaedcc3eb5de6f4 Mon Sep 17 00:00:00 2001 From: huaqiang Date: Wed, 12 Aug 2026 11:20:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(map):=20=E5=9C=B0=E5=9B=BE=E5=8F=B3?= =?UTF-8?q?=E4=B8=8B=E8=A7=92=E6=96=B0=E5=A2=9E=E5=85=A8=E5=B1=80=E7=BC=A9?= =?UTF-8?q?=E7=95=A5=E5=9B=BE=E9=A2=84=E8=A7=88=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=8B=96=E5=8A=A8=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/EquipmentManagementPage.vue | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/views/dashboard1/pages/EquipmentManagementPage.vue b/src/views/dashboard1/pages/EquipmentManagementPage.vue index 063b075..7383d48 100644 --- a/src/views/dashboard1/pages/EquipmentManagementPage.vue +++ b/src/views/dashboard1/pages/EquipmentManagementPage.vue @@ -267,9 +267,12 @@ const formError = ref(""); const mapEditorMode = ref("select"); const mapStageRef = ref(null); const mapCanvasRef = ref(null); +const mapOverviewRef = ref(null); const suppressStageClick = ref(false); const minMapZoom = 0.6; const maxMapZoom = 15; +const mapOverviewWidth = 200; +const mapOverviewHeight = 130; const mapZoom = ref(1); const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100)); @@ -1853,6 +1856,123 @@ function renderMapCanvas() { context.strokeRect(left, top, width, height); context.restore(); } + + drawMapOverview(); +} + +// 全局缩略图预览区中整张地图的实际绘制区域(contain 等比缩放) +function getMapOverviewDrawArea() { + const imageRatio = rosMapImage.naturalWidth && rosMapImage.naturalHeight + ? rosMapImage.naturalWidth / rosMapImage.naturalHeight + : 178 / 50; + const drawWidth = Math.min(mapOverviewWidth, mapOverviewHeight * imageRatio); + const drawHeight = drawWidth / imageRatio; + + return { + drawWidth, + drawHeight, + offsetX: (mapOverviewWidth - drawWidth) / 2, + offsetY: (mapOverviewHeight - drawHeight) / 2, + }; +} + +// 在右下角绘制全局缩略图,并标出当前视野在全局地图中的范围 +function drawMapOverview() { + const canvas = mapOverviewRef.value; + const rect = getMapStageRect(); + + if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) { + return; + } + + const dpr = window.devicePixelRatio || 1; + + if ( + canvas.width !== Math.round(mapOverviewWidth * dpr) || + canvas.height !== Math.round(mapOverviewHeight * dpr) + ) { + canvas.width = Math.round(mapOverviewWidth * dpr); + canvas.height = Math.round(mapOverviewHeight * dpr); + } + + const context = canvas.getContext("2d"); + if (!context) { + return; + } + + context.setTransform(dpr, 0, 0, dpr, 0, 0); + context.clearRect(0, 0, mapOverviewWidth, mapOverviewHeight); + + if (!rosMapImage.complete || !rosMapImage.naturalWidth) { + return; + } + + const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea(); + + context.globalAlpha = 0.92; + context.drawImage(rosMapImage, offsetX, offsetY, drawWidth, drawHeight); + context.globalAlpha = 1; + + // 当前视野对应到全局地图的比例范围 + const frame = getRosMapFrame(rect); + const leftRatio = Math.max(0, -frame.left) / frame.width; + const topRatio = Math.max(0, -frame.top) / frame.height; + const rightRatio = Math.min(rect.width - frame.left, frame.width) / frame.width; + const bottomRatio = Math.min(rect.height - frame.top, frame.height) / frame.height; + const viewX = offsetX + leftRatio * drawWidth; + const viewY = offsetY + topRatio * drawHeight; + const viewWidth = (rightRatio - leftRatio) * drawWidth; + const viewHeight = (bottomRatio - topRatio) * drawHeight; + + context.strokeStyle = "rgba(121, 234, 255, 0.95)"; + context.lineWidth = 1.2; + context.strokeRect(viewX, viewY, viewWidth, viewHeight); +} + +let mapOverviewDragging = false; + +// 缩略图点击或拖动:将大地图视野中心切换到预览上对应的比例位置 +function jumpMapOverview(event: PointerEvent) { + const canvas = mapOverviewRef.value; + const rect = getMapStageRect(); + + if (!canvas || !rect || rect.width <= 0 || rect.height <= 0) { + return; + } + + const overviewRect = canvas.getBoundingClientRect(); + const { drawWidth, drawHeight, offsetX, offsetY } = getMapOverviewDrawArea(); + const ratioX = Math.min(1, Math.max(0, (event.clientX - overviewRect.left - offsetX) / drawWidth)); + const ratioY = Math.min(1, Math.max(0, (event.clientY - overviewRect.top - offsetY) / drawHeight)); + + // 平移使该比例位置位于主画布中心 + const frame = getRosMapFrame(rect); + mapPan.x = frame.width * (0.5 - ratioX); + mapPan.y = frame.height * (0.5 - ratioY); + scheduleMapCanvasRender(); +} + +function startMapOverviewDrag(event: PointerEvent) { + mapOverviewDragging = true; + (event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId); + jumpMapOverview(event); +} + +function moveMapOverviewDrag(event: PointerEvent) { + if (!mapOverviewDragging) { + return; + } + + jumpMapOverview(event); +} + +function endMapOverviewDrag(event: PointerEvent) { + mapOverviewDragging = false; + const target = event.currentTarget as HTMLElement; + + if (target.hasPointerCapture?.(event.pointerId)) { + target.releasePointerCapture(event.pointerId); + } } function scheduleMapCanvasRender() { @@ -2910,6 +3030,12 @@ watch( +
+ +
全局视图
+
{{ currentConfig.mapLoaded @@ -3466,6 +3592,39 @@ watch( font-weight: 700; } +.map-overview { + position: absolute; + right: 14px; + bottom: 14px; + z-index: 6; + padding: 6px; + border: 1px solid rgba(86, 205, 255, 0.2); + background: rgba(4, 18, 38, 0.78); + border-radius: 4px; + box-shadow: 0 0 16px rgba(0, 10, 24, 0.28); + backdrop-filter: blur(2px); + cursor: grab; +} + +.map-overview:active { + cursor: grabbing; +} + +.map-overview-canvas { + display: block; + width: 200px; + height: 130px; + border: 1px solid rgba(91, 208, 255, 0.14); + background: rgba(2, 8, 18, 0.6); +} + +.map-overview-label { + margin-top: 4px; + text-align: center; + color: rgba(184, 235, 255, 0.72); + font-size: 11px; +} + .stage-overlay.bottom-left { left: 14px; bottom: 14px;