vue项目引入GoogleMap API进行网格区域圈选

根据后端返回的数据绘制

javascript 复制代码
// 返回数据格式,经纬度
[
  {
    "longitude": 106.8300012,
    "latitude": -6.1824352
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1824352
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1833283
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1833283
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8317979,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8317979,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8326962,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8326962,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1860076
  },
  {
    "longitude": 106.8326962,
    "latitude": -6.1860076
  },
  {
    "longitude": 106.8326962,
    "latitude": -6.1869007
  },
  {
    "longitude": 106.8353911,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8362894,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8362894,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8344928,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8344928,
    "latitude": -6.1824352
  },
  {
    "longitude": 106.8353911,
    "latitude": -6.1824352
  },
  {
    "longitude": 106.8353911,
    "latitude": -6.1842214
  },
  {
    "longitude": 106.8326962,
    "latitude": -6.1869007
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1869007
  },
  {
    "longitude": 106.8335945,
    "latitude": -6.1877938
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1877938
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1869007
  },
  {
    "longitude": 106.8317979,
    "latitude": -6.1869007
  },
  {
    "longitude": 106.8317979,
    "latitude": -6.1860076
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1860076
  },
  {
    "longitude": 106.8308996,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8300012,
    "latitude": -6.1851145
  },
  {
    "longitude": 106.8300012,
    "latitude": -6.1824352
  }
]
javascript 复制代码
<template>
  <div class="grid-map">
    <el-upload
      action="#"
      accept=".xlsx,.xls,.csv"
      :show-file-list="false"
      :http-request="handleUpload"
    >
      <el-button
        type="primary"
        :disabled="uploadLoading"
        class="mb20"
      >
        {{ $t('tools.importGridFile') }}
      </el-button>
    </el-upload>
    <div
      ref="mapRef"
      class="map"
      v-loading="uploadLoading"
      element-loading-background="rgba(0, 0, 0, 0.7)"
    ></div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';
import html2canvas from 'html2canvas';
import { uploadImgFile, griddingUpload } from '@/api/common';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';

const { t } = useI18n();
const emit = defineEmits(['updateGridImageUrl', 'update:pointList']);
const mapRef = ref(null);
const map = ref(null); // 地图绘制参数
const GOOGLE_MAP_API_KEY = 'AIzaSyBcoIe_T6s2uH0wDi2dRocGCtkyP2cJhd4'; // 谷歌地图唯一key
const polygons = ref([]); // 区域网格参数
const uploadLoading = ref(false);

const props = defineProps({
  pointList: {
    type: Array,
    default: () => [],
  },
});

onMounted(() => {
  const script = document.createElement('script');
  script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAP_API_KEY}&callback=initMap`;
  script.async = true; // 异步加载
  script.defer = true;
  window.initMap = () => {
    map.value = new google.maps.Map(mapRef.value, {
      center: { lat: -6.2088, lng: 106.8456 }, // 地图初始中心点坐标
      zoom: 17, // 地图初始缩放级别
      minZoom: 5, // 最小缩放级别
      maxZoom: 20, // 最大缩放级别
      clickableIcons: false, // 禁用地图上POI图标(如商家、地标)的点击交互,点击后不会触发默认弹窗
      disableDoubleClickZoom: true, // 禁用"双击地图自动放大"功能,双击后地图不会缩放
      disableDefaultUI: true, // 禁用谷歌地图所有默认UI控件(包括缩放、地图类型、街景等)
      // fullscreenControl: false, // 隐藏全屏按钮
      // streetViewControl: false, // 隐藏街景小人控件
      // mapTypeControl: false, // 隐藏"地图/卫星图"切换控件
      // zoomControl: false, // 隐藏"+/-"缩放按钮控件
      // draggable: true, // 禁用地图拖动(四向箭头交互)
    });
  };
  document.head.appendChild(script);
});

watch(
  () => props.pointList,
  (newVal) => {
    if (map.value && newVal?.length) {
      uploadLoading.value = true;
      drawGridOnMap(map.value, newVal);
    }
  },
  {
    deep: true,
  },
);

// 处理文件上传
const handleUpload = (params) => {
  if (uploadLoading.value) return;
  // 文件类型验证
  const file = params.file;
  const isExcelOrCsv =
    // 通过MIME类型判断
    file.type === 'application/vnd.ms-excel' ||
    file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
    file.type === 'text/csv' ||
    file.type === 'application/csv' ||
    file.type === 'text/comma-separated-values';

  // 文件格式验证(.xlsx,.xls,.csv)
  if (!isExcelOrCsv) {
    ElMessage.error(t('tools.gridFileFormatError'));
    return;
  }

  // 文件大小验证
  const isLt5M = file.size / 1024 / 1024 < 30;
  if (!isLt5M) {
    ElMessage.error(t('tools.fileSizeExceed30MB'));
    return;
  }

  const data = new FormData();
  data.append('file', file);
  uploadLoading.value = true;
  griddingUpload(data).then((res) => {
    if (res.success) {
      emit('update:pointList', res.data || []);
      // 处理后端返回的数据并绘制
      drawGridOnMap(map.value, res.data);
    }
  });
};

/**
 * 根据后端返回的经纬度数据绘制多边形网格
 * @param {google.maps.Map} map - 地图实例
 * @param {Array} coords - 后端返回的坐标数组:[{longitude, latitude}, ...]
 */
const drawGridOnMap = (map, coords) => {
  // 清除已有多边形
  polygons.value.forEach((poly) => poly.setMap(null));
  polygons.value = [];

  if (!coords || coords.length < 3) {
    console.warn('坐标数据不足,无法绘制多边形');
    return;
  }

  // 转换后端数据格式为Google Maps需要的LatLng对象数组
  const path = coords.map((item) => ({
    lat: item.latitude,
    lng: item.longitude,
  }));

  // 计算地图显示范围
  const bounds = new window.google.maps.LatLngBounds();
  path.forEach((point) => bounds.extend(point));

  // 创建并绘制多边形
  const polygon = new google.maps.Polygon({
    paths: path, // 多边形顶点路径
    strokeColor: 'transparent',
    strokeOpacity: 0,
    strokeWeight: 0,
    fillColor: '#00FF00',
    fillOpacity: 0.3,
    map: map,
  });
  polygons.value.push(polygon);

  // 调整地图视野以显示整个多边形
  map.fitBounds(bounds, {
    padding: { top: 50, bottom: 50, left: 50, right: 50 },
  });
  uploadLoading.value = false;
  // 延迟截图,确保地图渲染完成
  // setTimeout(() => {
  captureMapScreenshot();
  // }, 800);
};

const captureMapScreenshot = async () => {
  const mapDom = mapRef.value;
  if (!mapDom) return;

  try {
    const canvas = await html2canvas(mapDom, {
      useCORS: true,
      scale: 2,
      logging: false,
    });

    // 关键修改:将Blob转换为带元数据的File对象
    canvas.toBlob(
      async (blob) => {
        if (blob) {
          // 1. 生成唯一文件名(避免重复)
          const timestamp = new Date().getTime();
          const fileName = `map-screenshot-${timestamp}.jpg`; // 明确使用.jpg扩展名

          // 2. 将Blob转换为File对象,指定MIME类型为image/jpeg
          const file = new File([blob], fileName, {
            type: 'image/jpeg', // 匹配你提供的示例中的image/jpeg类型
            lastModified: Date.now(), // 添加最后修改时间
          });

          // 3. 上传转换后的File对象
          await uploadScreenshotToBackend(file);
        }
      },
      'image/jpeg',
      0.9,
    ); // 明确指定格式为jpeg,0.9为压缩质量(0-1)
  } catch (error) {
    console.error('截图失败:', error);
  }
};

// 上传截图到后端
const uploadScreenshotToBackend = async (file) => {
  // 参数已改为File对象
  const formData = new FormData();
  // 直接 append File对象,而非Blob
  formData.append('file', file);
  formData.append('business', 'product');

  const res = await uploadImgFile(formData);
  if (res.success) {
    emit('updateGridImageUrl', res.data.visitUrl || '');
  }
};
</script>

<style lang="scss" scoped>
.grid-map {
  .map {
    position: relative;
    width: 900px;
    height: 600px;
  }
}
</style>

根据本地上传的excel文件绘制

javascript 复制代码
<template>
  <div class="grid-map">
    <input
      type="file"
      accept=".xlsx,.xls,.csv"
      @change="handleFileUpload"
      class="file-upload"
      :disabled="uploadLoading"
    />
    <div
      ref="mapRef"
      class="map"
      v-loading="uploadLoading"
      element-loading-background="rgba(0, 0, 0, 0.7)"
    ></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import * as XLSX from 'xlsx';
import html2canvas from 'html2canvas';
import { uploadImgFile } from '@/api/common';

const emit = defineEmits(['updateGridImageUrl']);
const mapRef = ref(null);
const map = ref(null);
const gridData = ref([]);
const GOOGLE_MAP_API_KEY = 'AIzaSyBcoIe_T6s2uH0wDi2dRocGCtkyP2cJhd4';
const polygons = ref([]);
const uploadLoading = ref(false); // 添加上传loading状态

onMounted(() => {
  const script = document.createElement('script');
  script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAP_API_KEY}&callback=initMap`;
  script.async = true; // 异步加载
  script.defer = true;
  window.initMap = () => {
    map.value = new google.maps.Map(mapRef.value, {
      center: { lat: -6.2088, lng: 106.8456 }, // 地图初始中心点坐标
      zoom: 17, // 地图初始缩放级别
      minZoom: 5, // 最小缩放级别
      maxZoom: 20, // 最大缩放级别
      clickableIcons: false, // 禁用地图上POI图标(如商家、地标)的点击交互,点击后不会触发默认弹窗
      disableDoubleClickZoom: true, // 禁用"双击地图自动放大"功能,双击后地图不会缩放
      disableDefaultUI: true, // 禁用谷歌地图所有默认UI控件(包括缩放、地图类型、街景等)
      // fullscreenControl: false, // 隐藏全屏按钮
      // streetViewControl: false, // 隐藏街景小人控件
      // mapTypeControl: false, // 隐藏"地图/卫星图"切换控件
      // zoomControl: false, // 隐藏"+/-"缩放按钮控件
      // draggable: true, // 禁用地图拖动(四向箭头交互)
    });
    if (gridData.value.length) drawGridOnMap(map.value, gridData.value);
  };
  document.head.appendChild(script);
});

/**
 * 处理Excel文件上传事件:读取文件内容 → 解析Excel数据 → 提取网格顶点经纬度 → 触发地图绘制
 * @param {Event} event - 文件上传控件的change事件对象
 */
const handleFileUpload = (event) => {
  const file = event.target.files[0];
  if (!file) return;
  uploadLoading.value = true;
  event.target.value = '';

  // 2. 创建FileReader实例:用于读取文件的二进制数据
  const reader = new FileReader();

  // 3. 定义文件读取成功后的回调函数
  reader.onload = (e) => {
    try {
      const isCSV = file.name.toLowerCase().endsWith('.csv');
      let workbook;

      // 区分解析CSV和Excel文件
      if (isCSV) {
        // CSV文件:先转为字符串再解析
        const text = new TextDecoder().decode(e.target.result);
        workbook = XLSX.read(text, { type: 'string' });
      } else {
        // Excel文件:用ArrayBuffer解析
        const data = new Uint8Array(e.target.result);
        workbook = XLSX.read(data, { type: 'array' });
      }

      // 提取第一个工作表数据
      const sheet = workbook.Sheets[workbook.SheetNames[0]];
      const jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 });

      // 校验数据有效性(至少包含表头和一行数据)
      if (jsonData.length <= 1) {
        uploadLoading.value = false;
        return;
      }

      // 解析表头和数据行(重点处理C、D、E、F列)
      const headers = jsonData[0]; // 表头数组(C列索引为2,D为3,E为4,F为5)
      gridData.value = jsonData.slice(1).map((row) => {
        const grid = {};
        // 映射所有字段到grid对象(保留原始数据)
        headers.forEach((header, index) => {
          grid[header] = row[index];
        });

        /**
         * 解析坐标字符串(适配CSV中C-F列的格式:"经度,纬度",兼容空格)
         * 例如:"106.7936694, -6.1663594" → 拆分为经度106.7936694,纬度-6.1663594
         * @param {string} str - 坐标字符串
         * @returns {Object} 包含lat(纬度)和lng(经度)的坐标对象
         */
        const splitCoord = (str) => {
          if (!str || typeof str !== 'string') return { lat: NaN, lng: NaN };
          // 按逗号分割,处理可能的空格(如"106.79, -6.16")
          const [lngStr, latStr] = str.split(/\s*,\s*/).map((s) => s.trim());
          return {
            lat: parseFloat(latStr) || NaN, // 纬度(第二部分)
            lng: parseFloat(lngStr) || NaN, // 经度(第一部分)
          };
        };

        // 关键修改:强制绑定C、D、E、F列(索引2、3、4、5),兼容表头字段名变化
        // C列 → 顶点1,D列 → 顶点2,E列 → 顶点3,F列 → 顶点4
        grid.vertex1 = splitCoord(row[2]); // C列(索引2)
        grid.vertex2 = splitCoord(row[3]); // D列(索引3)
        grid.vertex3 = splitCoord(row[4]); // E列(索引4)
        grid.vertex4 = splitCoord(row[5]); // F列(索引5)

        return grid;
      });

      // 若地图已初始化,绘制网格
      if (map.value) {
        drawGridOnMap(map.value, gridData.value);
      }
    } catch (error) {
      console.error('文件解析失败:', error);
    } finally {
      uploadLoading.value = false;
    }
  };

  // 文件读取错误处理
  reader.onerror = () => {
    console.error('文件读取失败');
    uploadLoading.value = false;
  };

  // 读取文件(统一用ArrayBuffer,后续按需处理)
  reader.readAsArrayBuffer(file);
};

/**
 * 在地图上绘制网格多边形
 * @param {google.maps.Map} map - 地图实例
 * @param {Array} data - 网格数据数组
 */
const drawGridOnMap = (map, data) => {
  // 清除已有多边形
  polygons.value.forEach((poly) => poly.setMap(null));
  polygons.value = [];

  // 计算地图显示范围
  const bounds = new window.google.maps.LatLngBounds();

  // 遍历网格数据,绘制多边形
  data.forEach((grid) => {
    // 提取4个顶点坐标(C-F列解析结果)
    const vertices = [grid.vertex1, grid.vertex2, grid.vertex3, grid.vertex4];

    // 扩展地图范围以包含所有有效顶点
    vertices.forEach((vertex) => {
      if (!isNaN(vertex.lat) && !isNaN(vertex.lng)) {
        bounds.extend(vertex);
      }
    });

    // 过滤无效坐标,确保多边形至少有3个有效顶点
    const validVertices = vertices.filter((v) => !isNaN(v.lat) && !isNaN(v.lng));
    if (validVertices.length >= 3) {
      // 创建多边形(绿色半透明填充)
      const polygon = new google.maps.Polygon({
        paths: validVertices, // 多边形的有效顶点路径(paths支持单个多边形,兼容多区域场景)
        strokeColor: 'transparent', // 边框颜色:透明
        strokeOpacity: 0, // 边框透明度
        strokeWeight: 0, // 边框粗细
        fillColor: '#00FF00', // 多边形填充色
        fillOpacity: 0.3, // 填充透明度
        map, // 将多边形绑定到当前地图实例
      });
      polygons.value.push(polygon);
    }
  });

  // 调整地图视野以显示所有网格
  if (bounds.getNorthEast() && bounds.getSouthWest()) {
    map.fitBounds(bounds, {
      padding: { top: 50, bottom: 50, left: 50, right: 50 },
    });

    // 延迟截图,确保地图渲染完成
    setTimeout(() => {
      captureMapScreenshot();
    }, 800); // 延长延迟,确保复杂网格完全渲染
  }
};

/**
 * 捕获地图截图并上传
 */
const captureMapScreenshot = async () => {
  const mapDom = mapRef.value;
  if (!mapDom) return;

  try {
    // 生成截图(scale=2提升清晰度)
    const canvas = await html2canvas(mapDom, {
      useCORS: true, // 允许跨域图片(谷歌地图瓦片)
      scale: 2,
      logging: false,
    });

    // 转为Blob并上传
    canvas.toBlob(async (blob) => {
      if (blob) {
        await uploadScreenshotToBackend(blob);
      }
    }, 'image/png');
  } catch (error) {
    console.error('截图失败:', error);
  }
};

/**
 * 上传截图到后端
 * @param {Blob} blob - 截图的Blob对象
 */
const uploadScreenshotToBackend = async (blob) => {
  const formData = new FormData();
  formData.append('file', blob);
  formData.append('business', 'product');

  const res = await uploadImgFile(formData);
  if (res.success) {
    emit('updateGridImageUrl', res.data.visitUrl || '');
  }
};
</script>

<style lang="scss" scoped>
.grid-map {
  display: flex;
  flex-direction: column;
  gap: 16px;
  .file-upload {
    cursor: pointer;
    &:disabled {
      cursor: not-allowed;
      opacity: 0.6;
    }
  }
  .map {
    position: relative;
    width: 900px;
    height: 600px;
  }
}
</style>
相关推荐
j***89461 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
star_11121 小时前
Jenkins+nginx部署前端vue项目
前端·vue.js·jenkins
im_AMBER1 小时前
Canvas架构手记 05 鼠标事件监听 | 原生事件封装 | ctx 结构化对象
前端·笔记·学习·架构
JIngJaneIL1 小时前
农产品电商|基于SprinBoot+vue的农产品电商系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·农产品电商系统
Tongfront1 小时前
前端通用submit方法
开发语言·前端·javascript·react
c***72741 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
可爱又迷人的反派角色“yang”1 小时前
LVS+Keepalived群集
linux·运维·服务器·前端·nginx·lvs
han_1 小时前
前端高频面试题之CSS篇(二)
前端·css·面试
JIngJaneIL1 小时前
书店销售|书屋|基于SprinBoot+vue书店销售管理设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·书店销售管理设计与实现