xlqx/app/components/AMapComponent.vue
2025-10-29 14:44:25 +08:00

331 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="map-container">
<div
ref="mapContainer"
id="amap-container"
class="map-wrapper"
></div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<p>地图加载中...</p>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-overlay">
<div class="error-content">
<h3>地图加载失败</h3>
<p>{{ error }}</p>
<button @click="retryLoad" class="retry-btn">重试</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { amapConfig, getAMapScriptUrl, validateKeys } from '~/config/amap'
// 组件属性
const props = defineProps({
// 地图中心点坐标 [经度, 纬度]
center: {
type: Array,
default: () => [117.397428, 31.90923] // 默认武汉坐标
},
// 地图缩放级别
zoom: {
type: Number,
default: 11
},
// 视图模式2D 或 3D
viewMode: {
type: String,
default: '2D',
validator: (value) => ['2D', '3D'].includes(value)
},
// 地图高度
height: {
type: String,
default: '400px'
}
})
// 响应式数据
const mapContainer = ref(null)
const map = ref(null)
const isLoading = ref(true)
const error = ref(null)
// 动态加载高德地图脚本
const loadAMapScript = () => {
return new Promise((resolve, reject) => {
if (window.AMap) {
resolve()
return
}
const script = document.createElement('script')
script.src = getAMapScriptUrl()
script.async = true
script.onload = () => {
console.log('高德地图API加载成功')
resolve()
}
script.onerror = () => {
console.error('高德地图API加载失败')
reject(new Error('高德地图API加载失败'))
}
document.head.appendChild(script)
})
}
// 初始化地图
const initMap = async () => {
if (!mapContainer.value) return
try {
isLoading.value = true
error.value = null
// 验证密钥配置
const keyValidation = validateKeys()
if (!keyValidation.isValid) {
error.value = '请配置高德地图API密钥和安全密钥'
isLoading.value = false
return
}
// 动态导入高德地图API
if (!window.AMap) {
await loadAMapScript()
}
// 设置安全密钥
window.AMap.plugin('AMap.Security', () => {
window.AMap.Security.setSecurityKey(amapConfig.securityKey)
})
// 创建地图实例
map.value = new window.AMap.Map('amap-container', {
viewMode: props.viewMode, // 2D 或 3D 模式
zoom: props.zoom, // 初始化地图层级
center: props.center, // 初始化地图中心点
mapStyle: 'amap://styles/normal', // 地图样式
features: ['bg', 'road', 'building', 'point'], // 显示要素
showLabel: true, // 显示标注
resizeEnable: true, // 是否监控地图容器尺寸变化
rotateEnable: true, // 是否允许旋转
pitchEnable: true, // 是否允许倾斜
zoomEnable: true, // 是否允许缩放
dragEnable: true, // 是否允许拖拽
keyboardEnable: true, // 是否允许键盘操作
doubleClickZoom: true, // 是否允许双击缩放
scrollWheel: true, // 是否允许滚轮缩放
touchZoom: true, // 是否允许触摸缩放
mapStyle: 'amap://styles/normal' // 地图样式
})
// 地图加载完成事件
map.value.on('complete', () => {
console.log('地图加载完成')
isLoading.value = false
// 触发地图加载完成事件
emit('mapReady', map.value)
})
// 地图点击事件
map.value.on('click', (e) => {
emit('mapClick', {
lng: e.lnglat.getLng(),
lat: e.lnglat.getLat(),
pixel: e.pixel
})
})
// 地图缩放事件
map.value.on('zoomchange', () => {
emit('zoomChange', map.value.getZoom())
})
// 地图移动事件
map.value.on('moveend', () => {
const center = map.value.getCenter()
emit('centerChange', {
lng: center.getLng(),
lat: center.getLat()
})
})
} catch (err) {
console.error('地图初始化失败:', err)
error.value = err.message || '地图初始化失败'
isLoading.value = false
}
}
// 重试加载
const retryLoad = () => {
initMap()
}
// 组件事件
const emit = defineEmits(['mapReady', 'mapClick', 'zoomChange', 'centerChange'])
// 暴露地图实例给父组件
defineExpose({
map: map,
getMap: () => map.value,
setCenter: (center) => {
if (map.value) {
map.value.setCenter(center)
}
},
setZoom: (zoom) => {
if (map.value) {
map.value.setZoom(zoom)
}
},
addMarker: (position, options = {}) => {
if (map.value) {
return new window.AMap.Marker({
position: position,
map: map.value,
...options
})
}
}
})
// 生命周期
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (map.value) {
map.value.destroy()
}
})
</script>
<style scoped>
.map-container {
position: relative;
width: 100%;
height: v-bind(height);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.map-wrapper {
width: 100%;
height: 100%;
background-color: #f5f5f5;
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-overlay p {
color: #666;
font-size: 14px;
margin: 0;
}
/* 错误状态样式 */
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.error-content {
text-align: center;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-width: 300px;
}
.error-content h3 {
color: #dc3545;
margin: 0 0 12px 0;
font-size: 16px;
}
.error-content p {
color: #666;
margin: 0 0 16px 0;
font-size: 14px;
line-height: 1.4;
}
.retry-btn {
background-color: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.retry-btn:hover {
background-color: #0056b3;
}
/* 响应式设计 */
@media (max-width: 768px) {
.map-container {
height: 300px;
}
.error-content {
margin: 0 16px;
}
}
</style>