From 03d319ebf6cd65f36f939e39efa3e1f04221ec16 Mon Sep 17 00:00:00 2001 From: huaqiang Date: Wed, 12 Aug 2026 11:11:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(map):=20=E4=BC=98=E5=8C=96=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E7=BC=A9=E6=94=BE=E9=80=BB=E8=BE=91=E5=8F=8A=E7=82=B9?= =?UTF-8?q?=E4=BD=8D=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将设备管理地图最大缩放比例提升至15,支持更大范围缩放 - 修改地图缩放增量为按倍率缩放,提升缩放体验平滑度 - 新增根据地图宽高比计算默认缩放,消除宽扁地图显示过小问题 - 自动缩放时聚焦地图原点,提升用户定位便利性 - 调整地图点位渲染样式,统一圆圈大小,简化视觉效果,只用描边色区分 - 移除点位标签背景框绘制,减少视觉干扰 - 鼠标滚轮缩放时以光标为中心,缩放过程视野自动平移保持焦点 - 集成中心页面地图缩放支持按倍率调整和平滑过渡 - 优化保存巡检任务时进度条逻辑,取消模拟递增,调用完成立即完成进度 - 增强地图缩放动画过渡时长,从0.18秒提升至0.45秒 - 修复集成中心页面真实点位显示优化,起点终点显示标签,途经点隐藏标签 --- .../pages/EquipmentManagementPage.vue | 189 +++++++----------- .../dashboard1/pages/IntegratedCenterPage.vue | 39 +++- 2 files changed, 106 insertions(+), 122 deletions(-) diff --git a/src/views/dashboard1/pages/EquipmentManagementPage.vue b/src/views/dashboard1/pages/EquipmentManagementPage.vue index bfffd7f..53abbf0 100644 --- a/src/views/dashboard1/pages/EquipmentManagementPage.vue +++ b/src/views/dashboard1/pages/EquipmentManagementPage.vue @@ -269,8 +269,7 @@ const mapStageRef = ref(null); const mapCanvasRef = ref(null); const suppressStageClick = ref(false); const minMapZoom = 0.6; -const maxMapZoom = 3; -const mapZoomStep = 0.2; +const maxMapZoom = 15; const mapZoom = ref(1); const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100)); @@ -338,29 +337,21 @@ const mapPointPalette: Record< { stroke: string; glow: string; - labelBackground: string; - labelBorder: string; } > = { 业务点: { stroke: "#66d7ff", glow: "rgba(102, 215, 255, 0.28)", - labelBackground: "rgba(16, 74, 118, 0.92)", - labelBorder: "rgba(91, 208, 255, 0.18)", }, 途经点: { stroke: "#9af7c8", glow: "rgba(154, 247, 200, 0.3)", - labelBackground: "rgba(18, 90, 66, 0.94)", - labelBorder: "rgba(91, 208, 255, 0.18)", }, }; const selectedPointHighlight = { stroke: "#ffb26e", glow: "rgba(255, 178, 110, 0.32)", - labelBackground: "rgba(122, 72, 18, 0.94)", - labelBorder: "rgba(255, 196, 118, 0.72)", }; const defaultRosMapMeta: RosMapMetaState = { @@ -382,6 +373,10 @@ const mapConfigInputRef = ref(null); rosMapImage.onload = () => { rosMapMeta.width = rosMapImage.naturalWidth || rosMapMeta.width; rosMapMeta.height = rosMapImage.naturalHeight || rosMapMeta.height; + // 图片尺寸就绪后按实际宽高比重算默认缩放,避免宽扁地图默认显示过小 + if (configDialogVisible.value) { + resetMapView(); + } scheduleMapCanvasRender(); }; rosMapImage.src = rosMapMeta.imageUrl; @@ -1322,7 +1317,6 @@ async function openConfigDialog(device: DeviceRecord) { configuringDeviceId.value = device.id; configDialogVisible.value = true; mapEditorMode.value = "normal"; - resetMapView(); if (!config.plans.some((plan) => plan.id === config.activePlanId)) { config.activePlanId = config.plans[0]?.id ?? ""; @@ -1346,6 +1340,7 @@ async function openConfigDialog(device: DeviceRecord) { resetPlanForm(); nextTick(() => { attachMapResizeObserver(); + resetMapView(); scheduleMapCanvasRender(); }); } @@ -1518,27 +1513,80 @@ function setMapZoom(value: number) { scheduleMapCanvasRender(); } -// 按固定步长放大地图 +// 按固定倍率放大地图,倍率越高放大步进越大 function zoomInMap() { - setMapZoom(mapZoom.value + mapZoomStep); + setMapZoom(mapZoom.value * 1.1); } -// 按固定步长缩小地图 +// 按固定倍率缩小地图 function zoomOutMap() { - setMapZoom(mapZoom.value - mapZoomStep); + setMapZoom(mapZoom.value / 1.1); } -// 将地图缩放比例和平移位置恢复为默认状态 +// 将地图缩放比例和平移位置恢复为默认状态:宽高比自适应缩放并聚焦原点 function resetMapView() { mapPan.x = 0; mapPan.y = 0; - setMapZoom(1); + setMapZoom(computeDefaultMapZoom()); + focusMapOnOrigin(); } // 处理鼠标滚轮缩放地图 function handleMapWheel(event: WheelEvent) { - const zoomDirection = event.deltaY > 0 ? -1 : 1; - setMapZoom(mapZoom.value + zoomDirection * mapZoomStep); + const zoomFactor = event.deltaY > 0 ? 1 / 1.1 : 1.1; + setMapZoom(mapZoom.value * zoomFactor); +} + +// 根据地图宽高比计算默认缩放:常规比例地图保持完整显示,宽扁/高瘦地图放大让短边占满画布 +function computeDefaultMapZoom() { + const rect = getMapStageRect(); + + if (!rect || rect.width <= 0 || rect.height <= 0) { + return 1; + } + + const imageRatio = rosMapImage.naturalWidth && rosMapImage.naturalHeight + ? rosMapImage.naturalWidth / rosMapImage.naturalHeight + : 178 / 50; + const padding = 30; + const maxWidth = rect.width - padding * 2; + const maxHeight = rect.height - padding * 2; + + if (maxWidth <= 0 || maxHeight <= 0) { + return 1; + } + + // 与 getRosMapFrame 的 fit 逻辑保持一致:完整显示时短边占比过小才放大到铺满画布 + const fitHeight = maxWidth / imageRatio; + const fitWidth = maxHeight * imageRatio; + const shortSideRatio = Math.min(fitWidth / maxWidth, fitHeight / maxHeight); + + if (shortSideRatio >= 0.6) { + return 1; + } + + const defaultZoom = fitHeight <= maxHeight + ? maxHeight / fitHeight + : maxWidth / fitWidth; + + return Number(defaultZoom.toFixed(2)); +} + +// 将地图视野平移到原点位置,使原点位于画布中心 +function focusMapOnOrigin() { + const rect = getMapStageRect(); + + if (!rect || rect.width <= 0 || rect.height <= 0) { + return; + } + + const originRatioX = (0 - rosMapMeta.origin[0]) / rosMapMeta.resolution / rosMapMeta.width; + const originRatioY = 1 - (0 - rosMapMeta.origin[1]) / rosMapMeta.resolution / rosMapMeta.height; + const frame = getRosMapFrame(rect); + + // 原点位于画布中心时,平移量等于地图尺寸的一半减去原点偏移 + mapPan.x = frame.width * (0.5 - originRatioX); + mapPan.y = frame.height * (0.5 - originRatioY); } // 根据 ROS 地图图片比例计算画布中的实际地图显示区域 @@ -1577,28 +1625,6 @@ function percentToStagePixels(x: number, y: number, rect: DOMRect) { }; } -function estimatePointLabelWidth(name: string) { - return Math.max(72, name.length * 12 + 18); -} - -function getPointLabelFrame(point: PatrolPoint, rect: DOMRect) { - const pixelPoint = percentToStagePixels(point.x, point.y, rect); - const labelWidth = estimatePointLabelWidth(point.name); - const labelHeight = 24; - let left = pixelPoint.x + 18; - - if (left + labelWidth > rect.width - 10) { - left = pixelPoint.x - labelWidth - 18; - } - - return { - left, - top: pixelPoint.y - labelHeight / 2, - width: labelWidth, - height: labelHeight, - }; -} - function findHitPoint(event: MouseEvent) { const rect = getMapStageRect(); if (!rect) { @@ -1617,45 +1643,11 @@ function findHitPoint(event: MouseEvent) { if (Math.hypot(deltaX, deltaY) <= 18) { return point; } - - const labelFrame = getPointLabelFrame(point, rect); - - if ( - pointerX >= labelFrame.left && - pointerX <= labelFrame.left + labelFrame.width && - pointerY >= labelFrame.top && - pointerY <= labelFrame.top + labelFrame.height - ) { - return point; - } } return null; } -function drawRoundRect( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - width: number, - height: number, - radius: number, -) { - const clampedRadius = Math.min(radius, width / 2, height / 2); - - ctx.beginPath(); - ctx.moveTo(x + clampedRadius, y); - ctx.lineTo(x + width - clampedRadius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + clampedRadius); - ctx.lineTo(x + width, y + height - clampedRadius); - ctx.quadraticCurveTo(x + width, y + height, x + width - clampedRadius, y + height); - ctx.lineTo(x + clampedRadius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - clampedRadius); - ctx.lineTo(x, y + clampedRadius); - ctx.quadraticCurveTo(x, y, x + clampedRadius, y); - ctx.closePath(); -} - function renderMapCanvas() { const canvas = mapCanvasRef.value; const rect = getMapStageRect(); @@ -1770,17 +1762,6 @@ function renderMapCanvas() { context.beginPath(); context.arc(originPoint.x, originPoint.y, 4, 0, Math.PI * 2); context.fill(); - drawRoundRect(context, originPoint.x + 12, originPoint.y - 16, 126, 32, 8); - context.fillStyle = "rgba(72, 42, 13, 0.92)"; - context.strokeStyle = "rgba(255, 196, 118, 0.72)"; - context.lineWidth = 1; - context.fill(); - context.stroke(); - context.fillStyle = "#fff5df"; - context.font = '600 12px "Microsoft YaHei", sans-serif'; - context.textAlign = "left"; - context.textBaseline = "middle"; - context.fillText("原点 (0.00, 0.00)", originPoint.x + 22, originPoint.y); context.restore(); if (activeRoundPoints.value.length > 1) { @@ -1813,44 +1794,28 @@ function renderMapCanvas() { const palette = mapPointPalette[point.type]; const selected = currentConfig.value?.selectedPointIds.includes(point.id) ?? false; const highlightPalette = selected ? selectedPointHighlight : palette; - const labelFrame = getPointLabelFrame(point, rect); context.save(); context.fillStyle = selected ? highlightPalette.glow : "rgba(255, 255, 255, 0.02)"; context.beginPath(); - context.arc(pixelPoint.x, pixelPoint.y, selected ? 18 : 14, 0, Math.PI * 2); + context.arc(pixelPoint.x, pixelPoint.y, selected ? 14 : 10, 0, Math.PI * 2); context.fill(); context.fillStyle = "rgba(5, 18, 38, 0.94)"; context.strokeStyle = highlightPalette.stroke; context.lineWidth = selected ? 3 : 2; context.beginPath(); - context.arc(pixelPoint.x, pixelPoint.y, 11, 0, Math.PI * 2); + context.arc(pixelPoint.x, pixelPoint.y, 8, 0, Math.PI * 2); context.fill(); context.shadowColor = highlightPalette.glow; context.shadowBlur = selected ? 18 : 12; context.stroke(); - drawRoundRect(context, labelFrame.left, labelFrame.top, labelFrame.width, labelFrame.height, 8); - context.fillStyle = selected - ? highlightPalette.labelBackground - : "rgba(4, 19, 39, 0.82)"; - context.strokeStyle = selected - ? highlightPalette.labelBorder - : "rgba(91, 208, 255, 0.18)"; - context.lineWidth = 1; - context.shadowBlur = 0; - context.fill(); - context.stroke(); - + // 所有点位统一圆圈大小,仅以描边颜色区分类型,内部标注序号 context.fillStyle = "#eefcff"; - context.font = '700 11px "Microsoft YaHei", sans-serif'; + context.font = '700 10px "Microsoft YaHei", sans-serif'; context.textAlign = "center"; context.fillText(String(index + 1), pixelPoint.x, pixelPoint.y + 0.5); - - context.font = '500 12px "Microsoft YaHei", sans-serif'; - context.textAlign = "left"; - context.fillText(point.name, labelFrame.left + 8, labelFrame.top + labelFrame.height / 2 + 0.5); context.restore(); }); @@ -2462,14 +2427,8 @@ async function saveConfigDialog() { buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value), ); - saveProgressTimer = window.setInterval(() => { - const step = saveProgressValue.value < 72 ? 12 : saveProgressValue.value < 90 ? 6 : 3; - saveProgressValue.value = Math.min(100, saveProgressValue.value + step); - - if (saveProgressValue.value >= 100) { - finishSaveProgress(); - } - }, 180); + // 保存接口真实调用完成后,进度条立即走完,不再模拟递增动画 + finishSaveProgress(); } catch (error) { console.error(error); stopSaveProgressTimers(); @@ -3800,7 +3759,7 @@ watch( rgba(24, 102, 180, 0.92) 0%, rgba(35, 158, 215, 0.9) 100%); box-shadow: 0 0 18px rgba(52, 177, 255, 0.26); - transition: width 0.18s ease; + transition: width 0.45s ease; } .save-progress-value { diff --git a/src/views/dashboard1/pages/IntegratedCenterPage.vue b/src/views/dashboard1/pages/IntegratedCenterPage.vue index 0ac1495..9b53f73 100644 --- a/src/views/dashboard1/pages/IntegratedCenterPage.vue +++ b/src/views/dashboard1/pages/IntegratedCenterPage.vue @@ -668,7 +668,7 @@ const realRoutePath = computed(() => routeMapPoints.value .join(" ")); const realCheckPoints = computed(() => routeMapPoints.value.map((point, index) => ({ ...point, - label: index === 0 ? "起点" : index === routeMapPoints.value.length - 1 ? "终点" : "途经点", + label: index === 0 ? "起点" : index === routeMapPoints.value.length - 1 ? "终点" : "", }))); const inspectionRunning = computed(() => currentInspection.value?.status === 0); const inspectionStatusText = computed(() => inspectionRunning.value ? "巡检中" : "空闲"); @@ -1270,9 +1270,10 @@ function switchDriveMode(mode: DriveMode) { activeDriveMode.value = mode; } -// 调整真实 ROS 地图整体缩放,不影响底部状态浮层 +// 调整真实 ROS 地图整体缩放,不影响底部状态浮层,倍率越高放大步进越大 function changeMapScale(delta: number) { - mapScale.value = Math.min(3, Math.max(0.55, Number((mapScale.value + delta).toFixed(2)))); + const factor = delta > 0 ? 1.1 : 1 / 1.1; + mapScale.value = Math.min(15, Math.max(0.55, Number((mapScale.value * factor).toFixed(2)))); } function resetMapScale() { @@ -1280,10 +1281,34 @@ function resetMapScale() { mapOffset.value = { x: 0, y: 0 }; } -// 鼠标滚轮缩放地图内容,按住拖拽平移地图视野 +// 鼠标滚轮以光标为缩放中心缩放地图内容,按住拖拽平移地图视野 function handleMapWheel(event: WheelEvent) { event.preventDefault(); - changeMapScale(event.deltaY > 0 ? -0.05 : 0.05); + + if (!rosMap.value || !navMapSvgRef.value) { + return; + } + + const factor = event.deltaY > 0 ? 1 / 1.1 : 1.1; + const nextScale = Math.min(15, Math.max(0.55, Number((mapScale.value * factor).toFixed(2)))); + + if (nextScale === mapScale.value) { + return; + } + + // 缩放前后保持光标下的地图点位置不动,需要同步平移视野 + const rect = navMapSvgRef.value.getBoundingClientRect(); + const pointerX = (event.clientX - rect.left) * (rosMap.value.width / rect.width); + const pointerY = (event.clientY - rect.top) * (rosMap.value.height / rect.height); + const centerX = rosMap.value.width / 2; + const centerY = rosMap.value.height / 2; + const keepRatio = 1 - nextScale / mapScale.value; + + mapOffset.value = { + x: mapOffset.value.x + (pointerX - centerX) * keepRatio, + y: mapOffset.value.y + (pointerY - centerY) * keepRatio, + }; + mapScale.value = nextScale; } function startMapDrag(event: PointerEvent) { @@ -1539,10 +1564,10 @@ function getStatusClass(status: DeviceItem["status"]) { - - {{ point.label }} + {{ point.label }}