235 lines
5.3 KiB
Vue
235 lines
5.3 KiB
Vue
<template>
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import L from 'leaflet'
|
|
import 'leaflet/dist/leaflet.css'
|
|
|
|
const mapContainer = ref(null)
|
|
let map = null
|
|
let markerLayer = null
|
|
let currentMarkers = []
|
|
let markerByPoiId = {}
|
|
let activeMarker = null
|
|
let radiusCircle = null
|
|
|
|
const emit = defineEmits(['marker-click', 'map-click'])
|
|
|
|
// 高德地图瓦片
|
|
const amapTileUrl = 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
|
|
const amapSubdomains = ['1', '2', '3', '4']
|
|
|
|
// 蓝色默认图标
|
|
const blueIcon = L.divIcon({
|
|
className: '',
|
|
html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 42" width="32" height="42">
|
|
<path d="M16 2C8.268 2 2 8.268 2 16c0 9.678 14 24 14 24s14-14.322 14-24C30 8.268 23.732 2 16 2z" fill="#1a73e8" stroke="#fff" stroke-width="2"/>
|
|
<circle cx="16" cy="15" r="6" fill="#fff"/>
|
|
</svg>`,
|
|
iconSize: [32, 42],
|
|
iconAnchor: [16, 42],
|
|
popupAnchor: [0, -42],
|
|
})
|
|
|
|
// 红色选中图标
|
|
const redIcon = L.divIcon({
|
|
className: '',
|
|
html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 42" width="32" height="42">
|
|
<path d="M16 2C8.268 2 2 8.268 2 16c0 9.678 14 24 14 24s14-14.322 14-24C30 8.268 23.732 2 16 2z" fill="#e53935" stroke="#fff" stroke-width="2"/>
|
|
<circle cx="16" cy="15" r="6" fill="#fff"/>
|
|
</svg>`,
|
|
iconSize: [32, 42],
|
|
iconAnchor: [16, 42],
|
|
popupAnchor: [0, -42],
|
|
})
|
|
|
|
onMounted(() => {
|
|
map = L.map(mapContainer.value, {
|
|
center: [39.9042, 116.4074], // 北京
|
|
zoom: 12,
|
|
zoomControl: true,
|
|
})
|
|
|
|
// 高德地图瓦片层
|
|
L.tileLayer(amapTileUrl, {
|
|
subdomains: amapSubdomains,
|
|
attribution: '© 高德地图',
|
|
maxZoom: 18,
|
|
}).addTo(map)
|
|
|
|
markerLayer = L.layerGroup().addTo(map)
|
|
|
|
// 地图点击 → 设置中心点
|
|
map.on('click', (e) => {
|
|
emit('map-click', { lat: e.latlng.lat, lng: e.latlng.lng })
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (map) {
|
|
map.remove()
|
|
map = null
|
|
}
|
|
})
|
|
|
|
function clearMarkers() {
|
|
markerLayer.clearLayers()
|
|
currentMarkers = []
|
|
markerByPoiId = {}
|
|
activeMarker = null
|
|
}
|
|
|
|
function addMarkers(pois) {
|
|
clearMarkers()
|
|
if (!map || !pois.length) return
|
|
|
|
pois.forEach((poi) => {
|
|
const lat = parseFloat(poi.latitude)
|
|
const lng = parseFloat(poi.longitude)
|
|
if (isNaN(lat) || isNaN(lng)) return
|
|
|
|
const marker = L.marker([lat, lng], { icon: blueIcon }).addTo(markerLayer)
|
|
marker.bindPopup(`
|
|
<div class="map-popup">
|
|
<h4>${poi.name}</h4>
|
|
<p class="popup-phone">${poi.phone ? `<a href="tel:${poi.phone}">${poi.phone}</a>` : '暂无电话'}</p>
|
|
<p class="popup-addr">${poi.address || ''}</p>
|
|
${poi.businessArea ? `<p class="popup-area">商圈:${poi.businessArea}</p>` : ''}
|
|
</div>
|
|
`)
|
|
|
|
marker.on('click', () => {
|
|
setActiveMarker(marker)
|
|
emit('marker-click', poi)
|
|
})
|
|
|
|
currentMarkers.push(marker)
|
|
if (poi.poiId) {
|
|
markerByPoiId[poi.poiId] = marker
|
|
}
|
|
})
|
|
}
|
|
|
|
function setActiveMarker(marker) {
|
|
// 恢复上一个选中标记为蓝色
|
|
if (activeMarker && activeMarker !== marker) {
|
|
activeMarker.setIcon(blueIcon)
|
|
}
|
|
marker.setIcon(redIcon)
|
|
activeMarker = marker
|
|
}
|
|
|
|
function focusMarker(poi) {
|
|
const lat = parseFloat(poi.latitude)
|
|
const lng = parseFloat(poi.longitude)
|
|
if (isNaN(lat) || isNaN(lng)) return
|
|
|
|
// 先飞到该位置
|
|
flyTo(lat, lng, 15)
|
|
|
|
// 按 poiId 查找
|
|
let marker = poi.poiId ? markerByPoiId[poi.poiId] : null
|
|
if (!marker) {
|
|
// 按坐标查找
|
|
for (const m of currentMarkers) {
|
|
const pos = m.getLatLng()
|
|
if (Math.abs(pos.lat - lat) < 0.001 && Math.abs(pos.lng - lng) < 0.001) {
|
|
marker = m
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (!marker) return
|
|
|
|
setActiveMarker(marker)
|
|
marker.openPopup()
|
|
}
|
|
|
|
// 地图跳转到指定坐标
|
|
function flyTo(lat, lng, zoom) {
|
|
if (!map) return
|
|
map.setView([lat, lng], zoom || 12)
|
|
}
|
|
|
|
// 绘制半径圆圈
|
|
function drawRadiusCircle(lat, lng, radiusMeters) {
|
|
clearRadiusCircle()
|
|
if (!map) return
|
|
radiusCircle = L.circle([lat, lng], {
|
|
radius: radiusMeters,
|
|
color: '#1a73e8',
|
|
fillColor: '#1a73e8',
|
|
fillOpacity: 0.08,
|
|
weight: 2,
|
|
dashArray: '6, 6',
|
|
}).addTo(map)
|
|
}
|
|
|
|
function clearRadiusCircle() {
|
|
if (radiusCircle) {
|
|
map.removeLayer(radiusCircle)
|
|
radiusCircle = null
|
|
}
|
|
}
|
|
|
|
// 只更新已有圆圈的大小,不改变地图视角
|
|
function updateRadiusCircle(radiusMeters) {
|
|
if (radiusCircle) {
|
|
radiusCircle.setRadius(radiusMeters)
|
|
}
|
|
}
|
|
|
|
defineExpose({ addMarkers, clearMarkers, focusMarker, drawRadiusCircle, clearRadiusCircle, updateRadiusCircle, flyTo })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.map-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 400px;
|
|
}
|
|
|
|
/* 缩放控件左侧垂直居中 */
|
|
:deep(.leaflet-control-zoom) {
|
|
margin-top: 0 !important;
|
|
}
|
|
:deep(.leaflet-top.leaflet-left) {
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
position: absolute;
|
|
z-index: 1000;
|
|
}
|
|
|
|
:deep(.map-popup h4) {
|
|
margin: 0 0 4px;
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
:deep(.popup-phone) {
|
|
margin: 2px 0;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #1a73e8;
|
|
}
|
|
|
|
:deep(.popup-phone a) {
|
|
color: #1a73e8;
|
|
text-decoration: none;
|
|
}
|
|
|
|
:deep(.popup-addr) {
|
|
margin: 2px 0;
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
:deep(.popup-area) {
|
|
margin: 2px 0;
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
</style>
|