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>
相关推荐
Pedantic5 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘5 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆5 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师6 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆6 小时前
VSCode自动格式化三要素
前端
爱勇宝7 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen8 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user205855615181310 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode10 小时前
Redis 在生产项目的使用
前端·后端