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

html 复制代码
<template>
	<el-dialog v-model="visible" title="选择分校详细地址" width="90%" :close-on-click-modal="false" @closed="handleDialogClose">
		<!-- 地图组件 -->
		<el-row>
			<el-col :span="24" class="mb-2">
				<el-alert
					title="请在搜索框中输入分校详细地址点击搜索按钮或点击回车键进行搜索,地图搜索结果中选择对应分校标记,如果未搜索到分校地址请选择相近位置系统会自动获取经纬度。选择的地理位置会关联地图导航软件,为了能精确导航到分校位置所以请谨慎选择详细地址!"
					type="warning" :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" @clear="handleClearSearch">
						<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 v-if="!notContentStatus" :id="mapContainerId" class="container"></div>
					<el-empty v-else description="地图加载失败,请重试" />
				</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="200px">
						<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" @click="handleConfirm">确认选择</el-button>
			<el-button @click="visible = false">取消</el-button>
		</template>
	</el-dialog>
</template>

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

// 为每个地图容器生成唯一ID,避免DOM ID冲突
const mapContainerId = `map-container-${Date.now()}-${Math.floor(Math.random() * 1000)}`;

// 地图实例
const map = ref(null);
const placeSearch = ref(null);
const geocoder = ref(null);
const marker = ref(null);
const mapLoaded = ref(false); // 添加标记表示地图是否已加载

// 数据状态
const visible = ref(false);
const loading = ref(false);
const searchLoading = ref(false);
const notContentStatus = ref(false);
const keyword = ref('');
const searchResults = ref<any[]>([]);
const selectedPosition = ref<any>({
	address: '', // 详细地址
	lng: 0, // 经度
	lat: 0, // 纬度
});

// 父组件方法
const emit = defineEmits(['getAreaInfo']);

/**
 * 完全清理地图资源
 */
const cleanupMapResources = () => {
	try {
		// 清理标记
		if (marker.value) {
			if (map.value) map.value.remove(marker.value);
			marker.value = null;
		}

		// 清理事件监听
		if (map.value) {
			map.value.off('click', onMapClick);

			// 销毁地图
			map.value.destroy();
			map.value = null;
		}

		// 清理插件实例
		placeSearch.value = null;
		geocoder.value = null;

		// 重置状态
		mapLoaded.value = false;
	} catch (e) {
		console.warn('清理地图资源时出错:', e);
	}
};

/**
 * 暴露方法
 * @param address 详细地址
 * @param lng 经度
 * @param lat 纬度
 */
const openDialog = (address?: string, lng?: string, lat?: string) => {
	// 先清理之前的资源
	cleanupMapResources();

	visible.value = true;
	selectedPosition.value.address = address != undefined ? address : '';
	selectedPosition.value.lng = lng != undefined ? lng : '';
	selectedPosition.value.lat = lat != undefined ? lat : '';

	// 使用nextTick确保DOM已更新
	nextTick(() => {
		// 延迟初始化地图,确保DOM已完全渲染
		setTimeout(() => {
			initMap();
		}, 200);
	});
};

// 初始化地图
const initMap = async () => {
	if (mapLoaded.value) {
		console.warn('地图已加载,避免重复初始化');
		return;
	}

	loading.value = true;
	notContentStatus.value = false;

	// 确保安全配置正确设置
	window._AMapSecurityConfig = {
		securityJsCode: import.meta.env.VITE_GAODE_MAP_SECURITYJSCODE,
	};

	try {
		// 检查容器是否存在
		const container = document.getElementById(mapContainerId);
		if (!container) {
			console.error('地图容器不存在:', mapContainerId);
			notContentStatus.value = true;
			loading.value = false;
			return;
		}

		// 重置容器内容
		container.innerHTML = '';

		// 加载高德地图API
		const AMap = await AMapLoader.load({
			key: import.meta.env.VITE_GAODE_MAP_KEY,
			version: '2.0',
			plugins: ['AMap.PlaceSearch', 'AMap.Geocoder', 'AMap.AutoComplete'],
		});

		// 创建地图实例
		map.value = new AMap.Map(mapContainerId, {
			viewMode: '2D',
			zoom: 15,
			center: [selectedPosition.value.lng || 116.404, selectedPosition.value.lat || 39.915],
			resizeEnable: true,
		});

		// 等待地图加载完成
		map.value.on('complete', () => {
			mapLoaded.value = true;
			console.log('地图加载完成');
		});

		// 初始化插件
		placeSearch.value = new AMap.PlaceSearch({
			pageSize: 10,
			map: map.value,
			panel: false,
		});

		geocoder.value = new AMap.Geocoder({
			radius: 1000,
			extensions: 'all',
		});

		// 添加点击事件
		map.value.on('click', onMapClick);

		// 如果初始有坐标,添加标记
		if (selectedPosition.value.lng && selectedPosition.value.lat) {
			markerHandle(selectedPosition.value.lng, selectedPosition.value.lat);
		}

		notContentStatus.value = false;
	} catch (err) {
		console.error('地图初始化错误:', err);
		handleMapError(err);
	} finally {
		loading.value = false;
	}
};

// 地图点击事件
const onMapClick = (e: any) => {
	const { lng, lat } = e.lnglat;
	selectedPosition.value = { lng, lat };
	markerHandle(lng, lat);

	// 逆地理编码获取地址
	geocoder.value?.getAddress([lng, lat], (status: string, result: any) => {
		if (status === 'complete' && result.info === 'OK') {
			selectedPosition.value.address = result.regeocode.formattedAddress;
		}
	});
};

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

	searchLoading.value = true;
	searchResults.value = [];

	try {
		// 判断是否为经纬度格式
		if (/^-?\d+\.?\d*,-?\d+\.?\d*$/.test(keyword.value)) {
			const [lng, lat] = keyword.value.split(',').map(Number);
			selectedPosition.value = { lng, lat };
			map.value?.setCenter([lng, lat]);
			markerHandle(lng, lat);
			return;
		}

		// 普通关键词搜索
		placeSearch.value?.search(keyword.value, (status: string, result: any) => {
			if (status === 'complete' && result.poiList?.pois?.length) {
				searchResults.value = result.poiList.pois.map((poi: any) => ({
					id: poi.id,
					name: poi.name,
					address: poi.address,
					location: {
						lng: poi.location.lng,
						lat: poi.location.lat,
					},
				}));
			} else {
				ElMessage.warning('未找到相关位置');
			}
		});
	} catch (err) {
		console.error('搜索失败:', err);
		ElMessage.error('搜索失败,请重试');
	} finally {
		searchLoading.value = false;
	}
}, 300);

// 清除搜索结果
const handleClearSearch = () => {
	searchResults.value = [];
};

// 选择搜索结果
const handleSelectResult = (item: any) => {
	selectedPosition.value = {
		lng: item.location.lng,
		lat: item.location.lat,
		address: `${item.name}(${item.address})`,
	};
	markerHandle(item.location.lng, item.location.lat);
	map.value?.setCenter([item.location.lng, item.location.lat]);
	searchResults.value = [];
	keyword.value = item.name;
};

// 标记点处理
const markerHandle = (lng: number, lat: number) => {
	if (!map.value) return;

	// 移除旧标记
	if (marker.value) {
		map.value.remove(marker.value);
	}

	// 创建新标记
	marker.value = new AMap.Marker({
		position: [lng, lat],
		offset: new AMap.Pixel(-13, -30),
		icon: new AMap.Icon({
			size: new AMap.Size(46, 66),
			image: areaImg,
			imageSize: new AMap.Size(46, 66),
		}),
	});

	// 添加标记到地图
	map.value.add(marker.value);
	map.value.setCenter([lng, lat]);

	// 更新经纬度显示
	selectedPosition.value.lng = lng;
	selectedPosition.value.lat = lat;
};

// 确认选择
const handleConfirm = () => {
	if (!selectedPosition.value.lng) {
		ElMessage.warning('请先选择位置');
		return;
	}

	if (!selectedPosition.value.address) {
		ElMessage.warning('正在获取地址信息,请稍候...');
		return;
	}
	emit('getAreaInfo', selectedPosition.value);
	visible.value = false;
};

// 对话框关闭处理
const handleDialogClose = () => {
	cleanupMapResources();

	// 清空数据
	searchResults.value = [];
	keyword.value = '';
	selectedPosition.value.address = '';
	selectedPosition.value.lng = '';
	selectedPosition.value.lat = '';
};

// 错误处理
const handleMapError = (err: any) => {
	notContentStatus.value = true;
	const errorMap: Record<string, string> = {
		'10001': '密钥无效或过期',
		'10003': '开发者访问量超限',
		'10044': '用户查询超限',
	};
	ElMessage.error(errorMap[err.infocode] || '地图加载失败');
};

// 清理资源
onUnmounted(() => {
	cleanupMapResources();
});

defineExpose({ openDialog });
</script>

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

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

.search-box {
	z-index: 999;
	background: rgba(255, 255, 255, 0.9);
	padding: 10px;
	border-radius: 4px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.address-content {
	min-height: 60px;
	word-break: break-all;
}

.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;
		}
	}
}

.cell-item {
	display: flex;
	align-items: center;

	.el-icon {
		margin-right: 5px;
	}
}
</style>
相关推荐
小小码农一只1 小时前
Spring WebFlux与响应式编程:构建高效的异步Web应用
java·前端·spring·spring webflux
北极糊的狐1 小时前
使用 vue-awesome-swiper 实现轮播图(Vue3实现教程)
前端·javascript·vue.js
特种加菲猫1 小时前
解码TCP:如何实现可靠的数据传输
linux·网络·网络协议·tcp/ip
王兆龙1681 小时前
简易版增删改查
前端·vscode·vue
Jonathan Star1 小时前
`npx prettier --write . --end-of-line lf` 是一条用于**格式化代码**的命令
前端·css3
智链RFID1 小时前
信创RFID:涉密数据共享的“安全密钥”
网络·人工智能·安全
懒惰蜗牛1 小时前
Day65 | Java网络编程之TCP/IP协议
java·网络·tcp/ip·网络编程·osi七层模型·tcp/ip四层模型
pan3035074791 小时前
Tailwind CSS 实战
前端·tailwind
_lst_1 小时前
系统环境变量
前端·chrome