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>
|