vue3项目中集成天地图使用示例

html 复制代码
<template>
	<el-dialog draggable="true" v-model="visible" title="选择分校详细地址" width="90%" :close-on-click-modal="false"
		@closed="handleDialogClose" @opened="handleDialogOpened">
		<!-- 地图组件 -->
		<el-row>
			<el-col :span="24" class="mb-2">
				<el-alert
					title="请在搜索框中输入分校详细地址点击搜索按钮或点击回车键进行搜索,地图搜索结果中选择对应分校标记,如果未搜索到分校地址请选择相近位置系统会自动获取经纬度。选择的地理位置会关联地图导航软件,为了能精确导航到分校位置所以请谨慎选择详细地址!"
					type="error" :closable="false" />
			</el-col>
		</el-row>

		<el-row class="border p-2">
			<el-col :span="18">
				<!-- 搜索框集成到地图上 -->
				<div class="search-box mb-2">
					<el-input v-model="keyword" size="large" placeholder="请输入分校详细地址(如:内蒙古自治区呼和浩特市新城区锡林郭勒北路46号成公教育)"
						clearable @keyup.enter="handleSearch">
						<template #append>
							<el-button type="success" size="large" @click="handleSearch" :loading="searchLoading">
								搜索
							</el-button>
						</template>
					</el-input>
				</div>
				<!-- 地图容器 -->
				<div class="map-p" v-loading="loading">
					<div id="mapDiv" ref="mapContainer" class="container"></div>
				</div>
			</el-col>
			<el-col :span="6" class="ps-4">
				<!-- 详情地址 -->
				<el-descriptions class="margin-top" title="详情地址" :column="1" border direction="vertical">
					<el-descriptions-item>
						<template #label>
							<div class="cell-item">
								<el-icon>
									<LocationInformation />
								</el-icon>
								选中的位置
							</div>
						</template>
						{{ selectedPosition.address || '请在地图上选择位置' }}
					</el-descriptions-item>
				</el-descriptions>

				<!-- 经纬度 -->
				<el-descriptions class="margin-top mt-4" title="经纬度" :column="1" border>
					<el-descriptions-item>
						<template #label>
							<div class="cell-item">
								<el-icon>
									<Position />
								</el-icon>
								经度
							</div>
						</template>
						{{ selectedPosition?.lng || '--' }}
					</el-descriptions-item>
					<el-descriptions-item>
						<template #label>
							<div class="cell-item">
								<el-icon>
									<Position />
								</el-icon>
								纬度
							</div>
						</template>
						{{ selectedPosition?.lat || '--' }}
					</el-descriptions-item>
				</el-descriptions>

				<!-- 搜索结果列表 -->
				<div class="search-results mt-4" v-if="searchResults.length > 0">
					<div class="sub-title">搜索结果</div>
					<el-scrollbar height="450px">
						<div v-for="(item, index) in searchResults" :key="index" class="result-item"
							@click="handleSelectResult(item)">
							<div class="result-name">{{ item?.name }}</div>
							<div class="result-address">{{ item?.address }}</div>
						</div>
					</el-scrollbar>
				</div>
			</el-col>
		</el-row>

		<template #footer>
			<el-button type="primary">确认选择</el-button>
			<el-button @click="visible = false">取消</el-button>
		</template>
	</el-dialog>
</template>

<script setup lang="ts">
import { debounce } from 'lodash-es';
import { ElMessage } from 'element-plus';
import areaImg from '/@/assets/images/area.png';

const mapConfig = ref<any>({
	tk: import.meta.env.VITE_TIANDI_MAP_KEY,
	version: '4.0',
})

// 地图实例引用
const map = ref<any>(null)
const mapContainer = ref<HTMLElement>()
const visible = ref(false)
const loading = ref(false)
const keyword = ref(''); // 搜索关键词
const searchLoading = ref(false);
const searchResults = ref<any[]>([]);
const selectedPosition = ref<any>({
	address: '', // 详细地址
	lng: 0, // 经度
	lat: 0, // 纬度
});

/**
 * 搜索时间
 * 防抖300ms
 */
const handleSearch = debounce(async () => {
	if (!keyword.value.trim()) {
		ElMessage.warning('请输入搜索内容');
		return;
	}

	searchLoading.value = true;
	// 清空数据
	searchResults.value = [];

	try {
		const results: any = await searchPlaces(keyword.value)
		searchResults.value = results
	} catch (error) {
		ElMessage.error('搜索失败')
		console.error('搜索错误:', error)
	} finally {
		searchLoading.value = false
	}
}, 300);

/**
 * 调用天地图搜索服务
 */
const searchPlaces = (keyword: string): Promise<any[]> => {
	return new Promise((resolve, reject) => {
		if (!window.T) {
			reject(new Error('天地图API未加载'));
			return;
		}

		const T = window.T;
		// 每次搜索新建一个 LocalSearch 实例
		const localSearch = new T.LocalSearch(map.value, {
			pageCapacity: 10,
			autoViewport: true, // 自动调整地图视野以显示所有搜索结果
			onSearchComplete: (res: any) => {
				/**
				 * 返回数据结构说明
				 * resultType: 搜索结果类型 1=POI 点结果,2=统计,3=行政区,4=建议词,5=公交线
				 * keyword: 搜索关键词 
				 * count: 搜索结果总数 总命中条数(最多 100)
				 * pois: 搜索结果列表 当 resultType=1 时返回,见下方 Poi 结构
				 * statistics: 统计信息 resultType=2 时返回,城市-结果数统计
				 * area: 区域信息 resultType=3 时返回,行政区边界
				 * suggests: resultType=4 时返回,搜索建议词
				 * lineData: resultType=5 时返回,公交线信息
				 * 
				 * | 字段                               | 类型     | 示例                         | 说明                       |
| -------------------------------- | ------ | -------------------------- | ------------------------ |
| name                             | String | "天安门"                      | 名称                       |
| address                          | String | "北京市东城区"                   | 地址                       |
| lonlat                           | String | "116.397,39.909"           | **经纬度字符串**,用逗号分隔         |
| poiType                          | Number | 101                        | 101=普通POI,102=公交站        |
| province/city/county             | String | "北京市"/"北京市"/"东城区"          | 行政区划                     |
| provinceCode/cityCode/countyCode | String | "110000"/"110100"/"110101" | 对应编码                     |
| phone                            | String | "010-12345678"             | 电话(可能为空)                 |
| typeCode/typeName                | String | "110000"/"风景名胜"            | 分类编码/名称                  |
| distance                         | String | "1.2km"                    | 与中心点距离(需传 center+radius) |
| hotPointID                       | String | "12345678"                 | 热点 ID                    |

				 */
				switch (res.resultType) {
					case 1:
						// POI 点结果 返回搜索结果列表
						if (!res.pois || res.pois.length === 0) {
							ElMessage.warning('未找到相关地点');
							resolve([]);
							break;
						}
						// 飞向位置
						flyToPosition(res.pois[0].lonlat, 17);
						resolve(res.pois);
						break;
					case 3:
						// 行政区边界
						resolve([]);
						if (res.area && res.area.lonlat) {
							flyToPosition(res.area.lonlat, 10);
						}
						break;
					default:
						ElMessage.warning('未找到相关地点');
						resolve([]);
						break;
				}
			},
		});

		localSearch.search(keyword);
	});
};

/**
 * 飞向指定位置
 */
const flyToPosition = (lnglat: any, level: number) => {
	if (!map.value || !window.T) return;
	const tLngLat = new window.T.LngLat(lnglat.split(',')[0], lnglat.split(',')[1]);
	// 设置新的中心点和缩放级别
	map.value.centerAndZoom(tLngLat, level);
	// 调整地图大小
	resizeMap();

	clearMarkers()

	// 对于行政区边界,也可以添加一个中心点标记
	const marker = new window.T.Marker(lnglat)
	map.value.addOverLay(marker)

}

/**
 * 点击选择搜索结果
 * @param item 
 */
const handleSelectResult = (item: any) => {
	selectedPosition.value = {
		lng: item.lonlat.split(',')[0],
		lat: item.lonlat.split(',')[1],
		address: `${item.name}(${item.address})`,
	};
	// 自动定位到选中的搜索结果
	if (map.value && window.T) {
		const [lng, lat] = item.lonlat.split(',');
		map.value.centerAndZoom(new window.T.LngLat(parseFloat(lng), parseFloat(lat)), 17);
		// 添加标记
		addMarker(new window.T.LngLat(parseFloat(lng), parseFloat(lat)));
	}
	keyword.value = item.name;
};

/**
 * 确保天地图API已加载
 */
const ensureTMapLoaded = (): Promise<boolean> => {
	return new Promise((resolve) => {
		if (window.T) {
			resolve(true)
			return
		}

		// 动态加载天地图API
		const script = document.createElement('script')
		script.src = `https://api.tianditu.gov.cn/api?v=${mapConfig.value.version}&tk=${mapConfig.value.tk}`
		script.onload = () => {
			setTimeout(() => {
				resolve(!!window.T)
			}, 100)
		}

		script.onerror = () => {
			console.error('天地图API加载失败')
			resolve(false)
		}
		document.head.appendChild(script)
	})
}

/**
 * 初始化地图
 */
const initMap = async () => {
	try {
		// 1. 确保天地图API已加载
		const isLoaded = await ensureTMapLoaded()
		if (!isLoaded) {
			console.error('天地图API加载失败')
			return
		}

		// 2. 确保地图容器存在且可见
		if (!mapContainer.value) {
			console.error('地图容器未找到')
			return
		}

		const T = window.T

		// 3. 创建地图实例
		map.value = new T.Map(mapContainer.value, {
			/**
			 * WGS84经纬度坐标系(默认)
			 * Web墨卡托投影
			 */
			projection: 'EPSG:4326'
		})


		// 4. 设置地图类型和初始视图
		/**
			TMAP_NORMAL_MAP 此地图类型展示普通街道视图。
			TMAP_SATELLITE_MAP 此地图类型展示卫星视图。
			TMAP_HYBRID_MAP 此地图类型展示卫星和路网的混合视图。
			TMAP_TERRAIN_MAP 此地图类型展示地形视图。
			TMAP_TERRAIN_HYBRID_MAP 此地图类型展示地形和路网的混合视图。
		 */
		map.value.setMapType(TMAP_NORMAL_MAP)
		// 设置中心点
		map.value.centerAndZoom(new T.LngLat(111.66175, 40.81968), 17)

		// 5. 添加地图控件
		map.value.addControl(new T.Control.Zoom())
		map.value.addControl(new T.Control.Scale())

		/**
		 * 绑定点击事件
		 */
		map.value.addEventListener('click', onMapClick)
	} catch (error) {
		console.error(error.msg || '地图初始化失败')
	}
}

/**
 * 点击事件
 * 添加标记点击事件示例
 * 点击地图会在点击位置添加一个标记点
 * 获取经纬度
 * @param e 
 */
const onMapClick = async (e: any) => {
	const lng = e.lnglat.lng
	const lat = e.lnglat.lat

	selectedPosition.value.lng = lng
	selectedPosition.value.lat = lat
	addMarker(e.lnglat)

	/**
	 * 点击地图获取详细位置信息
	 * 逆地理位置编码
	 */
	try {
		// 获取详细位置信息
		const locationInfo = await getLocationInfo(lng, lat)
		// 详细地址
		selectedPosition.value.address = locationInfo?.formattedAddress
		// 提示用户选择地址成功
		ElMessage.success(`已选择: ${locationInfo.formattedAddress || locationInfo.address}`)
	} catch (error) {
		console.error('获取位置信息失败:', error)
		ElMessage.warning('获取详细地址失败,仅获取经纬度')
	}
}

/**
 * 逆地理编码获取位置信息
 * @param lng 经度
 * @param lat 纬度
 */
const getLocationInfo = (lng: number, lat: number): Promise<any> => {
	return new Promise((resolve, reject) => {
		if (!window.T) {
			reject(new Error('天地图API未加载'))
			return
		}

		const T = window.T

		// 创建逆地理编码服务
		const geocoder = new T.Geocoder()

		// 执行逆地理编码
		geocoder.getLocation(new T.LngLat(lng, lat), ({ result }) => {
			if (result.msg) {
				resolve({
					province: result.result.addressComponent?.province, // 省份
					provinceCode: result.result.addressComponent?.province_code, // 省份编码
					city: result.result.addressComponent?.city, // 城市
					cityCode: result.result.addressComponent?.city_code, // 城市编码
					county: result.result.addressComponent?.county, // 区县
					countyCode: result.result.addressComponent?.county_code, // 区县编码
					formattedAddress: result.result?.formatted_address, // 详细地址
					address: result.result.addressComponent?.address, // 地址
				})
			} else {
				reject(new Error('未找到位置信息'))
			}
		}, (error: any) => {
			reject(error)
		})
	})
}

/**
 * 添加标记点
 * @param lnglat 经纬度信息
 */
const addMarker = (lnglat: any) => {
	if (!map.value || !window.T) return

	// 清除现有标记
	clearMarkers()

	// 创建自定义图标
	const customIcon = new T.Icon({
		iconUrl: areaImg, // 替换为你的图片地址
		iconSize: new T.Point(50, 66), // 图标大小
	});

	// 添加到地图上
	const marker = new window.T.Marker(lnglat, {
		icon: customIcon
	})
	map.value.addOverLay(marker)
}

/**
 * 清除所有标记
 */
const clearMarkers = () => {
	if (!map.value) return
	const overlays = map.value.getOverlays()
	overlays.forEach((overlay: any) => {
		if (overlay instanceof window.T.Marker) {
			map.value.removeOverLay(overlay)
		}
	})
}

/**
 * 重置地图大小
 */
const resizeMap = () => {
	if (map.value) {
		// 延迟执行确保DOM更新完成
		setTimeout(() => {
			map.value?.checkResize()
		}, 100)
	}
}

/**
 * 对话框打开后回调
 */
const handleDialogOpened = () => {
	nextTick(() => {
		resizeMap()
	})
}

/**
 * 对话框关闭回调
 */
const handleDialogClose = () => {
	visible.value = false
}

/**
 * 暴露方法
 */
const openDialog = () => {
	visible.value = true
	// 使用nextTick确保DOM已更新
	nextTick(async () => {
		await initMap()
	})
}

defineExpose({ openDialog })
</script>

<style scoped lang="scss">
.map-p {
	position: relative;

	.container {
		width: 100%;
		height: 700px;
	}
}

// 搜索结果列表
.search-results {
	border: 1px solid var(--el-border-color);
	border-radius: 4px;
	padding: 10px;

	.sub-title {
		font-weight: bold;
		margin-bottom: 8px;
		color: var(--el-text-color-primary);
	}

	.result-item {
		padding: 8px;
		cursor: pointer;
		border-radius: 4px;
		margin-bottom: 4px;
		transition: all 0.3s;

		&:hover {
			background-color: var(--el-color-primary-light-9);
		}

		.result-name {
			font-weight: 500;
			color: var(--el-color-primary);
		}

		.result-address {
			font-size: 12px;
			color: var(--el-text-color-secondary);
			margin-top: 4px;
		}
	}
}
</style>
相关推荐
VX:Fegn089541 分钟前
计算机毕设|基springboot+Vue的校园打印系统设计与实现
java·前端·javascript·vue.js·spring boot·后端·课程设计
Haha_bj42 分钟前
二、Kotlin数组(Array)
android·app
t***265942 分钟前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb
参宿四南河三43 分钟前
Android Jetpack 存储篇(DataStore、Room)与 Flow 高效组合
android·app
m***11901 小时前
【前端】Node.js使用教程
前端·node.js·vim
y***13641 小时前
【MySQL】MVCC详解, 图文并茂简单易懂
android·数据库·mysql
QianhangQianping1 小时前
前端技术迭代深析:从 CSS 布局到状态管理的进化之路
前端·css