npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gis-maplibre

v1.0.6

Published

基于 MapLibre GL 的 Vue3 GIS 地图组件库

Downloads

367

Readme

GisMapLibre

修复若干问题

基于 Vue 3 + MapLibre GL + Naive UI 的企业级 GIS 地图组件库

组件概述

GisMapLibre 是一个功能丰富的地理信息系统(GIS)地图组件,专为 Vue 3 应用设计。该组件封装了 MapLibre GL 的强大地图渲染能力,结合 Naive UI 的现代化界面组件,提供了开箱即用的地图交互、图层管理、测量工具、搜索过滤等核心功能。

组件采用灵活的布局设计,支持左右分割和上下分割,通过插槽系统允许开发者自定义各个区域的内容。内置完整的状态管理和事件系统,便于与业务逻辑深度集成。

主要功能

  • 🗺️ 多图层支持:支持 GeoJSON、Vector Tiles、Raster、Raster-DEM、Image 等多种数据源
  • 🎨 丰富的地图控件:导航、全屏、比例尺、搜索、图层管理、图例、坐标拾取、图层导入等
  • 📐 测量工具:距离测量、面积测量、坐标拾取,支持拖拽编辑和删除管理
  • 🔍 智能搜索过滤:支持多字段条件过滤、年度/层位筛选、SQL 参数化查询、动态过滤条件
  • 📊 图层管理:树形图层结构、显隐控制、样式调整、顺序重排、动态导入/删除、图层定位
  • 🎯 交互事件:图层要素点击、地图事件、工具栏事件、控件点击、测量完成等完整事件体系
  • 🖼️ 灵活布局:支持左右分割(左侧面板)、上下分割(底部面板),可动态控制显示隐藏
  • 📦 TypeScript 支持:完整的类型定义,提供优秀的开发体验
  • 🌐 中文本地化:内置 Naive UI 中文语言包
  • 🔧 动态图层:支持运行时动态添加/删除图层,响应式更新
  • 💡 Popup 弹窗:支持图层要素点击弹窗,可自定义 HTML 内容

快速开始

环境要求

  • Node.js >= 18.0.0
  • Vue >= 3.4.0
  • Pinia >= 2.1.0
  • pnpm / npm / yarn

安装

# 使用 pnpm(推荐)
pnpm install gis-maplibre

# 使用 npm
npm install gis-maplibre

# 使用 yarn
yarn add gis-maplibre

基本使用

1. 注册组件

main.ts 中全局注册:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import GisMapLibre from 'gis-maplibre'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(GisMapLibre)

app.mount('#app')

或在组件中按需引入:

import { GisMapLibre } from 'gis-maplibre'
import 'gis-maplibre/dist/style.css'

2. 在组件中使用

<template>
  <div class="map-container">
    <GisMapLibre
      ref="mapRef"
      :configState="configState"
      :map-config="mapConfig"
      :base-layers="baseLayers"
      :overlay-layers="overlayLayers"
      :toolbar-data="toolbarData"
      :split-config="splitConfig"
      :addControlConfig="addControlConfig"
      @init-complete="handleMapInit"
      @toolbar-event="handleToolbarEvent"
      @map-event="handleMapEvent"
      @layer-feature-click="handleLayerClick"
    >
      <template #gisFooter>
        <!-- 自定义底部内容 -->
        <div v-if="showFooterPanel">自定义面板</div>
      </template>
    </GisMapLibre>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'

const mapRef = ref()
const showFooterPanel = ref(false)

// 组件状态配置
const configState = reactive({
  addLayerIsDel: false
})

// 地图配置
const mapConfig = {
  style: {
    glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf'
  }
}

// 底图配置
const baseLayers = [
  {
    id: 'satellite',
    name: '卫星影像',
    type: 'raster',
    url: 'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
    visible: true,
    opacity: 1,
    thumbColor: '#2d5016'
  }
]

// 叠加图层配置(响应式)
const overlayLayers = ref([
  {
    id: 'data-layer',
    name: '数据图层',
    type: 'geojson',
    shape: 'circle',
    visible: true,
    isClick: true,
    autoFly: true,
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          properties: { name: '测试点', year: '2024' },
          geometry: {
            type: 'Point',
            coordinates: [116.39748, 39.90882]
          }
        }
      ]
    },
    layers: [
      {
        type: 'circle',
        layerType: 'circle',
        layout: {},
        paint: {
          'circle-radius': 8,
          'circle-color': '#ff4d4f',
          'circle-opacity': 0.8
        }
      }
    ]
  }
])

// 工具栏配置
const toolbarData = [
  { key: 'translation', name: '平移', type: 'translation', isShow: true },
  { key: 'measure', name: '测距', type: 'measure', isShow: true },
  { key: 'area', name: '测面积', type: 'area', isShow: true },
  { key: 'coordinate', name: '坐标拾取', type: 'coordinate', isShow: true }
]

// 布局分割配置(响应式)
const splitConfig = reactive({
  split1: {
    defaultSize: 0,
    disabled: true,
    size: undefined,
    watchProps: ['defaultSize'],
    min: 0,
    max: 1
  },
  split2: {
    defaultSize: 1,
    disabled: true,
    size: undefined,
    watchProps: ['defaultSize'],
    min: 0,
    max: 1
  }
})

// 自定义控件配置
const addControlConfig = [
  { key: 'NavigationControl', icon: '', position: 'top-left' },
  { key: 'FullscreenControl', icon: '', position: 'top-left' },
  { key: 'ScaleControl', icon: '', position: 'bottom-left' },
  { key: 'SearchControl', icon: 'SearchOutline', position: 'top-right', type: 'icon' },
  { key: 'ToolbarControl', icon: 'GridOutline', position: 'top-right', title: '', type: 'icon' },
  { key: 'LayerControl', icon: 'LayersOutline', position: 'top-right', type: 'icon' },
  { key: 'MapControl', icon: 'MapOutline', position: 'top-right', type: 'icon' },
  { key: 'LegendControl', icon: 'OptionsOutline', position: 'top-right', type: 'icon' },
  { key: 'CoordinatePickControl', icon: 'LocationOutline', position: 'top-right', type: 'icon' },
  { key: 'ImportControl', icon: 'CloudUploadOutline', position: 'top-right', type: 'icon' }
]

// 地图初始化完成
const handleMapInit = (map: any) => {
  console.log('地图加载完成', map)
}

// 工具栏事件
const handleToolbarEvent = (event: any) => {
  console.log('工具栏事件:', event)
}

// 地图事件
const handleMapEvent = (event: any) => {
  console.log('地图事件:', event)
}

// 图层要素点击
const handleLayerClick = (dataInfo: any) => {
  console.log('图层要素点击:', dataInfo)
}
</script>

属性(Props)说明

| 属性名 | 类型 | 默认值 | 必填 | 说明 | |--------|------|--------|------|------| | mapConfig | Partial<MapOptions> | {} | 否 | MapLibre GL 地图初始化配置,会与默认配置合并。可配置 style.glyphs 字体路径等 | | configState | object | 见下方 | 否 | 组件状态配置对象 | | baseLayers | BaseLayersConfig[] | [] | 否 | 底图图层配置数组,用于底图切换 | | overlayLayers | LayerConfig[] | [] | 否 | 叠加图层配置数组(支持树形结构,建议使用 ref/reactive 以支持动态更新) | | toolbarData | any[] | [] | 否 | 工具栏按钮配置数组 | | splitConfig | SplitConfig | 见下方 | 否 | 布局分割配置(建议使用 reactive 以支持动态控制) | | addControlConfig | any[] \| null | null | 否 | 自定义地图控件配置数组 |

configState 配置

{
  showToolbar: false,        // 是否显示工具栏
  showLegend: false,         // 是否显示图例
  showLayerManager: false,   // 是否显示图层管理器
  showSearchManager: false,  // 是否显示搜索管理器
  addLayerIsDel: false       // 修复 图层 信息 overlayLayers 是否要删除 原来得图层
}

splitConfig 布局分割配置

组件使用 n-split 实现灵活的布局分割:

  • split1: 左右分割(左侧面板区域)
  • split2: 上下分割(底部面板区域)
{
  split1: {
    defaultSize: 0,              // 默认大小(0-1 比例或像素值如 '200px')
    disabled: true,              // 是否禁用分割(隐藏面板)
    size: undefined,             // 受控大小
    watchProps: ['defaultSize'], // 监听属性
    min: 0,                      // 最小值
    max: 1                       // 最大值
  },
  split2: {
    defaultSize: 1,              // 默认大小
    disabled: true,              // 是否禁用分割
    size: undefined,
    watchProps: ['defaultSize'],
    min: 0,
    max: 1
  }
}

LayerConfig 图层配置

interface LayerConfig {
  id: string                    // 图层唯一标识
  name: string                  // 图层名称
  type: 'geojson' | 'vector' | 'raster' | 'raster-dem' | 'image'  // 图层类型
  url?: string                  // 数据源 URL(raster/vector 类型)
  tiles?: any[]                 // 瓦片地址(raster/vector 类型)
  data?: GeoJSON        // GeoJSON 数据(geojson 类型)
  imageUrl?: string             // 图片 URL(image 类型)
  imageCoordinates?: [[number, number], [number, number], [number, number], [number, number]]  // 图片坐标范围 [左上, 右上, 右下, 左下]
  visible: boolean              // 是否可见
  isClick?: boolean             // 是否可点击交互(触发 layer-feature-click 事件)
  autoFly?: boolean             // 加载后是否自动飞行到图层范围
  shape?: 'circle' | 'square' | 'triangle' | 'diamond' | 'line' | 'polygon' | 'raster'  // 图层形状(用于图例显示)
  deletable?: boolean           // 是否可删除(默认 true)
  children?: LayerConfig[]      // 子图层(树形结构)
  zIndex?: number               // Z 轴顺序(数值越大越靠上)
  defaultFilter?: any[]         // 初始过滤条件 [{ field, comparator, value, isDisabledDelete }]
  filterFields?: Array<{        // 搜索过滤字段配置
    label: string
    value: string
    type: 'input' | 'select' | 'number'
    options?: any[],
    isDisabledDelete?: boolean  // 是否可以删除
    isParameter: boolean        // 是否是地址参数
    parameterType?: string      // 参数类型:sql 或 params
  }>
  source?: object               // 数据源配置(遵循 MapLibre GL Source 规范)
  layers?: object | object[]    // 图层样式配置(遵循 MapLibre GL Layer 规范)
  icon?: string                 // 图层图标路径
  iconName?: string             // 图层图标名称(Naive UI 图标)
  iconColor?: string            // 图层图标颜色
  [key: string]: any            // 允许其他自定义属性
}

BaseLayersConfig 底图配置

interface BaseLayersConfig {
  id: string           // 底图唯一标识
  name: string         // 底图名称
  type: string         // 底图类型(如 'raster')
  url: string          // 瓦片地址模板
  visible: boolean     // 是否可见
  opacity: number      // 透明度(0-1)
  thumbColor: string   // 缩略图颜色(用于底图切换 UI)
}

事件(Events)说明

| 事件名 | 参数 | 说明 | |--------|------|------| | init-complete | (map: maplibregl.Map) | 地图初始化完成事件。可在此获取地图实例,添加自定义控件 | | toolbar-event | (event: any) | 工具栏按钮点击/操作事件。包含 keynameactionvalue 等属性 | | map-event | (event: any) | 地图通用事件(缩放、平移、点击等) | | layer-feature-click | (dataInfo: any) | 图层要素点击事件(需设置图层 isClick: true)。返回要素属性、几何信息和事件对象 | | control-click | (event: any) | 自定义控件点击事件。包含控件 key 等信息 | | layer-delete | (layerId: string) | 图层删除事件 | | overlay-layers-reorder | (layers: LayerConfig[]) | 叠加图层重排事件,返回新的图层顺序数组 | | deleted-event | (event: any) | 测量删除事件 | | dragged-event | (event: any) | 测量拖拽事件 |

事件使用示例

// 地图初始化完成
const handleMapInit = (map: any) => {
  console.log('地图加载完成', map)
  // 可在此添加自定义控件
  map.addControl(new CustomControl(), 'top-left')
}

// 工具栏事件
const handleToolbarEvent = (event: any) => {
  console.log('工具栏事件:', event)
  if (event?.key === 'statisticalAnalysis') {
    // 打开统计分析面板
    state.showReserveDialog = true
  }
}

// 图层要素点击
const handleLayerClick = (dataInfo: any) => {
  console.log('图层要素点击:', dataInfo)
  // dataInfo 结构: { [layerId]: { feature, properties, event } }
}

// 控件点击事件
const handleControlClick = (event: any) => {
  console.log('控件点击:', event)
  if (event.key === 'AutomaticSearchControl') {
    // 处理自定义控件逻辑
  }
}

插槽(Slots)说明

组件提供三个命名插槽,用于自定义布局区域:

| 插槽名 | 说明 | 使用场景 | |--------|------|----------| | gitLeft | 左侧面板区域 | 可放置图层列表、数据面板、分析工具等 | | gisHeader | 顶部区域 | 可放置标题栏、搜索框、快捷操作等 | | gisFooter | 底部面板区域 | 可放置分析结果、详情面板、图表等 |

插槽使用示例

<GisMapLibre
  ref="mapComponentRef"
  :configState="configState"
  :split-config="splitConfig"
  @init-complete="handleMapInit"
>
  <!-- 左侧面板 -->
  <template #gitLeft>
    <div class="left-panel">
      <h3>数据列表</h3>
      <!-- 自定义内容 -->
    </div>
  </template>

  <!-- 顶部区域 -->
  <template #gisHeader>
    <div class="header-bar">
      <h2>GIS 地图系统</h2>
    </div>
  </template>

  <!-- 底部面板 -->
  <template #gisFooter>
    <div v-if="showBottomPanel" class="bottom-panel">
      <my-analysis-component @close="showBottomPanel = false" />
    </div>
  </template>
</GisMapLibre>

注意:插槽区域的显示需要通过 splitConfig 控制:

  • split1.disabled = false 时显示左侧面板(gitLeft 插槽)
  • split2.disabled = false 时显示底部面板(gisFooter 插槽)

暴露的方法与属性

通过 ref 获取组件实例后,可以访问以下方法和属性:

核心实例属性

| 属性名 | 类型 | 说明 | |--------|------|------| | map | maplibregl.Map | MapLibre GL 地图实例,可直接调用地图 API | | maplibregl | typeof maplibregl | MapLibre GL 命名空间,用于创建 Popup、Marker 等 | | turf | typeof turf | Turf.js 工具库,用于空间分析(计算中心点、面积、距离等) | | importedLayers | Ref<LayerConfig[]> | 导入的图层列表(响应式),包含通过导入控件添加的图层 | | unifiedLayers | Ref<LayerConfig[]> | 统一图层列表(响应式),包含 overlayLayers 和 importedLayers 的合并结果 | | measureMode | Ref<'distance' \| 'area' \| 'coordinate' \| null> | 当前测量模式,可用于判断用户正在使用哪种测量工具 | | measureResult | Ref<any> | 测量结果数据,包含距离、面积等测量信息 |

图层管理方法

| 方法名 | 参数 | 返回值 | 说明 | |--------|------|--------|------| | toggleLayerManager | 无 | void | 切换图层管理器面板显示/隐藏 | | toggleLayer | (layerId: string, visible: boolean) | void | 切换指定图层的可见性 | | setLayersVisible | (layerIds: string \| string[]) | void | 设置单个或多个图层的可见性(同步更新图层管理器勾选状态) | | allLayersVisibility | (layerState: boolean) | void | 全部图层显隐控制:true 全选,false 全不选 | | layerChecked | (checkedKeys: string[]) | void | 批量设置图层勾选状态 | | layerStyleChange | (layerId: string, style: LayerStyle) | void | 修改图层样式(颜色、透明度、边框等) | | layerDelete | (layerId: string) | void | 删除指定图层 | | layerReorder | (layers: LayerConfig[], layerType: 'overlay' \| 'imported', checkedKeys?: string[]) | void | 图层重排(overlay 叠加图层或 imported 导入图层) | | layerLocate | (layer: LayerConfig) | void | 定位到指定图层范围(自动飞行到图层边界) | | UnifiedLayersReorder | (unifiedLayersList: LayerConfig[], checkedKeys?: string[]) | void | 统一图层重排(叠加图层和导入图层的统一排序) |

过滤与搜索方法

| 方法名 | 参数 | 返回值 | 说明 | |-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------|--------|--------------------------------------| | searchLayerFilter | (layerId: string, filter: any, urlParams: any, layer?: object, config?: object) | void | 对指定图层应用过滤条件(支持 GeoJSON 和 Vector 数据源) | | layersFilterSet | (layers: Array<{layerId: string, filters: any}>) | void | 批量设置多个图层的过滤条件 | | searchMapliberParameter | (filterData: Array<{field: string ,comparator: string, value: any}>) | void | 搜索 MapLibre 参数(内部方法) | | searchUrlParamsOrSql | (filterData: Array<{field: string ,comparator: string, value: any, parameterType: string[params 、 sql], isParameter: boolean}>) | void | 搜索 URL 参数或 SQL 查询(内部方法) | | setLayerConditionsStore | (layerId: string, customConditions: any) | void | 设置图层的过滤条件存储(持久化) | | getLayerConditionsStore | (layerId: string) | any | 获取图层的过滤条件存储 |

动画与特效方法

| 方法名 | 参数 | 返回值 | 说明 | |--------|------|--------|------| | flyToLayer | (data: GeoJSON.FeatureCollection \| GeoJSON.Feature) | void | 飞行到指定 GeoJSON 数据范围(自动计算边界) | | createCustomBlinkLayer | (sourceId: string, config: CustomBlinkLayerType) | { stop: () => void } | 创建自定义闪烁图层,返回控制对象可调用 stop() 停止动画 | | startBatchBlink | (targets: Array<{layerId: string, featureIds: any[], attrKey: string}>, options?: BlinkOptions, features?: any) | void | 多图层批量闪烁动画(支持要素 ID 精准闪烁) |

控件方法

| 方法名 | 参数 | 返回值 | 说明 | |--------|------|--------|------| | addControl | (control: maplibregl.IControl, position?: string) | void | 向地图添加自定义控件 |

类型定义参考

// 图层样式配置
interface LayerStyle {
  color?: string | Array            // 填充颜色
  borderColor?: string | Array        // 边框颜色
  borderOpacity?: number  | Array     // 边框透明度
  borderWidth?: number        // 边框宽度
  fillOutlineColor?: string | Array   // 填充边框颜色
  layerOpacity?: number  | Array      // 图层透明度
  rasterSaturation?: number   // 栅格饱和度
  rasterContrast?: number     // 栅格对比度
  titleColor?: string   | Array       // 标题颜色
  titleSize?: number          // 标题大小
}

// 自定义闪烁图层配置
interface CustomBlinkLayerType {
  source: maplibregl.GeoJSONSourceSpecification  // 数据源配置
  layer: maplibregl.LayerSpecification           // 图层配置
  blink: {
    layerKey: 'paint' | 'layout'                 // 属性类型
    key: string                                  // 属性键名
    frequency?: number                           // 闪烁频率(Hz)
    minValue: number | string                    // 最小值
    maxValue: number | string                    // 最大值
    often?: number                               // 持续时间(毫秒)
  }
}

// 批量闪烁选项
interface BlinkOptions {
  duration?: number      // 单次闪烁时长(毫秒)
  blinkTimes?: number    // 闪烁次数(Infinity 为无限)
  flyTo?: boolean        // 是否自动飞行到要素范围
}

使用示例

const mapComponentRef = ref()

// 1. 访问核心实例
const map = mapComponentRef.value.map
const maplibregl = mapComponentRef.value.maplibregl
const turf = mapComponentRef.value.turf

// 2. 图层管理
mapComponentRef.value.toggleLayerManager()  // 打开图层管理器
mapComponentRef.value.toggleLayer('layer-1', false)  // 隐藏图层
mapComponentRef.value.allLayersVisibility(false)  // 隐藏所有图层

// 3. 图层定位
const layerConfig = { id: 'layer-1', type: 'geojson', data: geojsonData }
mapComponentRef.value.layerLocate(layerConfig)  // 飞行到图层范围

// 4. 图层过滤
mapComponentRef.value.searchLayerFilter(
  'layer-1',
  ['==', ['get', 'year'], '2024'],
  { params: '', sql: '' }
)

// 5. 批量过滤
mapComponentRef.value.layersFilterSet([
  {
    "layerId": "id",
    "filters": [
      {
        "field": "app_year",
        "comparator": "==",
        "value": 2026,
        "isDisabledDelete": true,
        "isParameter": true,
        "parameterType": "params"
      }
    ]
  }
])

// 6. 创建闪烁图层
const blinkControl = mapComponentRef.value.createCustomBlinkLayer('blink-layer', {
  source: { type: 'geojson', data: geojsonData },
  layer: { type: 'circle', paint: { 'circle-radius': 8, 'circle-color': '#ff0000' } },
  blink: {
    layerKey: 'paint',
    key: 'circle-opacity',
    frequency: 2,
    minValue: 0.2,
    maxValue: 1,
    often: 5000
  }
})
// 5 秒后停止闪烁
setTimeout(() => blinkControl.stop(), 5000)

// 7. 批量闪烁(按要素 ID)
mapComponentRef.value.startBatchBlink(
  [
    { layerId: 'layer-1', featureIds: [1, 2, 3], attrKey: 'id' },
    { layerId: 'layer-2', featureIds: [10, 20], attrKey: 'id' }
  ],
  { duration: 500, blinkTimes: 10 }
)

// 8. 添加自定义控件
class MyCustomControl {
  onAdd(map) { /* ... */ }
  onRemove() { /* ... */ }
}
mapComponentRef.value.addControl(new MyCustomControl(), 'top-right')

// 9. 使用 Turf.js 进行空间分析
const centroid = turf.centroid(geojsonFeature)
const area = turf.area(geojsonPolygon)
const distance = turf.distance(point1, point2, { units: 'kilometers' })

// 10. 使用 maplibregl 创建 Popup
const popup = new maplibregl.Popup({ closeButton: true })
  .setLngLat([116.397, 39.908])
  .setHTML('<h3>北京</h3>')
  .addTo(map)

使用方法示例

基础使用

<template>
  <div class="map-view">
    <GisMapLibre
      ref="mapComponentRef"
      :configState="configState"
      :show-toolbar="false"
      :map-config="mapConfig"
      :base-layers="baseLayers"
      :overlay-layers="overlayLayers"
      :toolbar-data="toolbarData"
      :split-config="splitConfig"
      :add-control-config="addControlConfig"
      @toolbar-event="toolbarEvent"
      @map-event="handleMapEvent"
      @init-complete="mapLoading"
      @control-click="controlClick"
      @layer-feature-click="layerFeatureClick"
    >
      <template #gisFooter>
        <!-- 自定义底部面板 -->
        <my-custom-panel v-if="showPanel" @close="showPanel = false" />
      </template>
    </GisMapLibre>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'

const mapComponentRef = ref()
const showPanel = ref(false)

// 组件状态配置
const configState = reactive({
  addLayerIsDel: false  // 动态添加的图层是否可删除
})

// 地图配置
const mapConfig = {
  style: {
    glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf'
  }
}

// 底图配置
const baseLayers = [
  {
    id: 'satellite',
    name: '卫星影像',
    type: 'raster',
    url: 'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
    visible: true,
    opacity: 1,
    thumbColor: '#2d5016'
  },
  {
    id: 'streets',
    name: '街道地图',
    type: 'raster',
    url: 'http://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
    visible: false,
    opacity: 1,
    thumbColor: '#e8e8e8'
  }
]

// 叠加图层配置(使用 ref 支持动态更新)
const overlayLayers = ref([
  {
    id: 'mineral-exploration',
    name: '测试',
    type: 'geojson',
    shape: 'polygon',
    visible: true,
    isClick: true,        // 允许点击交互
    deletable: true,      // 允许删除
    autoFly: true,        // 加载后自动飞行到图层范围
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          properties: { 
            name: '1111', 
            year: '2024',
            type: 'dddd'
          },
          geometry: {
            type: 'Polygon',
            coordinates: [[[116.0, 39.0], [117.0, 39.0], [117.0, 40.0], [116.0, 40.0], [116.0, 39.0]]]
          }
        }
      ]
    },
    layers: [
      {
        type: 'line',
        layerType: 'outline',
        layout: {},
        paint: {
          'line-color': '#000000',
          'line-width': 2,
          'line-opacity': 1
        }
      },
      {
        type: 'fill',
        layerType: 'fill',
        layout: {},
        paint: {
          'fill-color': '#0a76f1',
          'fill-opacity': 0.8,
          'fill-outline-color': '#0a76f1'
        }
      }
    ],
    // 初始过滤条件
    defaultFilter: [
      { field: 'year', comparator: '==', value: '2024', isDisabledDelete: true }
    ],
    // 搜索过滤字段配置
    filterFields: [
      { 
        label: '年度', 
        value: 'year', 
        type: 'select',
        options: ['2024', '2023', '2022'],
        isParameter: false 
      },
      { 
        label: '类型', 
        value: 'type', 
        type: 'input',
        options: [],
        isParameter: false 
      }
    ]
  }
])

// 工具栏配置
const toolbarData = [
  { key: 'translation', name: '平移', type: 'translation', isShow: true },
  { key: 'measure', name: '测距', type: 'measure', isShow: true },
  { key: 'area', name: '测面积', type: 'area', isShow: true },
  { key: 'coordinate', name: '坐标拾取', type: 'coordinate', isShow: true },
  { 
    key: 'statisticalAnalysis', 
    name: '统计分析', 
    type: 'button', 
    isShow: true,
    icon: 'BarChartOutline'
  }
]

// 布局分割配置(使用 reactive 支持动态控制)
const splitConfig = reactive({
  split1: {
    defaultSize: 0,
    disabled: true,
    size: undefined,
    watchProps: ['defaultSize'],
    min: 0,
    max: 1
  },
  split2: {
    defaultSize: 1,
    disabled: true,
    size: undefined,
    watchProps: ['defaultSize'],
    min: 0,
    max: 1
  }
})

// 自定义控件配置
const addControlConfig = [
  { key: 'NavigationControl', icon: '', position: 'top-left' },
  { key: 'FullscreenControl', icon: '', position: 'top-left' },
  { key: 'ScaleControl', icon: '', position: 'bottom-left' },
  { key: 'SearchControl', icon: 'SearchOutline', position: 'top-right', type: 'icon' },
  { key: 'ToolbarControl', icon: 'GridOutline', position: 'top-right', title: '', type: 'icon' },
  { key: 'LayerControl', icon: 'LayersOutline', position: 'top-right', type: 'icon' },
  { key: 'MapControl', icon: 'MapOutline', position: 'top-right', type: 'icon' },
  { key: 'LegendControl', icon: 'OptionsOutline', position: 'top-right', type: 'icon' },
  { key: 'CoordinatePickControl', icon: 'LocationOutline', position: 'top-right', type: 'icon' },
  { key: 'ImportControl', icon: 'CloudUploadOutline', position: 'top-right', type: 'icon' }
]

// 地图初始化完成
const mapLoading = (map: any) => {
  console.log('地图加载完成')
  // 可在此添加自定义控件
}

// 工具栏事件处理
const toolbarEvent = (result: any) => {
  console.log('工具栏事件:', result)
  if (result?.key === 'statisticalAnalysis') {
    showPanel.value = true
    // 打开底部面板
    splitConfig.split2.defaultSize = 0.5
    splitConfig.split2.disabled = false
  }
}

// 地图事件处理
const handleMapEvent = (event: any) => {
  console.log('地图事件:', event)
}

// 控件点击事件
const controlClick = (event: any) => {
  console.log('点击控件:', event)
}

// 图层要素点击事件
let currentPopup: any = null

const layerFeatureClick = (dataInfo: any) => {
  console.log('图层要素点击:', dataInfo)
  
  const attrKeys = Object.keys(dataInfo)
  const data = dataInfo[attrKeys[0]]
  const geometry = data.feature.geometry
  
  // 根据几何类型获取坐标
  let coordinates: [number, number]
  if (geometry.type === 'Point') {
    coordinates = geometry.coordinates
  } else if (geometry.type === 'LineString' && geometry.coordinates.length > 0) {
    coordinates = geometry.coordinates[0]
  } else if (geometry.type === 'Polygon' && geometry.coordinates[0].length > 0) {
    // 使用 Turf.js 计算多边形中心点
    const centroid = mapComponentRef.value.turf.centroid({
      type: 'Feature',
      geometry: { ...geometry }
    })
    coordinates = centroid.geometry.coordinates
  } else {
    coordinates = [data.event.lngLat.lng, data.event.lngLat.lat]
  }
  
  // 如果已有弹窗,先关闭
  if (currentPopup) {
    currentPopup.remove()
    currentPopup = null
  }
  
  // 创建新弹窗
  currentPopup = new mapComponentRef.value.maplibregl.Popup({
    closeButton: true,
    closeOnClick: false
  })
    .setLngLat(coordinates)
    .setHTML(`
      <div style="padding: 8px; min-width: 150px;">
        <h4 style="margin: 0 0 8px 0; font-size: 14px; color: #333;">
          ${data.properties.name || '未命名'}
        </h4>
        <p style="margin: 4px 0; font-size: 12px; color: #666;">
          年度: ${data.properties.year || '-'}
        </p>
        <p style="margin: 4px 0; font-size: 12px; color: #666;">
          类型: ${data.properties.type || '-'}
        </p>
      </div>
    `)
    .addTo(mapComponentRef.value.map)
    .on('close', () => {
      currentPopup = null
    })
}
</script>

<style>
.map-view {
  width: 100%;
  height: 100vh;
  position: relative;
}
</style>

动态添加图层示例

// 动态添加 GeoJSON 图层
function addGeoJsonLayer() {
  const newLayer = {
    id: `dynamic-layer-${Date.now()}`,
    name: '动态 GeoJSON 图层',
    type: 'geojson',
    shape: 'polygon',
    visible: true,
    isClick: true,
    deletable: true,
    autoFly: true,
    data: {
      type: 'FeatureCollection',
      features: [/* GeoJSON 要素数据 */]
    },
    layers: [
      {
        type: 'line',
        layerType: 'outline',
        layout: {},
        paint: {
          'line-color': '#000000',
          'line-width': 2,
          'line-opacity': 1
        }
      },
      {
        type: 'fill',
        layerType: 'fill',
        layout: {},
        paint: {
          'fill-color': '#0a76f1',
          'fill-opacity': 0.8,
          'fill-outline-color': '#0a76f1'
        }
      }
    ]
  }
  
  // 添加到图层列表顶部
  overlayLayers.value.unshift(newLayer)
}

// 添加矢量图层
function addVectorLayer() {
  const newLayer = {
    id: 'vector-layer',
    name: '矢量瓦片图层',
    type: 'vector',
    url: 'https://example.com/tiles/{z}/{x}/{y}.pbf',
    visible: true,
    layers: [/* 样式配置 */]
  }
  overlayLayers.value.unshift(newLayer)
}

// 添加栅格图层
function addRasterLayer() {
  const newLayer = {
    id: 'raster-layer',
    name: '栅格瓦片图层',
    type: 'raster',
    url: 'https://example.com/tiles/{z}/{x}/{y}.png',
    visible: true,
    opacity: 0.8
  }
  overlayLayers.value.unshift(newLayer)
}

// 添加栅格高程图层
function addRasterDemLayer() {
  const newLayer = {
    id: 'raster-dem-layer',
    name: '高程图层',
    type: 'raster-dem',
    url: 'https://example.com/dem-tiles/{z}/{x}/{y}.png',
    visible: true
  }
  overlayLayers.value.unshift(newLayer)
}

// 添加图片图层
function addImageLayer() {
  const newLayer = {
    id: 'image-layer',
    name: '图片图层',
    type: 'image',
    imageUrl: 'https://example.com/image.png',
    imageCoordinates: [
      [116.0, 40.0],  // 左上
      [117.0, 40.0],  // 右上
      [117.0, 39.0],  // 右下
      [116.0, 39.0]   // 左下
    ],
    visible: true
  }
  overlayLayers.value.unshift(newLayer)
}

布局控制示例

// 动态控制布局显示
function layoutControl(type: string) {
  switch(type) {
    case 'left':
      // 显示左侧面板
      splitConfig.split1.defaultSize = '200px'
      splitConfig.split1.disabled = false
      break
    case 'footer':
      // 显示底部面板
      splitConfig.split2.defaultSize = 0.8
      splitConfig.split2.disabled = false
      break
    case 'all':
      // 同时显示两个面板
      splitConfig.split1.defaultSize = '200px'
      splitConfig.split1.disabled = false
      splitConfig.split2.defaultSize = 0.8
      splitConfig.split2.disabled = false
      break
    case 'closeLeft':
      // 关闭左侧面板
      splitConfig.split1.defaultSize = 0
      splitConfig.split1.disabled = true
      break
    case 'closeFooter':
      // 关闭底部面板
      splitConfig.split2.defaultSize = 1
      splitConfig.split2.disabled = true
      break
    case 'closeAll':
      // 关闭所有面板
      splitConfig.split1.defaultSize = 0
      splitConfig.split1.disabled = true
      splitConfig.split2.defaultSize = 1
      splitConfig.split2.disabled = true
      break
  }
}

自定义地图控件示例

const mapLoading = (map: any) => {
  console.log('地图加载完成')
  
  // 创建自定义控件
  class CustomControl {
    map: any
    container: HTMLElement
    
    onAdd(map: any) {
      this.map = map
      // 使用 MapLibre GL 官方控件样式
      this.container = document.createElement('div')
      this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group'
      
      const btn = document.createElement('button')
      btn.style.width = '30px'
      btn.style.height = '30px'
      btn.style.border = 'none'
      btn.style.cursor = 'pointer'
      btn.innerText = '定'
      btn.onclick = () => {
        console.log('自定义控件被点击')
      }
      
      this.container.appendChild(btn)
      return this.container
    }
    
    onRemove() {
      this.container.remove()
      this.map = null
    }
  }
  
  // 添加自定义控件到地图
  map.addControl(new CustomControl(), 'top-left')
}

组件实例访问

通过 ref 获取组件实例后,可以访问以下属性和方法:

const mapComponentRef = ref()

// 访问 MapLibre GL 地图实例
const map = mapComponentRef.value.map

// 访问 Turf.js 工具库(用于空间分析)
const turf = mapComponentRef.value.turf

// 访问 MapLibre GL 命名空间
const maplibregl = mapComponentRef.value.maplibregl

// 示例:使用 maplibregl 创建 Popup
const popup = new maplibregl.Popup({ closeButton: true })
  .setLngLat([116.397, 39.908])
  .setHTML('<h3>北京</h3>')
  .addTo(map)

// 示例:使用 Turf.js 计算中心点
const centroid = turf.centroid(geojsonFeature)

技术栈

  • Vue 3 - 渐进式 JavaScript 框架
  • MapLibre GL - 开源 WebGL 地图渲染引擎
  • Naive UI - Vue 3 组件库
  • Pinia - Vue 状态管理
  • Turf.js - 地理空间分析库
  • TypeScript - 类型安全的 JavaScript 超集
  • Vite - 下一代前端构建工具

开发指南

本地开发

# 克隆仓库
git clone <repository-url>
cd gis-maplibre

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建生产版本
pnpm build

# 预览构建结果
pnpm preview

贡献指南

我们欢迎社区贡献!请遵循以下步骤:

  1. Fork 仓库并创建您的特性分支 (git checkout -b feature/AmazingFeature)
  2. 提交更改 (git commit -m 'Add some AmazingFeature')
  3. 推送到分支 (git push origin feature/AmazingFeature)
  4. 提交 Pull Request

代码规范

  • 使用 TypeScript 编写代码
  • 遵循 ESLint 和 Prettier 配置
  • 确保所有公共 API 都有完整的类型定义
  • 提交前运行 pnpm build 确保构建成功

提交信息规范

使用语义化提交信息:

  • feat: 新功能
  • fix: 修复 bug
  • docs: 文档更新
  • style: 代码格式调整
  • refactor: 重构
  • test: 测试相关
  • chore: 构建/工具链相关

注意事项

1. 依赖要求

  • Pinia 必须安装:组件内部使用 Pinia 进行状态管理,请确保在使用前已正确安装并注册 Pinia

    import { createPinia } from 'pinia'
    const pinia = createPinia()
    app.use(pinia)
  • Node.js 版本:建议使用 Node.js >= 18.0.0

  • Vue 版本:需要 Vue >= 3.4.0

2. 响应式数据

  • overlayLayers 必须使用 ref 或 reactive:组件监听响应式变化来动态更新图层,直接使用普通数组将无法触发更新

    // ✅ 正确
    const overlayLayers = ref([...])
  • splitConfig 建议使用 reactive:布局配置需要动态修改,使用 reactive 可以确保视图更新

    // ✅ 正确
    const splitConfig = reactive({ split1: {...}, split2: {...} })

3. 图层配置

  • 图层 ID 必须唯一:每个图层的 id 字段必须唯一,重复会导致渲染异常
  • isClick 属性:需要触发 layer-feature-click 事件的图层,必须设置 isClick: true
  • 图层样式规范layers 配置需遵循 MapLibre GL 的 Layer 规范,source 配置需遵循 Source 规范
  • GeoJSON 数据格式:确保 GeoJSON 数据格式正确,坐标顺序为 [经度, 纬度]

4. 布局控制

  • splitConfig 数值范围defaultSizesize 的值应在 minmax 之间(默认 0-1)
  • disabled 属性:设置 disabled: true 会隐藏对应面板区域,插槽内容不会显示
  • 像素值与比例值defaultSize 可以使用像素值(如 '200px')或比例值(如 0.5

5. 事件处理

  • init-complete 事件:地图初始化完成后触发,可在此获取地图实例并添加自定义控件
  • layer-feature-click 事件:返回的数据结构为 { [layerId]: { feature, properties, event } }
  • Popup 管理:建议保存 Popup 实例引用,在创建新 Popup 前先关闭旧的,避免多个 Popup 同时显示

6. 性能优化

  • 大量数据:对于大量 GeoJSON 数据,建议使用矢量瓦片(vector tiles)替代
  • 图层数量:避免同时显示过多图层,可通过 visible 属性控制图层显隐
  • 动态更新:频繁修改 overlayLayers 可能导致性能问题,建议批量更新或使用防抖

7. 常见问题

Q: 如何解决 useMessage 报错?

A: 组件已使用 createDiscreteApi 创建独立的 message 实例,无需额外配置 <n-message-provider>

Q: 图层点击事件不触发?

A: 确保图层配置中设置了 isClick: true,并且图层样式正确渲染。

Q: 如何动态更新图层数据?

A: 通过修改 overlayLayers 响应式数组,组件会自动监听变化并更新地图。建议使用 refreactive 包装。

Q: 如何添加自定义控件?

A: 在 init-complete 事件中获取地图实例,然后使用 MapLibre GL 的 addControl 方法添加。

Q: 动态添加的图层没有显示?

A: 检查图层 visible 是否为 true,数据格式是否正确,以及图层样式配置是否有效。

许可证

本项目采用 Apache-2.0 许可证。

作者

更新日志

v1.0.1 (当前版本)

  • 修复 useMessage 初始化问题,使用 createDiscreteApi 替代
  • 优化图层管理和搜索过滤功能
  • 完善 TypeScript 类型定义
  • 改进事件系统和交互体验
  • 支持灵活的布局分割控制

注意:本组件依赖 Pinia 进行状态管理,请确保在使用前已正确安装并注册 Pinia。