# OL Map 地图组件
基于 OpenLayers (opens new window) 封装的 Vue 2 地图组件,提供声明式的地图配置、覆盖物管理、事件绑定等能力,支持与其他 CTC UI 组件(如 Dialog、Chart)配合使用。
# 功能特性
- 声明式配置:通过
config属性统一管理地图视图、底图、控件等配置 - 响应式更新:支持配置项的响应式变更,自动同步到地图实例
- 事件透传:地图原生事件自动透传为 Vue 事件,支持自定义事件映射
- 覆盖物管理:
CtcOlOverlay组件提供声明式的覆盖物管理能力 - 主题切换:内置浅色/深色底图,支持自定义切片地址
- 工具函数:提供图层聚焦、点位定位、GeoJSON 转换等常用工具
# 安装
ol 采用宿主项目自行安装的方式管理,组件库不内置该依赖。接入项目需要先安装:
npm install ol
# 快速开始
# 全局注册
import Vue from 'vue'
import CtcOlMap from '@packages/ol-map'
Vue.use(CtcOlMap)
# 基础用法
<template>
<ctc-ol-map :config="mapConfig" style="height: 500px">
<template #default="{ map }">
<div v-if="map">地图已初始化完成</div>
</template>
</ctc-ol-map>
</template>
<script>
import { fromLonLat } from 'ol/proj'
export default {
data() {
return {
mapConfig: {
view: {
center: fromLonLat([116.397389, 39.908722]), // 北京
zoom: 10
}
}
}
}
}
</script>
# CtcOlMap 组件
# Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| config | Object | 见下方 | 地图配置对象,包含 view、theme、layers、controls 等 |
# config 配置项详解
{
// 视图配置
view: {
center: [0, 0], // 地图中心点坐标(EPSG:3857投影)
zoom: 4.8, // 初始缩放级别
minZoom: 4, // 最小缩放级别
maxZoom: undefined, // 最大缩放级别
rotation: undefined // 旋转角度(弧度)
},
// 主题配置
theme: 'lightTheme', // 'lightTheme' | 'blueTheme'
// 图层配置(优先级高于 theme)
layers: null, // OpenLayers Layer 实例数组
// 控件配置
controls: createDefaultControls({
zoom: false, // 禁用默认缩放控件
rotate: false, // 禁用旋转控件
attribution: false // 禁用版权信息
})
}
# Events
| 事件名 | 说明 | 参数 |
|---|---|---|
| ready | 地图初始化完成,首次渲染结束后触发 | map - OpenLayers Map 实例 |
| rendercomplete | 地图渲染完成事件 | event - OpenLayers 渲染事件对象 |
| click | 地图点击事件 | event - OpenLayers 点击事件对象 |
| pointermove | 鼠标移动事件 | event - OpenLayers 指针事件对象 |
| mapMove | 自定义事件映射,对应 pointermove | event - OpenLayers 指针事件对象 |
| mapClick | 自定义事件映射,对应 click | event - OpenLayers 点击事件对象 |
事件映射说明
组件内部通过 eventKeyTransfer 将 mapMove 映射为 pointermove,mapClick 映射为 click,便于语义化使用。
# Slots
| 插槽名 | 说明 | 作用域参数 |
|---|---|---|
| default | 地图容器内容 | { map } - OpenLayers Map 实例 |
# 方法
组件实例提供以下方法(通过 ref 获取):
| 方法名 | 说明 | 参数 |
|---|---|---|
| applyConfig | 手动应用配置更新 | 无 |
# CtcOlOverlay 组件
覆盖物组件,用于在地图上显示自定义 HTML 内容。必须作为 CtcOlMap 的子组件使用。
# Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| config | Object | {} | 覆盖物配置对象 |
# config 配置项详解
{
// 必填:覆盖物位置坐标 [经度, 纬度](EPSG:3857投影)
position: [0, 0],
// 可选:像素偏移量 [x, y]
offset: [0, 0],
// 可选:定位方式
// 'bottom-left' | 'bottom-center' | 'bottom-right' |
// 'center-left' | 'center-center' | 'center-right' |
// 'top-left' | 'top-center' | 'top-right'
positioning: 'top-left',
// 可选:是否插入到覆盖物列表最前面
insertFirst: false,
// 其他 OpenLayers Overlay 配置项...
}
# Slots
| 插槽名 | 说明 | 作用域参数 |
|---|---|---|
| default | 覆盖物内容 | { overlay } - OpenLayers Overlay 实例 |
# 使用示例
<template>
<ctc-ol-map :config="mapConfig" @ready="handleReady">
<ctc-ol-overlay :config="overlayConfig">
<template #default>
<div class="custom-popup">
<h3>{{ activeSite.name }}</h3>
<p>客流: {{ activeSite.metrics.visitor }}</p>
</div>
</template>
</ctc-ol-overlay>
</ctc-ol-map>
</template>
<script>
import { fromLonLat } from 'ol/proj'
export default {
data() {
return {
mapConfig: {
view: {
center: fromLonLat([116.397389, 39.908722]),
zoom: 10
}
},
activeSite: null
}
},
computed: {
overlayConfig() {
if (!this.activeSite) return {}
return {
position: fromLonLat([
this.activeSite.longitude,
this.activeSite.latitude
]),
positioning: 'bottom-center',
offset: [0, -18],
stopEvent: false // 允许事件穿透到地图
}
}
},
methods: {
handleReady(map) {
// 地图就绪后的操作
map.on('pointermove', (e) => {
// 处理鼠标移动,更新 activeSite
})
}
}
}
</script>
# 底图类型
组件内置三种底图工厂函数:
# defaultLayer(url?)
默认底图,优先使用传入的 URL,否则回退到 OpenStreetMap。
import { defaultLayer } from '@packages/ol-map'
// 使用自定义切片地址
const customLayer = defaultLayer('https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}')
// 使用 OSM(默认)
const osmBaseLayer = defaultLayer()
# blueImageLayer()
深色大屏常用的蓝色底图效果,基于 Raster 源对默认底图进行像素级滤镜处理。
import { blueImageLayer } from '@packages/ol-map'
const darkLayer = blueImageLayer()
# osmLayer()
纯 OpenStreetMap 底图。
import { osmLayer } from '@packages/ol-map'
const osm = osmLayer()
# 工具函数
# focusLayer(map, layer, config?)
将地图视图聚焦到指定图层的范围。
import { focusLayer } from '@packages/ol-map'
// 聚焦到矢量图层
focusLayer(map, vectorLayer, {
padding: [50, 50, 50, 50], // 内边距
duration: 1200, // 动画时长(毫秒)
easing: inAndOut // 缓动函数
})
# locationToMap(view, location, done?)
带缩放动画的定位到指定坐标。
import { locationToMap } from '@packages/ol-map'
locationToMap(map.getView(), fromLonLat([116.397389, 39.908722]), (complete) => {
if (complete) {
console.log('定位完成')
}
})
# calcExtentFeature(map, feature)
判断要素是否在视口范围内。
import { calcExtentFeature } from '@packages/ol-map'
const isVisible = calcExtentFeature(map, feature)
# pointJsonToGeo(data)
将点位数据转换为 OpenLayers GeoJSON Feature 数组。
import { pointJsonToGeo } from '@packages/ol-map'
const pointData = [
{ name: '点位1', xy: [116.397389, 39.908722] },
{ name: '点位2', longitude: 116.4, latitude: 39.91 }
]
const features = pointJsonToGeo(pointData)
# 综合示例
# 地图与 Dialog、Chart 联动
以下示例展示如何在地图点位点击后,弹出 Dialog 并在其中展示 Chart 图表:
<template>
<div class="map-demo">
<!-- 地图容器 -->
<ctc-ol-map
:config="mapConfig"
@ready="handleMapReady"
@pointermove="handlePointerMove"
style="height: 600px"
>
<!-- Hover 覆盖物 -->
<ctc-ol-overlay :config="hoverOverlayConfig">
<template #default>
<div v-if="hoverSite" class="hover-tooltip">
<strong>{{ hoverSite.name }}</strong>
<span>客流: {{ hoverSite.metrics.visitor }}</span>
</div>
</template>
</ctc-ol-overlay>
</ctc-ol-map>
<!-- 点位详情弹窗 -->
<ctc-dialog
:visible.sync="isDialogVisible"
title="点位指标分析"
width="780px"
>
<div v-if="selectedSite">
<h3>{{ selectedSite.name }}</h3>
<ctc-chart
height="320px"
:echarts="echarts"
:opt="chartOption"
/>
</div>
</ctc-dialog>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { fromLonLat } from 'ol/proj'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import VectorSource from 'ol/source/Vector'
import VectorLayer from 'ol/layer/Vector'
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'
export default {
data() {
return {
echarts,
mapInstance: null,
siteLayer: null,
hoverSite: null,
selectedSite: null,
isDialogVisible: false,
siteList: [
{ id: 1, name: '站点A', longitude: 116.397389, latitude: 39.908722, metrics: { visitor: 5000 } },
{ id: 2, name: '站点B', longitude: 116.407389, latitude: 39.918722, metrics: { visitor: 8000 } }
]
}
},
computed: {
mapConfig() {
return {
view: {
center: fromLonLat([116.397389, 39.908722]),
zoom: 11
}
}
},
hoverOverlayConfig() {
if (!this.hoverSite) return {}
return {
position: fromLonLat([this.hoverSite.longitude, this.hoverSite.latitude]),
positioning: 'bottom-center',
offset: [0, -12],
stopEvent: false
}
},
chartOption() {
if (!this.selectedSite) return {}
return {
xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [120, 200, 150, 80, 70]
}]
}
}
},
methods: {
handleMapReady(map) {
this.mapInstance = map
this.initSiteLayer()
// 绑定点击事件
map.on('click', (e) => {
const feature = map.forEachFeatureAtPixel(e.pixel, f => f)
if (feature && feature.get('featureType') === 'site') {
this.selectedSite = feature.get('site')
this.isDialogVisible = true
}
})
},
handlePointerMove(e) {
const feature = this.mapInstance?.forEachFeatureAtPixel(e.pixel, f => f)
const site = feature?.get('featureType') === 'site'
? feature.get('site')
: null
this.hoverSite = site
this.mapInstance.getTargetElement().style.cursor = site ? 'pointer' : 'default'
},
initSiteLayer() {
const features = this.siteList.map(site => {
const feature = new Feature({
geometry: new Point(fromLonLat([site.longitude, site.latitude])),
site
})
feature.set('featureType', 'site')
return feature
})
const source = new VectorSource({ features })
this.siteLayer = new VectorLayer({
source,
style: new Style({
image: new CircleStyle({
radius: 8,
fill: new Fill({ color: '#22c55e' }),
stroke: new Stroke({ color: '#fff', width: 2 })
})
})
})
this.mapInstance.addLayer(this.siteLayer)
}
}
}
</script>
<style scoped>
.hover-tooltip {
padding: 8px 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 4px;
font-size: 12px;
}
.hover-tooltip strong {
display: block;
margin-bottom: 4px;
}
</style>
# 主题切换示例
<template>
<div>
<el-button @click="switchTheme">
切换{{ mapTheme === 'lightTheme' ? '深色' : '浅色' }}主题
</el-button>
<ctc-ol-map :config="mapConfig" style="height: 500px" />
</div>
</template>
<script>
import { fromLonLat } from 'ol/proj'
export default {
data() {
return {
mapTheme: 'lightTheme'
}
},
computed: {
mapConfig() {
return {
theme: this.mapTheme,
view: {
center: fromLonLat([116.397389, 39.908722]),
zoom: 10
}
}
}
},
methods: {
switchTheme() {
this.mapTheme = this.mapTheme === 'lightTheme' ? 'blueTheme' : 'lightTheme'
}
}
}
</script>
# 注意事项
# 1. 坐标系转换
OpenLayers 默认使用 EPSG:3857(Web Mercator)投影。使用经纬度坐标时,需要通过 fromLonLat 进行转换:
import { fromLonLat } from 'ol/proj'
const center = fromLonLat([116.397389, 39.908722]) // [经度, 纬度]
# 2. 图层管理
- 通过
config.layers传入的图层会被组件标记为"托管",配置变更时会自动清理和重建 - 通过
map.addLayer()动态添加的图层不会被组件托管,需要手动管理生命周期
# 3. 事件绑定
- 推荐使用 Vue 的
@事件名语法绑定事件 - 也可以通过
map.on()在ready事件回调中直接绑定 OpenLayers 原生事件 - 组件销毁时会自动清理绑定的事件
# 4. 性能优化
- 大量点位建议使用
VectorLayer配合VectorSource,而非多个 Overlay - 频繁更新的覆盖物建议使用
v-show控制显示,而非动态创建/销毁 - 底图切换时,组件会自动复用已有图层实例
# 5. 样式隔离
地图容器内的样式建议添加 scoped 或使用 CSS Modules,避免样式污染:
<style scoped>
::v-deep .ol-map-container {
/* 自定义样式 */
}
</style>
# 最佳实践
- 统一配置管理:将地图配置抽取到独立的 config 文件或 composable 中
- 图层分层:将底图、业务图层、交互图层分离管理
- 事件节流:高频事件(如
pointermove)建议配合lodash/throttle使用 - 错误处理:地图初始化可能失败,建议添加错误边界处理
- 响应式适配:监听窗口 resize 事件,确保地图容器尺寸变化时调用
map.updateSize()