Skip to content

usePointermove

简介

在地图要素上移动鼠标时,提供悬浮提示(tooltip)逻辑。支持优先级匹配、可见性动态判断、位置偏移、是否固定在要素中心、以及自定义鼠标样式等。提示的 UI 需自行结合组件库实现(示例使用 NaiveUI 的 Popover)。

使用示例

点我查看代码
vue
<script lang="ts" setup>
import {
  createOpenStreetMapLayer,
  createPointFeature,
  createVectorLayer,
  OlMap,
} from '@summeruse/ol'
import { NCard, NPopover } from 'naive-ui'
import { Map as OLMap } from 'ol'
import { h } from 'vue'
import { usePointermove } from '.'

const olMap = new OLMap()
olMap.addLayer(createOpenStreetMapLayer())

const feature = createPointFeature([116.3912, 39.9072], {
  id: 'feature1',
  styleOptions: {
    circleOptions: {
      radius: 10,
      fillOptions: {
        color: 'red',
      },
      strokeOptions: {
        color: 'red',
        width: 10,
      },
    },
  },
  data: {
    name: '天安门',
    src: 'https://q6.itc.cn/images01/20251010/fc56df887f104e34b860f88976b12b74.jpeg',
  },
})

const feature2 = createPointFeature([116.4074, 39.9042], {
  id: 'feature2',
  styleOptions: {
    circleOptions: {
      radius: 10,
      fillOptions: {
        color: 'green',
      },
      strokeOptions: {
        color: 'green',
        width: 10,
      },
    },
  },
})

const { layer, source } = createVectorLayer()
source.addFeature(feature)
source.addFeature(feature2)

olMap.addLayer(layer)

const { visible, position, content, option } = usePointermove<{ raw?: boolean, showArrow?: boolean }>(
  {
    mapRef: olMap,
    items: [{
      raw: true,
      showArrow: false,
      offset: {
        x: 0,
        y: -20,
      },
      content: ({ feature }) => {
        // console.log(feature)
        const { name, src } = feature.get('data') || {}
        return h(NCard, {
          title: name,
          content() {
            return h('img', {
              src,
              style: {
                width: '300px',
                height: '200px',
              },
            })
          },
        })
      },
      priority: 99,
      cursor: 'pointer',
      visible: ({ feature }) => feature.get('id') === 'feature1',
    }, {
      content: ({ feature }) => feature.get('id'),
      cursor: 'progress',
      visible: ({ feature }) => feature.get('id') !== undefined,
      fixedFeatureCenter: false,
    }],
    forceUpdate: true,
  },
)
</script>

<template>
  <OlMap
    class="w-100% h-400px" :center="[116.3912, 39.9072]" :zoom="14" projection="EPSG:4326" :ol-map
  />
  <NPopover
    v-bind="option" :arrow-style="{ pointerEvents: 'none' }" style="pointer-events: none;" trigger="manual"
    :show="visible" :x="position.x" :y="position.y"
  >
    <template v-if="typeof content === 'function'">
      <component :is="content" />
    </template>
    <template v-else>
      {{ content }}
    </template>
  </NPopover>
</template>

泛型扩展

  • 支持通过泛型携带自定义配置类型:usePointermove<T extends Record<string, any>>(mapRef, items: MaybeRefOrGetter<PointermoveItem<T>[]>)
  • 当命中提示项时,返回的 optionComputedRef<T | undefined>,内容为该项除 contentcursorvisiblefixedFeatureCenteroffsetpriority 以外的所有自定义字段。

示例:

ts
const { option } = usePointermove<{ id: string }>(olMap, [{
  content: '信息',
  id: 'feature-id',
}])
// option.value?.id === 'feature-id'

API

名称类型
usePointermove<T>(...args: UsePointermoveParams<T>) => UsePointermoveReturn

UsePointermoveParams<T>

名称类型默认值说明
args[0]MaybeRefOrGetter<OLMap | undefined>-地图实例
args[1]MaybeRefOrGetter<PointermoveItem<T>[]>-提示项配置列表

UsePointermoveReturn

名称类型说明
visibleComputedRef<boolean>提示是否可见
offsetComputedRef<{ x: number, y: number }>位置偏移
positionComputedRef<PointermovePosition>提示当前位置(像素坐标)
originalPositionComputedRef<PointermovePosition>原始位置(鼠标事件触发位置)
featureComputedRef<FeatureLike | undefined>当前命中的要素
contentComputedRef<(() => VNodeChild) | string>提示内容,支持函数或字符串
coordinateComputedRef<Coordinate | undefined>当前命中的地图坐标
hide() => void关闭提示并恢复鼠标样式
optionComputedRef<T | undefined>当前命中的提示项自定义配置(来自匹配项的其他字段)

PointermoveItem

名称类型默认值说明
content((params: PointermoveContentParams) => VNodeChild) | string-提示内容,支持函数动态生成
visible((params: PointermoveContentParams) => boolean) | booleantrue是否显示提示
offset{ x?: number, y?: number }{ x: 0, y: 0 }位置偏移
prioritynumber0优先级,数字越大优先显示
cursorCSSProperties['cursor'] | ((params: PointermoveContentParams) => CSSProperties['cursor'])-鼠标样式,如 pointercrosshair
fixedFeatureCenterbooleantrue是否固定在要素中心位置
其他字段...args: Option-通过泛型扩展的自定义属性

PointermoveContentParams

名称类型说明
mapOLMap地图实例
positionPointermovePosition提示位置(像素坐标)
coordinateCoordinate当前命中的地图坐标
featureFeatureLike当前命中的要素
layerLayerLike | undefined要素所在图层

PointermovePosition

名称类型说明
xnumber横坐标(像素)
ynumber纵坐标(像素)

使用说明

  • 仅当指针位于某个要素上时才显示提示;离开要素则隐藏。
  • 当有多个提示项匹配时,按 priority 从高到低选择一个显示。
  • content 为函数时将接收 PointermoveContentParams,可按需生成内容或返回组件。
  • cursor 可设置地图视口的鼠标样式;调用 hide 会恢复原始样式。
  • fixedFeatureCentertrue 时,提示位置会锚定在要素中心;否则锚定在当前鼠标位置。

源代码

点我查看代码
ts
import type { MapBrowserEvent } from 'ol'
import type { Coordinate } from 'ol/coordinate'
import type { FeatureLike } from 'ol/Feature'
import type { CSSProperties, MaybeRefOrGetter, VNodeChild } from 'vue'
import type { LayerLike, OLMap } from '../../types'
import { getCenter } from 'ol/extent'
import { computed, onBeforeUnmount, ref, toValue, watch } from 'vue'

export interface PointermovePosition {
  x: number
  y: number
}

interface PointermoveContentParams {
  map: OLMap
  coordinate: Coordinate
  position: PointermovePosition
  feature: FeatureLike
  layer?: LayerLike
}

type Cursor = CSSProperties['cursor']

export type PointermoveItem<T extends Option = Option> = {
  /** 提示内容,支持函数动态生成 */
  content?: ((params: PointermoveContentParams) => VNodeChild) | string
  /** 是否显示提示,可根据 feature 动态判断 */
  visible?: ((params: PointermoveContentParams) => boolean | undefined | void) | boolean
  /** 位置偏移 */
  offset?: { x?: number, y?: number }
  /** 优先级,数字越大优先级越高,当多个 tooltip 匹配时,显示优先级最高的 */
  priority?: number
  /** 鼠标样式,如 'pointer', 'crosshair', 'move' 等 */
  cursor?: Cursor | ((params: PointermoveContentParams) => Cursor)
  /** 固定在feature center 默认启用,若要关闭需要同时开启强制更新 */
  fixedFeatureCenter?: boolean
} & T

export type PointermoveList<T extends Option = Option> = PointermoveItem<T>[]

export interface Option {
  [key: string]: any
}

export interface UsePointermoveOptions<T extends Option = Option> {
  /** 地图实例 */
  mapRef: MaybeRefOrGetter<OLMap | undefined>
  /** 提示配置列表 */
  items: MaybeRefOrGetter<PointermoveList<T>>
  /** 强制更新 (开启后,无论 feature 是否变化,都强制更新提示) */
  forceUpdate?: boolean
}

export function usePointermove<T extends Option>(
  { mapRef, items, forceUpdate = false }: UsePointermoveOptions<T>,
) {
  const visible = ref(false)
  // 原始位置
  const originalPosition = ref<PointermovePosition>({ x: 0, y: 0 })
  const feature = ref<FeatureLike>()
  const content = ref<(() => VNodeChild) | string>()
  const offset = ref<{ x: number, y: number }>({ x: 0, y: 0 })
  const originalCoordinate = ref<Coordinate>()
  const coordinate = ref<Coordinate>()
  const option = ref<T>()
  const position = computed(() => ({
    x: originalPosition.value.x + offset.value.x,
    y: originalPosition.value.y + offset.value.y,
  }))

  let currentMap: OLMap | undefined
  let viewport: HTMLElement | undefined
  let originalCursor: string = ''

  /** 查找匹配的 tooltip 配置 */
  function findMatchingPointermove(params: PointermoveContentParams): PointermoveItem<T> | undefined {
    const tooltips = toValue(items)
    // 先排序,优先级高的在前面
    tooltips.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
    // 拿到第一个匹配的
    return tooltips.find((item) => {
      const shouldShow = item.visible
      if (typeof shouldShow === 'function') {
        return shouldShow(params)
      }
      return shouldShow ?? true
    })
  }

  /** 显示提示 */
  function show(evt: MapBrowserEvent) {
    if (evt.dragging) {
      return
    }

    if (!currentMap || !viewport)
      return

    const _coordinate = evt.coordinate
    originalCoordinate.value = _coordinate
    coordinate.value = _coordinate

    let pixel = evt.pixel

    let foundFeature: FeatureLike | undefined
    let foundLayer: LayerLike | undefined

    currentMap.forEachFeatureAtPixel(pixel, (feature, layer) => {
      foundFeature = feature
      foundLayer = layer
      return true
    })

    // 如果没有找到 feature,不显示提示
    if (!foundFeature) {
      hide()
      return
    }

    if (!forceUpdate && feature.value && feature.value.getId() === foundFeature.getId()) {
      return
    }

    feature.value = foundFeature
    const { top, left } = viewport.getBoundingClientRect()
    const params = {
      map: currentMap,
      position: { x: pixel[0] + left, y: pixel[1] + top },
      coordinate: _coordinate,
      feature: foundFeature,
      layer: foundLayer,
    }

    // 查找匹配的 tooltip 配置
    const matchedPointermove = findMatchingPointermove(params)
    if (matchedPointermove) {
      const { content: _content, cursor: _cursor, visible: _visible, fixedFeatureCenter: _fixedFeatureCenter, offset: _offset, priority: _priority, ...rest } = matchedPointermove
      option.value = { ...rest } as unknown as T
    }
    if (!matchedPointermove) {
      hide()
      return
    }

    // 计算位置(带偏移)
    const offsetX = matchedPointermove.offset?.x ?? 0
    const offsetY = matchedPointermove.offset?.y ?? 0
    offset.value = { x: offsetX, y: offsetY }
    const fixedFeatureCenter = forceUpdate === false ? true : (matchedPointermove.fixedFeatureCenter ?? true)
    const geometry = foundFeature.getGeometry()
    if (fixedFeatureCenter && geometry) {
      const extent = geometry.getExtent()
      const center = getCenter(extent)
      coordinate.value = center
      pixel = currentMap.getPixelFromCoordinate(center)
    }
    originalPosition.value.x = pixel[0] + left
    originalPosition.value.y = pixel[1] + top
    // 设置内容
    const tooltipContent = matchedPointermove.content
    content.value = typeof tooltipContent === 'function'
      ? () => tooltipContent(params)
      : tooltipContent

    // 设置鼠标样式
    const cursor = matchedPointermove.cursor
    const cursorStyle = typeof cursor === 'function'
      ? cursor(params)
      : cursor

    if (cursorStyle !== undefined && cursorStyle !== viewport.style.cursor) {
      viewport.style.cursor = cursorStyle
    }
    visible.value = true
  }

  /** 隐藏提示 */
  function hide() {
    visible.value = false
    feature.value = undefined
    // 恢复原始鼠标样式
    if (viewport && viewport.style.cursor !== originalCursor) {
      viewport.style.cursor = originalCursor
    }
  }

  /** 绑定事件 */
  function bindMapEvents(map: OLMap) {
    map.on('pointermove', show)
  }

  /** 解绑事件 */
  function unbindMapEvents(map: OLMap) {
    map.un('pointermove', show)
  }

  /** 监听 mapRef 变化 */
  watch(
    () => toValue(mapRef),
    (newMap, oldMap) => {
      if (oldMap) {
        unbindMapEvents(oldMap)
      }
      if (oldMap !== newMap) {
        currentMap = newMap
        if (newMap) {
          viewport = newMap.getViewport()

          bindMapEvents(newMap)
          originalCursor = viewport.style.cursor
        }
      }
    },
    { immediate: true },
  )

  onBeforeUnmount(() => {
    if (currentMap) {
      unbindMapEvents(currentMap)
    }
  })

  return {
    visible: computed(() => visible.value),
    position: computed(() => position.value),
    offset: computed(() => offset.value),
    originalPosition: computed(() => originalPosition.value),
    feature: computed(() => feature.value),
    content: computed(() => content.value),
    originalCoordinate: computed(() => originalCoordinate.value),
    coordinate: computed(() => coordinate.value),
    option: computed(() => option.value),
    hide,
  }
}

export type UsePointermoveReturn = ReturnType<typeof usePointermove>
export type UsePointermoveParams<T extends Option> = Parameters<typeof usePointermove<T>>
export type UsePointermoveFn<T extends Option> = (...args: UsePointermoveParams<T>) => UsePointermoveReturn

Released under the ISC License.