高德地图实现经纬度及获取编码、所属行政区、GIS

安装就不讲了,直接上代码(填上自己的密钥、Key就可以使用)

父级调用

javascript 复制代码
<el-col :span="24" class="p-4 text-align-center">
          <el-form-item label=" ">
            <el-button type="primary" @click="openMapLocation">
              <SvgIcon size="small" iconClass="el-icon-location"></SvgIcon>
              <span>获取经纬度</span>
            </el-button>
          </el-form-item>


<map-location ref="mapLocationRef" @confirm="handleLocationConfirm"></map-location>


<script setup name="formData">
import MapLocation from '@/components/Map/index.vue' // 引入

// 打开弹窗获取经纬度
const mapLocationRef = ref(null);
const openMapLocation = () => {
  mapLocationRef.value.open();
}

// 经纬度确认
const handleLocationConfirm = (locationData) => {
  formData.value.longitude = locationData.lng
  formData.value.latitude = locationData.lat
}
</script>

地图封装

javascript 复制代码
<template>
  <el-dialog
    v-model="mapDialogFlg"
    title="选择位置与影响范围"
    width="70vw"
    :before-close="handleClose"
    :close-on-click-modal="false"
    draggable
    destroy-on-close
    style="border-radius: 5px;"
    align-center
    @open="handleDialogOpen"
  >
    <!-- 内容结构保持不变 -->
    <!-- 1. 功能切换与搜索区域 -->
    <div class="top-container mb-4">
      <el-radio-group v-model="selectMode" class="mb-3" @change="handleModeChange">
        <el-radio label="point">单点选址(经纬度)</el-radio>
        <el-radio label="circle">圆形范围(中心+半径)</el-radio>
        <el-radio label="polygon">多边形范围(顶点坐标)</el-radio>
      </el-radio-group>

      <div class="search-container flex gap-2">
        <el-input
          v-model="searchKeyword"
          placeholder="请输入地址搜索"
          clearable
          class="flex-1"
          @keyup.enter="handleSearch"
          :disabled="searchDisabled"
        >
          <template #append>
            <el-button
              type="primary"
              @click="handleSearch"
              :disabled="searchDisabled || !searchKeyword.trim()"
            >
              搜索
            </el-button>
          </template>
        </el-input>

        <el-button
          v-if="selectMode !== 'point' && (selectedRange || rangeShapes.length > 0)"
          type="text"
          color="danger"
          @click="clearRange"
        >
          清除范围
        </el-button>
      </div>
    </div>

    <!-- 2. 搜索次数超限提示 -->
    <el-alert
      v-if="showLimitAlert"
      title="今日搜索次数已达上限,请明日再试或联系管理员升级服务"
      type="error"
      show-icon
      class="mb-4"
    />

    <!-- 3. 地图容器 + 绘图提示 -->
    <div class="map-wrapper">
      <div id="container"></div>
      <el-tooltip
        v-if="selectMode === 'circle'"
        content="点击地图确定圆心,拖拽鼠标调整半径(双击结束)"
        placement="top"
        effect="light"
        class="map-tip"
      >
        <el-tag size="small">圆形绘制提示</el-tag>
      </el-tooltip>
      <el-tooltip
        v-if="selectMode === 'polygon'"
        content="点击地图添加顶点(至少3个),双击结束绘制"
        placement="top"
        effect="light"
        class="map-tip"
      >
        <el-tag size="small">多边形绘制提示</el-tag>
      </el-tooltip>
    </div>

    <!-- 4. 选中数据显示区域 -->
    <div class="selected-info mt-4">
      <el-descriptions :column="1" border>
        <el-descriptions-item label="选中经纬度">
          <span v-if="selectedLngLat">{{ selectedLngLat.lng }}, {{ selectedLngLat.lat }}</span>
          <span v-else class="text-gray-400">未选择位置</span>
        </el-descriptions-item>

        <el-descriptions-item label="行政区编码">
          <span v-if="selectedDistrict.adcode">{{ selectedDistrict.adcode }}</span>
          <span v-else class="text-gray-400">未获取到编码</span>
        </el-descriptions-item>

        <!-- 新增:行政区信息显示 -->
        <el-descriptions-item label="所属行政区">
          <span v-if="selectedDistrict.district">
            {{ selectedDistrict.province }} > {{ selectedDistrict.city }} > {{ selectedDistrict.district }}
          </span>
          <span v-else class="text-gray-400">未获取到行政区信息</span>
        </el-descriptions-item>

        <el-descriptions-item
          v-if="selectMode === 'circle' && selectedRange"
          label="圆形范围信息"
        >
          <div>中心经纬度:{{ selectedRange.center.lng }}, {{ selectedRange.center.lat }}</div>
          <div>半径:{{ (selectedRange.radius / 1000).toFixed(2) }} 公里({{ selectedRange.radius }} 米)</div>
        </el-descriptions-item>
        <el-descriptions-item
          v-if="selectMode === 'polygon' && selectedRange"
          label="多边形范围信息"
        >
          <div>顶点数量:{{ selectedRange.path.length }} 个</div>
          <div>顶点坐标:
            <el-tag
              v-for="(point, idx) in selectedRange.path"
              :key="idx"
              size="mini"
              class="mr-1 mb-1"
            >
              {{ point.lng.toFixed(6) }},{{ point.lat.toFixed(6) }}
            </el-tag>
          </div>
        </el-descriptions-item>
      </el-descriptions>
    </div>

    <!-- 5. 底部按钮 -->
    <template #footer>
      <el-button @click="handleClose">取消</el-button>
      <el-button
        type="primary"
        @click="confirmSelection"
        :disabled="!selectedLngLat || (selectMode !== 'point' && !selectedRange)"
      >
        确认选择
      </el-button>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref, defineExpose, defineEmits, onUnmounted } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'
import { ElMessage, ElTag } from 'element-plus'

const emit = defineEmits(['confirm', 'update:visible'])

// 响应式变量
const mapDialogFlg = ref(false)
const searchKeyword = ref('')
const selectedLngLat = ref(null)
const selectedRange = ref(null)
const selectMode = ref('point')
const map = ref(null)
const marker = ref(null)
const mouseTool = ref(null)
const placeSearch = ref(null)
const searchDisabled = ref(false)
const showLimitAlert = ref(false)
const rangeShapes = ref([])

// 新增:存储行政区信息(省/市/区/县)
const selectedDistrict = ref({
  province: '', // 省份
  city: '',     // 城市
  district: '',  // 区/县
  adcode: '' // 新增:行政区编码
})

// 图标配置 - 增加备用图标和默认样式
const markerIconConfig = {
  // 主图标地址(使用HTTPS确保兼容性)
  mainUrl: 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
  // 备用图标地址(如果主图标失效)
  fallbackUrl: 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png',
  // 图标尺寸
  size: [36, 42],
  // 图标偏移
  offset: [-18, -38]
}

// 检查图标是否可访问的工具函数
const checkIconUrl = (url) => {
  return new Promise((resolve) => {
    const img = new Image()
    img.src = url
    img.onload = () => resolve(true)
    img.onerror = () => resolve(false)
  })
}

// 弹窗关闭逻辑
const handleClose = () => {
  mapDialogFlg.value = false
  searchKeyword.value = ''
  selectedDistrict.value = {
    province: '',
    city: '',
    district: '',
    adcode: ''
  }
  if (mouseTool.value) {
    mouseTool.value.close()
    mouseTool.value = null
  }
  clearRangeShapes()
}

// 打开弹窗
const open = (initData = {}) => {
  mapDialogFlg.value = true
  if (initData.lng && initData.lat) {
    selectedLngLat.value = { lng: initData.lng, lat: initData.lat }
  }
  if (initData.range) {
    selectedRange.value = initData.range
    selectMode.value = initData.range.type
  }
}

// 模式切换处理
const handleModeChange = (newMode) => {
  if (mouseTool.value) {
    mouseTool.value.close()
    mouseTool.value = null
  }
  selectedRange.value = null
  clearRangeShapes()

  if (newMode !== 'point' && map.value) {
    initMouseTool(newMode)
  }
}

// 初始化绘图工具
const initMouseTool = (mode) => {
  AMapLoader.load({
    key: '4e4fc1871d4ec68a716f29fd52436bda',
    version: '2.0',
    plugins: ['AMap.MouseTool']
  }).then((AMap) => {
    mouseTool.value = new AMap.MouseTool(map.value)
    mouseTool.value.on('draw', (e) => {
      const { obj } = e
      rangeShapes.value.push(obj)

      if (mode === 'circle') {
        selectedRange.value = {
          type: 'circle',
          center: { lng: obj.getCenter().lng, lat: obj.getCenter().lat },
          radius: obj.getRadius(),
          path: []
        }
        selectedLngLat.value = selectedRange.value.center
        addMarker([selectedLngLat.value.lng, selectedLngLat.value.lat])
        // 新增:调用逆地理编码
        getDistrictByLngLat(selectedLngLat.value.lng, selectedLngLat.value.lat)
      } else if (mode === 'polygon') {
        const path = obj.getPath().map((lnglat) => ({
          lng: lnglat.lng,
          lat: lnglat.lat
        }))
        selectedRange.value = {
          type: 'polygon',
          path: path,
          center: {},
          radius: 0
        }
        // 计算多边形中心点(优化方案)
        const center = calculatePolygonCenter(path)
        selectedLngLat.value = center
        addMarker([center.lng, center.lat])
        // 新增:调用逆地理编码
        getDistrictByLngLat(center.lng, center.lat)
      }
      mouseTool.value.close()
    })

    if (mode === 'circle') {
      mouseTool.value.circle({
        strokeColor: '#2f54eb',
        fillColor: 'rgba(47, 84, 235, 0.2)',
        strokeWeight: 2
      })
    } else if (mode === 'polygon') {
      mouseTool.value.polygon({
        strokeColor: '#2f54eb',
        fillColor: 'rgba(47, 84, 235, 0.2)',
        strokeWeight: 2
      })
    }
  }).catch((e) => {
    console.error('绘图工具初始化失败:', e)
    ElMessage.error('范围绘制功能加载失败,请稍后重试')
  })
}

// 计算多边形几何中心点
const calculatePolygonCenter = (path) => {
  let totalX = 0
  let totalY = 0
  for (const point of path) {
    totalX += point.lng
    totalY += point.lat
  }
  return {
    lng: totalX / path.length,
    lat: totalY / path.length
  }
}

// 清除范围
const clearRange = () => {
  selectedRange.value = null
  clearRangeShapes()

  if (mouseTool.value) {
    mouseTool.value.close()
    mouseTool.value = null
    initMouseTool(selectMode.value)
  }
}

// 清除地图上所有范围图形
const clearRangeShapes = () => {
  if (map.value && rangeShapes.value.length > 0) {
    map.value.remove(rangeShapes.value)
    rangeShapes.value = []
  }
}

// 地址搜索逻辑
const handleSearch = () => {
  if (!searchKeyword.value.trim() || !placeSearch.value || searchDisabled.value) return

  placeSearch.value.search(searchKeyword.value, (status, result) => {
    if (status === 'complete') {
      if (result.info === 'OK') {
        if (result.poiList?.pois.length > 0) {
          const firstResult = result.poiList.pois[0]
          const { lng, lat } = firstResult.location
          map.value.setCenter([lng, lat])
          addMarker([lng, lat])
          selectedLngLat.value = { lng, lat }

          if (selectMode.value === 'circle' && !selectedRange) {
            clearRangeShapes()
            const AMap = window.AMap
            const circle = new AMap.Circle({
              center: [lng, lat],
              radius: 1000,
              strokeColor: '#2f54eb',
              fillColor: 'rgba(47, 84, 235, 0.2)',
              strokeWeight: 2,
              map: map.value
            })
            rangeShapes.value.push(circle)
            selectedRange.value = {
              type: 'circle',
              center: { lng, lat },
              radius: 1000,
              path: []
            }
          }
        } else {
          ElMessage.warning('未找到匹配的地址,请尝试其他关键词')
        }
      } else if (result.info === 'DAILY_QUERY_OVER_LIMIT') {
        handleSearchLimit()
      } else {
        ElMessage.error(`搜索失败: ${result.info}`)
      }
    } else if (status === 'error') {
      result.code === 10002 ? handleSearchLimit() : ElMessage.error(`搜索出错: ${result?.message || '未知错误'}`)
    }
  })
}

// 搜索超限处理
const handleSearchLimit = () => {
  searchDisabled.value = true
  showLimitAlert.value = true
  ElMessage.error('今日搜索次数已达上限,请明日再试')
}

// 修复的单点标记方法 - 增加图标检查和容错
const addMarker = async (position) => {
  // 先移除旧标记
  if (marker.value) {
    map.value.remove(marker.value)
    marker.value = null
  }

  if (!window.AMap) return

  // 检查图标可用性,优先使用可用图标
  let iconUrl = markerIconConfig.mainUrl
  const isMainIconValid = await checkIconUrl(markerIconConfig.mainUrl)

  if (!isMainIconValid) {
    console.warn('主标记图标不可用,尝试使用备用图标')
    const isFallbackValid = await checkIconUrl(markerIconConfig.fallbackUrl)
    if (isFallbackValid) {
      iconUrl = markerIconConfig.fallbackUrl
    } else {
      console.warn('备用图标也不可用,将使用默认样式')
      // 如果所有图标都不可用,使用默认样式
      marker.value = new window.AMap.Marker({
        position: position,
        map: map.value,
        // 使用默认样式
        color: '#2f54eb',
        size: new window.AMap.Size(16, 16),
        shape: 'circle'
      })
      return
    }
  }

  // 创建图标并添加标记
  marker.value = new AMap.Marker({
    position: position,
    map: map.value,
    icon: new AMap.Icon({
      size: new AMap.Size(36, 42),
      image: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
      imageSize: new AMap.Size(36, 42),
      imageOffset: new AMap.Pixel(0, 0)
    }),
    offset: new AMap.Pixel(-18, -38) // 图标偏移,使图标底部指向坐标点
  })
}

// 地图初始化
const handleDialogOpen = () => {
  window._AMapSecurityConfig = {
    securityJsCode: '' // 密钥
  }

  AMapLoader.load({
    key: '', // 高德key
    version: '2.0',
    plugins: [
      'AMap.Scale', 'AMap.OverView', 'AMap.Geolocation',
      'AMap.PlaceSearch', 'AMap.ToolBar', 'AMap.MouseTool', 'AMap.Geocoder'
    ]
  }).then((AMap) => {
    window.AMap = AMap
    map.value = new AMap.Map('container', {
      mapStyle: 'amap://styles/macaron',
      viewMode: '3D',
      zoom: 13,
      center: selectedLngLat.value ? [selectedLngLat.value.lng, selectedLngLat.value.lat] : [117.000923, 36.651242]
    })

    map.value.addControl(new AMap.ToolBar({ position: 'RB' }))
    map.value.addControl(new AMap.Scale())

    placeSearch.value = new AMap.PlaceSearch({
      pageSize: 10,
      pageIndex: 1,
      city: '',
      map: map.value,
      panel: ''
    })

    // 回显已选单点标记
    if (selectedLngLat.value) {
      addMarker([selectedLngLat.value.lng, selectedLngLat.value.lat])
    }

    // 回显已选范围
    if (selectedRange.value) {
      renderRange(selectedRange.value)
    }

    // 单点模式:地图点击选点
    map.value.on('click', (e) => {
      if (selectMode.value !== 'point') return
      const { lng, lat } = e.lnglat
      addMarker([lng, lat])
      selectedLngLat.value = { lng, lat }

      // 新增:点击后获取行政区信息
      getDistrictByLngLat(lng, lat)
    })

    // 范围模式:初始化绘图工具
    if (selectMode.value !== 'point') {
      initMouseTool(selectMode.value)
    }
  }).catch((e) => {
    console.error('地图加载失败:', e)
    ElMessage.error('地图加载失败,请稍后重试')
  })
}

// 回显已保存的范围
const renderRange = (rangeData) => {
  const AMap = window.AMap
  if (!AMap || !map.value) return

  clearRangeShapes()

  let shape
  if (rangeData.type === 'circle') {
    shape = new AMap.Circle({
      center: [rangeData.center.lng, rangeData.center.lat],
      radius: rangeData.radius,
      strokeColor: '#2f54eb',
      fillColor: 'rgba(47, 84, 235, 0.2)',
      strokeWeight: 2,
      map: map.value
    })
  }

  if (rangeData.type === 'polygon') {
    shape = new AMap.Polygon({
      path: rangeData.path.map((p) => [p.lng, p.lat]),
      strokeColor: '#2f54eb',
      fillColor: 'rgba(47, 84, 235, 0.2)',
      strokeWeight: 2,
      map: map.value
    })
  }

  if (shape) {
    rangeShapes.value.push(shape)
  }

  const center = rangeData.center || rangeData.path[0]
  map.value.setCenter([center.lng, center.lat])
}

/**
 * 通过经纬度获取行政区信息
 * @param {Number} lng - 经度
 * @param {Number} lat - 纬度
 */
// 修改后的 getDistrictByLngLat 函数
const getDistrictByLngLat = async (lng, lat) => {
  if (!window.AMap || !lng || !lat) return;

  // 初始化逆地理编码实例(新增 extensions: 'all')
  const geocoder = new window.AMap.Geocoder({
    radius: 1000, // 搜索半径(米)
    extensions: 'all' // 返回详细地址信息(含 adcode)
  });

  geocoder.getAddress([lng, lat], (status, result) => {
    if (status === 'complete' && result.info === 'OK') {
      const addressComponent = result.regeocode.addressComponent;
      // 解析行政区信息和编码
      selectedDistrict.value = {
        province: addressComponent.province || '',
        city: addressComponent.city || addressComponent.province,
        district: addressComponent.district || '',
        adcode: addressComponent.adcode // 新增:获取行政区编码
      };
      console.log('行政区编码:', selectedDistrict.value.adcode);
    } else {
      // 处理失败逻辑
      selectedDistrict.value = { province: '', city: '', district: '', adcode: '' };
    }
  });
};

// 确认选择
const confirmSelection = () => {
  if (!selectedLngLat) return

  const result = {
    lng: selectedLngLat.value.lng,
    lat: selectedLngLat.value.lat,
    range: selectedRange.value,
    region: selectedDistrict.value
  }

  emit('confirm', result)
  handleClose()
}

// 组件卸载
onUnmounted(() => {
  if (map.value) {
    map.value.destroy()
    map.value = null
  }
  if (mouseTool.value) {
    mouseTool.value.close()
    mouseTool.value = null
  }
  window.AMap = null
  rangeShapes.value = []
})

// 暴露方法给父组件
defineExpose({
  open
})
</script>

<style scoped lang="scss">
#container {
  width: 100%;
  height: 500px;
  border-radius: 4px;
  overflow: hidden;
}

.top-container {
  .search-container {
    display: flex;
    align-items: center;
  }
}

.map-wrapper {
  position: relative;

  .map-tip {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 100;
  }
}

.selected-info {
  font-size: 14px;

  .el-descriptions-item__content {
    line-height: 1.8;
  }
}

:deep(.amap-logo) {
  display: none !important;
}

:deep(.amap-copyright) {
  opacity: 0 !important;
}

:deep(.el-input.is-disabled .el-input__inner) {
  background-color: #f5f7fa;
  cursor: not-allowed;
}
</style>
相关推荐
岁月宁静2 小时前
深度定制:在 Vue 3.5 应用中集成流式 AI 写作助手的实践
前端·vue.js·人工智能
saadiya~3 小时前
ECharts 实时数据平滑更新实践(含 WebSocket 模拟)
前端·javascript·echarts
百锦再3 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
Sheldon一蓑烟雨任平生4 小时前
Vue3 表单输入绑定
vue.js·vue3·v-model·vue3 表单输入绑定·表单输入绑定·input和change区别·vue3 双向数据绑定
瓜瓜怪兽亚5 小时前
前端基础知识---Ajax
前端·javascript·ajax
AI智能研究院5 小时前
(四)从零学 React Props:数据传递 + 实战案例 + 避坑指南
前端·javascript·react.js
qq7798233405 小时前
React组件完全指南
前端·javascript·react.js
EndingCoder5 小时前
MongoDB基础与Mongoose ODM
服务器·javascript·数据库·mongodb·中间件·node.js
qq7798233405 小时前
React Hooks完全指南
前端·javascript·react.js
Moment5 小时前
性能狂飙!Next.js 16 重磅发布:Turbopack 稳定、编译提速 10 倍!🚀🚀🚀
前端·javascript·后端