Skip to content

全图及框选截图

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

示例

TIP

本示例展示了如何在 Leaflet 地图上实现截图功能。主要实现了以下功能:

  • 支持全图截图
  • 支持框选区域截图
  • 显示框选区域大小
  • 自定义截图样式和交互效果

新标签页预览

代码实现

vue
<template>
  <div id="map" class="w-full h-96"></div>
  <div class="control-panel">
    <button id="fullScreenshot" class="control-button">📸 全图截图</button>
    <button id="areaScreenshot" class="control-button">✂️ 框选截图</button>
  </div>
  <div class="tooltip"></div>
  <div class="selection-info"></div>
</template>

<script setup>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import html2canvas from "html2canvas";
import { onMounted, ref } from "vue";

const map = ref(null);

onMounted(() => {
  const urlLayer =
    "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";

  map.value = L.map("map", {
    center: [39.094899, 117.33083],
    zoom: 12,
    zoomControl: true,
    doubleClickZoom: false,
    attributionControl: false,
  });

  // 底图
  const baseLayer = L.tileLayer(urlLayer).addTo(map.value);

  const tooltip = document.querySelector(".tooltip");
  const selectionInfo = document.querySelector(".selection-info");

  // 全图截图
  const fullScreenshotBtn = document.getElementById("fullScreenshot");
  fullScreenshotBtn.addEventListener("click", async function () {
    this.disabled = true;
    this.style.background = "linear-gradient(145deg, #4CAF50, #388E3C)";
    this.textContent = "📸 截图中...";

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

  // 框选截图
  let isSelecting = false;
  let startPoint;
  let selectionBox;
  let isDrawing = false;

  const areaScreenshotBtn = document.getElementById("areaScreenshot");
  areaScreenshotBtn.addEventListener("click", function () {
    if (isSelecting) {
      // 取消框选模式
      cancelSelection();
      return;
    }

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

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

  function cancelSelection() {
    isSelecting = false;
    isDrawing = false;
    map.value.dragging.enable();
    map.value.getContainer().style.cursor = "";
    areaScreenshotBtn.classList.remove("active");
    areaScreenshotBtn.textContent = "✂️ 框选截图";
    tooltip.style.display = "none";
    selectionInfo.style.display = "none";
    if (selectionBox && selectionBox.parentNode) {
      selectionBox.parentNode.removeChild(selectionBox);
    }
    selectionBox = null;
    startPoint = null;
  }

  map.value.on("mousedown", function (e) {
    if (!isSelecting) return;
    isDrawing = true;
    startPoint = e.containerPoint;

    if (selectionBox && selectionBox.parentNode) {
      selectionBox.parentNode.removeChild(selectionBox);
    }

    selectionBox = L.DomUtil.create(
      "div",
      "selection-box",
      map.value.getContainer()
    );
    selectionBox.style.left = startPoint.x + "px";
    selectionBox.style.top = startPoint.y + "px";
  });

  map.value.on("mousemove", function (e) {
    if (!isSelecting) return;

    tooltip.style.left = e.originalEvent.pageX + 10 + "px";
    tooltip.style.top = e.originalEvent.pageY + 10 + "px";

    if (!isDrawing || !startPoint) return;

    const currentPoint = e.containerPoint;
    const bounds = {
      left: Math.min(startPoint.x, currentPoint.x),
      top: Math.min(startPoint.y, currentPoint.y),
      width: Math.abs(currentPoint.x - startPoint.x),
      height: Math.abs(currentPoint.y - startPoint.y),
    };

    selectionBox.style.left = bounds.left + "px";
    selectionBox.style.top = bounds.top + "px";
    selectionBox.style.width = bounds.width + "px";
    selectionBox.style.height = bounds.height + "px";

    // 显示选区尺寸信息
    selectionInfo.style.display = "block";
    selectionInfo.textContent = `选区大小: ${bounds.width} × ${bounds.height} 像素`;
  });

  map.value.on("mouseup", async function () {
    if (!isSelecting || !isDrawing || !startPoint || !selectionBox) return;
    isDrawing = false;

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

    try {
      selectionInfo.textContent = "正在生成截图...";

      // 临时隐藏选择框和蒙层
      selectionBox.style.display = "none";

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

      // 恢复选择框显示
      selectionBox.style.display = "block";

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

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

<style scoped>
.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);
}

.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>
关注公众号