331 lines
6.8 KiB
Vue
331 lines
6.8 KiB
Vue
<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>
|