欢迎光临殡葬白事网
详情描述

<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) // 北京
}

这个组件提供了完整的地图可视化功能,可以根据具体需求进一步扩展和定制。