feat(map): 地图右下角新增全局缩略图预览并支持点击拖动联动

This commit is contained in:
2026-08-12 11:20:46 +08:00
parent 0915529af0
commit 35e98b9ab1

View File

@@ -267,9 +267,12 @@ const formError = ref("");
const mapEditorMode = ref<EditorMode>("select"); const mapEditorMode = ref<EditorMode>("select");
const mapStageRef = ref<HTMLElement | null>(null); const mapStageRef = ref<HTMLElement | null>(null);
const mapCanvasRef = ref<HTMLCanvasElement | null>(null); const mapCanvasRef = ref<HTMLCanvasElement | null>(null);
const mapOverviewRef = ref<HTMLCanvasElement | null>(null);
const suppressStageClick = ref(false); const suppressStageClick = ref(false);
const minMapZoom = 0.6; const minMapZoom = 0.6;
const maxMapZoom = 15; const maxMapZoom = 15;
const mapOverviewWidth = 200;
const mapOverviewHeight = 130;
const mapZoom = ref(1); const mapZoom = ref(1);
const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100)); const mapZoomPercent = computed(() => Math.round(mapZoom.value * 100));
@@ -1853,6 +1856,123 @@ function renderMapCanvas() {
context.strokeRect(left, top, width, height); context.strokeRect(left, top, width, height);
context.restore(); 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() { function scheduleMapCanvasRender() {
@@ -2910,6 +3030,12 @@ watch(
<button type="button" @click="zoomInMap">+</button> <button type="button" @click="zoomInMap">+</button>
<button type="button" @click="resetMapView">重置</button> <button type="button" @click="resetMapView">重置</button>
</div> </div>
<div class="map-overview" title="全局地图预览(点击或拖动切换视野)"
@pointerdown="startMapOverviewDrag" @pointermove="moveMapOverviewDrag" @pointerup="endMapOverviewDrag"
@pointercancel="endMapOverviewDrag">
<canvas ref="mapOverviewRef" class="map-overview-canvas"></canvas>
<div class="map-overview-label">全局视图</div>
</div>
<div class="stage-overlay top-right"> <div class="stage-overlay top-right">
{{ {{
currentConfig.mapLoaded currentConfig.mapLoaded
@@ -3466,6 +3592,39 @@ watch(
font-weight: 700; 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 { .stage-overlay.bottom-left {
left: 14px; left: 14px;
bottom: 14px; bottom: 14px;