<template>
<div class="leaflet-map-container">
<!-- 地图容器 -->
<div ref="mapContainer" class="leaflet-map"></div>
<!-- 地图控制面板 -->
<div v-if="showControls" class="map-controls">
<div class="control-group">
<button @click="zoomIn" title="放大">
<svg-icon name="zoom-in" />
</button>
<button @click="zoomOut" title="缩小">
<svg-icon name="zoom-out" />
</button>
<button @click="resetView" title="重置视图">
<svg-icon name="reset" />
</button>
</div>
<div class="control-group">
<button @click="toggleFullscreen" title="全屏">
<svg-icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" />
</button>
<button @click="locateUser" title="定位到当前位置">
<svg-icon name="location" />
</button>
<button @click="toggleDrawTool" :class="{ active: drawMode }" title="绘制工具">
<svg-icon name="draw" />
</button>
</div>
<!-- 图层控制 -->
<div class="layer-control">
<h4>图层控制</h4>
<div v-for="(layer, index) in baseLayers" :key="layer.id" class="layer-item">
<input
type="radio"
:id="layer.id"
:value="layer.id"
v-model="selectedBaseLayer"
@change="changeBaseLayer"
/>
<label :for="layer.id">{{ layer.name }}</label>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>地图加载中...</span>
</div>
<!-- 信息弹窗 -->
<div v-if="popupInfo.show" class="custom-popup" :style="popupStyle">
<div class="popup-header">
<h3>{{ popupInfo.title }}</h3>
<button @click="closePopup" class="popup-close">×</button>
</div>
<div class="popup-content">
<slot name="popup-content" :data="popupInfo.data"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import 'leaflet-draw/dist/leaflet.draw.css'
import 'leaflet-draw'
// 修复Leaflet默认图标问题
delete L.Icon.Default.prototype._getIconUrl
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
})
const props = defineProps({
// 初始中心坐标
center: {
type: Array,
default: () => [31.2304, 121.4737] // 上海
},
// 初始缩放级别
zoom: {
type: Number,
default: 13
},
// 最小缩放级别
minZoom: {
type: Number,
default: 3
},
// 最大缩放级别
maxZoom: {
type: Number,
default: 18
},
// 是否显示控件
showControls: {
type: Boolean,
default: true
},
// 是否启用拖动
dragging: {
type: Boolean,
default: true
},
// 是否启用缩放
scrollWheelZoom: {
type: Boolean,
default: true
},
// 地图瓦片配置
tileLayers: {
type: Array,
default: () => [
{
id: 'osm',
name: 'OpenStreetMap',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors'
},
{
id: 'satellite',
name: '卫星影像',
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attribution: '© Esri'
}
]
},
// 覆盖层配置
overlayLayers: {
type: Array,
default: () => []
},
// 标记点数据
markers: {
type: Array,
default: () => []
},
// GeoJSON数据
geoJsonData: {
type: [Object, Array],
default: null
},
// 是否启用绘制工具
enableDraw: {
type: Boolean,
default: false
}
})
const emit = defineEmits([
'map-loaded',
'map-click',
'marker-click',
'draw-created',
'draw-edited',
'draw-deleted',
'zoom-change',
'center-change'
])
// 响应式数据
const mapContainer = ref(null)
const mapInstance = ref(null)
const loading = ref(true)
const isFullscreen = ref(false)
const drawMode = ref(false)
const selectedBaseLayer = ref(props.tileLayers[0]?.id || 'osm')
const drawControl = ref(null)
// 地图图层存储
const baseLayers = ref({})
const overlayLayers = ref({})
const markerLayers = ref({})
const geoJsonLayers = ref({})
// 基础图层配置
const baseLayerConfig = computed(() => {
const config = {}
props.tileLayers.forEach(layer => {
config[layer.id] = L.tileLayer(layer.url, {
attribution: layer.attribution,
maxZoom: props.maxZoom,
minZoom: props.minZoom
})
})
return config
})
// 弹窗信息
const popupInfo = reactive({
show: false,
title: '',
data: null,
position: null
})
const popupStyle = computed(() => {
if (!popupInfo.position) return {}
return {
left: `${popupInfo.position.x}px`,
top: `${popupInfo.position.y}px`
}
})
// 初始化地图
const initMap = () => {
if (!mapContainer.value) return
loading.value = true
try {
// 创建地图实例
mapInstance.value = L.map(mapContainer.value, {
center: props.center,
zoom: props.zoom,
minZoom: props.minZoom,
maxZoom: props.maxZoom,
dragging: props.dragging,
scrollWheelZoom: props.scrollWheelZoom,
zoomControl: false,
attributionControl: true
})
// 添加基础图层
Object.keys(baseLayerConfig.value).forEach(key => {
baseLayers.value[key] = baseLayerConfig.value[key]
if (key === selectedBaseLayer.value) {
baseLayerConfig.value[key].addTo(mapInstance.value)
}
})
// 添加覆盖层
props.overlayLayers.forEach(layer => {
if (layer.type === 'wms') {
overlayLayers.value[layer.id] = L.tileLayer.wms(layer.url, layer.options)
} else if (layer.type === 'geojson') {
overlayLayers.value[layer.id] = L.geoJSON(layer.data, layer.options)
}
if (layer.visible) {
overlayLayers.value[layer.id].addTo(mapInstance.value)
}
})
// 添加标记点
addMarkers(props.markers)
// 添加GeoJSON数据
if (props.geoJsonData) {
addGeoJsonLayer(props.geoJsonData)
}
// 初始化绘制工具
if (props.enableDraw) {
initDrawControl()
}
// 绑定事件
bindMapEvents()
// 添加自定义控件
addCustomControls()
emit('map-loaded', mapInstance.value)
} catch (error) {
console.error('地图初始化失败:', error)
} finally {
setTimeout(() => {
loading.value = false
}, 500)
}
}
// 绑定地图事件
const bindMapEvents = () => {
if (!mapInstance.value) return
// 地图点击事件
mapInstance.value.on('click', (e) => {
emit('map-click', {
latlng: e.latlng,
layerPoint: e.layerPoint,
containerPoint: e.containerPoint
})
})
// 缩放事件
mapInstance.value.on('zoomend', () => {
emit('zoom-change', mapInstance.value.getZoom())
})
// 移动事件
mapInstance.value.on('moveend', () => {
emit('center-change', mapInstance.value.getCenter())
})
// 右键菜单
mapInstance.value.on('contextmenu', (e) => {
showContextMenu(e)
})
}
// 添加自定义控件
const addCustomControls = () => {
// 缩放控件
L.control.zoom({
position: 'topright'
}).addTo(mapInstance.value)
// 比例尺
L.control.scale({
imperial: false,
metric: true
}).addTo(mapInstance.value)
}
// 添加标记点
const addMarkers = (markers) => {
if (!mapInstance.value) return
// 清除现有标记
Object.values(markerLayers.value).forEach(layer => {
mapInstance.value.removeLayer(layer)
})
markerLayers.value = {}
markers.forEach(marker => {
const { lat, lng, title, icon, draggable, data } = marker
// 自定义图标
let markerIcon = L.icon({
iconUrl: icon || 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
})
const markerLayer = L.marker([lat, lng], {
icon: markerIcon,
title,
draggable: draggable || false,
data // 存储自定义数据
}).addTo(mapInstance.value)
// 点击事件
markerLayer.on('click', (e) => {
emit('marker-click', { marker, event: e })
// 显示自定义弹窗
if (marker.popupContent) {
showPopup({
title: title || '标记点',
content: marker.popupContent,
position: e.latlng
})
}
})
// 拖拽事件
if (draggable) {
markerLayer.on('dragend', (e) => {
const newPos = e.target.getLatLng()
console.log('标记点拖拽到新位置:', newPos)
})
}
markerLayers.value[marker.id || `${lat}_${lng}`] = markerLayer
})
}
// 添加GeoJSON图层
const addGeoJsonLayer = (data) => {
if (!mapInstance.value || !data) return
const geoJsonLayer = L.geoJSON(data, {
style: (feature) => {
return {
color: '#3388ff',
weight: 2,
opacity: 0.7,
fillColor: '#3388ff',
fillOpacity: 0.2
}
},
onEachFeature: (feature, layer) => {
if (feature.properties) {
const popupContent = `
<div class="geojson-popup">
<h4>${feature.properties.name || '未命名'}</h4>
<p>${JSON.stringify(feature.properties, null, 2)}</p>
</div>
`
layer.bindPopup(popupContent)
}
}
}).addTo(mapInstance.value)
geoJsonLayers.value['main'] = geoJsonLayer
}
// 初始化绘制工具
const initDrawControl = () => {
if (!mapInstance.value) return
// 绘制选项
const drawOptions = {
position: 'topright',
draw: {
polygon: {
allowIntersection: false,
drawError: {
color: '#e1e100',
message: '<strong>多边形不能交叉!</strong>'
},
shapeOptions: {
color: '#3388ff'
}
},
polyline: {
shapeOptions: {
color: '#3388ff',
weight: 4
}
},
rectangle: {
shapeOptions: {
color: '#3388ff'
}
},
circle: {
shapeOptions: {
color: '#3388ff'
}
},
marker: {
icon: L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
})
}
},
edit: {
featureGroup: new L.FeatureGroup(),
remove: true
}
}
drawControl.value = new L.Control.Draw(drawOptions)
// 绘制完成事件
mapInstance.value.on(L.Draw.Event.CREATED, (e) => {
const layer = e.layer
const type = e.layerType
const data = {
type,
layer,
geometry: layer.toGeoJSON()
}
emit('draw-created', data)
// 添加到编辑组
drawOptions.edit.featureGroup.addLayer(layer)
})
// 编辑事件
mapInstance.value.on('draw:edited', (e) => {
emit('draw-edited', e)
})
// 删除事件
mapInstance.value.on('draw:deleted', (e) => {
emit('draw-deleted', e)
})
}
// 地图操作方法
const zoomIn = () => {
mapInstance.value?.zoomIn()
}
const zoomOut = () => {
mapInstance.value?.zoomOut()
}
const resetView = () => {
mapInstance.value?.setView(props.center, props.zoom)
}
const toggleFullscreen = () => {
const container = mapContainer.value.parentElement
if (!document.fullscreenElement) {
container.requestFullscreen?.()
isFullscreen.value = true
} else {
document.exitFullscreen?.()
isFullscreen.value = false
}
}
const locateUser = () => {
if (!navigator.geolocation) {
alert('浏览器不支持地理定位')
return
}
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords
mapInstance.value?.setView([latitude, longitude], 15)
// 添加当前位置标记
L.marker([latitude, longitude], {
icon: L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
})
})
.addTo(mapInstance.value)
.bindPopup('您的位置')
.openPopup()
},
(error) => {
console.error('定位失败:', error)
alert('无法获取当前位置')
}
)
}
const toggleDrawTool = () => {
if (!mapInstance.value || !drawControl.value) return
drawMode.value = !drawMode.value
if (drawMode.value) {
mapInstance.value.addControl(drawControl.value)
} else {
mapInstance.value.removeControl(drawControl.value)
}
}
const changeBaseLayer = () => {
if (!mapInstance.value) return
// 移除所有基础图层
Object.values(baseLayers.value).forEach(layer => {
mapInstance.value.removeLayer(layer)
})
// 添加选中的基础图层
if (baseLayers.value[selectedBaseLayer.value]) {
baseLayers.value[selectedBaseLayer.value].addTo(mapInstance.value)
}
}
// 显示弹窗
const showPopup = (info) => {
popupInfo.show = true
popupInfo.title = info.title
popupInfo.data = info.data || info.content
popupInfo.position = mapInstance.value?.latLngToContainerPoint(info.position)
}
const closePopup = () => {
popupInfo.show = false
popupInfo.data = null
popupInfo.position = null
}
// 显示右键菜单
const showContextMenu = (e) => {
// 实现右键菜单逻辑
console.log('右键点击位置:', e.latlng)
}
// 生命周期
onMounted(() => {
nextTick(() => {
initMap()
})
})
onUnmounted(() => {
if (mapInstance.value) {
mapInstance.value.remove()
mapInstance.value = null
}
})
// 监听props变化
watch(() => props.markers, (newMarkers) => {
addMarkers(newMarkers)
}, { deep: true })
watch(() => props.geoJsonData, (newData) => {
if (geoJsonLayers.value['main']) {
mapInstance.value.removeLayer(geoJsonLayers.value['main'])
}
addGeoJsonLayer(newData)
}, { deep: true })
watch(() => props.center, (newCenter) => {
if (mapInstance.value && newCenter) {
mapInstance.value.setView(newCenter, mapInstance.value.getZoom())
}
})
watch(() => props.zoom, (newZoom) => {
if (mapInstance.value && newZoom) {
mapInstance.value.setZoom(newZoom)
}
})
// 暴露给父组件的方法
defineExpose({
getMapInstance: () => mapInstance.value,
addMarker: (marker) => {
const newMarkers = [...props.markers, marker]
addMarkers(newMarkers)
},
removeMarker: (id) => {
const marker = markerLayers.value[id]
if (marker) {
mapInstance.value.removeLayer(marker)
delete markerLayers.value[id]
}
},
fitBounds: (bounds) => {
if (mapInstance.value && bounds) {
mapInstance.value.fitBounds(bounds)
}
},
setView: (center, zoom) => {
if (mapInstance.value) {
mapInstance.value.setView(center, zoom)
}
},
getCenter: () => mapInstance.value?.getCenter(),
getZoom: () => mapInstance.value?.getZoom(),
addLayer: (layer) => {
if (mapInstance.value) {
layer.addTo(mapInstance.value)
}
},
removeLayer: (layer) => {
if (mapInstance.value) {
mapInstance.value.removeLayer(layer)
}
}
})
</script>
<style scoped>
.leaflet-map-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.leaflet-map {
width: 100%;
height: 100%;
z-index: 1;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group {
display: flex;
gap: 4px;
}
.control-group button {
width: 36px;
height: 36px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.control-group button:hover {
background: #f5f5f5;
border-color: #40a9ff;
}
.control-group button.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.layer-control {
border-top: 1px solid #f0f0f0;
padding-top: 8px;
margin-top: 4px;
}
.layer-control h4 {
margin: 0 0 8px 0;
font-size: 12px;
color: #666;
}
.layer-item {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.layer-item label {
font-size: 12px;
cursor: pointer;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2000;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.custom-popup {
position: absolute;
background: white;
border-radius: 4px;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
z-index: 1001;
min-width: 200px;
max-width: 300px;
transform: translate(-50%, -100%);
margin-top: -10px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
}
.popup-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.popup-close {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
line-height: 1;
color: #666;
}
.popup-close:hover {
color: #333;
}
.popup-content {
padding: 12px;
}
</style>
然后创建一个使用示例组件:
<template>
<div class="demo-container">
<h1>Vue3 + Leaflet 地图组件演示</h1>
<div class="map-wrapper">
<LeafletMap
ref="mapRef"
:center="center"
:zoom="zoom"
:markers="markers"
:enable-draw="enableDraw"
@map-loaded="onMapLoaded"
@marker-click="onMarkerClick"
@draw-created="onDrawCreated"
>
<template #popup-content="{ data }">
<div class="custom-popup-content">
<h4>{{ data.title }}</h4>
<p>{{ data.description }}</p>
<button @click="handlePopupAction(data)">查看详情</button>
</div>
</template>
</LeafletMap>
</div>
<div class="control-panel">
<div class="control-group">
<label>中心坐标:</label>
<input v-model="centerInput" placeholder="纬度,经度" />
<button @click="updateCenter">更新中心</button>
</div>
<div class="control-group">
<label>缩放级别:</label>
<input type="range" v-model="zoom" min="3" max="18" />
<span>{{ zoom }}</span>
</div>
<div class="control-group">
<button @click="addRandomMarker">添加随机标记</button>
<button @click="clearMarkers">清除标记</button>
<button @click="toggleDrawTool">{{ enableDraw ? '关闭' : '开启' }}绘制工具</button>
</div>
<div class="data-display">
<h3>地图信息</h3>
<p>当前中心: {{ currentCenter }}</p>
<p>当前缩放: {{ currentZoom }}</p>
<p>标记数量: {{ markers.length }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import LeafletMap from './LeafletMap.vue'
const mapRef = ref(null)
const center = ref([31.2304, 121.4737])
const zoom = ref(13)
const enableDraw = ref(false)
// 中心坐标输入
const centerInput = ref('31.2304,121.4737')
const markers = ref([
{
id: '1',
lat: 31.2304,
lng: 121.4737,
title: '上海',
popupContent: {
title: '上海市',
description: '中国直辖市,经济中心'
}
},
{
id: '2',
lat: 31.2152,
lng: 121.4131,
title: '徐家汇',
popupContent: {
title: '徐家汇商圈',
description: '上海重要商业中心'
}
}
])
// 计算属性
const currentCenter = computed(() => {
const map = mapRef.value?.getMapInstance?.()
if (map) {
const center = map.getCenter()
return `${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}`
}
return '-'
})
const currentZoom = computed(() => {
return mapRef.value?.getZoom?.() || '-'
})
// 事件处理
const onMapLoaded = (mapInstance) => {
console.log('地图加载完成', mapInstance)
}
const onMarkerClick = (data) => {
console.log('标记点点击:', data)
}
const onDrawCreated = (data) => {
console.log('绘制完成:', data)
}
const updateCenter = () => {
const [lat, lng] = centerInput.value.split(',').map(Number)
if (!isNaN(lat) && !isNaN(lng)) {
center.value = [lat, lng]
}
}
const addRandomMarker = () => {
const lat = 31.2 + Math.random() * 0.1
const lng = 121.4 + Math.random() * 0.1
markers.value.push({
id: `marker_${Date.now()}`,
lat,
lng,
title: `随机点${markers.value.length + 1}`,
popupContent: {
title: '随机添加的标记',
description: `坐标: ${lat.toFixed(4)}, ${lng.toFixed(4)}`
}
})
}
const clearMarkers = () => {
markers.value = []
}
const toggleDrawTool = () => {
enableDraw.value = !enableDraw.value
}
const handlePopupAction = (data) => {
alert(`处理弹窗数据: ${JSON.stringify(data)}`)
}
</script>
<style scoped>
.demo-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.map-wrapper {
flex: 1;
min-height: 0;
position: relative;
}
.control-panel {
background: #f5f5f5;
padding: 20px;
border-top: 1px solid #ddd;
}
.control-group {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 15px;
}
.control-group label {
font-weight: bold;
min-width: 80px;
}
.control-group input[type="range"] {
width: 200px;
}
.control-group button {
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.control-group button:hover {
background: #40a9ff;
}
.data-display {
background: white;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.custom-popup-content {
padding: 10px;
}
.custom-popup-content button {
margin-top: 10px;
padding: 5px 10px;
background: #1890ff;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
</style>
最后,创建一个包装组件以简化使用:
<!-- LeafletMap.vue -->
<template>
<div :class="['leaflet-map-wrapper', fullscreenClass]" :style="wrapperStyle">
<div ref="mapContainer" class="leaflet-map"></div>
<!-- 可以添加一些自定义控件 -->
<slot></slot>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
// 修复Leaflet图标问题
import icon from 'leaflet/dist/images/marker-icon.png'
import iconShadow from 'leaflet/dist/images/marker-shadow.png'
let DefaultIcon = L.Icon.Default
DefaultIcon.prototype.options.iconUrl = icon
DefaultIcon.prototype.options.shadowUrl = iconShadow
const props = defineProps({
center: {
type: Array,
default: () => [31.2304, 121.4737]
},
zoom: {
type: Number,
default: 13
},
options: {
type: Object,
default: () => ({})
},
layers: {
type: Array,
default: () => []
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
},
fullscreen: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['ready', 'click', 'moveend', 'zoomend'])
const mapContainer = ref(null)
const map = ref(null)
const wrapperStyle = computed(() => ({
width: props.width,
height: props.height
}))
const fullscreenClass = computed(() =>
props.fullscreen ? 'fullscreen' : ''
)
const initMap = () => {
if (!mapContainer.value) return
map.value = L.map(mapContainer.value, {
center: props.center,
zoom: props.zoom,
...props.options
})
// 添加默认瓦片图层
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map.value)
// 添加自定义图层
props.layers.forEach(layer => {
layer.addTo(map.value)
})
// 绑定事件
map.value.on('click', (e) => emit('click', e))
map.value.on('moveend', () => emit('moveend', map.value.getCenter()))
map.value.on('zoomend', () => emit('zoomend', map.value.getZoom()))
emit('ready', map.value)
}
// 暴露给父组件的方法
const getMap = () => map.value
const addLayer = (layer) => {
if (map.value) {
layer.addTo(map.value)
}
}
const removeLayer = (layer) => {
if (map.value) {
map.value.removeLayer(layer)
}
}
const setView = (center, zoom) => {
if (map.value) {
map.value.setView(center, zoom)
}
}
const flyTo = (center, zoom) => {
if (map.value) {
map.value.flyTo(center, zoom)
}
}
defineExpose({
getMap,
addLayer,
removeLayer,
setView,
flyTo
})
// 生命周期
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (map.value) {
map.value.remove()
}
})
// 监听props变化
watch(() => props.center, (newCenter) => {
if (map.value) {
map.value.setView(newCenter, map.value.getZoom())
}
})
watch(() => props.zoom, (newZoom) => {
if (map.value) {
map.value.setZoom(newZoom)
}
})
</script>
<style scoped>
.leaflet-map-wrapper {
position: relative;
}
.leaflet-map {
width: 100%;
height: 100%;
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
}
</style>
安装依赖
npm install leaflet @types/leaflet leaflet-draw
主要功能说明
基础地图功能
标记点管理
绘制工具
- 点、线、多边形绘制
- 编辑和删除图形
- GeoJSON支持
图层控制
交互功能
性能优化
使用示例
// 在父组件中使用
import LeafletMap from '@/components/LeafletMap.vue'
// 添加标记点
const markers = [
{
lat: 31.2304,
lng: 121.4737,
title: '上海',
icon: '/custom-marker.png',
popupContent: '这里是上海'
}
]
// 监听地图事件
const onMapClick = (e) => {
console.log('地图点击位置:', e.latlng)
}
// 使用地图实例方法
const mapRef = ref()
const zoomToLocation = () => {
mapRef.value?.setView([39.9042, 116.4074], 15) // 北京
}
这个组件提供了完整的地图可视化功能,可以根据具体需求进一步扩展和定制。