gis-maplibre
v1.0.6
Published
基于 MapLibre GL 的 Vue3 GIS 地图组件库
Downloads
367
Maintainers
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) | 工具栏按钮点击/操作事件。包含 key、name、action、value 等属性 |
| 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贡献指南
我们欢迎社区贡献!请遵循以下步骤:
- Fork 仓库并创建您的特性分支 (
git checkout -b feature/AmazingFeature) - 提交更改 (
git commit -m 'Add some AmazingFeature') - 推送到分支 (
git push origin feature/AmazingFeature) - 提交 Pull Request
代码规范
- 使用 TypeScript 编写代码
- 遵循 ESLint 和 Prettier 配置
- 确保所有公共 API 都有完整的类型定义
- 提交前运行
pnpm build确保构建成功
提交信息规范
使用语义化提交信息:
feat:新功能fix:修复 bugdocs:文档更新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 数值范围:
defaultSize和size的值应在min和max之间(默认 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 响应式数组,组件会自动监听变化并更新地图。建议使用 ref 或 reactive 包装。
Q: 如何添加自定义控件?
A: 在 init-complete 事件中获取地图实例,然后使用 MapLibre GL 的 addControl 方法添加。
Q: 动态添加的图层没有显示?
A: 检查图层 visible 是否为 true,数据格式是否正确,以及图层样式配置是否有效。
许可证
本项目采用 Apache-2.0 许可证。
作者
- mayunhui - [email protected]
更新日志
v1.0.1 (当前版本)
- 修复
useMessage初始化问题,使用createDiscreteApi替代 - 优化图层管理和搜索过滤功能
- 完善 TypeScript 类型定义
- 改进事件系统和交互体验
- 支持灵活的布局分割控制
注意:本组件依赖 Pinia 进行状态管理,请确保在使用前已正确安装并注册 Pinia。
