Vue项目使用天地图

最近的项目中需要将高德地图替换成天地图,由于第一次接触,官方的网站也没有进行详细的介绍,网上对于天地图使用的文章也比较少,分享总结一下vue项目天地图的使用。

lbs.tianditu.gov.cn/home.html 这是天地图的官方地址,我使用的是最新的版本,4.0的api具体的可以进入官网去看看。

前置条件

使用API之前,需要申请应用Key点击该链接注册账号并申请KEY

  • 步骤一:注册账号
  • 步骤二:点击控制台进入应用管理
  • 步骤三:创建新应用,创建后可以看到自己申请的Key
引入天地图
  • 方式一:直接在项目的index.html中引入
js 复制代码
<script src="https://api.tianditu.gov.cn/api?v=4.0&tk=您的密钥" type="text/javascript"></script>
  • 方式二:使用动态加载器加载天地图
js 复制代码
// 天地图配置 config.js
export const tMapVersion = '4.0'
export const tMapKey = 'c0075044cad46971d3270e8db148b491'
export const tMapOrigin = 'https://api.tianditu.gov.cn'
js 复制代码
import { tMapKey, tMapOrigin, tMapVersion } from './config'
const T_MAP_API_URL = `${tMapOrigin}/api?v=${tMapVersion}&tk=${tMapKey}`
let loadPromise = null
let currentScript = null
/**
 * 动态加载天地图 API
 * @returns {Promise<TMapAPI>} 天地图 API 对象
 */
export function loadTMapAPI() {
  // 如果已经加载完成,直接返回
  if (window.T && validateTMapAPI()) {
    // console.debug('✅ 天地图 API 已存在,直接返回')
    return Promise.resolve(window.T)
  }

  // 如果正在加载中,返回现有的 Promise
  if (loadPromise) {
    return loadPromise
  }
  
  loadPromise = new Promise((resolve, reject) => {
    let retryCount = 0
    const maxRetries = 3

    function attemptLoad() {
      // 创建 script 标签
      const script = document.createElement('script')
      script.src = T_MAP_API_URL
      script.type = 'text/javascript'
      script.async = true
      script.defer = true

      // 保存当前脚本引用
      currentScript = script

      // 成功加载回调
      script.onload = () => {
        // 给 API 更多时间完全初始化
        setTimeout(() => {
          if (window.T && validateTMapAPI()) {
            // console.debug('✅ 天地图 API 加载成功')
            return resolve(window.T)
          }

          // 再等待一段时间
          console.warn('⚠️ API 对象存在但验证失败,等待更长时间...')
          setTimeout(() => {
            if (window.T && validateTMapAPI()) {
              // console.debug('✅ 天地图 API 延迟验证成功')
              return resolve(window.T)
            }

            const error = new Error('天地图 API 加载失败:API 对象无效或关键方法缺失')
            console.error('❌', error.message)
            reject(error)
          }, 1000)

          // 不立即清理脚本,让 API 保持可用
        }, 500)
      }

      // 错误处理
      script.onerror = (event) => {
        console.error('❌ 脚本加载错误:', event)
        cleanupScript(script)

        if (retryCount < maxRetries) {
          retryCount++
          // console.debug(`🔄 重试加载天地图 API (${retryCount}/${maxRetries})...`)
          setTimeout(() => attemptLoad(), 1000 * retryCount)
        }
        else {
          const error = new Error(`天地图 API 脚本加载失败:已重试 ${maxRetries} 次,可能是网络问题或 CORS 限制`)
          console.error('❌', error.message)
          reject(error)
        }
      }

      // 添加到页面头部
      document.head.appendChild(script)
      // console.debug(`🚀 正在加载天地图 API... (尝试 ${retryCount + 1}/${maxRetries + 1})`)
    }

    // 开始首次加载尝试
    attemptLoad()

    // 设置总体超时处理,增加到30秒总超时
    setTimeout(() => {
      if (!window.T || !validateTMapAPI()) {
        const error = new Error('天地图 API 加载总体超时(30秒)')
        console.error('❌', error.message)
        if (currentScript) {
          cleanupScript(currentScript)
        }
        reject(error)
      }
    }, 30000)
  })

  return loadPromise
}


/**
 * 清理脚本标签
 * @param {HTMLScriptElement} script 要清理的脚本元素
 */
function cleanupScript(script) {
  if (script && script.parentNode) {
    script.parentNode.removeChild(script)
  }
  if (currentScript === script) {
    currentScript = null
  }
}

/**
 * 验证天地图 API 是否正确加载
 * @returns {boolean}
 */
function validateTMapAPI() {
  try {
    if (!window.T)
      return false

    // 检查关键构造函数是否存在
    const requiredMethods = ['Map', 'LngLat', 'Marker']
    for (const method of requiredMethods) {
      if (typeof window.T[method] !== 'function') {
        console.warn(`⚠️ 天地图 API 缺少方法: ${method}`)
        return false
      }
    }

    return true
  }
  catch (error) {
    console.error('❌ 验证天地图 API 时出错:', error)
    return false
  }
}


/**
 * 检查天地图 API 是否已加载
 * @returns {boolean}
 */
export function isTMapLoaded() {
  return !!window.T && validateTMapAPI()
}
地图公用方法提取

将使用到的地图方法提取到t-map.js文件中

js 复制代码
import { ref, reactive, nextTick } from 'vue'
import { tMapKey, tMapOrigin } from '@/config'
import { loadTMapAPI, isTMapLoaded } from '@/libs/t-map-loader'

export function useTMap(addrInfo) {
  const map = ref()
  const nearList = ref([])
  const currentCity = ref('')
  const isMapLoading = ref(false)
  const mapLoadError = ref('')
  const stateMap = reactive({
    center: [114.0579, 22.5431],
    marker: [114.0579, 22.5431],
    zoom: 11,
  })

  let T = null

  // 地图公用模块抽取
  function setMapData() {
    if (!T || !map.value)
      return
    map.value.centerAndZoom(new T.LngLat(stateMap.center[0], stateMap.center[1]), stateMap.zoom) // 中心点坐标和缩放级别
    const marker = new T.Marker(new T.LngLat(stateMap.marker[0], stateMap.marker[1]))
    map.value.clearOverLays()
    map.value.addOverLay(marker)
  }

  // 初始化地图
  async function initMap() {
    try {
      isMapLoading.value = true
      mapLoadError.value = ''
      // 动态加载天地图 API
      T = await loadTMapAPI()

      // 等待 DOM 更新并验证容器存在
      await nextTick()

      // 验证容器是否存在
      const container = document.getElementById('container')
      if (!container) {
        throw new Error('地图容器 #container 不存在,请确保 DOM 元素已渲染')
      }

      // 验证天地图 API 是否可用
      if (!T || typeof T.Map !== 'function') {
        throw new Error('天地图 API 未正确加载或 T.Map 构造函数不可用')
      }

      // 清空容器内容
      container.innerHTML = ''

      // 创建地图实例
      map.value = new T.Map('container', { projection: 'EPSG:4326' })

      // 设置地图数据
      const { province, city, longitude, latitude } = addrInfo.value
      currentCity.value = province?.includes('市') ? province : city || '深圳市'
      if (longitude && latitude) {
        stateMap.center = [longitude, latitude]
        stateMap.marker = [longitude, latitude]
      }

      setMapData()
      // 向地图上添加标注
      map.value.addEventListener('click', onMapClick)
      isMapLoading.value = false

    }
    catch (error) {
      console.error('地图初始化失败:', error)
      mapLoadError.value = error instanceof Error ? error.message : '地图加载失败,请刷新页面重试'
      isMapLoading.value = false
    }
  }

  // 地图选点
  async function onMapClick(event) {
    if (!T)
      return

    const { lnglat } = event || {}
    const data = await getMapAddress(lnglat.lng, lnglat.lat)
    const { location, addressComponent } = data || {}
    stateMap.center = [location.lon, location.lat]
    stateMap.marker = [location.lon, location.lat]
    setMapData()
    const value = addressComponent.address
    await searchAddressFn(value)
  }

  async function searchAddressFn(searchValue) {
    if (!T || !map.value)
      return

    const value = searchValue
    const bounds = map.value?.getBounds()
    const mapBound = `${bounds?.getSouthWest()?.lng},${bounds?.getSouthWest()?.lat},${bounds?.getNorthEast()?.lng},${bounds?.getNorthEast()?.lat}`
    const params = {
      postStr: JSON.stringify({
        keyWord: value,
        level: '18', // 搜索等级
        mapBound,
        queryRadius: 10000,
        pointLonlat: `${stateMap.center[0]},${stateMap.center[1]}`,
        queryType: '1', // 查询类型 1:普通搜索
        start: 0,
        count: 100,
        show: 2,
      }),
      tk: tMapKey,
      type: 'query',
    }
    const response = await fetch(`${tMapOrigin}/v2/search?postStr=${encodeURIComponent(params.postStr)}&tk=${tMapKey}&type=${params.type}`)
    const data = await response.json()
    const { pois } = data || {}
    nearList.value = pois || []
  }

  // 逆地理编码
  async function getMapAddress(lng, lat) {
    const response = await fetch(`${tMapOrigin}/geocoder?postStr={'lon':${lng},'lat':${lat},'ver':1}&tk=${tMapKey}`)
    const data = await response.json()
    return data.result
  }

  return {
    map,
    nearList,
    currentCity,
    stateMap,
    isMapLoading,
    mapLoadError,
    setMapData,
    getMapAddress,
    initMap,
    onMapClick,
    searchAddressFn,
  }
} 
具体使用
js 复制代码
<script setup>
import { ref, computed,nextTick} from 'vue'
import { useTMap } from '@/composables/t-map'

const {
  stateMap,
  nearList,
  initMap,
  searchAddressFn,
  setMapData,
  getMapAddress,
  isMapLoading,
  mapLoadError
} = useTMap(addrInfo)

// 打开弹框初始化地图
async function showDialog() {
  // 延迟初始化地图,确保 DOM 完全渲染
  await nextTick()
  initMap()
}
html 复制代码
<template>
  <div class="map-container">
    <div v-if="dialogVisible" id="container" />
    <!-- 地图加载状态覆盖层 -->
    <div v-if="isMapLoading" class="map-loading map-overlay">
      <i class="el-icon el-icon-loading"></i>
      <p>地图加载中...</p>
    </div>
    <!-- 地图加载错误覆盖层 -->
    <div v-else-if="mapLoadError" class="map-error map-overlay">
      <i class="el-icon el-icon-warning-outline"></i>
      <p>{{ mapLoadError }}</p>
      <el-button type="primary" size="mini" @click="initMap">
        重新加载
      </el-button>
    </div>
  </div>
</template>
相关推荐
whysqwhw3 小时前
Node-API 学习二
前端
whysqwhw3 小时前
Node-API 学习一
前端
Jenna的海糖3 小时前
Vue 中 v-model 的 “双向绑定”:从原理到自定义组件适配
前端·javascript·vue.js
一碗清汤面3 小时前
打造AI代码审查员:使用 Gemini + Git Hooks 自动化 Code Review
前端·git·代码规范
Sagittarius_A*3 小时前
SpringBoot Web 入门指南:从零搭建第一个SpringBoot程序
java·前端·spring boot·后端
我是ed3 小时前
# Vue 前端封装组件基础知识点
前端
芦苇Z4 小时前
CSS :has() 父级选择器与关系查询
前端·css
前端康师傅4 小时前
Javascript 中循环的使用
前端·javascript
毕了业就退休4 小时前
从 WebSocket 转向 SSE:轻量实时推送的另一种选择
前端·javascript·https
子兮曰4 小时前
🚀 图片加载速度提升300%!Vue/React项目WebP兼容方案大揭秘
前端·vue.js·react.js