# 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 点击事件对象

事件映射说明

组件内部通过 eventKeyTransfermapMove 映射为 pointermovemapClick 映射为 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>

# 最佳实践

  1. 统一配置管理:将地图配置抽取到独立的 config 文件或 composable 中
  2. 图层分层:将底图、业务图层、交互图层分离管理
  3. 事件节流:高频事件(如 pointermove)建议配合 lodash/throttle 使用
  4. 错误处理:地图初始化可能失败,建议添加错误边界处理
  5. 响应式适配:监听窗口 resize 事件,确保地图容器尺寸变化时调用 map.updateSize()
Last Updated: 4/16/2026, 7:46:43 PM