Skip to content

全图及框选截图

上次更新 2025年8月20日星期三 3:16:20 字数 0 字 时长 0 分钟

本章将介绍如何在 OpenLayers 中实现全图截图和框选截图功能。

示例

新标签页预览

代码实现

vue
<template>
  <div class="h-screen">
    <div id="map" class="w-full h-full"></div>
    <div class="control-panel">
      <button
        id="fullScreenshot"
        class="control-button"
        @click="fullScreenshot"
      >
        📸 全图截图
      </button>
      <button
        id="areaScreenshot"
        class="control-button"
        @click="toggleAreaScreenshot"
      >
        {{ areaScreenshotText }}
      </button>
    </div>
    <div class="tooltip" ref="tooltip"></div>
    <div class="selection-info" ref="selectionInfo"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import "ol/ol.css";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import XYZ from "ol/source/XYZ";
import { fromLonLat } from "ol/proj";
import { DragBox } from "ol/interaction";
import html2canvas from "html2canvas";

const tooltip = ref(null);
const selectionInfo = ref(null);
const isSelecting = ref(false);
const areaScreenshotText = ref("✂️ 框选截图");
let map;
let dragBox;

onMounted(() => {
  // 创建地图
  map = new Map({
    target: "map",
    view: new View({
      center: fromLonLat([117.33083, 39.094899]), // 天津
      zoom: 12,
    }),
  });

  // 添加天地图图层
  const tdtLayer = new TileLayer({
    source: new XYZ({
      url: "http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=d32c6748c80f81a44acd8633cea41dfd",
      crossOrigin: "anonymous",
    }),
  });
  map.addLayer(tdtLayer);

  // ESC键取消框选
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && isSelecting.value) {
      cancelSelection();
    }
  });

  // 更新提示框位置
  map.on("pointermove", (e) => {
    if (!isSelecting.value) return;
    tooltip.value.style.left = e.originalEvent.pageX + 10 + "px";
    tooltip.value.style.top = e.originalEvent.pageY + 10 + "px";
  });
});

// 全图截图
const fullScreenshot = async (event) => {
  const button = event.target;
  button.disabled = true;
  button.style.background = "linear-gradient(145deg, #4CAF50, #388E3C)";
  button.textContent = "📸 截图中...";

  try {
    const canvas = await html2canvas(document.querySelector("#map"), {
      allowTaint: false,
      useCORS: true,
    });
    const link = document.createElement("a");
    link.download = `地图全图截图_${new Date().toLocaleString()}.png`;
    link.href = canvas.toDataURL();
    link.click();
  } catch (error) {
    alert("截图失败,请重试");
  } finally {
    button.disabled = false;
    button.style.background = "";
    button.textContent = "📸 全图截图";
  }
};

// 切换框选截图模式
const toggleAreaScreenshot = () => {
  if (isSelecting.value) {
    cancelSelection();
    return;
  }

  // 开启框选模式
  isSelecting.value = true;
  areaScreenshotText.value = "❌ 取消框选";
  map.getTargetElement().style.cursor = "crosshair";
  tooltip.value.style.display = "block";
  tooltip.value.textContent = "按住鼠标左键拖动选择区域,ESC取消";

  // 添加框选交互
  dragBox = new DragBox({
    className: "selection-box",
  });
  map.addInteraction(dragBox);

  dragBox.on("boxend", async () => {
    const extent = dragBox.getGeometry().getExtent();
    const bottomLeft = map.getPixelFromCoordinate([extent[0], extent[1]]);
    const topRight = map.getPixelFromCoordinate([extent[2], extent[3]]);

    const bounds = {
      left: bottomLeft[0],
      top: topRight[1],
      width: topRight[0] - bottomLeft[0],
      height: bottomLeft[1] - topRight[1],
    };

    if (bounds.width < 50 || bounds.height < 50) {
      selectionInfo.value.textContent = "选区太小,请重新选择(最小 50×50 像素)";
      selectionInfo.value.style.display = "block";
      return;
    }

    try {
      selectionInfo.value.textContent = "正在生成截图...";
      selectionInfo.value.style.display = "block";

      const canvas = await html2canvas(document.querySelector("#map"), {
        x: bounds.left,
        y: bounds.top,
        width: bounds.width,
        height: bounds.height,
        allowTaint: false,
        useCORS: true,
      });

      const link = document.createElement("a");
      link.download = `地图区域截图_${new Date().toLocaleString()}.png`;
      link.href = canvas.toDataURL();
      link.click();

      cancelSelection();
    } catch (error) {
      selectionInfo.value.textContent = "截图失败,请重试";
      setTimeout(cancelSelection, 2000);
    }
  });

  dragBox.on("boxdrag", () => {
    const extent = dragBox.getGeometry().getExtent();
    const bottomLeft = map.getPixelFromCoordinate([extent[0], extent[1]]);
    const topRight = map.getPixelFromCoordinate([extent[2], extent[3]]);

    const width = Math.abs(topRight[0] - bottomLeft[0]);
    const height = Math.abs(bottomLeft[1] - topRight[1]);

    selectionInfo.value.style.display = "block";
    selectionInfo.value.textContent = `选区大小: ${width.toFixed(
      0
    )} × ${height.toFixed(0)} 像素`;
  });
};

// 取消框选
const cancelSelection = () => {
  isSelecting.value = false;
  map.getTargetElement().style.cursor = "";
  areaScreenshotText.value = "✂️ 框选截图";
  tooltip.value.style.display = "none";
  selectionInfo.value.style.display = "none";
  if (dragBox) {
    map.removeInteraction(dragBox);
    dragBox = null;
  }
};
</script>

<style scoped>
.h-screen {
  height: 24rem;
}

.w-full {
  width: 100%;
}

.h-full {
  height: 100%;
}

.control-panel {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
  background: rgba(255, 255, 255, 0.95);
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  transition: transform 0.3s ease;
}

.control-panel:hover {
  transform: translateY(-2px);
}

.control-button {
  display: block;
  width: 100%;
  margin: 8px 0;
  padding: 10px 20px;
  background: linear-gradient(145deg, #2196f3, #1976d2);
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.5px;
  transition: all 0.3s ease;
  box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3);
}

.control-button:hover {
  background: linear-gradient(145deg, #1e88e5, #1565c0);
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(33, 150, 243, 0.4);
}

.control-button:active {
  transform: translateY(1px);
}

.control-button.active {
  background: linear-gradient(145deg, #4caf50, #388e3c);
}

:deep(.selection-box) {
  border: 2px solid #2196f3;
  background: rgba(33, 150, 243, 0.1);
  position: absolute;
  pointer-events: none;
  border-radius: 4px;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
  animation: pulse 2s infinite;
  z-index: 1000;
}

@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.4);
  }
  70% {
    box-shadow: 0 0 0 6px rgba(33, 150, 243, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(33, 150, 243, 0);
  }
}

.tooltip {
  position: fixed;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 6px 12px;
  border-radius: 4px;
  font-size: 12px;
  pointer-events: none;
  z-index: 1001;
  display: none;
}

.selection-info {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 14px;
  display: none;
  z-index: 1001;
}
</style>
关注公众号