xlqx/app/components/AMapComponent.vue

331 lines
6.8 KiB
Vue
Raw Normal View History

2025-10-29 14:44:18 +08:00
<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>